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.
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
import {
|
|
MutationCache,
|
|
QueryCache,
|
|
QueryClient,
|
|
QueryClientProvider,
|
|
} from "@tanstack/react-query"
|
|
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
|
import { MotionConfig } from "motion/react"
|
|
import { StrictMode } from "react"
|
|
import ReactDOM from "react-dom/client"
|
|
import { ApiError, OpenAPI } from "./client"
|
|
import { ThemeProvider } from "./components/theme-provider"
|
|
import { Toaster } from "./components/ui/sonner"
|
|
import "./index.css"
|
|
import { connectionStore, offlineDetail } from "./lib/connectionStore"
|
|
import { apiToken, portalConfig } from "./lib/portal"
|
|
import { routeTree } from "./routeTree.gen"
|
|
|
|
const portal = portalConfig()
|
|
|
|
// Served through a portal, the API is a path on this same origin and the
|
|
// credential arrives with the page. Everywhere else this is the build-time URL
|
|
// and the token in storage, exactly as before.
|
|
OpenAPI.BASE = portal
|
|
? window.location.origin + portal.apiBase
|
|
: import.meta.env.VITE_API_URL
|
|
OpenAPI.TOKEN = async () => apiToken()
|
|
|
|
/** A session the server will not accept, whatever we do next. */
|
|
const isAuthFailure = (error: unknown) =>
|
|
error instanceof ApiError && [401, 403].includes(error.status)
|
|
|
|
const handleApiError = (error: Error) => {
|
|
const offline = offlineDetail(error)
|
|
if (offline) {
|
|
// The installation is unreachable, not the session invalid: keep the user
|
|
// where they are and let the banner explain.
|
|
connectionStore.setOffline(offline.lastSeen)
|
|
return
|
|
}
|
|
if (isAuthFailure(error)) {
|
|
// A paired wall panel has no login screen to go back to — it asks for a
|
|
// new code instead. Only a 401 is worth throwing its credential away for:
|
|
// a 403 there is a dashboard it was just unassigned from, which the next
|
|
// read of the panel corrects on its own.
|
|
if (window.location.pathname.startsWith("/panel")) {
|
|
if (error instanceof ApiError && error.status === 401) {
|
|
localStorage.removeItem("access_token")
|
|
window.location.href = "/panel"
|
|
}
|
|
return
|
|
}
|
|
if (portal) {
|
|
// The portal knows whether they are still signed in; it can mint a fresh
|
|
// handoff or send them to the login screen.
|
|
window.location.href = `${portal.portalUrl}?reauth=${portal.installationId}`
|
|
return
|
|
}
|
|
localStorage.removeItem("access_token")
|
|
window.location.href = "/login"
|
|
}
|
|
}
|
|
|
|
const queryClient = new QueryClient({
|
|
queryCache: new QueryCache({
|
|
onError: handleApiError,
|
|
// Any answer at all means the tunnel is up again.
|
|
onSuccess: () => connectionStore.setOnline(),
|
|
}),
|
|
mutationCache: new MutationCache({
|
|
onError: handleApiError,
|
|
}),
|
|
defaultOptions: {
|
|
queries: {
|
|
// Retrying an expired session only delays the trip to the login screen.
|
|
retry: (count, error) => !isAuthFailure(error) && count < 3,
|
|
},
|
|
mutations: {
|
|
retry: false,
|
|
},
|
|
},
|
|
})
|
|
|
|
const router = createRouter({ routeTree, basepath: portal?.basePath })
|
|
declare module "@tanstack/react-router" {
|
|
interface Register {
|
|
router: typeof router
|
|
}
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
<StrictMode>
|
|
<ThemeProvider defaultTheme="system" storageKey="fluksio-ui-theme">
|
|
<MotionConfig reducedMotion="user">
|
|
<QueryClientProvider client={queryClient}>
|
|
<RouterProvider router={router} />
|
|
<Toaster richColors closeButton />
|
|
</QueryClientProvider>
|
|
</MotionConfig>
|
|
</ThemeProvider>
|
|
</StrictMode>,
|
|
)
|