112 lines
3.9 KiB
TypeScript
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([])}
|
|
/>
|
|
)
|
|
}
|