Files
app/frontend/src/components/Dashboard/dataContext.tsx
T

87 lines
3.3 KiB
TypeScript

import { createContext, useContext } from "react"
import type { HistoryPoint } from "@/client"
import {
type LiveValue,
useLiveValue,
useLiveValues,
} from "@/components/Flow/liveStore"
/** One member's curve for a bound message: a run, later a replayed day. */
export type ContextLine = {
label: string
/** `ts` carries whatever the x axis is — a step for a run, a moment for a
* replay. Which of the two it is, is `xTime` on the context. */
points: HistoryPoint[]
}
/**
* Where a dashboard's widgets read from, when it is not the live engine.
*
* A dashboard binds its widgets to message names. Normally those resolve
* against what the engine is holding right now, which is what makes a wall
* panel a wall panel. A data context puts something else behind the same
* names, so the page someone already built to watch a training run live is
* also the page that shows a finished one — no second dashboard, no widget
* that knows what a run is.
*
* `kind` is for the bar at the top to name what is being shown. Widgets must
* not switch on it: the point of this seam is that "three runs" and "last
* Tuesday, replayed" are the same shape, and Labs is the second one.
*
* Deliberately plain data and two lookups rather than hooks, so a provider is
* free to re-render with a fresh object whenever its data moves — a run
* finishing, a replay ticking — without every consumer subscribing to
* something of its own.
*
* What a consumer may not assume: that x is a time (`xTime`), that there is
* exactly one member (`lines` returns as many as there are), or that the data
* is settled (`pending`).
*/
export type DataContext = {
kind: string
/** What is being shown, in words: "3 runs of demo_training". */
label: string
/** Nothing here can be published to; the input widgets go quiet. */
readOnly: boolean
/** Whether the x values are moments. False for a run, whose x is a step. */
xTime: boolean
pending: boolean
/** The single value a stat or a gauge should show for this name. */
value: (name: string) => LiveValue | undefined
/** One line per member for this name, or undefined if it carries none. */
lines: (name: string) => ContextLine[] | undefined
}
const DataCtx = createContext<DataContext | null>(null)
export const DataContextProvider = DataCtx.Provider
/** The context a widget is being drawn in, or null on a live dashboard. */
export const useDataContext = () => useContext(DataCtx)
/**
* What a widget bound to this name should show.
*
* The one reading every value widget goes through, so putting a dashboard in
* a context is a change in one place rather than in each tile. Both hooks are
* called either way — React counts them — and which answer is used is decided
* afterwards.
*
* Not `useLiveValue` itself: a dashboard's own settings read live values too,
* and opening a page against a run must not change what look or theme it is
* drawn in.
*/
export function useBoundValue(name: string | undefined) {
const context = useDataContext()
const live = useLiveValue(name)
return context && name ? context.value(name) : live
}
/** The same, for a widget that binds several names at once. */
export function useBoundValues(names: string[]) {
const context = useDataContext()
const live = useLiveValues(names)
return context ? names.map((name) => context.value(name)) : live
}