Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 84 additions & 29 deletions dashi/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,106 @@
import { ReactElement, useEffect, useState } from "react";
import "./App.css";
import { EventHandler, PanelModel } from "./lib/model.ts";
import { fetchPanelInit, fetchPanelUpdate, FetchResponse } from "./api.ts";
import { PanelEventHandler, PanelModel, Panels } from "./lib/model.ts";
import { fetchPanels, fetchPanel, FetchResponse } from "./api.ts";
import DashiPanel from "./lib/DashiPanel.tsx";

function App() {
const [panelModelResponse, setPanelModelResponse] = useState<
FetchResponse<PanelModel>
const [panelsResponse, setPanelsResponse] = useState<FetchResponse<Panels>>(
{},
);

const [panelResponses, setPanelResponses] = useState<
Record<string, FetchResponse<PanelModel>>
>({});

const [panelVisibilities, setPanelVisibilities] = useState<
Record<string, boolean>
>({});

useEffect(() => {
fetchPanelInit().then(
(panelModelResponse) => void setPanelModelResponse(panelModelResponse),
);
fetchPanels().then(setPanelsResponse);
}, []);

const handleEvent: EventHandler = (event) => {
fetchPanelUpdate(event).then(
(panelModelResponse) => void setPanelModelResponse(panelModelResponse),
);
useEffect(() => {
Object.getOwnPropertyNames(panelVisibilities).forEach((panelId) => {
const panelVisible = panelVisibilities[panelId];
const panelResponse = panelResponses[panelId];
if (panelVisible && !panelResponse) {
fetchPanel(panelId).then((panelResponse) => {
setPanelResponses({ ...panelResponses, [panelId]: panelResponse });
});
}
});
}, [panelVisibilities, panelResponses]);

const handlePanelEvent: PanelEventHandler = (event) => {
fetchPanel(event.panelId, event).then((result) => {
setPanelResponses({ ...panelResponses, [event.panelId]: result });
});
};

console.info("panelModelResponse:", panelModelResponse);

const { result, error } = panelModelResponse;
let panelComponent: ReactElement | undefined;
if (result) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { type, ...panelProps } = result;
panelComponent = (
<DashiPanel
width={500}
height={300}
{...panelProps}
onEvent={handleEvent}
/>
let panelSelector: ReactElement;
if (panelsResponse.result) {
const panelIds = panelsResponse.result.panels;
panelSelector = (
<div style={{ padding: 5 }}>
{panelIds.map((panelId) => (
<div key={panelId}>
<input
type="checkbox"
checked={Boolean(panelVisibilities[panelId])}
value={panelId}
onChange={(e) => {
setPanelVisibilities({
...panelVisibilities,
[panelId]: e.currentTarget.checked,
});
}}
/>
<label htmlFor={panelId}> {panelId} </label>
</div>
))}
</div>
);
} else if (error) {
panelComponent = <div>Error: {error}</div>;
} else if (panelsResponse.error) {
panelSelector = <div>Error: {panelsResponse.error}</div>;
} else {
panelComponent = <div>Loading chart...</div>;
panelSelector = <div>Loading panels...</div>;
}

const panelComponents: ReactElement[] = [];
Object.getOwnPropertyNames(panelVisibilities).forEach((panelId) => {
const panelVisible = panelVisibilities[panelId];
const panelResponse = panelResponses[panelId];
let panelComponent: ReactElement;
if (panelVisible && panelResponse) {
if (panelResponse.result) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { type, ...panelProps } = panelResponse.result as PanelModel;
panelComponent = (
<DashiPanel
key={panelId}
panelId={panelId}
width={500}
height={300}
{...panelProps}
onEvent={handlePanelEvent}
/>
);
} else if (panelResponse.error) {
panelComponent = <div>Error: {panelResponse.error}</div>;
} else {
panelComponent = <div>Loading chart...</div>;
}
panelComponents.push(panelComponent);
}
});

return (
<>
<h2>Dashi Demo</h2>
{panelComponent}
{panelSelector}
<div style={{ display: "flex", gap: 5 }}>{panelComponents}</div>
</>
);
}
Expand Down
27 changes: 15 additions & 12 deletions dashi/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import { EventData, PanelModel } from "./lib/model.ts";
import { PanelEventData, PanelModel, Panels } from "./lib/model.ts";

export interface FetchResponse<T> {
result?: T;
error?: string;
}

const API_URL = `http://localhost:8888/panel`;

export async function fetchPanelInit(): Promise<FetchResponse<PanelModel>> {
return fetchJson(API_URL);
export async function fetchPanels(): Promise<FetchResponse<Panels>> {
return fetchJson("http://localhost:8888/panels");
}

export async function fetchPanelUpdate(
event: EventData,
export async function fetchPanel(
panelId: string,
event?: PanelEventData,
): Promise<FetchResponse<PanelModel>> {
return fetchJson(API_URL, {
method: "post",
headers: {},
body: JSON.stringify(event),
});
const url = `http://localhost:8888/panels/${panelId}`;
if (event) {
return fetchJson(url, {
method: "post",
body: JSON.stringify(event),
});
} else {
return fetchJson(url);
}
}

export async function fetchJson<T>(
Expand Down
11 changes: 8 additions & 3 deletions dashi/src/lib/DashiPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { EventHandler, makeId, PanelModel } from "./model";
import { PanelEventHandler, makeId, PanelModel, EventData } from "./model";
import DashiContainer from "./DashiContainer";

export interface DashiPanelProps extends Omit<PanelModel, "type"> {
panelId: string;
width: number;
height: number;
onEvent: EventHandler;
onEvent: PanelEventHandler;
}

function DashiPanel({
panelId,
width,
height,
id,
style,
components,
onEvent,
}: DashiPanelProps) {
const handleEvent = (event: EventData) => {
onEvent({ panelId, ...event });
};
return (
<div id={makeId("panel", id)} style={{ width, height, ...style }}>
<DashiContainer components={components} onEvent={onEvent} />
<DashiContainer components={components} onEvent={handleEvent} />
</div>
);
}
Expand Down
13 changes: 11 additions & 2 deletions dashi/src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ export interface PlotModel extends ComponentModel {
config: Partial<Config>;
}

export interface BoxModel extends ContainerModel {
type: "box";
}

export interface PanelModel extends ContainerModel {
type: "panel";
}

export interface BoxModel extends ContainerModel {
type: "box";
export interface Panels {
panels: string[];
}

export interface EventData<T = object> {
Expand All @@ -47,7 +51,12 @@ export interface EventData<T = object> {
eventData?: T;
}

export interface PanelEventData<T = object> extends EventData<T> {
panelId: string;
}

export type EventHandler<T = object> = (data: EventData<T>) => void;
export type PanelEventHandler<T = object> = (data: PanelEventData<T>) => void;

export function makeId(type: ComponentType, id: string | undefined) {
if (typeof id === "string" && id !== "") {
Expand Down
7 changes: 6 additions & 1 deletion dashipy/dashipy/lib/plot.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from typing import Any

import plotly.graph_objects as go
Expand All @@ -20,5 +21,9 @@ def __init__(
def to_dict(self) -> dict[str, Any]:
return {
**super().to_dict(),
**self.figure.to_dict(),
# TODO: this is stupid, but if using self.figure.to_dict()
# for plotly.express figures we get
# TypeError: Object of type ndarray is not JSON serializable
**json.loads(self.figure.to_json()),
# **self.figure.to_dict(),
}
80 changes: 60 additions & 20 deletions dashipy/dashipy/server.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import asyncio
import importlib
from typing import Callable, Any

import tornado
import tornado.web
import tornado.log
import yaml

from dashipy.context import Context
from dashipy.panel import get_panel
from dashipy import __version__


class RootHandler(tornado.web.RequestHandler):
def get(self):
self.set_header("Content-Type", "text/plain")
self.write(f"dashi-server {__version__}")


class PanelHandler(tornado.web.RequestHandler):
class RequestHandler(tornado.web.RequestHandler):

def set_default_headers(self):
self.set_header("Access-Control-Allow-Origin", "*")
Expand All @@ -26,30 +22,74 @@ def set_default_headers(self):
"authorization,content-type",
)


class RootHandler(RequestHandler):
def get(self):
context: Context = self.settings["context"]
panel = get_panel(context)
self.set_header("Content-Type", "text/plain")
self.write(f"dashi-server {__version__}")


class PanelsHandler(RequestHandler):

# GET /panels
def get(self):
panels: dict[str, Callable] = self.settings["panels"]
self.set_header("Content-Type", "text/json")
self.write(panel.to_dict())
self.write({"panels": list(panels.keys())})

def post(self):
context: Context = self.settings["context"]

class PanelRendererHandler(RequestHandler):
# GET /panels/{panel_id}
def get(self, panel_id: str):
self.render_panel(panel_id, {})

# POST /panels/{panel_id}
def post(self, panel_id: str):
event = tornado.escape.json_decode(self.request.body)
print(event)
event_data = event.get("eventData") or {}
panel = get_panel(context, **event_data)
self.set_header("Content-Type", "text/json")
self.write(panel.to_dict())
panel_props = event.get("eventData") or {}
self.render_panel(panel_id, panel_props)

def render_panel(self, panel_id: str, panel_props: dict[str, Any]):
context: Context = self.settings["context"]
panels: dict[str, Callable] = self.settings["panels"]
panel_renderer = panels.get(panel_id)
if panel_renderer is not None:
self.set_header("Content-Type", "text/json")
self.write(panel_renderer(context, **panel_props).to_dict())
else:
self.set_status(404, f"panel not found: {panel_props}")


def make_app():
# Read config
with open("my-config.yaml") as f:
server_config = yaml.load(f, yaml.SafeLoader)

# Parse panel renderers
panels: dict[str, Callable] = {}
for panel_ref in server_config.get("panels", []):
try:
module_name, function_name = panel_ref.rsplit(".", maxsplit=2)
except (ValueError, AttributeError):
raise TypeError(f"panel renderer syntax error: {panel_ref!r}")
module = importlib.import_module(module_name)
render_function = getattr(module, function_name)
if not callable(render_function):
raise TypeError(
f"panel renderer {panel_ref!r} does not refer to a callable"
)
panels[panel_ref.replace(".", "-").replace("_", "-").lower()] = render_function

# Create app
app = tornado.web.Application(
[
(r"/", RootHandler),
(r"/panel", PanelHandler),
(r"/panels", PanelsHandler),
(r"/panels/([a-z0-9-]+)", PanelRendererHandler),
]
)
app.settings["context"] = Context()
app.settings["panels"] = panels
return app


Expand All @@ -58,7 +98,7 @@ async def main():
port = 8888
app = make_app()
app.listen(port)
print(f"Listening http://127.0.0.1:{port}...")
print(f"Listening on http://127.0.0.1:{port}...")
shutdown_event = asyncio.Event()
await shutdown_event.wait()

Expand Down
1 change: 1 addition & 0 deletions dashipy/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ channels:
dependencies:
- python
# Dependencies
- pandas
- plotly
- pyaml
- tornado
Expand Down
3 changes: 3 additions & 0 deletions dashipy/my-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
panels:
- my_panels.render_panel_1
- my_panels.render_panel_2
Loading