import { useMutation, useQueryClient } from "@tanstack/react-query" import { 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. Superuser-only on the server. */ 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 }) }, }) } /** * 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 ?? "", }, }), }) }