Panels: per-device dashboard sets, paired by code
A panel is one screen and the ordered set of whole dashboards it shows, so a hallway tablet and a workshop tablet carry different sets without either dashboard knowing about the other. More than one and the device draws a rail to switch between them — the same rail the editor puts on screen, because the wall has it and it takes room off the canvas. A screen has no keyboard, so it pairs rather than logs in: it shows a six-character code, somebody approves it against a panel from the dashboards overview, and the credential that mints reaches that panel's published dashboards and the message endpoints its widgets speak, and nothing else. Deleting the panel revokes it. Closes the per-device view and the kiosk credential; supersedes the multi-page/multi-section UI, since a page is now a dashboard of its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AHpLJHozysQXjsxAyU1WHj
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router"
|
||||
|
||||
import type { Dashboard } from "@/components/Dashboard/DashboardView"
|
||||
import { PanelRail, RAIL_INSET } from "@/components/Dashboard/PanelRail"
|
||||
import { PanelSurface } from "@/components/Dashboard/PanelSurface"
|
||||
import {
|
||||
dashboardQueryOptions,
|
||||
panelQueryOptions,
|
||||
} from "@/components/Dashboard/queries"
|
||||
import { useFlowSocket } from "@/components/Flow/useFlowSocket"
|
||||
import { isLoggedIn } from "@/hooks/useAuth"
|
||||
import { useIsMobile } from "@/hooks/useMobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/**
|
||||
* What a paired device shows: the dashboards this panel was assigned.
|
||||
*
|
||||
* The same full-bleed surface as `/view/{name}`, with a rail down the left
|
||||
* when there is more than one to switch between — so a hallway tablet and a
|
||||
* workshop tablet can carry different sets without either dashboard knowing
|
||||
* anything about the other.
|
||||
*
|
||||
* Which one is open lives in the URL, so a panel that reboots comes back where
|
||||
* it was rather than at the first one.
|
||||
*/
|
||||
export const Route = createFileRoute("/panel/$id")({
|
||||
component: PanelRoute,
|
||||
validateSearch: (search: Record<string, unknown>): { d?: string } =>
|
||||
typeof search.d === "string" ? { d: search.d } : {},
|
||||
beforeLoad: async () => {
|
||||
// Unpaired, or a credential the server stopped honouring. Either way this
|
||||
// device needs a new code, not a login form it cannot type into.
|
||||
if (!isLoggedIn()) {
|
||||
throw redirect({ to: "/panel" })
|
||||
}
|
||||
},
|
||||
head: ({ params }) => ({ meta: [{ title: `${params.id} - Fluksio` }] }),
|
||||
})
|
||||
|
||||
function PanelRoute() {
|
||||
const { id } = Route.useParams()
|
||||
const { d } = Route.useSearch()
|
||||
useFlowSocket()
|
||||
|
||||
const { data: panel } = useQuery(panelQueryOptions(id))
|
||||
const dashboards = panel?.dashboards ?? []
|
||||
// A name in the URL that this panel no longer carries falls back to the
|
||||
// first, which is what a dashboard removed from under a running panel does.
|
||||
const current = d && dashboards.includes(d) ? d : dashboards[0]
|
||||
|
||||
const { data: dashboard } = useQuery({
|
||||
...dashboardQueryOptions(current ?? ""),
|
||||
enabled: Boolean(current),
|
||||
})
|
||||
const stacked = useIsMobile()
|
||||
const rail = dashboards.length > 1
|
||||
|
||||
if (panel && dashboards.length === 0) {
|
||||
return (
|
||||
<main className="grid h-svh w-full place-items-center p-8">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No dashboards are assigned to this panel yet.
|
||||
</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
className={cn(
|
||||
"relative h-svh w-full p-4",
|
||||
stacked ? "overflow-y-auto" : "overflow-hidden",
|
||||
)}
|
||||
style={rail ? { paddingLeft: RAIL_INSET } : undefined}
|
||||
>
|
||||
{rail && current ? (
|
||||
<PanelRail
|
||||
dashboards={dashboards}
|
||||
current={current}
|
||||
linkFor={(name) => ({
|
||||
to: "/panel/$id",
|
||||
params: { id },
|
||||
search: { d: name },
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
{dashboard ? (
|
||||
<PanelSurface dashboard={dashboard as Dashboard} stacked={stacked} />
|
||||
) : null}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user