87 lines
3.3 KiB
TypeScript
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
|
|
}
|