Dashboards as documents, and messages as something to bind to
A dashboard is its own document rather than widgets placed in a flow. Node-RED's dashboard tab is 260 nodes, about forty of them pure layout, which is exactly what the small-graph principle exists to avoid — and since the graph is already wired by message name, a widget can bind to a name without belonging to any flow. Stored beside the flows in the same repository, sharing their write lock and commit, under a directory the flow listing ignores. No draft/publish split: nothing executes a dashboard, so edit mode is its own staging area. Two things it needs from the engine. A message catalog spanning every flow, because a wall panel shows the heating next to the solar and the flow-scoped API is the wrong shape for that. And a way to put a value in without owning a node — a slider is a real value that happened to come from a person — which runs whatever consumes it and applies the same type check a node's output gets. Only a message some flow declares can be published to; flows own the namespace. Charts also need more past than the 120 points a sparkline wanted, so a chart widget declares its depth and the engine keeps that message's series that deep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
This commit is contained in:
@@ -12,6 +12,7 @@ from app.core import security
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.db import engine
|
from app.core.db import engine
|
||||||
from app.flow.controller import FlowController
|
from app.flow.controller import FlowController
|
||||||
|
from app.flow.dashboards import DashboardStore
|
||||||
from app.models import TokenPayload, User
|
from app.models import TokenPayload, User
|
||||||
|
|
||||||
reusable_oauth2 = OAuth2PasswordBearer(
|
reusable_oauth2 = OAuth2PasswordBearer(
|
||||||
@@ -102,6 +103,16 @@ def get_flow_controller(request: Request) -> FlowController:
|
|||||||
FlowControllerDep = Annotated[FlowController, Depends(get_flow_controller)]
|
FlowControllerDep = Annotated[FlowController, Depends(get_flow_controller)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_dashboard_store(request: Request) -> DashboardStore:
|
||||||
|
store: DashboardStore | None = getattr(request.app.state, "dashboard_store", None)
|
||||||
|
if store is None:
|
||||||
|
raise HTTPException(status_code=503, detail="The flow engine is not running")
|
||||||
|
return store
|
||||||
|
|
||||||
|
|
||||||
|
DashboardStoreDep = Annotated[DashboardStore, Depends(get_dashboard_store)]
|
||||||
|
|
||||||
|
|
||||||
def get_current_active_superuser(current_user: CurrentUser) -> User:
|
def get_current_active_superuser(current_user: CurrentUser) -> User:
|
||||||
if not current_user.is_superuser:
|
if not current_user.is_superuser:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ from fastapi import APIRouter
|
|||||||
|
|
||||||
from app.api.routes import (
|
from app.api.routes import (
|
||||||
alerts,
|
alerts,
|
||||||
|
dashboards,
|
||||||
flows,
|
flows,
|
||||||
login,
|
login,
|
||||||
|
messages,
|
||||||
oauth,
|
oauth,
|
||||||
private,
|
private,
|
||||||
secrets,
|
secrets,
|
||||||
@@ -19,6 +21,8 @@ api_router.include_router(flows.router)
|
|||||||
api_router.include_router(flows.ws_router)
|
api_router.include_router(flows.ws_router)
|
||||||
api_router.include_router(secrets.router)
|
api_router.include_router(secrets.router)
|
||||||
api_router.include_router(alerts.router)
|
api_router.include_router(alerts.router)
|
||||||
|
api_router.include_router(dashboards.router)
|
||||||
|
api_router.include_router(messages.router)
|
||||||
# Always mounted so the generated SDK stays the same shape; the endpoints
|
# Always mounted so the generated SDK stays the same shape; the endpoints
|
||||||
# themselves refuse to work unless MCP is switched on.
|
# themselves refuse to work unless MCP is switched on.
|
||||||
api_router.include_router(oauth.router)
|
api_router.include_router(oauth.router)
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Dashboards: documents of widgets bound to message names."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.concurrency import run_in_threadpool
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.api.deps import DashboardStoreDep, FlowControllerDep, get_current_user
|
||||||
|
from app.flow.dashboards import (
|
||||||
|
DashboardDef,
|
||||||
|
DashboardExists,
|
||||||
|
DashboardNotFound,
|
||||||
|
DashboardsPublic,
|
||||||
|
default_dashboard,
|
||||||
|
)
|
||||||
|
from app.flow.store import StaleVersion
|
||||||
|
from app.models import Message
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/dashboards", tags=["dashboards"], dependencies=[Depends(get_current_user)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RenameRequest(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_history_limits(store: Any, controller: Any) -> None:
|
||||||
|
"""Tell the engine how much past each charted message needs kept."""
|
||||||
|
controller.set_history_limits(store.history_requirements())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=DashboardsPublic)
|
||||||
|
async def read_dashboards(store: DashboardStoreDep) -> Any:
|
||||||
|
"""Every dashboard, without its contents."""
|
||||||
|
summaries = await run_in_threadpool(store.list)
|
||||||
|
return DashboardsPublic(data=summaries, count=len(summaries))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{name}", response_model=DashboardDef)
|
||||||
|
async def read_dashboard(name: str, store: DashboardStoreDep) -> Any:
|
||||||
|
try:
|
||||||
|
return await run_in_threadpool(store.read, name)
|
||||||
|
except DashboardNotFound:
|
||||||
|
raise HTTPException(status_code=404, detail=f"No dashboard named '{name}'")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{name}", response_model=DashboardDef)
|
||||||
|
async def create_dashboard(
|
||||||
|
name: str, store: DashboardStoreDep, controller: FlowControllerDep
|
||||||
|
) -> Any:
|
||||||
|
"""Start a dashboard: one page, one section, nothing on it yet."""
|
||||||
|
if await run_in_threadpool(store.exists, name):
|
||||||
|
raise HTTPException(status_code=409, detail=f"'{name}' already exists")
|
||||||
|
try:
|
||||||
|
defn = default_dashboard(name)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=422, detail=str(exc))
|
||||||
|
saved = await run_in_threadpool(store.write, defn, 0)
|
||||||
|
await run_in_threadpool(_apply_history_limits, store, controller)
|
||||||
|
return saved
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{name}", response_model=DashboardDef)
|
||||||
|
async def save_dashboard(
|
||||||
|
name: str,
|
||||||
|
body: DashboardDef,
|
||||||
|
store: DashboardStoreDep,
|
||||||
|
controller: FlowControllerDep,
|
||||||
|
) -> Any:
|
||||||
|
"""Replace a dashboard, refusing a save someone else has moved past."""
|
||||||
|
if body.name != name:
|
||||||
|
raise HTTPException(status_code=422, detail="The name in the body must match")
|
||||||
|
try:
|
||||||
|
saved = await run_in_threadpool(store.write, body, body.version)
|
||||||
|
except StaleVersion as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail={
|
||||||
|
"message": "Someone else saved this dashboard first",
|
||||||
|
"current_version": exc.current,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await run_in_threadpool(_apply_history_limits, store, controller)
|
||||||
|
return saved
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{name}", response_model=Message)
|
||||||
|
async def delete_dashboard(
|
||||||
|
name: str, store: DashboardStoreDep, controller: FlowControllerDep
|
||||||
|
) -> Any:
|
||||||
|
try:
|
||||||
|
await run_in_threadpool(store.delete, name)
|
||||||
|
except DashboardNotFound:
|
||||||
|
raise HTTPException(status_code=404, detail=f"No dashboard named '{name}'")
|
||||||
|
await run_in_threadpool(_apply_history_limits, store, controller)
|
||||||
|
return Message(message=f"Deleted dashboard '{name}'")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{name}/rename", response_model=DashboardDef)
|
||||||
|
async def rename_dashboard(
|
||||||
|
name: str, body: RenameRequest, store: DashboardStoreDep
|
||||||
|
) -> Any:
|
||||||
|
try:
|
||||||
|
return await run_in_threadpool(store.rename, name, body.name)
|
||||||
|
except DashboardNotFound:
|
||||||
|
raise HTTPException(status_code=404, detail=f"No dashboard named '{name}'")
|
||||||
|
except DashboardExists:
|
||||||
|
raise HTTPException(status_code=409, detail=f"'{body.name}' already exists")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=422, detail=str(exc))
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Messages: what a dashboard binds to, across every flow.
|
||||||
|
|
||||||
|
The flow API is scoped to one flow, which is the wrong shape here — a wall
|
||||||
|
panel shows the heating alongside the solar. These endpoints are the whole
|
||||||
|
namespace at once: what exists, what it last was, and a way to put a value
|
||||||
|
into it without owning a node.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.concurrency import run_in_threadpool
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.api.deps import FlowControllerDep, get_current_user
|
||||||
|
from app.flow.messages import flow_of
|
||||||
|
from app.flow.state import as_number
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/messages", tags=["messages"], dependencies=[Depends(get_current_user)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageInfo(BaseModel):
|
||||||
|
"""One message, as something to bind a widget to."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
flow: str
|
||||||
|
dtype: str
|
||||||
|
#: Nodes publishing it. Empty means the flow declares it as an input.
|
||||||
|
providers: list[str] = []
|
||||||
|
#: Whether a dashboard may publish to it — that is, whether it is declared.
|
||||||
|
writable: bool = True
|
||||||
|
numeric: bool = False
|
||||||
|
value: Any = None
|
||||||
|
ts: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MessagesPublic(BaseModel):
|
||||||
|
data: list[MessageInfo]
|
||||||
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
class PublishRequest(BaseModel):
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
|
class MessageValue(BaseModel):
|
||||||
|
name: str
|
||||||
|
value: Any
|
||||||
|
ts: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MessagePoints(BaseModel):
|
||||||
|
message: str
|
||||||
|
numeric: bool
|
||||||
|
points: list[dict[str, float]]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=MessagesPublic)
|
||||||
|
async def read_messages(controller: FlowControllerDep) -> Any:
|
||||||
|
"""Every message any published flow declares, with its last value."""
|
||||||
|
infos = await run_in_threadpool(controller.message_catalog)
|
||||||
|
return MessagesPublic(data=infos, count=len(infos))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{name}", response_model=MessageValue)
|
||||||
|
async def publish_message(
|
||||||
|
name: str, body: PublishRequest, controller: FlowControllerDep
|
||||||
|
) -> Any:
|
||||||
|
"""Put a value into the graph, as a dashboard control does.
|
||||||
|
|
||||||
|
Only a message some flow declares can be published to: flows own the
|
||||||
|
namespace, and a dashboard is a client of it rather than a second author.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await run_in_threadpool(controller.publish_message, name, body.value)
|
||||||
|
except KeyError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No flow declares a message named '{name}'"
|
||||||
|
)
|
||||||
|
except TypeError as exc:
|
||||||
|
raise HTTPException(status_code=422, detail=str(exc))
|
||||||
|
|
||||||
|
values = controller.values(flow_of(name))
|
||||||
|
current = values.get(name, {})
|
||||||
|
return MessageValue(name=name, value=current.get("value"), ts=current.get("ts"))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{name}/history", response_model=MessagePoints)
|
||||||
|
def read_message_history(name: str, controller: FlowControllerDep) -> Any:
|
||||||
|
"""The series behind a chart. Numbers only — nothing else plots."""
|
||||||
|
if controller.pipeline is None:
|
||||||
|
return MessagePoints(message=name, numeric=False, points=[])
|
||||||
|
series = controller.state.history(name)
|
||||||
|
numeric = as_number(controller.state.get(name)) is not None
|
||||||
|
return MessagePoints(
|
||||||
|
message=name,
|
||||||
|
numeric=numeric or bool(series),
|
||||||
|
points=[{"ts": ts, "value": value} for ts, value in series],
|
||||||
|
)
|
||||||
@@ -25,7 +25,7 @@ from fastapi.concurrency import run_in_threadpool
|
|||||||
from app.flow.alerts import AlertManager
|
from app.flow.alerts import AlertManager
|
||||||
from app.flow.events import EventBus
|
from app.flow.events import EventBus
|
||||||
from app.flow.executor import ExecutionService
|
from app.flow.executor import ExecutionService
|
||||||
from app.flow.messages import MessageSpec, qualify
|
from app.flow.messages import MessageSpec, flow_of, qualify
|
||||||
from app.flow.nodes import (
|
from app.flow.nodes import (
|
||||||
ChangeNode,
|
ChangeNode,
|
||||||
DelayNode,
|
DelayNode,
|
||||||
@@ -282,6 +282,7 @@ class FlowController:
|
|||||||
self.issues: list[ValidationIssue] = []
|
self.issues: list[ValidationIssue] = []
|
||||||
self.disabled: set[str] = set()
|
self.disabled: set[str] = set()
|
||||||
self.supervisor = Supervisor(events)
|
self.supervisor = Supervisor(events)
|
||||||
|
self.history_limits: dict[str, int] = {}
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -339,6 +340,7 @@ class FlowController:
|
|||||||
work_queue=self.execution.queue if self.execution else None,
|
work_queue=self.execution.queue if self.execution else None,
|
||||||
node_pool=self.execution.node_pool if self.execution else None,
|
node_pool=self.execution.node_pool if self.execution else None,
|
||||||
)
|
)
|
||||||
|
self.pipeline.history_limits = self.history_limits
|
||||||
if self.execution is not None:
|
if self.execution is not None:
|
||||||
self.execution.bind(self.pipeline)
|
self.execution.bind(self.pipeline)
|
||||||
self.issues = _collect_issues(loaded, self.pipeline, flow_inputs)
|
self.issues = _collect_issues(loaded, self.pipeline, flow_inputs)
|
||||||
@@ -598,6 +600,80 @@ class FlowController:
|
|||||||
for item in self.execution.queue.unpark(flow):
|
for item in self.execution.queue.unpark(flow):
|
||||||
self.execution.queue.add(item)
|
self.execution.queue.add(item)
|
||||||
|
|
||||||
|
def set_history_limits(self, limits: dict[str, int]) -> None:
|
||||||
|
"""How deep to keep each charted message's series. Applies at once."""
|
||||||
|
self.history_limits = limits
|
||||||
|
if self.pipeline is not None:
|
||||||
|
self.pipeline.history_limits = limits
|
||||||
|
|
||||||
|
def message_catalog(self) -> list[Any]:
|
||||||
|
"""Every message the published flows declare, with its last value.
|
||||||
|
|
||||||
|
What a dashboard picks from, so it spans flows rather than sitting
|
||||||
|
inside one.
|
||||||
|
"""
|
||||||
|
from app.api.routes.messages import MessageInfo
|
||||||
|
|
||||||
|
specs: dict[str, MessageSpec] = {}
|
||||||
|
providers: dict[str, list[str]] = {}
|
||||||
|
for flow in self.store.read_all():
|
||||||
|
for node in flow.nodes:
|
||||||
|
for spec in _bound(node.provides):
|
||||||
|
name = qualify(flow.name, spec.name)
|
||||||
|
specs.setdefault(name, spec)
|
||||||
|
providers.setdefault(name, []).append(f"{flow.name}.{node.id}")
|
||||||
|
for spec in _bound(node.requires):
|
||||||
|
specs.setdefault(qualify(flow.name, spec.name), spec)
|
||||||
|
for declared in flow.inputs:
|
||||||
|
if declared.spec.name:
|
||||||
|
specs.setdefault(
|
||||||
|
qualify(flow.name, declared.spec.name), declared.spec
|
||||||
|
)
|
||||||
|
|
||||||
|
values = self.values()
|
||||||
|
infos = []
|
||||||
|
for name, spec in sorted(specs.items()):
|
||||||
|
current = values.get(name, {})
|
||||||
|
infos.append(
|
||||||
|
MessageInfo(
|
||||||
|
name=name,
|
||||||
|
flow=flow_of(name),
|
||||||
|
dtype=spec.dtype.value,
|
||||||
|
providers=sorted(providers.get(name, [])),
|
||||||
|
writable=True,
|
||||||
|
numeric=spec.dtype.value in ("float", "int"),
|
||||||
|
value=current.get("value"),
|
||||||
|
ts=current.get("ts"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return infos
|
||||||
|
|
||||||
|
def publish_message(self, name: str, value: Any) -> None:
|
||||||
|
"""Put a value into the graph from outside. Blocking.
|
||||||
|
|
||||||
|
Refuses a name no flow declares: the flows own the namespace, and a
|
||||||
|
message nothing reads or writes would just be a key nobody sees.
|
||||||
|
"""
|
||||||
|
if self.pipeline is None:
|
||||||
|
raise KeyError(name)
|
||||||
|
|
||||||
|
spec = None
|
||||||
|
for flow in self.store.read_all():
|
||||||
|
for node in flow.nodes:
|
||||||
|
for candidate in [*_bound(node.provides), *_bound(node.requires)]:
|
||||||
|
if qualify(flow.name, candidate.name) == name:
|
||||||
|
spec = candidate
|
||||||
|
break
|
||||||
|
for declared in flow.inputs:
|
||||||
|
if qualify(flow.name, declared.spec.name) == name:
|
||||||
|
spec = declared.spec
|
||||||
|
if spec is None:
|
||||||
|
raise KeyError(name)
|
||||||
|
|
||||||
|
# The same check a node's output gets; a dashboard is not looser.
|
||||||
|
spec.check(value)
|
||||||
|
self.pipeline.publish({name: value})
|
||||||
|
|
||||||
def queue_stats(self) -> dict[str, Any]:
|
def queue_stats(self) -> dict[str, Any]:
|
||||||
return self.execution.stats() if self.execution is not None else {}
|
return self.execution.stats() if self.execution is not None else {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
"""Dashboards: what a wall panel shows, and what its buttons do.
|
||||||
|
|
||||||
|
A dashboard is its own document, not a set of nodes placed in a flow. Widgets
|
||||||
|
bind to message names — the same names that wire the graph — so a dashboard
|
||||||
|
reads across flows without being part of any of them, and a flow stays the
|
||||||
|
logic it was.
|
||||||
|
|
||||||
|
Stored beside the flows in the same git repository, under a directory the flow
|
||||||
|
listing ignores. No draft/publish split: nothing executes a dashboard, so edit
|
||||||
|
mode is its own staging area and the version counter is enough to stop two
|
||||||
|
clients overwriting each other.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
from app.flow.schemas import _validate_name
|
||||||
|
from app.flow.store import FlowStore, StaleVersion
|
||||||
|
|
||||||
|
#: Sibling of the shared-node library, and likewise not a flow.
|
||||||
|
DASHBOARD_DIR = "_dashboards"
|
||||||
|
|
||||||
|
#: A chart cannot ask for an unbounded series; this is the ceiling.
|
||||||
|
HISTORY_CAP = 5000
|
||||||
|
|
||||||
|
WidgetType = Literal[
|
||||||
|
# Display
|
||||||
|
"stat",
|
||||||
|
"gauge",
|
||||||
|
"chart",
|
||||||
|
"markdown",
|
||||||
|
# Input
|
||||||
|
"button",
|
||||||
|
"switch",
|
||||||
|
"slider",
|
||||||
|
"input",
|
||||||
|
"dropdown",
|
||||||
|
]
|
||||||
|
|
||||||
|
INPUT_WIDGETS = {"button", "switch", "slider", "input", "dropdown"}
|
||||||
|
|
||||||
|
|
||||||
|
class Placement(BaseModel):
|
||||||
|
"""Where a widget sits in its section's grid, in grid units."""
|
||||||
|
|
||||||
|
x: int = 0
|
||||||
|
y: int = 0
|
||||||
|
w: int = 3
|
||||||
|
h: int = 2
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetDef(BaseModel):
|
||||||
|
"""One tile: what it shows or does, and where it sits.
|
||||||
|
|
||||||
|
``config`` is per type — a chart names its series, a button names the
|
||||||
|
message it publishes — and is validated against the type below rather than
|
||||||
|
by a schema per class, because the whole set is small and closed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
type: WidgetType
|
||||||
|
title: str = ""
|
||||||
|
#: Keyed by breakpoint (``lg``/``md``/``sm``); missing ones are derived by
|
||||||
|
#: the client from the widest one it has.
|
||||||
|
layout: dict[str, Placement] = Field(default_factory=dict)
|
||||||
|
config: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
@field_validator("id")
|
||||||
|
@classmethod
|
||||||
|
def _check_id(cls, value: str) -> str:
|
||||||
|
return _validate_name(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def messages(self) -> list[str]:
|
||||||
|
"""Every message name this widget reads."""
|
||||||
|
if self.type == "chart":
|
||||||
|
return [
|
||||||
|
str(series.get("message"))
|
||||||
|
for series in self.config.get("series") or []
|
||||||
|
if series.get("message")
|
||||||
|
]
|
||||||
|
name = self.config.get("message")
|
||||||
|
return [str(name)] if name else []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target(self) -> str:
|
||||||
|
"""The message this widget publishes, if it is an input."""
|
||||||
|
return str(self.config.get("target") or "")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def history_points(self) -> int:
|
||||||
|
"""How much past this widget needs kept for it."""
|
||||||
|
if self.type != "chart":
|
||||||
|
return 0
|
||||||
|
points = int((self.config.get("history") or {}).get("points") or 0)
|
||||||
|
return min(points, HISTORY_CAP)
|
||||||
|
|
||||||
|
|
||||||
|
class SectionDef(BaseModel):
|
||||||
|
"""A grid of widgets under a heading."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
title: str = ""
|
||||||
|
widgets: list[WidgetDef] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@field_validator("id")
|
||||||
|
@classmethod
|
||||||
|
def _check_id(cls, value: str) -> str:
|
||||||
|
return _validate_name(value)
|
||||||
|
|
||||||
|
|
||||||
|
class PageDef(BaseModel):
|
||||||
|
"""One tab of a dashboard."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
title: str = ""
|
||||||
|
#: A lucide icon name, or empty.
|
||||||
|
icon: str = ""
|
||||||
|
sections: list[SectionDef] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@field_validator("id")
|
||||||
|
@classmethod
|
||||||
|
def _check_id(cls, value: str) -> str:
|
||||||
|
return _validate_name(value)
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardDef(BaseModel):
|
||||||
|
"""A dashboard as stored, and as the API hands it over."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
title: str = ""
|
||||||
|
pages: list[PageDef] = Field(default_factory=list)
|
||||||
|
#: Bumped on every save; a save based on an older one is refused.
|
||||||
|
version: int = 1
|
||||||
|
|
||||||
|
@field_validator("name")
|
||||||
|
@classmethod
|
||||||
|
def _check_name(cls, value: str) -> str:
|
||||||
|
return _validate_name(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def widgets(self) -> list[WidgetDef]:
|
||||||
|
return [w for p in self.pages for s in p.sections for w in s.widgets]
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardSummary(BaseModel):
|
||||||
|
"""A dashboard in a list, without its contents."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
title: str = ""
|
||||||
|
page_count: int = 0
|
||||||
|
widget_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardsPublic(BaseModel):
|
||||||
|
data: list[DashboardSummary]
|
||||||
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardNotFound(KeyError):
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
super().__init__(name)
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardExists(ValueError):
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
super().__init__(name)
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardStore:
|
||||||
|
"""Dashboards in the flow store's repository, invisible to the flow listing.
|
||||||
|
|
||||||
|
Shares the flow store's write lock and commit, so a dashboard save and a
|
||||||
|
flow save cannot interleave into one confused commit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, flows: FlowStore) -> None:
|
||||||
|
self.flows = flows
|
||||||
|
self.root = flows.root / DASHBOARD_DIR
|
||||||
|
# Read-modify-write of the version counter, same as the flow store.
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def _file(self, name: str) -> Path:
|
||||||
|
return self.root / name / "dashboard.json"
|
||||||
|
|
||||||
|
def list(self) -> list[DashboardSummary]:
|
||||||
|
summaries = []
|
||||||
|
for path in sorted(self.root.glob("*/dashboard.json")):
|
||||||
|
try:
|
||||||
|
defn = DashboardDef.model_validate_json(path.read_text())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
summaries.append(
|
||||||
|
DashboardSummary(
|
||||||
|
name=defn.name,
|
||||||
|
title=defn.title,
|
||||||
|
page_count=len(defn.pages),
|
||||||
|
widget_count=len(defn.widgets),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return summaries
|
||||||
|
|
||||||
|
def exists(self, name: str) -> bool:
|
||||||
|
return self._file(name).exists()
|
||||||
|
|
||||||
|
def read(self, name: str) -> DashboardDef:
|
||||||
|
path = self._file(name)
|
||||||
|
if not path.exists():
|
||||||
|
raise DashboardNotFound(name)
|
||||||
|
return DashboardDef.model_validate_json(path.read_text())
|
||||||
|
|
||||||
|
def write(
|
||||||
|
self, defn: DashboardDef, base_version: int | None = None
|
||||||
|
) -> DashboardDef:
|
||||||
|
"""Save, refusing a write based on a version someone has moved past."""
|
||||||
|
with self._lock, self.flows._write_lock:
|
||||||
|
path = self._file(defn.name)
|
||||||
|
current = 0
|
||||||
|
if path.exists():
|
||||||
|
current = DashboardDef.model_validate_json(path.read_text()).version
|
||||||
|
if base_version is not None and base_version != current:
|
||||||
|
raise StaleVersion(defn.name, current)
|
||||||
|
|
||||||
|
saved = defn.model_copy(update={"version": current + 1})
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(saved.model_dump_json(indent=2))
|
||||||
|
self.flows._commit(f"Save dashboard '{defn.name}'")
|
||||||
|
return saved
|
||||||
|
|
||||||
|
def delete(self, name: str) -> None:
|
||||||
|
path = self._file(name)
|
||||||
|
if not path.exists():
|
||||||
|
raise DashboardNotFound(name)
|
||||||
|
with self.flows._write_lock:
|
||||||
|
path.unlink()
|
||||||
|
try:
|
||||||
|
path.parent.rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self.flows._commit(f"Delete dashboard '{name}'")
|
||||||
|
|
||||||
|
def rename(self, name: str, new_name: str) -> DashboardDef:
|
||||||
|
defn = self.read(name)
|
||||||
|
if self.exists(new_name):
|
||||||
|
raise DashboardExists(new_name)
|
||||||
|
with self.flows._write_lock:
|
||||||
|
renamed = defn.model_copy(update={"name": new_name})
|
||||||
|
target = self._file(new_name)
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_text(renamed.model_dump_json(indent=2))
|
||||||
|
self._file(name).unlink()
|
||||||
|
try:
|
||||||
|
self._file(name).parent.rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self.flows._commit(f"Rename dashboard '{name}' to '{new_name}'")
|
||||||
|
return renamed
|
||||||
|
|
||||||
|
def history_requirements(self) -> dict[str, int]:
|
||||||
|
"""How many points to keep per message, so charts have a past to draw.
|
||||||
|
|
||||||
|
The deepest chart bound to a message wins; a message no chart reads
|
||||||
|
keeps the default.
|
||||||
|
"""
|
||||||
|
limits: dict[str, int] = {}
|
||||||
|
for path in self.root.glob("*/dashboard.json"):
|
||||||
|
try:
|
||||||
|
defn = DashboardDef.model_validate_json(path.read_text())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
for widget in defn.widgets:
|
||||||
|
points = widget.history_points
|
||||||
|
if not points:
|
||||||
|
continue
|
||||||
|
for message in widget.messages:
|
||||||
|
limits[message] = max(limits.get(message, 0), points)
|
||||||
|
return limits
|
||||||
|
|
||||||
|
|
||||||
|
def default_dashboard(name: str) -> DashboardDef:
|
||||||
|
"""A new dashboard: one page, one section, nothing in it yet."""
|
||||||
|
return DashboardDef(
|
||||||
|
name=name,
|
||||||
|
title=name.replace("_", " ").capitalize(),
|
||||||
|
pages=[PageDef(id="main", title="Overview", sections=[SectionDef(id="main")])],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DASHBOARD_DIR",
|
||||||
|
"HISTORY_CAP",
|
||||||
|
"INPUT_WIDGETS",
|
||||||
|
"DashboardDef",
|
||||||
|
"DashboardExists",
|
||||||
|
"DashboardNotFound",
|
||||||
|
"DashboardStore",
|
||||||
|
"DashboardSummary",
|
||||||
|
"DashboardsPublic",
|
||||||
|
"PageDef",
|
||||||
|
"Placement",
|
||||||
|
"SectionDef",
|
||||||
|
"WidgetDef",
|
||||||
|
"default_dashboard",
|
||||||
|
]
|
||||||
@@ -64,6 +64,7 @@ class Pipeline:
|
|||||||
"_gate_lock",
|
"_gate_lock",
|
||||||
"_queue",
|
"_queue",
|
||||||
"_node_pool",
|
"_node_pool",
|
||||||
|
"history_limits",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -94,6 +95,9 @@ class Pipeline:
|
|||||||
self._queue = work_queue
|
self._queue = work_queue
|
||||||
# A pool owned by the execution service, so a wave does not build one.
|
# A pool owned by the execution service, so a wave does not build one.
|
||||||
self._node_pool = node_pool
|
self._node_pool = node_pool
|
||||||
|
# How deep to keep each message's series; a chart asking for more
|
||||||
|
# than the default puts its message in here. Swapped, never mutated.
|
||||||
|
self.history_limits: dict[str, int] = {}
|
||||||
|
|
||||||
# A message may have several producers; every one of them is upstream
|
# A message may have several producers; every one of them is upstream
|
||||||
# of the nodes consuming it.
|
# of the nodes consuming it.
|
||||||
@@ -489,7 +493,7 @@ class Pipeline:
|
|||||||
{self._timestamp_key(name): ts for name in result},
|
{self._timestamp_key(name): ts for name in result},
|
||||||
)
|
)
|
||||||
# Append-only, so it needs no lock of its own.
|
# Append-only, so it needs no lock of its own.
|
||||||
state.append_history(result, ts)
|
state.append_history(result, ts, self.history_limits)
|
||||||
self._increment_message_versions(result)
|
self._increment_message_versions(result)
|
||||||
for name, value in result.items():
|
for name, value in result.items():
|
||||||
self._publish(
|
self._publish(
|
||||||
@@ -679,7 +683,7 @@ class Pipeline:
|
|||||||
with state.lock():
|
with state.lock():
|
||||||
state.update(outputs)
|
state.update(outputs)
|
||||||
state.update({self._timestamp_key(name): ts for name in outputs})
|
state.update({self._timestamp_key(name): ts for name in outputs})
|
||||||
state.append_history(outputs, ts)
|
state.append_history(outputs, ts, self.history_limits)
|
||||||
self._increment_message_versions(outputs)
|
self._increment_message_versions(outputs)
|
||||||
for name, value in outputs.items():
|
for name, value in outputs.items():
|
||||||
self._publish(
|
self._publish(
|
||||||
@@ -756,6 +760,42 @@ class Pipeline:
|
|||||||
self.apply_outputs(node, outputs)
|
self.apply_outputs(node, outputs)
|
||||||
return self.run_downstream(node)
|
return self.run_downstream(node)
|
||||||
|
|
||||||
|
def publish(self, values: dict[str, Any]) -> None:
|
||||||
|
"""Put values into the graph without a node having produced them.
|
||||||
|
|
||||||
|
This is what a dashboard control does: the value is real, it just came
|
||||||
|
from a person rather than a sensor. Everything consuming those names
|
||||||
|
runs, the same as if a node had published them.
|
||||||
|
"""
|
||||||
|
if not values:
|
||||||
|
return
|
||||||
|
|
||||||
|
ts = time.time()
|
||||||
|
with self._state.lock():
|
||||||
|
self._state.update(values)
|
||||||
|
self._state.update({self._timestamp_key(name): ts for name in values})
|
||||||
|
self._state.append_history(values, ts, self.history_limits)
|
||||||
|
self._increment_message_versions(values)
|
||||||
|
for name, value in values.items():
|
||||||
|
self._publish(
|
||||||
|
{
|
||||||
|
"type": "message_value",
|
||||||
|
"flow": flow_of(name),
|
||||||
|
"name": name,
|
||||||
|
"value": value,
|
||||||
|
"ts": ts,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
targets: set[Node] = set()
|
||||||
|
for name in values:
|
||||||
|
for consumer in self._nodes:
|
||||||
|
if name in consumer.requires:
|
||||||
|
targets.add(consumer)
|
||||||
|
targets.update(self._get_downstream(consumer))
|
||||||
|
if targets:
|
||||||
|
self._execute_parallel(targets, self._state, check_ready=True)
|
||||||
|
|
||||||
def defer(
|
def defer(
|
||||||
self,
|
self,
|
||||||
node: Node,
|
node: Node,
|
||||||
|
|||||||
@@ -173,7 +173,9 @@ class StateBackend(ABC):
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def append_history(self, values: dict[str, Any], ts: float) -> None:
|
def append_history(
|
||||||
|
self, values: dict[str, Any], ts: float, limits: dict[str, int] | None = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Record freshly published values in each message's capped history.
|
Record freshly published values in each message's capped history.
|
||||||
|
|
||||||
@@ -182,6 +184,8 @@ class StateBackend(ABC):
|
|||||||
:param values: Message names mapped to the value just published.
|
:param values: Message names mapped to the value just published.
|
||||||
:type values: dict[str, Any]
|
:type values: dict[str, Any]
|
||||||
:param ts: When they were published.
|
:param ts: When they were published.
|
||||||
|
:param limits: How many points to keep per message, where a chart asks
|
||||||
|
for more than the default.
|
||||||
:type ts: float
|
:type ts: float
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
@@ -307,14 +311,21 @@ class MemoryState(StateBackend):
|
|||||||
self._data.update(updates)
|
self._data.update(updates)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def append_history(self, values: dict[str, Any], ts: float) -> None:
|
def append_history(
|
||||||
|
self, values: dict[str, Any], ts: float, limits: dict[str, int] | None = None
|
||||||
|
) -> None:
|
||||||
"""Append the numeric values to their message's bounded series."""
|
"""Append the numeric values to their message's bounded series."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for key, value in values.items():
|
for key, value in values.items():
|
||||||
number = as_number(value)
|
number = as_number(value)
|
||||||
if number is None:
|
if number is None:
|
||||||
continue
|
continue
|
||||||
series = self._history.setdefault(key, deque(maxlen=HISTORY_LIMIT))
|
cap = (limits or {}).get(key, HISTORY_LIMIT)
|
||||||
|
series = self._history.get(key)
|
||||||
|
if series is None or series.maxlen != cap:
|
||||||
|
# A widget asking for a deeper series re-caps it in place.
|
||||||
|
series = deque(series or (), maxlen=cap)
|
||||||
|
self._history[key] = series
|
||||||
series.append((ts, number))
|
series.append((ts, number))
|
||||||
|
|
||||||
def history(self, key: str) -> list[tuple[float, float]]:
|
def history(self, key: str) -> list[tuple[float, float]]:
|
||||||
@@ -563,7 +574,9 @@ class RedisState(StateBackend):
|
|||||||
def _history_key(self, key: str) -> str:
|
def _history_key(self, key: str) -> str:
|
||||||
return self._key(f"__history__:{key}")
|
return self._key(f"__history__:{key}")
|
||||||
|
|
||||||
def append_history(self, values: dict[str, Any], ts: float) -> None:
|
def append_history(
|
||||||
|
self, values: dict[str, Any], ts: float, limits: dict[str, int] | None = None
|
||||||
|
) -> None:
|
||||||
"""Push the numeric values onto their capped list, in one round-trip."""
|
"""Push the numeric values onto their capped list, in one round-trip."""
|
||||||
pipe = self._client.pipeline()
|
pipe = self._client.pipeline()
|
||||||
queued = False
|
queued = False
|
||||||
@@ -572,8 +585,9 @@ class RedisState(StateBackend):
|
|||||||
if number is None:
|
if number is None:
|
||||||
continue
|
continue
|
||||||
history_key = self._history_key(key)
|
history_key = self._history_key(key)
|
||||||
|
cap = (limits or {}).get(key, HISTORY_LIMIT)
|
||||||
pipe.lpush(history_key, json.dumps([ts, number]))
|
pipe.lpush(history_key, json.dumps([ts, number]))
|
||||||
pipe.ltrim(history_key, 0, HISTORY_LIMIT - 1)
|
pipe.ltrim(history_key, 0, cap - 1)
|
||||||
if self._ttl:
|
if self._ttl:
|
||||||
pipe.expire(history_key, self._ttl)
|
pipe.expire(history_key, self._ttl)
|
||||||
queued = True
|
queued = True
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from app.core.config import settings
|
|||||||
from app.flow import logs
|
from app.flow import logs
|
||||||
from app.flow.alerts import AlertManager
|
from app.flow.alerts import AlertManager
|
||||||
from app.flow.controller import FlowController
|
from app.flow.controller import FlowController
|
||||||
|
from app.flow.dashboards import DashboardStore
|
||||||
from app.flow.events import event_bus
|
from app.flow.events import event_bus
|
||||||
from app.flow.executor import ExecutionService
|
from app.flow.executor import ExecutionService
|
||||||
from app.flow.nodes.http import close_shared_client
|
from app.flow.nodes.http import close_shared_client
|
||||||
@@ -83,6 +84,11 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|||||||
alerts=alerts,
|
alerts=alerts,
|
||||||
)
|
)
|
||||||
app.state.flow_controller = controller
|
app.state.flow_controller = controller
|
||||||
|
dashboards = DashboardStore(controller.store)
|
||||||
|
app.state.dashboard_store = dashboards
|
||||||
|
# Charts need a deeper series than the default; tell the engine
|
||||||
|
# before it starts recording.
|
||||||
|
controller.set_history_limits(dashboards.history_requirements())
|
||||||
watchdog = LoopWatchdog(event_bus)
|
watchdog = LoopWatchdog(event_bus)
|
||||||
app.state.watchdog = watchdog
|
app.state.watchdog = watchdog
|
||||||
watchdog_task = asyncio.create_task(watchdog.run(), name="loop-watchdog")
|
watchdog_task = asyncio.create_task(watchdog.run(), name="loop-watchdog")
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"""Dashboards: documents beside the flows, and the values their widgets move."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.flow.dashboards import (
|
||||||
|
DashboardDef,
|
||||||
|
DashboardNotFound,
|
||||||
|
DashboardStore,
|
||||||
|
PageDef,
|
||||||
|
SectionDef,
|
||||||
|
WidgetDef,
|
||||||
|
default_dashboard,
|
||||||
|
)
|
||||||
|
from app.flow.messages import DType, MessageSpec
|
||||||
|
from app.flow.nodes import Node
|
||||||
|
from app.flow.pipeline import Pipeline
|
||||||
|
from app.flow.state import MemoryState
|
||||||
|
from app.flow.store import FlowStore, StaleVersion
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def store(tmp_path) -> DashboardStore:
|
||||||
|
return DashboardStore(FlowStore(tmp_path / "flows"))
|
||||||
|
|
||||||
|
|
||||||
|
def chart(message: str, points: int) -> WidgetDef:
|
||||||
|
return WidgetDef(
|
||||||
|
id="temp",
|
||||||
|
type="chart",
|
||||||
|
config={"series": [{"message": message}], "history": {"points": points}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_dashboard_survives_a_round_trip(store: DashboardStore):
|
||||||
|
saved = store.write(default_dashboard("house"))
|
||||||
|
|
||||||
|
read = store.read("house")
|
||||||
|
|
||||||
|
assert read.name == "house"
|
||||||
|
assert [p.id for p in read.pages] == ["main"]
|
||||||
|
assert read.version == saved.version
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboards_are_invisible_to_the_flow_listing(store: DashboardStore):
|
||||||
|
"""They share the repository; they are not flows."""
|
||||||
|
store.write(default_dashboard("house"))
|
||||||
|
|
||||||
|
assert store.flows.list_flows() == []
|
||||||
|
assert [d.name for d in store.list()] == ["house"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_save_based_on_a_version_someone_moved_past_is_refused(
|
||||||
|
store: DashboardStore,
|
||||||
|
):
|
||||||
|
first = store.write(default_dashboard("house"))
|
||||||
|
store.write(first, first.version)
|
||||||
|
|
||||||
|
with pytest.raises(StaleVersion):
|
||||||
|
store.write(first, first.version)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deleting_and_renaming(store: DashboardStore):
|
||||||
|
store.write(default_dashboard("house"))
|
||||||
|
|
||||||
|
renamed = store.rename("house", "home")
|
||||||
|
assert renamed.name == "home"
|
||||||
|
assert not store.exists("house")
|
||||||
|
|
||||||
|
store.delete("home")
|
||||||
|
with pytest.raises(DashboardNotFound):
|
||||||
|
store.read("home")
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_deepest_chart_decides_how_much_past_is_kept(store: DashboardStore):
|
||||||
|
store.write(
|
||||||
|
DashboardDef(
|
||||||
|
name="house",
|
||||||
|
pages=[
|
||||||
|
PageDef(
|
||||||
|
id="main",
|
||||||
|
sections=[
|
||||||
|
SectionDef(id="a", widgets=[chart("heating.temp", 400)]),
|
||||||
|
SectionDef(
|
||||||
|
id="b",
|
||||||
|
widgets=[
|
||||||
|
chart("heating.temp", 900),
|
||||||
|
chart("solar.watts", 100),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert store.history_requirements() == {"heating.temp": 900, "solar.watts": 100}
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_chart_cannot_ask_for_an_unbounded_series(store: DashboardStore):
|
||||||
|
store.write(
|
||||||
|
DashboardDef(
|
||||||
|
name="house",
|
||||||
|
pages=[
|
||||||
|
PageDef(
|
||||||
|
id="main",
|
||||||
|
sections=[
|
||||||
|
SectionDef(id="a", widgets=[chart("heating.temp", 10**9)])
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert store.history_requirements() == {"heating.temp": 5000}
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_is_kept_to_the_depth_a_chart_asked_for():
|
||||||
|
state = MemoryState()
|
||||||
|
limits = {"f.temp": 300}
|
||||||
|
|
||||||
|
for i in range(400):
|
||||||
|
state.append_history({"f.temp": float(i)}, float(i), limits)
|
||||||
|
|
||||||
|
assert len(state.history("f.temp")) == 300
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# What an input widget does
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_publishing_a_value_runs_what_consumes_it():
|
||||||
|
"""A slider is a value arriving; the graph should not care who sent it."""
|
||||||
|
seen: list[float] = []
|
||||||
|
|
||||||
|
def consume(setpoint, params):
|
||||||
|
seen.append(setpoint)
|
||||||
|
return {"applied": setpoint}
|
||||||
|
|
||||||
|
node = Node(
|
||||||
|
f=consume,
|
||||||
|
requires=[MessageSpec(name="setpoint", port="setpoint", dtype=DType.FLOAT)],
|
||||||
|
provides=[MessageSpec(name="applied", port="applied", dtype=DType.FLOAT)],
|
||||||
|
name="thermostat",
|
||||||
|
)
|
||||||
|
node.assign_flow("heating", "thermostat")
|
||||||
|
state = MemoryState()
|
||||||
|
pipeline = Pipeline(nodes=[node], state=state)
|
||||||
|
|
||||||
|
pipeline.publish({"heating.setpoint": 21.5})
|
||||||
|
|
||||||
|
assert seen == [21.5]
|
||||||
|
assert state["heating.applied"] == 21.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_publishing_nothing_does_nothing():
|
||||||
|
pipeline = Pipeline(nodes=[], state=MemoryState())
|
||||||
|
|
||||||
|
pipeline.publish({})
|
||||||
|
|
||||||
|
assert pipeline.values() == {}
|
||||||
@@ -200,6 +200,113 @@ Binary payloads (tensors, images) will arrive later as explicitly declared
|
|||||||
codec fields; until then everything on the wire is JSON.`
|
codec fields; until then everything on the wire is JSON.`
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const DashboardDef_InputSchema = {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
items: {
|
||||||
|
'$ref': '#/components/schemas/PageDef-Input'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Pages'
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Version',
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['name'],
|
||||||
|
title: 'DashboardDef',
|
||||||
|
description: 'A dashboard as stored, and as the API hands it over.'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DashboardDef_OutputSchema = {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
items: {
|
||||||
|
'$ref': '#/components/schemas/PageDef-Output'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Pages'
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Version',
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['name'],
|
||||||
|
title: 'DashboardDef',
|
||||||
|
description: 'A dashboard as stored, and as the API hands it over.'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DashboardSummarySchema = {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
page_count: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Page Count',
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
widget_count: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Widget Count',
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['name'],
|
||||||
|
title: 'DashboardSummary',
|
||||||
|
description: 'A dashboard in a list, without its contents.'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DashboardsPublicSchema = {
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
items: {
|
||||||
|
'$ref': '#/components/schemas/DashboardSummary'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Data'
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Count'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['data', 'count'],
|
||||||
|
title: 'DashboardsPublic'
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const FlowDef_InputSchema = {
|
export const FlowDef_InputSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
@@ -367,7 +474,7 @@ export const FlowStatePublicSchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
values: {
|
values: {
|
||||||
additionalProperties: {
|
additionalProperties: {
|
||||||
'$ref': '#/components/schemas/MessageValue'
|
'$ref': '#/components/schemas/app__flow__schemas__MessageValue'
|
||||||
},
|
},
|
||||||
type: 'object',
|
type: 'object',
|
||||||
title: 'Values'
|
title: 'Values'
|
||||||
@@ -541,6 +648,85 @@ Only numbers are recorded, so \`\`numeric\`\` tells the panel whether an empty
|
|||||||
series means "nothing plottable here" or "nothing has arrived yet".`
|
series means "nothing plottable here" or "nothing has arrived yet".`
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const MessageInfoSchema = {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name'
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Flow'
|
||||||
|
},
|
||||||
|
dtype: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Dtype'
|
||||||
|
},
|
||||||
|
providers: {
|
||||||
|
items: {
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Providers',
|
||||||
|
default: []
|
||||||
|
},
|
||||||
|
writable: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'Writable',
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
numeric: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'Numeric',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
title: 'Value'
|
||||||
|
},
|
||||||
|
ts: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Ts'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['name', 'flow', 'dtype'],
|
||||||
|
title: 'MessageInfo',
|
||||||
|
description: 'One message, as something to bind a widget to.'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const MessagePointsSchema = {
|
||||||
|
properties: {
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Message'
|
||||||
|
},
|
||||||
|
numeric: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'Numeric'
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
items: {
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
type: 'object'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Points'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['message', 'numeric', 'points'],
|
||||||
|
title: 'MessagePoints'
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const MessageSpecSchema = {
|
export const MessageSpecSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
@@ -588,26 +774,23 @@ export const MessageSpecSchema = {
|
|||||||
message it also produces without depending on itself.`
|
message it also produces without depending on itself.`
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const MessageValueSchema = {
|
export const MessagesPublicSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
value: {
|
data: {
|
||||||
title: 'Value'
|
items: {
|
||||||
|
'$ref': '#/components/schemas/MessageInfo'
|
||||||
},
|
},
|
||||||
ts: {
|
type: 'array',
|
||||||
anyOf: [
|
title: 'Data'
|
||||||
{
|
|
||||||
type: 'number'
|
|
||||||
},
|
},
|
||||||
{
|
count: {
|
||||||
type: 'null'
|
type: 'integer',
|
||||||
}
|
title: 'Count'
|
||||||
],
|
|
||||||
title: 'Ts'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
type: 'object',
|
type: 'object',
|
||||||
title: 'MessageValue',
|
required: ['data', 'count'],
|
||||||
description: 'The last payload seen on a message.'
|
title: 'MessagesPublic'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const NewPasswordSchema = {
|
export const NewPasswordSchema = {
|
||||||
@@ -1006,6 +1189,94 @@ export const OAuthClientRegisterSchema = {
|
|||||||
description: 'RFC 7591 dynamic client registration request.'
|
description: 'RFC 7591 dynamic client registration request.'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const PageDef_InputSchema = {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Id'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Icon',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
sections: {
|
||||||
|
items: {
|
||||||
|
'$ref': '#/components/schemas/SectionDef-Input'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Sections'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['id'],
|
||||||
|
title: 'PageDef',
|
||||||
|
description: 'One tab of a dashboard.'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PageDef_OutputSchema = {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Id'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Icon',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
sections: {
|
||||||
|
items: {
|
||||||
|
'$ref': '#/components/schemas/SectionDef-Output'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Sections'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['id'],
|
||||||
|
title: 'PageDef',
|
||||||
|
description: 'One tab of a dashboard.'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PlacementSchema = {
|
||||||
|
properties: {
|
||||||
|
x: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'X',
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Y',
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
w: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'W',
|
||||||
|
default: 3
|
||||||
|
},
|
||||||
|
h: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'H',
|
||||||
|
default: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
title: 'Placement',
|
||||||
|
description: "Where a widget sits in its section's grid, in grid units."
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const PositionSchema = {
|
export const PositionSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
x: {
|
x: {
|
||||||
@@ -1049,30 +1320,6 @@ export const PrivateUserCreateSchema = {
|
|||||||
title: 'PrivateUserCreate'
|
title: 'PrivateUserCreate'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PublishRequestSchema = {
|
|
||||||
properties: {
|
|
||||||
version: {
|
|
||||||
type: 'integer',
|
|
||||||
title: 'Version'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: 'object',
|
|
||||||
required: ['version'],
|
|
||||||
title: 'PublishRequest'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const RenameRequestSchema = {
|
|
||||||
properties: {
|
|
||||||
new_name: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'New Name'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
type: 'object',
|
|
||||||
required: ['new_name'],
|
|
||||||
title: 'RenameRequest'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const RuleSchema = {
|
export const RuleSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
events: {
|
events: {
|
||||||
@@ -1144,6 +1391,56 @@ export const SecretValueSchema = {
|
|||||||
title: 'SecretValue'
|
title: 'SecretValue'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const SectionDef_InputSchema = {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Id'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
widgets: {
|
||||||
|
items: {
|
||||||
|
'$ref': '#/components/schemas/WidgetDef'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Widgets'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['id'],
|
||||||
|
title: 'SectionDef',
|
||||||
|
description: 'A grid of widgets under a heading.'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const SectionDef_OutputSchema = {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Id'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
widgets: {
|
||||||
|
items: {
|
||||||
|
'$ref': '#/components/schemas/WidgetDef'
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
title: 'Widgets'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['id'],
|
||||||
|
title: 'SectionDef',
|
||||||
|
description: 'A grid of widgets under a heading.'
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const ShareRequestSchema = {
|
export const ShareRequestSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
lib_name: {
|
lib_name: {
|
||||||
@@ -1547,3 +1844,137 @@ export const ValidationResultSchema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
title: 'ValidationResult'
|
title: 'ValidationResult'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const WidgetDefSchema = {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Id'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['stat', 'gauge', 'chart', 'markdown', 'button', 'switch', 'slider', 'input', 'dropdown'],
|
||||||
|
title: 'Type'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Title',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
additionalProperties: {
|
||||||
|
'$ref': '#/components/schemas/Placement'
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
title: 'Layout'
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
additionalProperties: true,
|
||||||
|
type: 'object',
|
||||||
|
title: 'Config'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['id', 'type'],
|
||||||
|
title: 'WidgetDef',
|
||||||
|
description: `One tile: what it shows or does, and where it sits.
|
||||||
|
|
||||||
|
\`\`config\`\` is per type — a chart names its series, a button names the
|
||||||
|
message it publishes — and is validated against the type below rather than
|
||||||
|
by a schema per class, because the whole set is small and closed.`
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const app__api__routes__dashboards__RenameRequestSchema = {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['name'],
|
||||||
|
title: 'RenameRequest'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const app__api__routes__flows__PublishRequestSchema = {
|
||||||
|
properties: {
|
||||||
|
version: {
|
||||||
|
type: 'integer',
|
||||||
|
title: 'Version'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['version'],
|
||||||
|
title: 'PublishRequest'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const app__api__routes__flows__RenameRequestSchema = {
|
||||||
|
properties: {
|
||||||
|
new_name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'New Name'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['new_name'],
|
||||||
|
title: 'RenameRequest'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const app__api__routes__messages__MessageValueSchema = {
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
title: 'Value'
|
||||||
|
},
|
||||||
|
ts: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Ts'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['name', 'value'],
|
||||||
|
title: 'MessageValue'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const app__api__routes__messages__PublishRequestSchema = {
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
|
title: 'Value'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['value'],
|
||||||
|
title: 'PublishRequest'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const app__flow__schemas__MessageValueSchema = {
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
|
title: 'Value'
|
||||||
|
},
|
||||||
|
ts: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'null'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: 'Ts'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
title: 'MessageValue',
|
||||||
|
description: 'The last payload seen on a message.'
|
||||||
|
} as const;
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { CancelablePromise } from './core/CancelablePromise';
|
import type { CancelablePromise } from './core/CancelablePromise';
|
||||||
import { OpenAPI } from './core/OpenAPI';
|
import { OpenAPI } from './core/OpenAPI';
|
||||||
import { request as __request } from './core/request';
|
import { request as __request } from './core/request';
|
||||||
import type { AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsTestChannelData, AlertsTestChannelResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, PrivateCreateUserData, PrivateCreateUserResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse } from './types.gen';
|
import type { AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsTestChannelData, AlertsTestChannelResponse, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, PrivateCreateUserData, PrivateCreateUserResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse } from './types.gen';
|
||||||
|
|
||||||
export class AlertsService {
|
export class AlertsService {
|
||||||
/**
|
/**
|
||||||
@@ -61,6 +61,129 @@ export class AlertsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DashboardsService {
|
||||||
|
/**
|
||||||
|
* Read Dashboards
|
||||||
|
* Every dashboard, without its contents.
|
||||||
|
* @returns DashboardsPublic Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static readDashboards(): CancelablePromise<DashboardsReadDashboardsResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/dashboards/'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read Dashboard
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.name
|
||||||
|
* @returns DashboardDef_Output Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static readDashboard(data: DashboardsReadDashboardData): CancelablePromise<DashboardsReadDashboardResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/dashboards/{name}',
|
||||||
|
path: {
|
||||||
|
name: data.name
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Dashboard
|
||||||
|
* Start a dashboard: one page, one section, nothing on it yet.
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.name
|
||||||
|
* @returns DashboardDef_Output Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static createDashboard(data: DashboardsCreateDashboardData): CancelablePromise<DashboardsCreateDashboardResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/dashboards/{name}',
|
||||||
|
path: {
|
||||||
|
name: data.name
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save Dashboard
|
||||||
|
* Replace a dashboard, refusing a save someone else has moved past.
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.name
|
||||||
|
* @param data.requestBody
|
||||||
|
* @returns DashboardDef_Output Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static saveDashboard(data: DashboardsSaveDashboardData): CancelablePromise<DashboardsSaveDashboardResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/api/v1/dashboards/{name}',
|
||||||
|
path: {
|
||||||
|
name: data.name
|
||||||
|
},
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: 'application/json',
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete Dashboard
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.name
|
||||||
|
* @returns Message Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static deleteDashboard(data: DashboardsDeleteDashboardData): CancelablePromise<DashboardsDeleteDashboardResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '/api/v1/dashboards/{name}',
|
||||||
|
path: {
|
||||||
|
name: data.name
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename Dashboard
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.name
|
||||||
|
* @param data.requestBody
|
||||||
|
* @returns DashboardDef_Output Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static renameDashboard(data: DashboardsRenameDashboardData): CancelablePromise<DashboardsRenameDashboardResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/dashboards/{name}/rename',
|
||||||
|
path: {
|
||||||
|
name: data.name
|
||||||
|
},
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: 'application/json',
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FlowsService {
|
export class FlowsService {
|
||||||
/**
|
/**
|
||||||
* Read Flows
|
* Read Flows
|
||||||
@@ -671,6 +794,69 @@ export class LoginService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MessagesService {
|
||||||
|
/**
|
||||||
|
* Read Messages
|
||||||
|
* Every message any published flow declares, with its last value.
|
||||||
|
* @returns MessagesPublic Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static readMessages(): CancelablePromise<MessagesReadMessagesResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/messages/'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish Message
|
||||||
|
* Put a value into the graph, as a dashboard control does.
|
||||||
|
*
|
||||||
|
* Only a message some flow declares can be published to: flows own the
|
||||||
|
* namespace, and a dashboard is a client of it rather than a second author.
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.name
|
||||||
|
* @param data.requestBody
|
||||||
|
* @returns app__api__routes__messages__MessageValue Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static publishMessage(data: MessagesPublishMessageData): CancelablePromise<MessagesPublishMessageResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/messages/{name}',
|
||||||
|
path: {
|
||||||
|
name: data.name
|
||||||
|
},
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: 'application/json',
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read Message History
|
||||||
|
* The series behind a chart. Numbers only — nothing else plots.
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.name
|
||||||
|
* @returns MessagePoints Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static readMessageHistory(data: MessagesReadMessageHistoryData): CancelablePromise<MessagesReadMessageHistoryResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/messages/{name}/history',
|
||||||
|
path: {
|
||||||
|
name: data.name
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class OauthService {
|
export class OauthService {
|
||||||
/**
|
/**
|
||||||
* Register Client
|
* Register Client
|
||||||
|
|||||||
@@ -9,6 +9,36 @@ export type AlertsConfig = {
|
|||||||
rules?: Array<Rule>;
|
rules?: Array<Rule>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type app__api__routes__dashboards__RenameRequest = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type app__api__routes__flows__PublishRequest = {
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type app__api__routes__flows__RenameRequest = {
|
||||||
|
new_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type app__api__routes__messages__MessageValue = {
|
||||||
|
name: string;
|
||||||
|
value: unknown;
|
||||||
|
ts?: (number | null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type app__api__routes__messages__PublishRequest = {
|
||||||
|
value: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last payload seen on a message.
|
||||||
|
*/
|
||||||
|
export type app__flow__schemas__MessageValue = {
|
||||||
|
value?: unknown;
|
||||||
|
ts?: (number | null);
|
||||||
|
};
|
||||||
|
|
||||||
export type Body_login_login_access_token = {
|
export type Body_login_login_access_token = {
|
||||||
grant_type?: (string | null);
|
grant_type?: (string | null);
|
||||||
username: string;
|
username: string;
|
||||||
@@ -42,6 +72,41 @@ export type Channel = {
|
|||||||
|
|
||||||
export type kind = 'ntfy' | 'smtp' | 'webhook';
|
export type kind = 'ntfy' | 'smtp' | 'webhook';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dashboard as stored, and as the API hands it over.
|
||||||
|
*/
|
||||||
|
export type DashboardDef_Input = {
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
pages?: Array<PageDef_Input>;
|
||||||
|
version?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dashboard as stored, and as the API hands it over.
|
||||||
|
*/
|
||||||
|
export type DashboardDef_Output = {
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
pages?: Array<PageDef_Output>;
|
||||||
|
version?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardsPublic = {
|
||||||
|
data: Array<DashboardSummary>;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dashboard in a list, without its contents.
|
||||||
|
*/
|
||||||
|
export type DashboardSummary = {
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
page_count?: number;
|
||||||
|
widget_count?: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializable payload types.
|
* Serializable payload types.
|
||||||
*
|
*
|
||||||
@@ -111,7 +176,7 @@ export type FlowsPublic = {
|
|||||||
|
|
||||||
export type FlowStatePublic = {
|
export type FlowStatePublic = {
|
||||||
values?: {
|
values?: {
|
||||||
[key: string]: MessageValue;
|
[key: string]: app__flow__schemas__MessageValue;
|
||||||
};
|
};
|
||||||
nodes?: Array<NodeStatusPublic>;
|
nodes?: Array<NodeStatusPublic>;
|
||||||
};
|
};
|
||||||
@@ -163,6 +228,28 @@ export type MessageHistory = {
|
|||||||
points?: Array<HistoryPoint>;
|
points?: Array<HistoryPoint>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One message, as something to bind a widget to.
|
||||||
|
*/
|
||||||
|
export type MessageInfo = {
|
||||||
|
name: string;
|
||||||
|
flow: string;
|
||||||
|
dtype: string;
|
||||||
|
providers?: Array<(string)>;
|
||||||
|
writable?: boolean;
|
||||||
|
numeric?: boolean;
|
||||||
|
value?: unknown;
|
||||||
|
ts?: (number | null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessagePoints = {
|
||||||
|
message: string;
|
||||||
|
numeric: boolean;
|
||||||
|
points: Array<{
|
||||||
|
[key: string]: (number);
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single port of a node, and the message it is bound to.
|
* A single port of a node, and the message it is bound to.
|
||||||
*
|
*
|
||||||
@@ -188,12 +275,9 @@ export type MessageSpec = {
|
|||||||
trigger?: boolean;
|
trigger?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export type MessagesPublic = {
|
||||||
* The last payload seen on a message.
|
data: Array<MessageInfo>;
|
||||||
*/
|
count: number;
|
||||||
export type MessageValue = {
|
|
||||||
value?: unknown;
|
|
||||||
ts?: (number | null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NewPassword = {
|
export type NewPassword = {
|
||||||
@@ -302,6 +386,36 @@ export type OAuthClientRegister = {
|
|||||||
token_endpoint_auth_method?: (string | null);
|
token_endpoint_auth_method?: (string | null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One tab of a dashboard.
|
||||||
|
*/
|
||||||
|
export type PageDef_Input = {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
icon?: string;
|
||||||
|
sections?: Array<SectionDef_Input>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One tab of a dashboard.
|
||||||
|
*/
|
||||||
|
export type PageDef_Output = {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
icon?: string;
|
||||||
|
sections?: Array<SectionDef_Output>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a widget sits in its section's grid, in grid units.
|
||||||
|
*/
|
||||||
|
export type Placement = {
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
w?: number;
|
||||||
|
h?: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where a node sits on the canvas.
|
* Where a node sits on the canvas.
|
||||||
*/
|
*/
|
||||||
@@ -317,14 +431,6 @@ export type PrivateUserCreate = {
|
|||||||
is_verified?: boolean;
|
is_verified?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PublishRequest = {
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RenameRequest = {
|
|
||||||
new_name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which events go to which channels.
|
* Which events go to which channels.
|
||||||
*/
|
*/
|
||||||
@@ -349,6 +455,24 @@ export type SecretValue = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A grid of widgets under a heading.
|
||||||
|
*/
|
||||||
|
export type SectionDef_Input = {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
widgets?: Array<WidgetDef>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A grid of widgets under a heading.
|
||||||
|
*/
|
||||||
|
export type SectionDef_Output = {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
widgets?: Array<WidgetDef>;
|
||||||
|
};
|
||||||
|
|
||||||
export type ShareRequest = {
|
export type ShareRequest = {
|
||||||
lib_name: string;
|
lib_name: string;
|
||||||
};
|
};
|
||||||
@@ -435,6 +559,27 @@ export type ValidationResult = {
|
|||||||
issues?: Array<ValidationIssue>;
|
issues?: Array<ValidationIssue>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One tile: what it shows or does, and where it sits.
|
||||||
|
*
|
||||||
|
* ``config`` is per type — a chart names its series, a button names the
|
||||||
|
* message it publishes — and is validated against the type below rather than
|
||||||
|
* by a schema per class, because the whole set is small and closed.
|
||||||
|
*/
|
||||||
|
export type WidgetDef = {
|
||||||
|
id: string;
|
||||||
|
type: 'stat' | 'gauge' | 'chart' | 'markdown' | 'button' | 'switch' | 'slider' | 'input' | 'dropdown';
|
||||||
|
title?: string;
|
||||||
|
layout?: {
|
||||||
|
[key: string]: Placement;
|
||||||
|
};
|
||||||
|
config?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type type = 'stat' | 'gauge' | 'chart' | 'markdown' | 'button' | 'switch' | 'slider' | 'input' | 'dropdown';
|
||||||
|
|
||||||
export type AlertsReadAlertsConfigResponse = (AlertsConfig);
|
export type AlertsReadAlertsConfigResponse = (AlertsConfig);
|
||||||
|
|
||||||
export type AlertsSaveAlertsConfigData = {
|
export type AlertsSaveAlertsConfigData = {
|
||||||
@@ -449,6 +594,40 @@ export type AlertsTestChannelData = {
|
|||||||
|
|
||||||
export type AlertsTestChannelResponse = (Message);
|
export type AlertsTestChannelResponse = (Message);
|
||||||
|
|
||||||
|
export type DashboardsReadDashboardsResponse = (DashboardsPublic);
|
||||||
|
|
||||||
|
export type DashboardsReadDashboardData = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardsReadDashboardResponse = (DashboardDef_Output);
|
||||||
|
|
||||||
|
export type DashboardsCreateDashboardData = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardsCreateDashboardResponse = (DashboardDef_Output);
|
||||||
|
|
||||||
|
export type DashboardsSaveDashboardData = {
|
||||||
|
name: string;
|
||||||
|
requestBody: DashboardDef_Input;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardsSaveDashboardResponse = (DashboardDef_Output);
|
||||||
|
|
||||||
|
export type DashboardsDeleteDashboardData = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardsDeleteDashboardResponse = (Message);
|
||||||
|
|
||||||
|
export type DashboardsRenameDashboardData = {
|
||||||
|
name: string;
|
||||||
|
requestBody: app__api__routes__dashboards__RenameRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardsRenameDashboardResponse = (DashboardDef_Output);
|
||||||
|
|
||||||
export type FlowsReadFlowsResponse = (FlowsPublic);
|
export type FlowsReadFlowsResponse = (FlowsPublic);
|
||||||
|
|
||||||
export type FlowsReadNodeTypesResponse = (Array<NodeTypeInfo>);
|
export type FlowsReadNodeTypesResponse = (Array<NodeTypeInfo>);
|
||||||
@@ -482,7 +661,7 @@ export type FlowsDeleteFlowResponse = (Message);
|
|||||||
|
|
||||||
export type FlowsPublishFlowData = {
|
export type FlowsPublishFlowData = {
|
||||||
name: string;
|
name: string;
|
||||||
requestBody: PublishRequest;
|
requestBody: app__api__routes__flows__PublishRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FlowsPublishFlowResponse = (FlowDetail);
|
export type FlowsPublishFlowResponse = (FlowDetail);
|
||||||
@@ -495,7 +674,7 @@ export type FlowsDiscardDraftResponse = (FlowDetail);
|
|||||||
|
|
||||||
export type FlowsRenameFlowData = {
|
export type FlowsRenameFlowData = {
|
||||||
name: string;
|
name: string;
|
||||||
requestBody: RenameRequest;
|
requestBody: app__api__routes__flows__RenameRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FlowsRenameFlowResponse = (FlowDetail);
|
export type FlowsRenameFlowResponse = (FlowDetail);
|
||||||
@@ -614,6 +793,21 @@ export type LoginRecoverPasswordHtmlContentData = {
|
|||||||
|
|
||||||
export type LoginRecoverPasswordHtmlContentResponse = (string);
|
export type LoginRecoverPasswordHtmlContentResponse = (string);
|
||||||
|
|
||||||
|
export type MessagesReadMessagesResponse = (MessagesPublic);
|
||||||
|
|
||||||
|
export type MessagesPublishMessageData = {
|
||||||
|
name: string;
|
||||||
|
requestBody: app__api__routes__messages__PublishRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessagesPublishMessageResponse = (app__api__routes__messages__MessageValue);
|
||||||
|
|
||||||
|
export type MessagesReadMessageHistoryData = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessagesReadMessageHistoryResponse = (MessagePoints);
|
||||||
|
|
||||||
export type OauthRegisterClientData = {
|
export type OauthRegisterClientData = {
|
||||||
requestBody: OAuthClientRegister;
|
requestBody: OAuthClientRegister;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user