- IconPicker replaces the three icon selects (rail icon, icon-widget rule,
"Otherwise"): the glyphs in a grid, and a button that clears back to none —
which a Radix SelectItem could never offer.
- ModePicker/StylePicker drop out in favour of a shared ui/Segmented, the same
sliding-thumb shape RangePicker and the widget-side control already wear.
- PanelRail draws no scrollbars at all: hiding them also takes back the gutter
a vertical bar claimed from a column exactly as wide as its buttons, which is
what pushed a horizontal bar under them.
- The panels dialog can re-pair one screen (POST /panels/{id}/unpair) without
deleting the panel it hangs on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
179 lines
5.6 KiB
TypeScript
179 lines
5.6 KiB
TypeScript
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 ?? "",
|
|
},
|
|
}),
|
|
})
|
|
}
|