Files
app/frontend/src/components/Dashboard/queries.ts
T
stroblme 43498037be PUT /panels/ carries a version and refuses a stale one with 409
The Panels dialog re-reads the list on 409. A panels.json without a
version reads as 0.
2026-10-15 10:07:50 +02:00

190 lines
5.9 KiB
TypeScript

import { useMutation, useQueryClient } from "@tanstack/react-query"
import {
ApiError,
type DashboardDef_Input,
DashboardsService,
MessagesService,
type PanelsConfig,
PanelsService,
} from "@/client"
export const dashboardKeys = {
all: ["dashboards"] as const,
/** The working copy and the published document are two different reads. */
detail: (name: string, draft = false) =>
["dashboards", name, draft ? "draft" : "published"] as const,
messages: ["messages"] as const,
history: (message: string) => ["messages", message, "history"] as const,
}
export const dashboardsQueryOptions = () => ({
queryKey: dashboardKeys.all,
queryFn: () => DashboardsService.readDashboards(),
})
/**
* A dashboard as a panel shows it, or with `draft` the copy being edited.
*
* A wall panel asks for the published one, which is the whole point of the
* split: nothing half-arranged reaches the wall until someone publishes.
*/
export const dashboardQueryOptions = (name: string, draft = false) => ({
queryKey: dashboardKeys.detail(name, draft),
queryFn: () => DashboardsService.readDashboard({ name, draft }),
})
export const panelKeys = {
all: ["panels"] as const,
detail: (id: string) => ["panels", id] as const,
}
/** Every panel and what it shows. Read by the editor to know if a rail is due. */
export const panelsQueryOptions = () => ({
queryKey: panelKeys.all,
queryFn: () => PanelsService.readPanels(),
})
/**
* One panel, as the device hanging on the wall reads it.
*
* A panel credential may read exactly this and the dashboards it names, so
* this is the query the panel route is built on rather than the list above.
*/
export const panelQueryOptions = (id: string) => ({
queryKey: panelKeys.detail(id),
queryFn: () => PanelsService.readPanel({ panelId: id }),
})
/**
* Replace the panels, carrying the version they were read at. Superuser-only
* on the server.
*
* A 409 means someone else saved first, so the list is read again and the
* next change is based on theirs.
*/
export function useSavePanels() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (body: PanelsConfig) =>
PanelsService.savePanels({ requestBody: body }),
onSuccess: (saved) => {
queryClient.setQueryData(panelKeys.all, saved)
queryClient.invalidateQueries({ queryKey: panelKeys.all })
},
onError: (error) => {
if (error instanceof ApiError && error.status === 409)
queryClient.invalidateQueries({ queryKey: panelKeys.all })
},
})
}
/**
* Stop honouring one panel's credential, and keep the panel.
*
* The screen goes back to showing a pairing code while its dashboards and their
* arrangement stay — deleting the panel is what throws all three away.
* Superuser-only on the server.
*/
export function useUnpairPanel(id: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => PanelsService.unpairPanel({ panelId: id }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: panelKeys.all }),
})
}
/** Every message any flow declares — what a widget can be pointed at. */
export const messageCatalogQueryOptions = () => ({
queryKey: dashboardKeys.messages,
queryFn: () => MessagesService.readMessages(),
})
export const messageHistoryQueryOptions = (message: string) => ({
queryKey: dashboardKeys.history(message),
queryFn: () => MessagesService.readMessageHistory({ name: message }),
})
/** Saving a dashboard, carrying the version it was based on.
*
* This writes the draft; panels keep showing the published document.
*/
export function useSaveDashboard(name: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (body: DashboardDef_Input) =>
DashboardsService.saveDashboard({ name, requestBody: body }),
onSuccess: (saved) => {
queryClient.setQueryData(dashboardKeys.detail(name, true), saved)
queryClient.invalidateQueries({ queryKey: dashboardKeys.all })
},
})
}
/** Publish the unpublished changes, which is what puts them on the panels. */
export function usePublishDashboard(name: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (version: number) =>
DashboardsService.publishDashboard({
name,
requestBody: { version },
}),
onSuccess: (published) => {
queryClient.setQueryData(dashboardKeys.detail(name, true), published)
queryClient.setQueryData(dashboardKeys.detail(name), published)
queryClient.invalidateQueries({ queryKey: dashboardKeys.all })
},
})
}
/** Throw the unpublished edit away; the panels keep showing what they had. */
export function useDiscardDashboardDraft(name: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => DashboardsService.discardDashboardDraft({ name }),
onSuccess: (published) => {
queryClient.setQueryData(dashboardKeys.detail(name, true), published)
queryClient.setQueryData(dashboardKeys.detail(name), published)
queryClient.invalidateQueries({ queryKey: dashboardKeys.all })
},
})
}
/** What an input widget does: put a value into the graph.
*
* The widget names itself so the flow canvas can show the value arriving from
* here, rather than crediting whichever node is drawn as a producer.
*/
export function usePublishMessage() {
return useMutation({
mutationFn: ({
name,
value,
dashboard,
widget,
label,
kind,
}: {
name: string
value: unknown
dashboard?: string
widget?: string
label?: string
kind?: string
}) =>
MessagesService.publishMessage({
name,
requestBody: {
value,
source_kind: "dashboard",
// Matches the endpoint id the canvas builds for this widget.
source_id:
dashboard && widget ? `dashboard:${dashboard}:${widget}` : "",
source_label: label ?? widget ?? "Dashboard",
source_detail: kind ?? "",
},
}),
})
}