Let a dashboard be read against runs instead of the live engine
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user