Skip to content

Commit edb9abb

Browse files
authored
Merge pull request #1 from bcdev/forman-multiple_panels
Multiple panels
2 parents 08e4e08 + 913dc93 commit edb9abb

File tree

10 files changed

+225
-69
lines changed

10 files changed

+225
-69
lines changed

dashi/src/App.tsx

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,106 @@
11
import { ReactElement, useEffect, useState } from "react";
22
import "./App.css";
3-
import { EventHandler, PanelModel } from "./lib/model.ts";
4-
import { fetchPanelInit, fetchPanelUpdate, FetchResponse } from "./api.ts";
3+
import { PanelEventHandler, PanelModel, Panels } from "./lib/model.ts";
4+
import { fetchPanels, fetchPanel, FetchResponse } from "./api.ts";
55
import DashiPanel from "./lib/DashiPanel.tsx";
66

77
function App() {
8-
const [panelModelResponse, setPanelModelResponse] = useState<
9-
FetchResponse<PanelModel>
8+
const [panelsResponse, setPanelsResponse] = useState<FetchResponse<Panels>>(
9+
{},
10+
);
11+
12+
const [panelResponses, setPanelResponses] = useState<
13+
Record<string, FetchResponse<PanelModel>>
14+
>({});
15+
16+
const [panelVisibilities, setPanelVisibilities] = useState<
17+
Record<string, boolean>
1018
>({});
1119

1220
useEffect(() => {
13-
fetchPanelInit().then(
14-
(panelModelResponse) => void setPanelModelResponse(panelModelResponse),
15-
);
21+
fetchPanels().then(setPanelsResponse);
1622
}, []);
1723

18-
const handleEvent: EventHandler = (event) => {
19-
fetchPanelUpdate(event).then(
20-
(panelModelResponse) => void setPanelModelResponse(panelModelResponse),
21-
);
24+
useEffect(() => {
25+
Object.getOwnPropertyNames(panelVisibilities).forEach((panelId) => {
26+
const panelVisible = panelVisibilities[panelId];
27+
const panelResponse = panelResponses[panelId];
28+
if (panelVisible && !panelResponse) {
29+
fetchPanel(panelId).then((panelResponse) => {
30+
setPanelResponses({ ...panelResponses, [panelId]: panelResponse });
31+
});
32+
}
33+
});
34+
}, [panelVisibilities, panelResponses]);
35+
36+
const handlePanelEvent: PanelEventHandler = (event) => {
37+
fetchPanel(event.panelId, event).then((result) => {
38+
setPanelResponses({ ...panelResponses, [event.panelId]: result });
39+
});
2240
};
2341

24-
console.info("panelModelResponse:", panelModelResponse);
25-
26-
const { result, error } = panelModelResponse;
27-
let panelComponent: ReactElement | undefined;
28-
if (result) {
29-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
30-
const { type, ...panelProps } = result;
31-
panelComponent = (
32-
<DashiPanel
33-
width={500}
34-
height={300}
35-
{...panelProps}
36-
onEvent={handleEvent}
37-
/>
42+
let panelSelector: ReactElement;
43+
if (panelsResponse.result) {
44+
const panelIds = panelsResponse.result.panels;
45+
panelSelector = (
46+
<div style={{ padding: 5 }}>
47+
{panelIds.map((panelId) => (
48+
<div key={panelId}>
49+
<input
50+
type="checkbox"
51+
checked={Boolean(panelVisibilities[panelId])}
52+
value={panelId}
53+
onChange={(e) => {
54+
setPanelVisibilities({
55+
...panelVisibilities,
56+
[panelId]: e.currentTarget.checked,
57+
});
58+
}}
59+
/>
60+
<label htmlFor={panelId}> {panelId} </label>
61+
</div>
62+
))}
63+
</div>
3864
);
39-
} else if (error) {
40-
panelComponent = <div>Error: {error}</div>;
65+
} else if (panelsResponse.error) {
66+
panelSelector = <div>Error: {panelsResponse.error}</div>;
4167
} else {
42-
panelComponent = <div>Loading chart...</div>;
68+
panelSelector = <div>Loading panels...</div>;
4369
}
4470

71+
const panelComponents: ReactElement[] = [];
72+
Object.getOwnPropertyNames(panelVisibilities).forEach((panelId) => {
73+
const panelVisible = panelVisibilities[panelId];
74+
const panelResponse = panelResponses[panelId];
75+
let panelComponent: ReactElement;
76+
if (panelVisible && panelResponse) {
77+
if (panelResponse.result) {
78+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
79+
const { type, ...panelProps } = panelResponse.result as PanelModel;
80+
panelComponent = (
81+
<DashiPanel
82+
key={panelId}
83+
panelId={panelId}
84+
width={500}
85+
height={300}
86+
{...panelProps}
87+
onEvent={handlePanelEvent}
88+
/>
89+
);
90+
} else if (panelResponse.error) {
91+
panelComponent = <div>Error: {panelResponse.error}</div>;
92+
} else {
93+
panelComponent = <div>Loading chart...</div>;
94+
}
95+
panelComponents.push(panelComponent);
96+
}
97+
});
98+
4599
return (
46100
<>
47101
<h2>Dashi Demo</h2>
48-
{panelComponent}
102+
{panelSelector}
103+
<div style={{ display: "flex", gap: 5 }}>{panelComponents}</div>
49104
</>
50105
);
51106
}

dashi/src/api.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
1-
import { EventData, PanelModel } from "./lib/model.ts";
1+
import { PanelEventData, PanelModel, Panels } from "./lib/model.ts";
22

33
export interface FetchResponse<T> {
44
result?: T;
55
error?: string;
66
}
77

8-
const API_URL = `http://localhost:8888/panel`;
9-
10-
export async function fetchPanelInit(): Promise<FetchResponse<PanelModel>> {
11-
return fetchJson(API_URL);
8+
export async function fetchPanels(): Promise<FetchResponse<Panels>> {
9+
return fetchJson("http://localhost:8888/panels");
1210
}
1311

14-
export async function fetchPanelUpdate(
15-
event: EventData,
12+
export async function fetchPanel(
13+
panelId: string,
14+
event?: PanelEventData,
1615
): Promise<FetchResponse<PanelModel>> {
17-
return fetchJson(API_URL, {
18-
method: "post",
19-
headers: {},
20-
body: JSON.stringify(event),
21-
});
16+
const url = `http://localhost:8888/panels/${panelId}`;
17+
if (event) {
18+
return fetchJson(url, {
19+
method: "post",
20+
body: JSON.stringify(event),
21+
});
22+
} else {
23+
return fetchJson(url);
24+
}
2225
}
2326

2427
export async function fetchJson<T>(

dashi/src/lib/DashiPanel.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1-
import { EventHandler, makeId, PanelModel } from "./model";
1+
import { PanelEventHandler, makeId, PanelModel, EventData } from "./model";
22
import DashiContainer from "./DashiContainer";
33

44
export interface DashiPanelProps extends Omit<PanelModel, "type"> {
5+
panelId: string;
56
width: number;
67
height: number;
7-
onEvent: EventHandler;
8+
onEvent: PanelEventHandler;
89
}
910

1011
function DashiPanel({
12+
panelId,
1113
width,
1214
height,
1315
id,
1416
style,
1517
components,
1618
onEvent,
1719
}: DashiPanelProps) {
20+
const handleEvent = (event: EventData) => {
21+
onEvent({ panelId, ...event });
22+
};
1823
return (
1924
<div id={makeId("panel", id)} style={{ width, height, ...style }}>
20-
<DashiContainer components={components} onEvent={onEvent} />
25+
<DashiContainer components={components} onEvent={handleEvent} />
2126
</div>
2227
);
2328
}

dashi/src/lib/model.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ export interface PlotModel extends ComponentModel {
3232
config: Partial<Config>;
3333
}
3434

35+
export interface BoxModel extends ContainerModel {
36+
type: "box";
37+
}
38+
3539
export interface PanelModel extends ContainerModel {
3640
type: "panel";
3741
}
3842

39-
export interface BoxModel extends ContainerModel {
40-
type: "box";
43+
export interface Panels {
44+
panels: string[];
4145
}
4246

4347
export interface EventData<T = object> {
@@ -47,7 +51,12 @@ export interface EventData<T = object> {
4751
eventData?: T;
4852
}
4953

54+
export interface PanelEventData<T = object> extends EventData<T> {
55+
panelId: string;
56+
}
57+
5058
export type EventHandler<T = object> = (data: EventData<T>) => void;
59+
export type PanelEventHandler<T = object> = (data: PanelEventData<T>) => void;
5160

5261
export function makeId(type: ComponentType, id: string | undefined) {
5362
if (typeof id === "string" && id !== "") {

dashipy/dashipy/lib/plot.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from typing import Any
23

34
import plotly.graph_objects as go
@@ -20,5 +21,9 @@ def __init__(
2021
def to_dict(self) -> dict[str, Any]:
2122
return {
2223
**super().to_dict(),
23-
**self.figure.to_dict(),
24+
# TODO: this is stupid, but if using self.figure.to_dict()
25+
# for plotly.express figures we get
26+
# TypeError: Object of type ndarray is not JSON serializable
27+
**json.loads(self.figure.to_json()),
28+
# **self.figure.to_dict(),
2429
}

dashipy/dashipy/server.py

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import asyncio
2+
import importlib
3+
from typing import Callable, Any
24

35
import tornado
46
import tornado.web
57
import tornado.log
8+
import yaml
69

710
from dashipy.context import Context
8-
from dashipy.panel import get_panel
911
from dashipy import __version__
1012

1113

12-
class RootHandler(tornado.web.RequestHandler):
13-
def get(self):
14-
self.set_header("Content-Type", "text/plain")
15-
self.write(f"dashi-server {__version__}")
16-
17-
18-
class PanelHandler(tornado.web.RequestHandler):
14+
class RequestHandler(tornado.web.RequestHandler):
1915

2016
def set_default_headers(self):
2117
self.set_header("Access-Control-Allow-Origin", "*")
@@ -26,30 +22,74 @@ def set_default_headers(self):
2622
"authorization,content-type",
2723
)
2824

25+
26+
class RootHandler(RequestHandler):
2927
def get(self):
30-
context: Context = self.settings["context"]
31-
panel = get_panel(context)
28+
self.set_header("Content-Type", "text/plain")
29+
self.write(f"dashi-server {__version__}")
30+
31+
32+
class PanelsHandler(RequestHandler):
33+
34+
# GET /panels
35+
def get(self):
36+
panels: dict[str, Callable] = self.settings["panels"]
3237
self.set_header("Content-Type", "text/json")
33-
self.write(panel.to_dict())
38+
self.write({"panels": list(panels.keys())})
3439

35-
def post(self):
36-
context: Context = self.settings["context"]
40+
41+
class PanelRendererHandler(RequestHandler):
42+
# GET /panels/{panel_id}
43+
def get(self, panel_id: str):
44+
self.render_panel(panel_id, {})
45+
46+
# POST /panels/{panel_id}
47+
def post(self, panel_id: str):
3748
event = tornado.escape.json_decode(self.request.body)
38-
print(event)
39-
event_data = event.get("eventData") or {}
40-
panel = get_panel(context, **event_data)
41-
self.set_header("Content-Type", "text/json")
42-
self.write(panel.to_dict())
49+
panel_props = event.get("eventData") or {}
50+
self.render_panel(panel_id, panel_props)
51+
52+
def render_panel(self, panel_id: str, panel_props: dict[str, Any]):
53+
context: Context = self.settings["context"]
54+
panels: dict[str, Callable] = self.settings["panels"]
55+
panel_renderer = panels.get(panel_id)
56+
if panel_renderer is not None:
57+
self.set_header("Content-Type", "text/json")
58+
self.write(panel_renderer(context, **panel_props).to_dict())
59+
else:
60+
self.set_status(404, f"panel not found: {panel_props}")
4361

4462

4563
def make_app():
64+
# Read config
65+
with open("my-config.yaml") as f:
66+
server_config = yaml.load(f, yaml.SafeLoader)
67+
68+
# Parse panel renderers
69+
panels: dict[str, Callable] = {}
70+
for panel_ref in server_config.get("panels", []):
71+
try:
72+
module_name, function_name = panel_ref.rsplit(".", maxsplit=2)
73+
except (ValueError, AttributeError):
74+
raise TypeError(f"panel renderer syntax error: {panel_ref!r}")
75+
module = importlib.import_module(module_name)
76+
render_function = getattr(module, function_name)
77+
if not callable(render_function):
78+
raise TypeError(
79+
f"panel renderer {panel_ref!r} does not refer to a callable"
80+
)
81+
panels[panel_ref.replace(".", "-").replace("_", "-").lower()] = render_function
82+
83+
# Create app
4684
app = tornado.web.Application(
4785
[
4886
(r"/", RootHandler),
49-
(r"/panel", PanelHandler),
87+
(r"/panels", PanelsHandler),
88+
(r"/panels/([a-z0-9-]+)", PanelRendererHandler),
5089
]
5190
)
5291
app.settings["context"] = Context()
92+
app.settings["panels"] = panels
5393
return app
5494

5595

@@ -58,7 +98,7 @@ async def main():
5898
port = 8888
5999
app = make_app()
60100
app.listen(port)
61-
print(f"Listening http://127.0.0.1:{port}...")
101+
print(f"Listening on http://127.0.0.1:{port}...")
62102
shutdown_event = asyncio.Event()
63103
await shutdown_event.wait()
64104

dashipy/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ channels:
44
dependencies:
55
- python
66
# Dependencies
7+
- pandas
78
- plotly
89
- pyaml
910
- tornado

dashipy/my-config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
panels:
2+
- my_panels.render_panel_1
3+
- my_panels.render_panel_2

0 commit comments

Comments
 (0)