Files
app/frontend/src/routes/view.$name.tsx
T

112 lines
3.9 KiB
TypeScript

import { useQuery } from "@tanstack/react-query"
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router"
import { ContextBar } from "@/components/Dashboard/ContextBar"
import type { Dashboard } from "@/components/Dashboard/DashboardView"
import { useDataContext } from "@/components/Dashboard/dataContext"
import { PanelSurface } from "@/components/Dashboard/PanelSurface"
import { dashboardQueryOptions } from "@/components/Dashboard/queries"
import {
RunsContextProvider,
runIdsOf,
} from "@/components/Dashboard/RunsContext"
import { useCanvasRoot } from "@/components/Dashboard/ui/core/look"
import { useFlowSocket } from "@/components/Flow/useFlowSocket"
import { isLoggedIn } from "@/hooks/useAuth"
import { useIsMobile } from "@/hooks/useMobile"
import { cn } from "@/lib/utils"
/**
* What a wall panel is pointed at when it shows one dashboard and nothing else.
*
* Deliberately outside both shells: no sidebar, no footer, no editing, and no
* column cap — the widgets are the whole page. Route splitting means a panel
* never downloads the editor or the grid library either.
*
* A device that was paired to a panel uses `/panel/{id}` instead, which is the
* same surface with a rail for switching between the dashboards it was
* assigned. This route stays for a browser tab pointed straight at one.
*/
export const Route = createFileRoute("/view/$name")({
component: PanelView,
beforeLoad: async () => {
if (!isLoggedIn()) {
throw redirect({ to: "/login" })
}
},
// Which runs this dashboard is being read against, if any. In the address
// rather than in the document: it is a way of looking at the page, not a
// change to it, and a comparison someone found is then a link they can send.
validateSearch: (search: Record<string, unknown>): { runs?: string } => ({
runs:
typeof search.runs === "string" && search.runs ? search.runs : undefined,
}),
head: ({ params }) => ({ meta: [{ title: `${params.name} - Fluksio` }] }),
})
function PanelView() {
const { name } = Route.useParams()
const { runs } = Route.useSearch()
const navigate = useNavigate()
useFlowSocket()
const { data: dashboard } = useQuery(dashboardQueryOptions(name))
const stacked = useIsMobile()
const runIds = runIdsOf(runs)
// A tab pointed at one dashboard is that dashboard, so its appearance
// covers the whole page rather than only the canvas inside it.
const root = useCanvasRoot(dashboard as Dashboard | undefined)
if (!dashboard) return <main className="h-svh w-full" />
return (
<main
data-look={root["data-look"]}
data-touch={root["data-touch"]}
data-palette={root["data-palette"]}
className={cn(
"h-svh w-full bg-background p-4 text-foreground",
root.className,
stacked ? "overflow-y-auto" : "overflow-hidden",
)}
style={root.style}
>
{runIds.length > 0 ? (
<RunsContextProvider runIds={runIds} dashboard={dashboard as Dashboard}>
<ContextBarFor
members={runIds}
onChange={(next) =>
navigate({
to: "/view/$name",
params: { name },
search: next.length ? { runs: next.join(",") } : {},
})
}
/>
<PanelSurface dashboard={dashboard as Dashboard} stacked={stacked} />
</RunsContextProvider>
) : (
<PanelSurface dashboard={dashboard as Dashboard} stacked={stacked} />
)}
</main>
)
}
/** The bar, drawn from inside the provider so it can read what it resolved. */
function ContextBarFor({
members,
onChange,
}: {
members: string[]
onChange: (next: string[]) => void
}) {
const context = useDataContext()
if (!context) return null
return (
<ContextBar
context={context}
members={members}
onRemove={(id) => onChange(members.filter((one) => one !== id))}
onExit={() => onChange([])}
/>
)
}