131 lines
4.1 KiB
TypeScript
131 lines
4.1 KiB
TypeScript
import { useQueries } from "@tanstack/react-query"
|
|
import type { ReactNode } from "react"
|
|
|
|
import type { RunDetail } from "@/client"
|
|
import type { LiveValue } from "@/components/Flow/liveStore"
|
|
import {
|
|
compareQueryOptions,
|
|
runQueryOptions,
|
|
shortId,
|
|
} from "@/components/Runs/queries"
|
|
import { type Dashboard, widgetsOf } from "./DashboardView"
|
|
import {
|
|
type ContextLine,
|
|
type DataContext,
|
|
DataContextProvider,
|
|
} from "./dataContext"
|
|
|
|
/**
|
|
* The message names this dashboard would want a curve for.
|
|
*
|
|
* Only charts: a stat shows one number and reads the run's result instead, so
|
|
* asking the server for its whole series would be a query nobody draws.
|
|
*/
|
|
function chartedNames(dashboard: Dashboard | undefined): string[] {
|
|
if (!dashboard) return []
|
|
const names = new Set<string>()
|
|
for (const widget of widgetsOf(dashboard)) {
|
|
if (widget.type !== "chart") continue
|
|
const config = (widget.config ?? {}) as {
|
|
series?: { message?: string }[]
|
|
runs?: { metric?: string }
|
|
}
|
|
for (const entry of config.series ?? [])
|
|
if (entry.message) names.add(entry.message)
|
|
if (config.runs?.metric) names.add(config.runs.metric)
|
|
}
|
|
return [...names]
|
|
}
|
|
|
|
/** A run's own label in a legend, beside four others. */
|
|
const runLabel = (run: RunDetail) =>
|
|
run.seed === null || run.seed === undefined
|
|
? shortId(run.id)
|
|
: `${shortId(run.id)} (seed ${run.seed})`
|
|
|
|
/**
|
|
* A dashboard drawn against runs rather than against the engine.
|
|
*
|
|
* The values a run finished with are its `result`, which the API reports with
|
|
* the flow prefix stripped — a run of `study` reports `accuracy`, while the
|
|
* dashboard binds `study.accuracy` — so they are qualified again here. The
|
|
* curves come from the comparison endpoint, which already answers in the
|
|
* shape a chart draws.
|
|
*
|
|
* Where several runs are shown, a chart draws all of them and anything with
|
|
* room for one number shows the first, which is the one the bar names.
|
|
*/
|
|
export function RunsContextProvider({
|
|
runIds,
|
|
dashboard,
|
|
children,
|
|
}: {
|
|
runIds: string[]
|
|
dashboard: Dashboard | undefined
|
|
children: ReactNode
|
|
}) {
|
|
const runs = useQueries({
|
|
queries: runIds.map((id) => runQueryOptions(id)),
|
|
})
|
|
const names = chartedNames(dashboard)
|
|
const series = useQueries({
|
|
queries: names.map((name) => compareQueryOptions(runIds, name)),
|
|
})
|
|
|
|
const loaded = runs
|
|
.map((query) => query.data)
|
|
.filter((run): run is RunDetail => Boolean(run))
|
|
const primary = loaded[0]
|
|
|
|
const finals = new Map<string, LiveValue>()
|
|
if (primary) {
|
|
const at = primary.finished_at ? Date.parse(String(primary.finished_at)) : 0
|
|
for (const [key, value] of Object.entries(primary.result ?? {})) {
|
|
finals.set(key.includes(".") ? key : `${primary.flow}.${key}`, {
|
|
value,
|
|
ts: at ? at / 1000 : null,
|
|
})
|
|
}
|
|
}
|
|
|
|
const curves = new Map<string, ContextLine[]>()
|
|
names.forEach((name, index) => {
|
|
const answer = series[index]?.data
|
|
if (!answer) return
|
|
curves.set(
|
|
name,
|
|
(answer.lines ?? []).map((line, position) => ({
|
|
// The endpoint labels by run id; the run rows carry the nicer name.
|
|
label: loaded[position] ? runLabel(loaded[position]) : line.label,
|
|
points: (line.points ?? []).map(([step, value]) => ({
|
|
ts: step,
|
|
value,
|
|
})),
|
|
})),
|
|
)
|
|
})
|
|
|
|
const flow = primary?.flow ?? ""
|
|
const context: DataContext = {
|
|
kind: "runs",
|
|
label:
|
|
runIds.length === 1
|
|
? `run ${shortId(runIds[0])}${flow ? ` of ${flow}` : ""}`
|
|
: `${runIds.length} runs${flow ? ` of ${flow}` : ""}`,
|
|
readOnly: true,
|
|
// A run's metrics are indexed by step, not by the clock.
|
|
xTime: false,
|
|
pending:
|
|
runs.some((query) => query.isPending) ||
|
|
series.some((query) => query.isPending),
|
|
value: (name) => finals.get(name),
|
|
lines: (name) => curves.get(name),
|
|
}
|
|
|
|
return <DataContextProvider value={context}>{children}</DataContextProvider>
|
|
}
|
|
|
|
/** The runs a dashboard was opened against, from `?runs=a,b,c`. */
|
|
export const runIdsOf = (runs: string | undefined) =>
|
|
runs ? runs.split(",").filter(Boolean) : []
|