From d4104be841640f8b15d5259e982db5967d2d61c6 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 25 Apr 2026 07:37:17 -0400 Subject: [PATCH] refactor(web): simplify SWR event plumbing Share SSE subscription management, reuse query key builders, and remove duplicate route mapping/error helpers from the SWR refactor. --- .../app/components/stage-sidebar.tsx | 4 - apps/fabro-web/app/lib/board-events.test.tsx | 4 +- apps/fabro-web/app/lib/board-events.ts | 86 ++---- apps/fabro-web/app/lib/mutations.ts | 42 +-- apps/fabro-web/app/lib/query-keys.ts | 2 + apps/fabro-web/app/lib/run-actions.ts | 7 +- apps/fabro-web/app/lib/run-events.test.tsx | 7 +- apps/fabro-web/app/lib/run-events.ts | 111 ++------ apps/fabro-web/app/lib/sse.ts | 164 +++++++++++ apps/fabro-web/app/lib/stage-sidebar.ts | 21 ++ apps/fabro-web/app/routes/run-files.tsx | 3 - .../fabro-web/app/routes/run-files/states.tsx | 28 +- apps/fabro-web/app/routes/run-graph.tsx | 32 +-- apps/fabro-web/app/routes/run-overview.tsx | 24 +- apps/fabro-web/app/routes/run-settings.tsx | 20 +- apps/fabro-web/app/routes/run-stages.tsx | 85 +++--- .../{entry-zm3ds8ft.js => entry-axjk9fdf.js} | 266 +++++++++--------- lib/crates/fabro-spa/assets/index.html | 2 +- 18 files changed, 475 insertions(+), 433 deletions(-) create mode 100644 apps/fabro-web/app/lib/sse.ts create mode 100644 apps/fabro-web/app/lib/stage-sidebar.ts rename lib/crates/fabro-spa/assets/assets/{entry-zm3ds8ft.js => entry-axjk9fdf.js} (61%) diff --git a/apps/fabro-web/app/components/stage-sidebar.tsx b/apps/fabro-web/app/components/stage-sidebar.tsx index 206fcef82..aba87efa1 100644 --- a/apps/fabro-web/app/components/stage-sidebar.tsx +++ b/apps/fabro-web/app/components/stage-sidebar.tsx @@ -9,7 +9,6 @@ import { } from "@heroicons/react/24/solid"; import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline"; import { formatDurationSecs } from "../lib/format"; -import { useRunEvents } from "../lib/run-events"; export type StageStatus = "completed" | "running" | "pending" | "failed" | "cancelled"; @@ -41,9 +40,6 @@ export function StageSidebar({ stages, runId, selectedStageId, activeLink }: Sta const runningStartRef = useRef>(new Map()); const [, setTick] = useState(0); - // Subscribe to run-specific SSE for live stage updates - useRunEvents(runId); - // Track start times for running stages useEffect(() => { const running = new Set( diff --git a/apps/fabro-web/app/lib/board-events.test.tsx b/apps/fabro-web/app/lib/board-events.test.tsx index 3033fc8c3..853d300b9 100644 --- a/apps/fabro-web/app/lib/board-events.test.tsx +++ b/apps/fabro-web/app/lib/board-events.test.tsx @@ -43,10 +43,10 @@ describe("subscribeToBoardEvents", () => { const firstCleanup = subscribeToBoardEvents(mutate, (url) => { created.push(url); return source; - }); + }, { debounceMs: 0 }); const secondCleanup = subscribeToBoardEvents(mutate, () => { throw new Error("source should be reused"); - }); + }, { debounceMs: 0 }); source.emit({ event: "run.running" }); diff --git a/apps/fabro-web/app/lib/board-events.ts b/apps/fabro-web/app/lib/board-events.ts index 1a2d7e284..ef30009b1 100644 --- a/apps/fabro-web/app/lib/board-events.ts +++ b/apps/fabro-web/app/lib/board-events.ts @@ -1,20 +1,15 @@ import { useEffect } from "react"; -import { useSWRConfig, type MutatorCallback } from "swr"; +import { useSWRConfig } from "swr"; import { queryKeys } from "./query-keys"; - -type MutateFn = (key: string) => ReturnType; - -interface BoardEventSourceLike { - onmessage: ((event: { data: string }) => void) | null; - close(): void; -} - -interface BoardSubscription { - source: BoardEventSourceLike; - refcount: number; - mutators: Map; -} +import { + createBrowserEventSource, + subscribeToSharedEventSource, + type EventPayload, + type EventSourceLike, + type MutateFn, + type SharedEventSubscription, +} from "./sse"; const BOARD_STATUS_EVENTS = new Set([ "run.submitted", @@ -36,11 +31,8 @@ const BOARD_STATUS_EVENTS = new Set([ "interview.interrupted", ]); -let subscription: BoardSubscription | null = null; - -function createBrowserEventSource(url: string): BoardEventSourceLike { - return new EventSource(url); -} +const subscriptions = new Map(); +const BOARD_SUBSCRIPTION_KEY = "board"; export function shouldRefreshBoardForEvent(event: string) { return BOARD_STATUS_EVENTS.has(event); @@ -48,48 +40,22 @@ export function shouldRefreshBoardForEvent(event: string) { export function subscribeToBoardEvents( mutate: MutateFn, - eventSourceFactory: (url: string) => BoardEventSourceLike = createBrowserEventSource, + eventSourceFactory: (url: string) => EventSourceLike = createBrowserEventSource, + { debounceMs = 500 }: { debounceMs?: number } = {}, ): () => void { - if (!subscription) { - const source = eventSourceFactory("/api/v1/attach"); - subscription = { - source, - refcount: 0, - mutators: new Map(), - }; - - source.onmessage = (message) => { - try { - const payload = JSON.parse(message.data) as { event?: string }; - if (!payload.event || !shouldRefreshBoardForEvent(payload.event)) return; - - for (const mutator of subscription?.mutators.keys() ?? []) { - void mutator(queryKeys.boards.runs()); - } - } catch { - // Ignore malformed events. - } - }; - } - - subscription.refcount += 1; - subscription.mutators.set(mutate, (subscription.mutators.get(mutate) ?? 0) + 1); - - return () => { - if (!subscription) return; - const mutateCount = subscription.mutators.get(mutate) ?? 0; - if (mutateCount <= 1) { - subscription.mutators.delete(mutate); - } else { - subscription.mutators.set(mutate, mutateCount - 1); - } - - subscription.refcount -= 1; - if (subscription.refcount <= 0) { - subscription.source.close(); - subscription = null; - } - }; + return subscribeToSharedEventSource({ + subscriptions, + subscriptionKey: BOARD_SUBSCRIPTION_KEY, + url: queryKeys.system.attach(), + mutate, + eventSourceFactory, + debounceMs, + resolveInvalidation: (payload) => ({ + keys: payload.event && shouldRefreshBoardForEvent(payload.event) + ? [queryKeys.boards.runs()] + : [], + }), + }); } export function useBoardEvents() { diff --git a/apps/fabro-web/app/lib/mutations.ts b/apps/fabro-web/app/lib/mutations.ts index dd4d43117..249683d95 100644 --- a/apps/fabro-web/app/lib/mutations.ts +++ b/apps/fabro-web/app/lib/mutations.ts @@ -1,17 +1,17 @@ import useSWRMutation from "swr/mutation"; import { useSWRConfig } from "swr"; import type { - ErrorResponseEntry, PreviewUrlResponse, RunStatusResponse, } from "@qltysh/fabro-api-client"; -import { apiJsonMutation, apiRequest } from "./api-client"; +import { apiJsonMutation } from "./api-client"; import { queryKeys } from "./query-keys"; import type { LifecycleAction, LifecycleActionError } from "./run-actions"; import { archiveRun, cancelRun, + isLifecycleActionError, unarchiveRun, } from "./run-actions"; @@ -78,7 +78,7 @@ function useLifecycleMutation( return { intent, ok: false, - error: serializeLifecycleActionError(error), + error: isLifecycleActionError(error) ? error : null, }; } }, @@ -98,18 +98,7 @@ export function useToggleDemoMode() { return useSWRMutation( queryKeys.demo.toggle(), async (key: string, { arg }: { arg: { enabled: boolean } }) => { - const response = await apiRequest(key, { - init: { - method: "POST", - credentials: "include", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(arg), - }, - }); - if (!response.ok) { - throw new Error(response.statusText || `HTTP ${response.status}`); - } - return response; + await apiJsonMutation(key, { arg }); }, { onSuccess: () => { @@ -136,26 +125,3 @@ export function useLoginDevToken() { }, ); } - -function serializeLifecycleActionError(error: unknown): LifecycleActionError | null { - if (!error || typeof error !== "object") return null; - const record = error as Record; - if (typeof record.status !== "number" || !Array.isArray(record.errors)) { - return null; - } - - return { - status: record.status, - errors: record.errors.filter(isErrorResponseEntry), - }; -} - -function isErrorResponseEntry(value: unknown): value is ErrorResponseEntry { - if (!value || typeof value !== "object") return false; - const record = value as Record; - return ( - typeof record.status === "string" && - typeof record.title === "string" && - typeof record.detail === "string" - ); -} diff --git a/apps/fabro-web/app/lib/query-keys.ts b/apps/fabro-web/app/lib/query-keys.ts index 3ca20ef7e..c97a3d7e8 100644 --- a/apps/fabro-web/app/lib/query-keys.ts +++ b/apps/fabro-web/app/lib/query-keys.ts @@ -21,6 +21,7 @@ export const queryKeys = { }, system: { info: () => "/api/v1/system/info", + attach: () => "/api/v1/attach", }, boards: { runs: () => "/api/v1/boards/runs", @@ -46,6 +47,7 @@ export const queryKeys = { cancel: (id: string) => `/api/v1/runs/${pathSegment(id)}/cancel`, archive: (id: string) => `/api/v1/runs/${pathSegment(id)}/archive`, unarchive: (id: string) => `/api/v1/runs/${pathSegment(id)}/unarchive`, + attach: (id: string) => `/api/v1/runs/${pathSegment(id)}/attach`, }, workflows: { list: () => "/api/v1/workflows", diff --git a/apps/fabro-web/app/lib/run-actions.ts b/apps/fabro-web/app/lib/run-actions.ts index 052cb11e6..ff7fb9f10 100644 --- a/apps/fabro-web/app/lib/run-actions.ts +++ b/apps/fabro-web/app/lib/run-actions.ts @@ -1,6 +1,7 @@ import type { ErrorResponseEntry, RunStatusResponse } from "@qltysh/fabro-api-client"; import { apiRequest } from "./api-client"; +import { queryKeys } from "./query-keys"; import type { RunStatus } from "../data/runs"; export type LifecycleAction = "cancel" | "archive" | "unarchive"; @@ -89,11 +90,11 @@ async function runLifecycleAction( action: LifecycleAction, request?: Request, ): Promise { - const response = await apiRequest(`/runs/${id}/${action}`, { + const response = await apiRequest(queryKeys.runs[action](id), { init: { method: "POST", - ...(request?.signal ? { signal: request.signal } : {}), }, + request, }); if (!response.ok) { @@ -128,7 +129,7 @@ async function parseLifecycleActionError(response: Response): Promise; return typeof record.status === "number" && Array.isArray(record.errors); diff --git a/apps/fabro-web/app/lib/run-events.test.tsx b/apps/fabro-web/app/lib/run-events.test.tsx index 960b40969..14acf4f23 100644 --- a/apps/fabro-web/app/lib/run-events.test.tsx +++ b/apps/fabro-web/app/lib/run-events.test.tsx @@ -51,10 +51,10 @@ describe("subscribeToRunEvents", () => { const firstCleanup = subscribeToRunEvents("run-refcount", mutate, (url) => { created.push(url); return source; - }); + }, { debounceMs: 0 }); const secondCleanup = subscribeToRunEvents("run-refcount", mutate, () => { throw new Error("source should be reused"); - }); + }, { debounceMs: 0 }); expect(created).toEqual(["/api/v1/runs/run-refcount/attach"]); @@ -78,6 +78,7 @@ describe("subscribeToRunEvents", () => { return Promise.resolve(); }, () => source, + { debounceMs: 0 }, ); source.emit({ event: "run.failed" }); @@ -102,6 +103,7 @@ describe("subscribeToRunEvents", () => { return Promise.resolve(); }, () => sources.shift()!, + { debounceMs: 0 }, ); firstSource.emitRaw("{broken"); firstCleanup(); @@ -113,6 +115,7 @@ describe("subscribeToRunEvents", () => { return Promise.resolve(); }, () => sources.shift()!, + { debounceMs: 0 }, ); secondCleanup(); diff --git a/apps/fabro-web/app/lib/run-events.ts b/apps/fabro-web/app/lib/run-events.ts index ff90f1a6f..8b143690b 100644 --- a/apps/fabro-web/app/lib/run-events.ts +++ b/apps/fabro-web/app/lib/run-events.ts @@ -1,28 +1,23 @@ import { useEffect } from "react"; -import { useSWRConfig, type MutatorCallback } from "swr"; +import { useSWRConfig } from "swr"; import { queryKeys } from "./query-keys"; +import { + createBrowserEventSource, + subscribeToSharedEventSource, + type EventPayload, + type EventSourceLike, + type MutateFn, + type SharedEventSubscription, +} from "./sse"; -type MutateFn = (key: string) => ReturnType; - -interface RunEventPayload { +interface RunEventPayload extends EventPayload { event?: string; node_id?: string; properties?: Record; } -interface RunEventSourceLike { - onmessage: ((event: { data: string }) => void) | null; - close(): void; -} - -interface RunSubscription { - source: RunEventSourceLike; - refcount: number; - mutators: Map; -} - -const subscriptions = new Map(); +const subscriptions = new Map(); const TERMINAL_EVENTS = new Set(["run.completed", "run.failed"]); const RUN_SUMMARY_EVENTS = new Set([ @@ -40,10 +35,6 @@ const RUN_SUMMARY_EVENTS = new Set([ const STAGE_EVENTS = new Set(["stage.started", "stage.completed", "stage.failed"]); const COMMAND_EVENTS = new Set(["command.started", "command.completed"]); -function createBrowserEventSource(url: string): RunEventSourceLike { - return new EventSource(url); -} - export function queryKeysForRunEvent( runId: string, event: string, @@ -99,65 +90,30 @@ export function queryKeysForRunEvent( export function subscribeToRunEvents( runId: string, mutate: MutateFn, - eventSourceFactory: (url: string) => RunEventSourceLike = createBrowserEventSource, + eventSourceFactory: (url: string) => EventSourceLike = createBrowserEventSource, + { debounceMs = 300 }: { debounceMs?: number } = {}, ): () => void { - let subscription = subscriptions.get(runId); - if (!subscription) { - const source = eventSourceFactory(`/api/v1/runs/${runId}/attach`); - subscription = { - source, - refcount: 0, - mutators: new Map(), - }; - subscriptions.set(runId, subscription); - - source.onmessage = (message) => { - let payload: RunEventPayload; - try { - payload = JSON.parse(message.data) as RunEventPayload; - } catch { - return; - } - + return subscribeToSharedEventSource({ + subscriptions, + subscriptionKey: runId, + url: queryKeys.runs.attach(runId), + mutate, + eventSourceFactory, + debounceMs, + resolveInvalidation: (payload) => { const event = payload.event; - if (!event) return; + if (!event) return { keys: [] }; const stageId = stageIdFromPayload(payload); const keys = queryKeysForRunEvent(runId, event, stageId); - if (keys.length > 0) { - const current = subscriptions.get(runId); - for (const mutator of current?.mutators.keys() ?? []) { - for (const key of keys) { - void mutator(key); - } - } - } - - if (TERMINAL_EVENTS.has(event)) { - closeRunSubscription(runId); - } - }; - } - - subscription.refcount += 1; - subscription.mutators.set(mutate, (subscription.mutators.get(mutate) ?? 0) + 1); - - return () => { - const current = subscriptions.get(runId); - if (!current) return; - - const mutateCount = current.mutators.get(mutate) ?? 0; - if (mutateCount <= 1) { - current.mutators.delete(mutate); - } else { - current.mutators.set(mutate, mutateCount - 1); - } - - current.refcount -= 1; - if (current.refcount <= 0) { - closeRunSubscription(runId); - } - }; + const terminal = TERMINAL_EVENTS.has(event); + return { + keys, + close: terminal, + immediate: terminal, + }; + }, + }); } function stageIdFromPayload(payload: RunEventPayload): string | undefined { @@ -166,13 +122,6 @@ function stageIdFromPayload(payload: RunEventPayload): string | undefined { return typeof nodeId === "string" ? nodeId : undefined; } -function closeRunSubscription(runId: string) { - const subscription = subscriptions.get(runId); - if (!subscription) return; - subscription.source.close(); - subscriptions.delete(runId); -} - export function useRunEvents(runId: string | undefined) { const { mutate } = useSWRConfig(); diff --git a/apps/fabro-web/app/lib/sse.ts b/apps/fabro-web/app/lib/sse.ts new file mode 100644 index 000000000..408ff3c99 --- /dev/null +++ b/apps/fabro-web/app/lib/sse.ts @@ -0,0 +1,164 @@ +import type { MutatorCallback } from "swr"; + +export type MutateFn = (key: string) => ReturnType; + +export interface EventPayload { + event?: string; + [key: string]: unknown; +} + +export interface EventSourceLike { + onmessage: ((event: { data: string }) => void) | null; + close(): void; +} + +export interface EventInvalidation { + keys: string[]; + close?: boolean; + immediate?: boolean; +} + +export interface SharedEventSubscription { + source: EventSourceLike; + refcount: number; + mutators: Map; + pendingKeys: Set; + debounceTimer: ReturnType | null; +} + +export function createBrowserEventSource(url: string): EventSourceLike { + return new EventSource(url); +} + +export function subscribeToSharedEventSource({ + subscriptions, + subscriptionKey, + url, + mutate, + resolveInvalidation, + eventSourceFactory = createBrowserEventSource, + debounceMs = 300, +}: { + subscriptions: Map; + subscriptionKey: string; + url: string; + mutate: MutateFn; + resolveInvalidation: (payload: TPayload) => EventInvalidation; + eventSourceFactory?: (url: string) => EventSourceLike; + debounceMs?: number; +}): () => void { + let subscription = subscriptions.get(subscriptionKey); + if (!subscription) { + const source = eventSourceFactory(url); + subscription = { + source, + refcount: 0, + mutators: new Map(), + pendingKeys: new Set(), + debounceTimer: null, + }; + subscriptions.set(subscriptionKey, subscription); + + source.onmessage = (message) => { + const current = subscriptions.get(subscriptionKey); + if (!current) return; + + let payload: TPayload; + try { + payload = JSON.parse(message.data) as TPayload; + } catch { + return; + } + + const invalidation = resolveInvalidation(payload); + queueInvalidations(current, invalidation.keys, { + debounceMs, + immediate: invalidation.immediate, + }); + + if (invalidation.close) { + closeSharedEventSource(subscriptions, subscriptionKey, { flushPending: true }); + } + }; + } + + subscription.refcount += 1; + subscription.mutators.set(mutate, (subscription.mutators.get(mutate) ?? 0) + 1); + + return () => { + const current = subscriptions.get(subscriptionKey); + if (!current) return; + + const mutateCount = current.mutators.get(mutate) ?? 0; + if (mutateCount <= 1) { + current.mutators.delete(mutate); + } else { + current.mutators.set(mutate, mutateCount - 1); + } + + current.refcount -= 1; + if (current.refcount <= 0) { + closeSharedEventSource(subscriptions, subscriptionKey); + } + }; +} + +function queueInvalidations( + subscription: SharedEventSubscription, + keys: string[], + { + debounceMs, + immediate, + }: { + debounceMs: number; + immediate?: boolean; + }, +) { + if (keys.length === 0) return; + for (const key of keys) { + subscription.pendingKeys.add(key); + } + + if (immediate || debounceMs <= 0) { + flushInvalidations(subscription); + return; + } + + if (subscription.debounceTimer) { + clearTimeout(subscription.debounceTimer); + } + subscription.debounceTimer = setTimeout(() => { + subscription.debounceTimer = null; + flushInvalidations(subscription); + }, debounceMs); +} + +function flushInvalidations(subscription: SharedEventSubscription) { + if (subscription.pendingKeys.size === 0) return; + const keys = [...subscription.pendingKeys]; + subscription.pendingKeys.clear(); + + for (const mutator of subscription.mutators.keys()) { + for (const key of keys) { + void mutator(key); + } + } +} + +function closeSharedEventSource( + subscriptions: Map, + subscriptionKey: string, + { flushPending = false }: { flushPending?: boolean } = {}, +) { + const subscription = subscriptions.get(subscriptionKey); + if (!subscription) return; + + if (flushPending) { + flushInvalidations(subscription); + } + if (subscription.debounceTimer) { + clearTimeout(subscription.debounceTimer); + } + subscription.source.close(); + subscriptions.delete(subscriptionKey); +} diff --git a/apps/fabro-web/app/lib/stage-sidebar.ts b/apps/fabro-web/app/lib/stage-sidebar.ts new file mode 100644 index 000000000..dea3c8f12 --- /dev/null +++ b/apps/fabro-web/app/lib/stage-sidebar.ts @@ -0,0 +1,21 @@ +import type { PaginatedRunStageList } from "@qltysh/fabro-api-client"; + +import type { Stage } from "../components/stage-sidebar"; +import { isVisibleStage } from "../data/runs"; +import { formatDurationSecs } from "./format"; + +export function mapRunStagesToSidebarStages( + stagesResult: PaginatedRunStageList | null | undefined, +): Stage[] { + return (stagesResult?.data ?? []) + .filter((stage) => isVisibleStage(stage.id)) + .map((stage) => ({ + id: stage.id, + name: stage.name, + dotId: stage.dot_id ?? stage.id, + status: stage.status as Stage["status"], + duration: stage.duration_secs != null + ? formatDurationSecs(stage.duration_secs) + : "--", + })); +} diff --git a/apps/fabro-web/app/routes/run-files.tsx b/apps/fabro-web/app/routes/run-files.tsx index 30f602761..23c6ed221 100644 --- a/apps/fabro-web/app/routes/run-files.tsx +++ b/apps/fabro-web/app/routes/run-files.tsx @@ -28,7 +28,6 @@ import { import { useFileKeyboardNav } from "./run-files/keyboard"; import { Toolbar, type DiffStyle } from "./run-files/toolbar"; import { ApiError, extractRequestId } from "../lib/api-client"; -import { useRunEvents } from "../lib/run-events"; import { useRun, useRunFiles } from "../lib/queries"; export { extractRequestId }; @@ -211,8 +210,6 @@ export default function RunFiles() { const data: PaginatedRunFileList | null = filesQuery.data ?? lastGoodDataRef.current; - useRunEvents(params.id); - const isInitialLoading = filesQuery.isLoading && !data; const isRevalidating = filesQuery.isValidating; diff --git a/apps/fabro-web/app/routes/run-files/states.tsx b/apps/fabro-web/app/routes/run-files/states.tsx index fc1966292..02080d939 100644 --- a/apps/fabro-web/app/routes/run-files/states.tsx +++ b/apps/fabro-web/app/routes/run-files/states.tsx @@ -1,4 +1,5 @@ import { isRouteErrorResponse, useRouteError } from "react-router"; +import { extractRequestId } from "../../lib/api-client"; /** * R4 empty-state taxonomy. See plan § Unit 11: @@ -187,7 +188,7 @@ export function RunFilesErrorBoundary() { if (isRouteErrorResponse(error)) { return renderStatusError({ status: error.status, - requestId: extractRequestIdFromUnknown(error.data), + requestId: extractRequestId(error.data), onRetry: () => window.location.reload(), }); } @@ -200,28 +201,3 @@ export function RunFilesErrorBoundary() { ); } - -/** - * Request-ID parser used only by the ErrorBoundary path. The loader path - * already extracts request_id into `RunFilesLoaderResult.error.requestId` - * via `run-files.tsx::extractRequestId` — this is the body shape - * react-router hands us in `useRouteError().data` for non-Response errors. - */ -function extractRequestIdFromUnknown(body: unknown): string | null { - if (!body || typeof body !== "object") return null; - const b = body as Record; - if (typeof b.request_id === "string") return b.request_id; - const errors = b.errors; - if (Array.isArray(errors) && errors.length > 0) { - const first = errors[0]; - if (first && typeof first === "object") { - const rec = first as Record; - if (typeof rec.request_id === "string") return rec.request_id; - if (typeof rec.detail === "string") { - const match = rec.detail.match(/request[_ ]id[=:]?\s*([a-zA-Z0-9-_]+)/i); - if (match) return match[1]; - } - } - } - return null; -} diff --git a/apps/fabro-web/app/routes/run-graph.tsx b/apps/fabro-web/app/routes/run-graph.tsx index b8cdf01d6..ef0c13f11 100644 --- a/apps/fabro-web/app/routes/run-graph.tsx +++ b/apps/fabro-web/app/routes/run-graph.tsx @@ -1,29 +1,17 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "react-router"; import { graphTheme } from "../lib/graph-theme"; -import { isVisibleStage } from "../data/runs"; -import { formatDurationSecs } from "../lib/format"; import { useRunGraph, useRunStages } from "../lib/queries"; import { StageSidebar } from "../components/stage-sidebar"; -import type { Stage } from "../components/stage-sidebar"; import { GRAPH_DEFAULT_ZOOM_INDEX, GRAPH_ZOOM_STEPS, GraphToolbar, } from "../components/graph-toolbar"; +import { mapRunStagesToSidebarStages } from "../lib/stage-sidebar"; export const handle = { wide: true }; -function mapStages(stagesResult: ReturnType["data"]): Stage[] { - return (stagesResult?.data ?? []).filter((s) => isVisibleStage(s.id)).map((s) => ({ - id: s.id, - name: s.name, - dotId: s.dot_id ?? s.id, - status: s.status as Stage["status"], - duration: s.duration_secs != null ? formatDurationSecs(s.duration_secs) : "--", - })); -} - type Direction = "LR" | "TB"; function buildDot(direction: Direction) { @@ -86,7 +74,10 @@ export default function RunGraph() { const [direction, setDirection] = useState("LR"); const stagesQuery = useRunStages(id); const graphQuery = useRunGraph(id, direction); - const stages = mapStages(stagesQuery.data); + const stages = useMemo( + () => mapRunStagesToSidebarStages(stagesQuery.data), + [stagesQuery.data], + ); const graphSvg = graphQuery.data; const containerRef = useRef(null); const innerRef = useRef(null); @@ -98,10 +89,19 @@ export default function RunGraph() { const zoom = GRAPH_ZOOM_STEPS[zoomIndex]; useEffect(() => { + if (graphSvg === undefined && !graphQuery.error) return; + let cancelled = false; async function render() { try { + setError(null); + + if (graphQuery.error) { + setError("Failed to load graph"); + return; + } + let svg: SVGSVGElement; if (graphSvg) { @@ -135,7 +135,7 @@ export default function RunGraph() { setPan({ x: 0, y: 0 }); render(); return () => { cancelled = true; }; - }, [direction, graphSvg, id]); + }, [direction, graphQuery.error, graphSvg]); const onPointerDown = useCallback((e: React.PointerEvent) => { if ((e.target as HTMLElement).closest("button")) return; diff --git a/apps/fabro-web/app/routes/run-overview.tsx b/apps/fabro-web/app/routes/run-overview.tsx index 323467974..5331242b9 100644 --- a/apps/fabro-web/app/routes/run-overview.tsx +++ b/apps/fabro-web/app/routes/run-overview.tsx @@ -1,7 +1,5 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router"; -import { isVisibleStage } from "../data/runs"; -import { formatDurationSecs } from "../lib/format"; import { graphTheme } from "../lib/graph-theme"; import { useRun, useRunGraph, useRunStages } from "../lib/queries"; import { StageSidebar } from "../components/stage-sidebar"; @@ -12,19 +10,10 @@ import { GraphToolbar, } from "../components/graph-toolbar"; import { EmptyState } from "../components/state"; +import { mapRunStagesToSidebarStages } from "../lib/stage-sidebar"; export const handle = { wide: true }; -function mapStages(stagesResult: ReturnType["data"]): Stage[] { - return (stagesResult?.data ?? []).filter((s) => isVisibleStage(s.id)).map((s) => ({ - id: s.id, - name: s.name, - status: s.status as Stage["status"], - duration: s.duration_secs != null ? formatDurationSecs(s.duration_secs) : "--", - dotId: s.dot_id ?? s.id, - })); -} - type Direction = "LR" | "TB"; export default function RunOverview() { @@ -33,7 +22,10 @@ export default function RunOverview() { const stagesQuery = useRunStages(id); const graphQuery = useRunGraph(id, direction); const runQuery = useRun(id); - const stages = mapStages(stagesQuery.data); + const stages = useMemo( + () => mapRunStagesToSidebarStages(stagesQuery.data), + [stagesQuery.data], + ); const graphSvg = graphQuery.data; const runStatus = runQuery.data?.status?.kind ?? null; const containerRef = useRef(null); @@ -193,7 +185,9 @@ export default function RunOverview() {
- {graphSvg ? ( + {graphSvg === undefined && graphQuery.isLoading ? ( +
+ ) : graphSvg ? (
; -function mapStages(stagesResult: PaginatedRunStageList | null | undefined): Stage[] { - return (stagesResult?.data ?? []).filter((s) => isVisibleStage(s.id)).map((s) => ({ - id: s.id, - name: s.name, - status: s.status as Stage["status"], - duration: s.duration_secs != null ? formatDurationSecs(s.duration_secs) : "--", - })); -} - export default function RunSettingsPage() { const { id } = useParams(); const stagesQuery = useRunStages(id); const settingsQuery = useRunSettings(id); - const stages = mapStages(stagesQuery.data); + const stages = useMemo( + () => mapRunStagesToSidebarStages(stagesQuery.data), + [stagesQuery.data], + ); const settings = settingsQuery.data ?? {}; return ( diff --git a/apps/fabro-web/app/routes/run-stages.tsx b/apps/fabro-web/app/routes/run-stages.tsx index 84e6aadc0..bdd4bd1c0 100644 --- a/apps/fabro-web/app/routes/run-stages.tsx +++ b/apps/fabro-web/app/routes/run-stages.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router"; import { Marked } from "marked"; @@ -39,10 +39,10 @@ import { StageSidebar, statusConfig } from "../components/stage-sidebar"; import type { Stage } from "../components/stage-sidebar"; import { EmptyState } from "../components/state"; import { CopyButton } from "../components/ui"; -import { isVisibleStage } from "../data/runs"; import { formatDurationSecs } from "../lib/format"; import { useRunEventsList, useRunStageTurns, useRunStages } from "../lib/queries"; -import type { PaginatedRunStageList, StageTurn as ApiStageTurn, PaginatedStageTurnList, PaginatedEventList } from "@qltysh/fabro-api-client"; +import { mapRunStagesToSidebarStages } from "../lib/stage-sidebar"; +import type { StageTurn as ApiStageTurn, PaginatedStageTurnList, PaginatedEventList } from "@qltysh/fabro-api-client"; export const handle = { wide: true }; @@ -147,15 +147,6 @@ function turnsFromEvents(events: RawEvent[], stageId: string): TurnType[] { return turns; } -function mapStages(stagesResult: PaginatedRunStageList | null | undefined): Stage[] { - return (stagesResult?.data ?? []).filter((s) => isVisibleStage(s.id)).map((s) => ({ - id: s.id, - name: s.name, - status: s.status as Stage["status"], - duration: s.duration_secs != null ? formatDurationSecs(s.duration_secs) : "--", - })); -} - function mapTurns( turnsResult: PaginatedStageTurnList | null | undefined, eventsResult: PaginatedEventList | null | undefined, @@ -365,35 +356,57 @@ function CommandBlock({ turn }: { turn: Extract } ); } -export default function RunStages() { - const { id, stageId } = useParams(); - const stagesQuery = useRunStages(id); - const stages = mapStages(stagesQuery.data); - - const selectedStage = stages.find((s: Stage) => s.id === stageId) ?? stages[0]; - const turnsQuery = useRunStageTurns(id, selectedStage?.id); - const eventsQuery = useRunEventsList(id, !!selectedStage?.id && !turnsQuery.data?.data?.length); - const turns = mapTurns(turnsQuery.data, eventsQuery.data, selectedStage?.id); - const isRunning = selectedStage?.status === "running"; - - // Ticking timer for running stage header - const runningStartRef = useRef(isRunning ? Date.now() : 0); +function RunningStageDuration({ + isRunning, + duration, +}: { + isRunning: boolean; + duration: string; +}) { + const [startedAt, setStartedAt] = useState(() => + isRunning ? Date.now() : null, + ); const [, setTick] = useState(0); useEffect(() => { - if (isRunning && runningStartRef.current === 0) { - runningStartRef.current = Date.now(); - } else if (!isRunning) { - runningStartRef.current = 0; - } + setStartedAt((current) => { + if (!isRunning) return null; + return current ?? Date.now(); + }); }, [isRunning]); useEffect(() => { if (!isRunning) return; - const interval = setInterval(() => setTick((t) => t + 1), 1000); + const interval = setInterval(() => setTick((tick) => tick + 1), 1000); return () => clearInterval(interval); }, [isRunning]); + if (isRunning && startedAt) { + return formatDurationSecs(Math.floor((Date.now() - startedAt) / 1000)); + } + return duration; +} + +export default function RunStages() { + const { id, stageId } = useParams(); + const stagesQuery = useRunStages(id); + const stages = useMemo( + () => mapRunStagesToSidebarStages(stagesQuery.data), + [stagesQuery.data], + ); + + const selectedStage = stages.find((s: Stage) => s.id === stageId) ?? stages[0]; + const turnsQuery = useRunStageTurns(id, selectedStage?.id); + const hasStageTurns = (turnsQuery.data?.data.length ?? 0) > 0; + const shouldLoadEventFallback = + !!selectedStage?.id && !turnsQuery.isLoading && !turnsQuery.error && !hasStageTurns; + const eventsQuery = useRunEventsList(id, shouldLoadEventFallback); + const turns = useMemo( + () => mapTurns(turnsQuery.data, eventsQuery.data, selectedStage?.id), + [eventsQuery.data, selectedStage?.id, turnsQuery.data], + ); + const isRunning = selectedStage?.status === "running"; + if (!stages.length) { return (
@@ -407,9 +420,6 @@ export default function RunStages() { const selectedConfig = statusConfig[selectedStage.status]; const SelectedIcon = selectedConfig.icon; - const headerDuration = isRunning && runningStartRef.current - ? formatDurationSecs(Math.floor((Date.now() - runningStartRef.current) / 1000)) - : selectedStage.duration; return (
@@ -419,7 +429,12 @@ export default function RunStages() {

{selectedStage.name}

- {headerDuration} + + +
{turns.map((turn: TurnType, i: number) => { diff --git a/lib/crates/fabro-spa/assets/assets/entry-zm3ds8ft.js b/lib/crates/fabro-spa/assets/assets/entry-axjk9fdf.js similarity index 61% rename from lib/crates/fabro-spa/assets/assets/entry-zm3ds8ft.js rename to lib/crates/fabro-spa/assets/assets/entry-axjk9fdf.js index 426b2f7a6..fc6e45372 100644 --- a/lib/crates/fabro-spa/assets/assets/entry-zm3ds8ft.js +++ b/lib/crates/fabro-spa/assets/assets/entry-axjk9fdf.js @@ -1,4 +1,4 @@ -import{X as g,Y as i6,Z as a5,_ as k}from"./chunk-q07bg6gn.js";var Z0=i6((Fo,bU)=>{(function(){function Z(j,J0){Object.defineProperty(q.prototype,j,{get:function(){console.warn("%s(...) is deprecated in plain JavaScript React classes. %s",J0[0],J0[1])}})}function Y(j){if(j===null||typeof j!=="object")return null;return j=q1&&j[q1]||j["@@iterator"],typeof j==="function"?j:null}function J(j,J0){j=(j=j.constructor)&&(j.displayName||j.name)||"ReactClass";var F0=j+"."+J0;v0[F0]||(console.error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.",J0,j),v0[F0]=!0)}function q(j,J0,F0){this.props=j,this.context=J0,this.refs=C5,this.updater=F0||L1}function z(){}function K(j,J0,F0){this.props=j,this.context=J0,this.refs=C5,this.updater=F0||L1}function $(){}function W(j){return""+j}function U(j){try{W(j);var J0=!1}catch(f0){J0=!0}if(J0){J0=console;var F0=J0.error,R0=typeof Symbol==="function"&&Symbol.toStringTag&&j[Symbol.toStringTag]||j.constructor.name||"Object";return F0.call(J0,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",R0),W(j)}}function M(j){if(j==null)return null;if(typeof j==="function")return j.$$typeof===m1?null:j.displayName||j.name||null;if(typeof j==="string")return j;switch(j){case Q0:return"Fragment";case W0:return"Profiler";case f:return"StrictMode";case E0:return"Suspense";case G0:return"SuspenseList";case M1:return"Activity"}if(typeof j==="object")switch(typeof j.tag==="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),j.$$typeof){case o:return"Portal";case s:return j.displayName||"Context";case N0:return(j._context.displayName||"Context")+".Consumer";case w0:var J0=j.render;return j=j.displayName,j||(j=J0.displayName||J0.name||"",j=j!==""?"ForwardRef("+j+")":"ForwardRef"),j;case k0:return J0=j.displayName||null,J0!==null?J0:M(j.type)||"Memo";case m0:J0=j._payload,j=j._init;try{return M(j(J0))}catch(F0){}}return null}function H(j){if(j===Q0)return"<>";if(typeof j==="object"&&j!==null&&j.$$typeof===m0)return"<...>";try{var J0=M(j);return J0?"<"+J0+">":"<...>"}catch(F0){return"<...>"}}function O(){var j=g0.A;return j===null?null:j.getOwner()}function _(){return Error("react-stack-top-frame")}function A(j){if(l1.call(j,"key")){var J0=Object.getOwnPropertyDescriptor(j,"key").get;if(J0&&J0.isReactWarning)return!1}return j.key!==void 0}function P(j,J0){function F0(){J2||(J2=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",J0))}F0.isReactWarning=!0,Object.defineProperty(j,"key",{get:F0,configurable:!0})}function L(){var j=M(this.type);return Q5[j]||(Q5[j]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),j=this.props.ref,j!==void 0?j:null}function T(j,J0,F0,R0,f0,J1){var u0=F0.ref;return j={$$typeof:$0,type:j,key:J0,props:F0,_owner:R0},(u0!==void 0?u0:null)!==null?Object.defineProperty(j,"ref",{enumerable:!1,get:L}):Object.defineProperty(j,"ref",{enumerable:!1,value:null}),j._store={},Object.defineProperty(j._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(j,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(j,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:f0}),Object.defineProperty(j,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:J1}),Object.freeze&&(Object.freeze(j.props),Object.freeze(j)),j}function R(j,J0){return J0=T(j.type,J0,j.props,j._owner,j._debugStack,j._debugTask),j._store&&(J0._store.validated=j._store.validated),J0}function v(j){C(j)?j._store&&(j._store.validated=1):typeof j==="object"&&j!==null&&j.$$typeof===m0&&(j._payload.status==="fulfilled"?C(j._payload.value)&&j._payload.value._store&&(j._payload.value._store.validated=1):j._store&&(j._store.validated=1))}function C(j){return typeof j==="object"&&j!==null&&j.$$typeof===$0}function E(j){var J0={"=":"=0",":":"=2"};return"$"+j.replace(/[=:]/g,function(F0){return J0[F0]})}function y(j,J0){return typeof j==="object"&&j!==null&&j.key!=null?(U(j.key),E(""+j.key)):J0.toString(36)}function S(j){switch(j.status){case"fulfilled":return j.value;case"rejected":throw j.reason;default:switch(typeof j.status==="string"?j.then($,$):(j.status="pending",j.then(function(J0){j.status==="pending"&&(j.status="fulfilled",j.value=J0)},function(J0){j.status==="pending"&&(j.status="rejected",j.reason=J0)})),j.status){case"fulfilled":return j.value;case"rejected":throw j.reason}}throw j}function D(j,J0,F0,R0,f0){var J1=typeof j;if(J1==="undefined"||J1==="boolean")j=null;var u0=!1;if(j===null)u0=!0;else switch(J1){case"bigint":case"string":case"number":u0=!0;break;case"object":switch(j.$$typeof){case $0:case o:u0=!0;break;case m0:return u0=j._init,D(u0(j._payload),J0,F0,R0,f0)}}if(u0){u0=j,f0=f0(u0);var z1=R0===""?"."+y(u0,0):R0;return w1(f0)?(F0="",z1!=null&&(F0=z1.replace(L2,"$&/")+"/"),D(f0,J0,F0,"",function(b5){return b5})):f0!=null&&(C(f0)&&(f0.key!=null&&(u0&&u0.key===f0.key||U(f0.key)),F0=R(f0,F0+(f0.key==null||u0&&u0.key===f0.key?"":(""+f0.key).replace(L2,"$&/")+"/")+z1),R0!==""&&u0!=null&&C(u0)&&u0.key==null&&u0._store&&!u0._store.validated&&(F0._store.validated=2),f0=F0),J0.push(f0)),1}if(u0=0,z1=R0===""?".":R0+":",w1(j))for(var j0=0;j0{(function(){function Z(j,J0){Object.defineProperty(q.prototype,j,{get:function(){console.warn("%s(...) is deprecated in plain JavaScript React classes. %s",J0[0],J0[1])}})}function Y(j){if(j===null||typeof j!=="object")return null;return j=z1&&j[z1]||j["@@iterator"],typeof j==="function"?j:null}function J(j,J0){j=(j=j.constructor)&&(j.displayName||j.name)||"ReactClass";var F0=j+"."+J0;v0[F0]||(console.error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.",J0,j),v0[F0]=!0)}function q(j,J0,F0){this.props=j,this.context=J0,this.refs=C5,this.updater=F0||L1}function z(){}function K(j,J0,F0){this.props=j,this.context=J0,this.refs=C5,this.updater=F0||L1}function $(){}function W(j){return""+j}function U(j){try{W(j);var J0=!1}catch(f0){J0=!0}if(J0){J0=console;var F0=J0.error,R0=typeof Symbol==="function"&&Symbol.toStringTag&&j[Symbol.toStringTag]||j.constructor.name||"Object";return F0.call(J0,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",R0),W(j)}}function M(j){if(j==null)return null;if(typeof j==="function")return j.$$typeof===m1?null:j.displayName||j.name||null;if(typeof j==="string")return j;switch(j){case Q0:return"Fragment";case W0:return"Profiler";case f:return"StrictMode";case E0:return"Suspense";case G0:return"SuspenseList";case M1:return"Activity"}if(typeof j==="object")switch(typeof j.tag==="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),j.$$typeof){case o:return"Portal";case s:return j.displayName||"Context";case N0:return(j._context.displayName||"Context")+".Consumer";case w0:var J0=j.render;return j=j.displayName,j||(j=J0.displayName||J0.name||"",j=j!==""?"ForwardRef("+j+")":"ForwardRef"),j;case k0:return J0=j.displayName||null,J0!==null?J0:M(j.type)||"Memo";case m0:J0=j._payload,j=j._init;try{return M(j(J0))}catch(F0){}}return null}function H(j){if(j===Q0)return"<>";if(typeof j==="object"&&j!==null&&j.$$typeof===m0)return"<...>";try{var J0=M(j);return J0?"<"+J0+">":"<...>"}catch(F0){return"<...>"}}function O(){var j=g0.A;return j===null?null:j.getOwner()}function _(){return Error("react-stack-top-frame")}function A(j){if(l1.call(j,"key")){var J0=Object.getOwnPropertyDescriptor(j,"key").get;if(J0&&J0.isReactWarning)return!1}return j.key!==void 0}function P(j,J0){function F0(){J2||(J2=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",J0))}F0.isReactWarning=!0,Object.defineProperty(j,"key",{get:F0,configurable:!0})}function L(){var j=M(this.type);return Q5[j]||(Q5[j]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),j=this.props.ref,j!==void 0?j:null}function T(j,J0,F0,R0,f0,Q1){var h0=F0.ref;return j={$$typeof:$0,type:j,key:J0,props:F0,_owner:R0},(h0!==void 0?h0:null)!==null?Object.defineProperty(j,"ref",{enumerable:!1,get:L}):Object.defineProperty(j,"ref",{enumerable:!1,value:null}),j._store={},Object.defineProperty(j._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(j,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(j,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:f0}),Object.defineProperty(j,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:Q1}),Object.freeze&&(Object.freeze(j.props),Object.freeze(j)),j}function R(j,J0){return J0=T(j.type,J0,j.props,j._owner,j._debugStack,j._debugTask),j._store&&(J0._store.validated=j._store.validated),J0}function v(j){C(j)?j._store&&(j._store.validated=1):typeof j==="object"&&j!==null&&j.$$typeof===m0&&(j._payload.status==="fulfilled"?C(j._payload.value)&&j._payload.value._store&&(j._payload.value._store.validated=1):j._store&&(j._store.validated=1))}function C(j){return typeof j==="object"&&j!==null&&j.$$typeof===$0}function E(j){var J0={"=":"=0",":":"=2"};return"$"+j.replace(/[=:]/g,function(F0){return J0[F0]})}function y(j,J0){return typeof j==="object"&&j!==null&&j.key!=null?(U(j.key),E(""+j.key)):J0.toString(36)}function S(j){switch(j.status){case"fulfilled":return j.value;case"rejected":throw j.reason;default:switch(typeof j.status==="string"?j.then($,$):(j.status="pending",j.then(function(J0){j.status==="pending"&&(j.status="fulfilled",j.value=J0)},function(J0){j.status==="pending"&&(j.status="rejected",j.reason=J0)})),j.status){case"fulfilled":return j.value;case"rejected":throw j.reason}}throw j}function D(j,J0,F0,R0,f0){var Q1=typeof j;if(Q1==="undefined"||Q1==="boolean")j=null;var h0=!1;if(j===null)h0=!0;else switch(Q1){case"bigint":case"string":case"number":h0=!0;break;case"object":switch(j.$$typeof){case $0:case o:h0=!0;break;case m0:return h0=j._init,D(h0(j._payload),J0,F0,R0,f0)}}if(h0){h0=j,f0=f0(h0);var B1=R0===""?"."+y(h0,0):R0;return w1(f0)?(F0="",B1!=null&&(F0=B1.replace(L2,"$&/")+"/"),D(f0,J0,F0,"",function(b5){return b5})):f0!=null&&(C(f0)&&(f0.key!=null&&(h0&&h0.key===f0.key||U(f0.key)),F0=R(f0,F0+(f0.key==null||h0&&h0.key===f0.key?"":(""+f0.key).replace(L2,"$&/")+"/")+B1),R0!==""&&h0!=null&&C(h0)&&h0.key==null&&h0._store&&!h0._store.validated&&(F0._store.validated=2),f0=F0),J0.push(f0)),1}if(h0=0,B1=R0===""?".":R0+":",w1(j))for(var j0=0;j0 import('./MyComponent')) @@ -10,67 +10,67 @@ Your code should look like: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app -See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.`),j}function r(){g0.asyncTransitions--}function q0(j){if(t5===null)try{var J0=("require"+Math.random()).slice(0,7);t5=(bU&&bU[J0]).call(bU,"timers").setImmediate}catch(F0){t5=function(R0){e1===!1&&(e1=!0,typeof MessageChannel>"u"&&console.error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var f0=new MessageChannel;f0.port1.onmessage=R0,f0.port2.postMessage(void 0)}}return t5(j)}function z0(j){return 1 ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),{then:function(j0,b5){f0=!0,u0.then(function(N5){if(M0(J0,F0),F0===0){try{Y0(R0),q0(function(){return n(N5,j0,b5)})}catch(U2){g0.thrownErrors.push(U2)}if(0 ...)"))}),g0.actQueue=null),0g0.recentlyCreatedOwnerStacks++;return T(j,f0,R0,O(),j0?Error("react-stack-top-frame"):M5,j0?l0(H(j)):M7)},Fo.createRef=function(){var j={current:null};return Object.seal(j),j},Fo.forwardRef=function(j){j!=null&&j.$$typeof===k0?console.error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof j!=="function"?console.error("forwardRef requires a render function but was given %s.",j===null?"null":typeof j):j.length!==0&&j.length!==2&&console.error("forwardRef render functions accept exactly two parameters: props and ref. %s",j.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),j!=null&&j.defaultProps!=null&&console.error("forwardRef render functions do not support defaultProps. Did you accidentally pass a React component?");var J0={$$typeof:w0,render:j},F0;return Object.defineProperty(J0,"displayName",{enumerable:!1,configurable:!0,get:function(){return F0},set:function(R0){F0=R0,j.name||j.displayName||(Object.defineProperty(j,"name",{value:R0}),j.displayName=R0)}}),J0},Fo.isValidElement=C,Fo.lazy=function(j){j={_status:-1,_result:j};var J0={$$typeof:m0,_payload:j,_init:c},F0={name:"lazy",start:-1,end:-1,value:null,owner:null,debugStack:Error("react-stack-top-frame"),debugTask:console.createTask?console.createTask("lazy()"):null};return j._ioInfo=F0,J0._debugInfo=[{awaited:F0}],J0},Fo.memo=function(j,J0){j==null&&console.error("memo: The first argument must be a component. Instead received: %s",j===null?"null":typeof j),J0={$$typeof:k0,type:j,compare:J0===void 0?null:J0};var F0;return Object.defineProperty(J0,"displayName",{enumerable:!1,configurable:!0,get:function(){return F0},set:function(R0){F0=R0,j.name||j.displayName||(Object.defineProperty(j,"name",{value:R0}),j.displayName=R0)}}),J0},Fo.startTransition=function(j){var J0=g0.T,F0={};F0._updatedFibers=new Set,g0.T=F0;try{var R0=j(),f0=g0.S;f0!==null&&f0(F0,R0),typeof R0==="object"&&R0!==null&&typeof R0.then==="function"&&(g0.asyncTransitions++,R0.then(r,r),R0.then($,F5))}catch(J1){F5(J1)}finally{J0===null&&F0._updatedFibers&&(j=F0._updatedFibers.size,F0._updatedFibers.clear(),10{(function(){function Z(){if(E=!1,u){var n=Po.unstable_now();r=n;var Y0=!0;try{Z:{v=!1,C&&(C=!1,S(c),c=-1),R=!0;var $0=T;try{Y:{K(n);for(L=J(_);L!==null&&!(L.expirationTime>n&&W());){var o=L.callback;if(typeof o==="function"){L.callback=null,T=L.priorityLevel;var Q0=o(L.expirationTime<=n);if(n=Po.unstable_now(),typeof Q0==="function"){L.callback=Q0,K(n),Y0=!0;break Y}L===J(_)&&q(_),K(n)}else q(_);L=J(_)}if(L!==null)Y0=!0;else{var f=J(A);f!==null&&U($,f.startTime-n),Y0=!1}}break Z}finally{L=null,T=$0,R=!1}Y0=void 0}}finally{Y0?q0():u=!1}}}function Y(n,Y0){var $0=n.length;n.push(Y0);Z:for(;0<$0;){var o=$0-1>>>1,Q0=n[o];if(0>>1;oz(N0,$0))sz(w0,N0)?(n[o]=w0,n[s]=$0,o=s):(n[o]=N0,n[W0]=$0,o=W0);else if(sz(w0,$0))n[o]=w0,n[s]=$0,o=s;else break Z}}return Y0}function z(n,Y0){var $0=n.sortIndex-Y0.sortIndex;return $0!==0?$0:n.id-Y0.id}function K(n){for(var Y0=J(A);Y0!==null;){if(Y0.callback===null)q(A);else if(Y0.startTime<=n)q(A),Y0.sortIndex=Y0.expirationTime,Y(_,Y0);else break;Y0=J(A)}}function $(n){if(C=!1,K(n),!v)if(J(_)!==null)v=!0,u||(u=!0,q0());else{var Y0=J(A);Y0!==null&&U($,Y0.startTime-n)}}function W(){return E?!0:Po.unstable_now()-rn||125o?(n.sortIndex=$0,Y(A,n),J(_)===null&&n===J(A)&&(C?(S(c),c=-1):C=!0,U($,$0-o))):(n.sortIndex=Q0,Y(_,n),v||R||(v=!0,u||(u=!0,q0()))),n},Po.unstable_shouldYield=W,Po.unstable_wrapCallback=function(n){var Y0=T;return function(){var $0=T;T=Y0;try{return n.apply(this,arguments)}finally{T=$0}}},typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var sy=i6((Vo)=>{var TF=g(Z0());(function(){function Z(){}function Y(H){return""+H}function J(H,O,_){var A=3"u"&&console.error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var f0=new MessageChannel;f0.port1.onmessage=R0,f0.port2.postMessage(void 0)}}return t5(j)}function z0(j){return 1 ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),{then:function(j0,b5){f0=!0,h0.then(function(N5){if(M0(J0,F0),F0===0){try{Y0(R0),q0(function(){return n(N5,j0,b5)})}catch(U2){g0.thrownErrors.push(U2)}if(0 ...)"))}),g0.actQueue=null),0g0.recentlyCreatedOwnerStacks++;return T(j,f0,R0,O(),j0?Error("react-stack-top-frame"):M5,j0?l0(H(j)):H7)},vo.createRef=function(){var j={current:null};return Object.seal(j),j},vo.forwardRef=function(j){j!=null&&j.$$typeof===k0?console.error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof j!=="function"?console.error("forwardRef requires a render function but was given %s.",j===null?"null":typeof j):j.length!==0&&j.length!==2&&console.error("forwardRef render functions accept exactly two parameters: props and ref. %s",j.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),j!=null&&j.defaultProps!=null&&console.error("forwardRef render functions do not support defaultProps. Did you accidentally pass a React component?");var J0={$$typeof:w0,render:j},F0;return Object.defineProperty(J0,"displayName",{enumerable:!1,configurable:!0,get:function(){return F0},set:function(R0){F0=R0,j.name||j.displayName||(Object.defineProperty(j,"name",{value:R0}),j.displayName=R0)}}),J0},vo.isValidElement=C,vo.lazy=function(j){j={_status:-1,_result:j};var J0={$$typeof:m0,_payload:j,_init:c},F0={name:"lazy",start:-1,end:-1,value:null,owner:null,debugStack:Error("react-stack-top-frame"),debugTask:console.createTask?console.createTask("lazy()"):null};return j._ioInfo=F0,J0._debugInfo=[{awaited:F0}],J0},vo.memo=function(j,J0){j==null&&console.error("memo: The first argument must be a component. Instead received: %s",j===null?"null":typeof j),J0={$$typeof:k0,type:j,compare:J0===void 0?null:J0};var F0;return Object.defineProperty(J0,"displayName",{enumerable:!1,configurable:!0,get:function(){return F0},set:function(R0){F0=R0,j.name||j.displayName||(Object.defineProperty(j,"name",{value:R0}),j.displayName=R0)}}),J0},vo.startTransition=function(j){var J0=g0.T,F0={};F0._updatedFibers=new Set,g0.T=F0;try{var R0=j(),f0=g0.S;f0!==null&&f0(F0,R0),typeof R0==="object"&&R0!==null&&typeof R0.then==="function"&&(g0.asyncTransitions++,R0.then(r,r),R0.then($,F5))}catch(Q1){F5(Q1)}finally{J0===null&&F0._updatedFibers&&(j=F0._updatedFibers.size,F0._updatedFibers.clear(),10{(function(){function Z(){if(E=!1,h){var n=Ro.unstable_now();r=n;var Y0=!0;try{Z:{v=!1,C&&(C=!1,S(c),c=-1),R=!0;var $0=T;try{Y:{K(n);for(L=J(_);L!==null&&!(L.expirationTime>n&&W());){var o=L.callback;if(typeof o==="function"){L.callback=null,T=L.priorityLevel;var Q0=o(L.expirationTime<=n);if(n=Ro.unstable_now(),typeof Q0==="function"){L.callback=Q0,K(n),Y0=!0;break Y}L===J(_)&&q(_),K(n)}else q(_);L=J(_)}if(L!==null)Y0=!0;else{var f=J(A);f!==null&&U($,f.startTime-n),Y0=!1}}break Z}finally{L=null,T=$0,R=!1}Y0=void 0}}finally{Y0?q0():h=!1}}}function Y(n,Y0){var $0=n.length;n.push(Y0);Z:for(;0<$0;){var o=$0-1>>>1,Q0=n[o];if(0>>1;oz(N0,$0))sz(w0,N0)?(n[o]=w0,n[s]=$0,o=s):(n[o]=N0,n[W0]=$0,o=W0);else if(sz(w0,$0))n[o]=w0,n[s]=$0,o=s;else break Z}}return Y0}function z(n,Y0){var $0=n.sortIndex-Y0.sortIndex;return $0!==0?$0:n.id-Y0.id}function K(n){for(var Y0=J(A);Y0!==null;){if(Y0.callback===null)q(A);else if(Y0.startTime<=n)q(A),Y0.sortIndex=Y0.expirationTime,Y(_,Y0);else break;Y0=J(A)}}function $(n){if(C=!1,K(n),!v)if(J(_)!==null)v=!0,h||(h=!0,q0());else{var Y0=J(A);Y0!==null&&U($,Y0.startTime-n)}}function W(){return E?!0:Ro.unstable_now()-rn||125o?(n.sortIndex=$0,Y(A,n),J(_)===null&&n===J(A)&&(C?(S(c),c=-1):C=!0,U($,$0-o))):(n.sortIndex=Q0,Y(_,n),v||R||(v=!0,h||(h=!0,q0()))),n},Ro.unstable_shouldYield=W,Ro.unstable_wrapCallback=function(n){var Y0=T;return function(){var $0=T;T=Y0;try{return n.apply(this,arguments)}finally{T=$0}}},typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var iy=o6((To)=>{var TF=g(Z0());(function(){function Z(){}function Y(H){return""+H}function J(H,O,_){var A=3` tag.%s',_),typeof H==="string"&&typeof O==="object"&&O!==null&&typeof O.as==="string"){_=O.as;var A=q(_,O.crossOrigin);W.d.L(H,_,{crossOrigin:A,integrity:typeof O.integrity==="string"?O.integrity:void 0,nonce:typeof O.nonce==="string"?O.nonce:void 0,type:typeof O.type==="string"?O.type:void 0,fetchPriority:typeof O.fetchPriority==="string"?O.fetchPriority:void 0,referrerPolicy:typeof O.referrerPolicy==="string"?O.referrerPolicy:void 0,imageSrcSet:typeof O.imageSrcSet==="string"?O.imageSrcSet:void 0,imageSizes:typeof O.imageSizes==="string"?O.imageSizes:void 0,media:typeof O.media==="string"?O.media:void 0})}},Vo.preloadModule=function(H,O){var _="";typeof H==="string"&&H||(_+=" The `href` argument encountered was "+z(H)+"."),O!==void 0&&typeof O!=="object"?_+=" The `options` argument encountered was "+z(O)+".":O&&("as"in O)&&typeof O.as!=="string"&&(_+=" The `as` option encountered was "+z(O.as)+"."),_&&console.error('ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `` tag.%s',_),typeof H==="string"&&(O?(_=q(O.as,O.crossOrigin),W.d.m(H,{as:typeof O.as==="string"&&O.as!=="script"?O.as:void 0,crossOrigin:_,integrity:typeof O.integrity==="string"?O.integrity:void 0})):W.d.m(H))},Vo.requestFormReset=function(H){W.d.r(H)},Vo.unstable_batchedUpdates=function(H,O){return H(O)},Vo.useFormState=function(H,O,_){return $().useFormState(H,O,_)},Vo.useFormStatus=function(){return $().useHostTransitionStatus()},Vo.version="19.2.4",typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var d3=i6((oq0,oy)=>{var Lo=g(sy());oy.exports=Lo});var iy=i6((vo)=>{var t1=g(ay()),HX=g(Z0()),CF=g(d3());(function(){function Z(Q,X){for(Q=Q.memoizedState;Q!==null&&0=X.length)return G;var w=X[B],N=g2(Q)?Q.slice():y1({},Q);return N[w]=Y(Q[w],X,B+1,G),N}function J(Q,X,B){if(X.length!==B.length)console.warn("copyWithRename() expects paths of the same length");else{for(var G=0;GS8?console.error("Unexpected pop."):(X!==XA[S8]&&console.error("Unexpected Fiber popped."),Q.current=QA[S8],QA[S8]=null,XA[S8]=null,S8--)}function z0(Q,X,B){S8++,QA[S8]=Q.current,XA[S8]=B,Q.current=X}function M0(Q){return Q===null&&console.error("Expected host context to exist. This error is likely caused by a bug in React. Please file an issue."),Q}function n(Q,X){z0(i9,X,Q),z0(FB,Q,Q),z0(o9,null,Q);var B=X.nodeType;switch(B){case 9:case 11:B=B===9?"#document":"#fragment",X=(X=X.documentElement)?(X=X.namespaceURI)?ED(X):s8:s8;break;default:if(B=X.tagName,X=X.namespaceURI)X=ED(X),X=ID(X,B);else switch(B){case"svg":X=MX;break;case"math":X=PU;break;default:X=s8}}B=B.toLowerCase(),B=vT(null,B),B={context:X,ancestorInfo:B},q0(o9,Q),z0(o9,B,Q)}function Y0(Q){q0(o9,Q),q0(FB,Q),q0(i9,Q)}function $0(){return M0(o9.current)}function o(Q){Q.memoizedState!==null&&z0(vG,Q,Q);var X=M0(o9.current),B=Q.type,G=ID(X.context,B);B=vT(X.ancestorInfo,B),G={context:G,ancestorInfo:B},X!==G&&(z0(FB,Q,Q),z0(o9,G,Q))}function Q0(Q){FB.current===Q&&(q0(o9,Q),q0(FB,Q)),vG.current===Q&&(q0(vG,Q),GK._currentValue=tY)}function f(){}function W0(){if(PB===0){BE=console.log,KE=console.info,$E=console.warn,WE=console.error,GE=console.group,UE=console.groupCollapsed,ME=console.groupEnd;var Q={configurable:!0,enumerable:!0,value:f,writable:!0};Object.defineProperties(console,{info:Q,log:Q,warn:Q,error:Q,group:Q,groupCollapsed:Q,groupEnd:Q})}PB++}function N0(){if(PB--,PB===0){var Q={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:y1({},Q,{value:BE}),info:y1({},Q,{value:KE}),warn:y1({},Q,{value:$E}),error:y1({},Q,{value:WE}),group:y1({},Q,{value:GE}),groupCollapsed:y1({},Q,{value:UE}),groupEnd:y1({},Q,{value:ME})})}0>PB&&console.error("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}function s(Q){var X=Error.prepareStackTrace;if(Error.prepareStackTrace=void 0,Q=Q.stack,Error.prepareStackTrace=X,Q.startsWith(`Error: react-stack-top-frame +See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.`),H}typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());var W={d:{f:Z,r:function(){throw Error("Invalid form element. requestFormReset must be passed a form that was rendered by React.")},D:Z,C:Z,L:Z,m:Z,X:Z,S:Z,M:Z},p:0,findDOMNode:null},U=Symbol.for("react.portal"),M=TF.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;typeof Map==="function"&&Map.prototype!=null&&typeof Map.prototype.forEach==="function"&&typeof Set==="function"&&Set.prototype!=null&&typeof Set.prototype.clear==="function"&&typeof Set.prototype.forEach==="function"||console.error("React depends on Map and Set built-in types. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),To.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=W,To.createPortal=function(H,O){var _=2` tag.%s',_),typeof H==="string"&&typeof O==="object"&&O!==null&&typeof O.as==="string"){_=O.as;var A=q(_,O.crossOrigin);W.d.L(H,_,{crossOrigin:A,integrity:typeof O.integrity==="string"?O.integrity:void 0,nonce:typeof O.nonce==="string"?O.nonce:void 0,type:typeof O.type==="string"?O.type:void 0,fetchPriority:typeof O.fetchPriority==="string"?O.fetchPriority:void 0,referrerPolicy:typeof O.referrerPolicy==="string"?O.referrerPolicy:void 0,imageSrcSet:typeof O.imageSrcSet==="string"?O.imageSrcSet:void 0,imageSizes:typeof O.imageSizes==="string"?O.imageSizes:void 0,media:typeof O.media==="string"?O.media:void 0})}},To.preloadModule=function(H,O){var _="";typeof H==="string"&&H||(_+=" The `href` argument encountered was "+z(H)+"."),O!==void 0&&typeof O!=="object"?_+=" The `options` argument encountered was "+z(O)+".":O&&("as"in O)&&typeof O.as!=="string"&&(_+=" The `as` option encountered was "+z(O.as)+"."),_&&console.error('ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `` tag.%s',_),typeof H==="string"&&(O?(_=q(O.as,O.crossOrigin),W.d.m(H,{as:typeof O.as==="string"&&O.as!=="script"?O.as:void 0,crossOrigin:_,integrity:typeof O.integrity==="string"?O.integrity:void 0})):W.d.m(H))},To.requestFormReset=function(H){W.d.r(H)},To.unstable_batchedUpdates=function(H,O){return H(O)},To.useFormState=function(H,O,_){return $().useFormState(H,O,_)},To.useFormStatus=function(){return $().useHostTransitionStatus()},To.version="19.2.4",typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var d3=o6((sq0,ty)=>{var Co=g(iy());ty.exports=Co});var ny=o6((bo)=>{var t1=g(oy()),wX=g(Z0()),CF=g(d3());(function(){function Z(Q,X){for(Q=Q.memoizedState;Q!==null&&0=X.length)return G;var w=X[B],N=g2(Q)?Q.slice():y1({},Q);return N[w]=Y(Q[w],X,B+1,G),N}function J(Q,X,B){if(X.length!==B.length)console.warn("copyWithRename() expects paths of the same length");else{for(var G=0;GS8?console.error("Unexpected pop."):(X!==XA[S8]&&console.error("Unexpected Fiber popped."),Q.current=QA[S8],QA[S8]=null,XA[S8]=null,S8--)}function z0(Q,X,B){S8++,QA[S8]=Q.current,XA[S8]=B,Q.current=X}function M0(Q){return Q===null&&console.error("Expected host context to exist. This error is likely caused by a bug in React. Please file an issue."),Q}function n(Q,X){z0(s9,X,Q),z0(OB,Q,Q),z0(a9,null,Q);var B=X.nodeType;switch(B){case 9:case 11:B=B===9?"#document":"#fragment",X=(X=X.documentElement)?(X=X.namespaceURI)?yD(X):s8:s8;break;default:if(B=X.tagName,X=X.namespaceURI)X=yD(X),X=jD(X,B);else switch(B){case"svg":X=UX;break;case"math":X=FU;break;default:X=s8}}B=B.toLowerCase(),B=TT(null,B),B={context:X,ancestorInfo:B},q0(a9,Q),z0(a9,B,Q)}function Y0(Q){q0(a9,Q),q0(OB,Q),q0(s9,Q)}function $0(){return M0(a9.current)}function o(Q){Q.memoizedState!==null&&z0(LG,Q,Q);var X=M0(a9.current),B=Q.type,G=jD(X.context,B);B=TT(X.ancestorInfo,B),G={context:G,ancestorInfo:B},X!==G&&(z0(OB,Q,Q),z0(a9,G,Q))}function Q0(Q){OB.current===Q&&(q0(a9,Q),q0(OB,Q)),LG.current===Q&&(q0(LG,Q),KK._currentValue=oY)}function f(){}function W0(){if(_B===0){$E=console.log,WE=console.info,GE=console.warn,UE=console.error,ME=console.group,wE=console.groupCollapsed,HE=console.groupEnd;var Q={configurable:!0,enumerable:!0,value:f,writable:!0};Object.defineProperties(console,{info:Q,log:Q,warn:Q,error:Q,group:Q,groupCollapsed:Q,groupEnd:Q})}_B++}function N0(){if(_B--,_B===0){var Q={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:y1({},Q,{value:$E}),info:y1({},Q,{value:WE}),warn:y1({},Q,{value:GE}),error:y1({},Q,{value:UE}),group:y1({},Q,{value:ME}),groupCollapsed:y1({},Q,{value:wE}),groupEnd:y1({},Q,{value:HE})})}0>_B&&console.error("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}function s(Q){var X=Error.prepareStackTrace;if(Error.prepareStackTrace=void 0,Q=Q.stack,Error.prepareStackTrace=X,Q.startsWith(`Error: react-stack-top-frame `)&&(Q=Q.slice(29)),X=Q.indexOf(` `),X!==-1&&(Q=Q.slice(X+1)),X=Q.indexOf("react_stack_bottom_frame"),X!==-1&&(X=Q.lastIndexOf(` -`,X)),X!==-1)Q=Q.slice(0,X);else return"";return Q}function w0(Q){if(qA===void 0)try{throw Error()}catch(B){var X=B.stack.trim().match(/\n( *(at )?)/);qA=X&&X[1]||"",wE=-1)":-1F||I[N]!==i[F]){var t=` `+I[N].replace(" at new "," at ");return Q.displayName&&t.includes("")&&(t=t.replace("",Q.displayName)),typeof Q==="function"&&BA.set(Q,t),t}while(1<=N&&0<=F);break}}}finally{zA=!1,H0.H=G,N0(),Error.prepareStackTrace=B}return I=(I=Q?Q.displayName||Q.name:"")?w0(I):"",typeof Q==="function"&&BA.set(Q,I),I}function G0(Q,X){switch(Q.tag){case 26:case 27:case 5:return w0(Q.type);case 16:return w0("Lazy");case 13:return Q.child!==X&&X!==null?w0("Suspense Fallback"):w0("Suspense");case 19:return w0("SuspenseList");case 0:case 15:return E0(Q.type,!1);case 11:return E0(Q.type.render,!1);case 1:return E0(Q.type,!0);case 31:return w0("Activity");default:return""}}function k0(Q){try{var X="",B=null;do{X+=G0(Q,B);var G=Q._debugInfo;if(G)for(var w=G.length-1;0<=w;w--){var N=G[w];if(typeof N.name==="string"){var F=X;Z:{var{name:V,env:b,debugLocation:I}=N;if(I!=null){var i=s(I),t=i.lastIndexOf(` `),d=t===-1?i:i.slice(t+1);if(d.indexOf(V)!==-1){var B0=` `+d;break Z}}B0=w0(V+(b?" ["+b+"]":""))}X=F+B0}}B=Q,Q=Q.return}while(Q);return X}catch(T0){return` Error generating stack: `+T0.message+` -`+T0.stack}}function m0(Q){return(Q=Q?Q.displayName||Q.name:"")?w0(Q):""}function M1(){if(m4===null)return null;var Q=m4._debugOwner;return Q!=null?c(Q):null}function q1(){if(m4===null)return"";var Q=m4;try{var X="";switch(Q.tag===6&&(Q=Q.return),Q.tag){case 26:case 27:case 5:X+=w0(Q.type);break;case 13:X+=w0("Suspense");break;case 19:X+=w0("SuspenseList");break;case 31:X+=w0("Activity");break;case 30:case 0:case 15:case 1:Q._debugOwner||X!==""||(X+=m0(Q.type));break;case 11:Q._debugOwner||X!==""||(X+=m0(Q.type.render))}for(;Q;)if(typeof Q.tag==="number"){var B=Q;Q=B._debugOwner;var G=B._debugStack;if(Q&&G){var w=s(G);w!==""&&(X+=` +`+T0.stack}}function m0(Q){return(Q=Q?Q.displayName||Q.name:"")?w0(Q):""}function M1(){if(h4===null)return null;var Q=h4._debugOwner;return Q!=null?c(Q):null}function z1(){if(h4===null)return"";var Q=h4;try{var X="";switch(Q.tag===6&&(Q=Q.return),Q.tag){case 26:case 27:case 5:X+=w0(Q.type);break;case 13:X+=w0("Suspense");break;case 19:X+=w0("SuspenseList");break;case 31:X+=w0("Activity");break;case 30:case 0:case 15:case 1:Q._debugOwner||X!==""||(X+=m0(Q.type));break;case 11:Q._debugOwner||X!==""||(X+=m0(Q.type.render))}for(;Q;)if(typeof Q.tag==="number"){var B=Q;Q=B._debugOwner;var G=B._debugStack;if(Q&&G){var w=s(G);w!==""&&(X+=` `+w)}}else if(Q.debugStack!=null){var N=Q.debugStack;(Q=Q.owner)&&N&&(X+=` `+s(N))}else break;var F=X}catch(V){F=` Error generating stack: `+V.message+` -`+V.stack}return F}function v0(Q,X,B,G,w,N,F){var V=m4;L1(Q);try{return Q!==null&&Q._debugTask?Q._debugTask.run(X.bind(null,B,G,w,N,F)):X(B,G,w,N,F)}finally{L1(V)}throw Error("runWithFiberInDEV should never be called in production. This is a bug in React.")}function L1(Q){H0.getCurrentStack=Q===null?null:q1,S3=!1,m4=Q}function J5(Q){return typeof Symbol==="function"&&Symbol.toStringTag&&Q[Symbol.toStringTag]||Q.constructor.name||"Object"}function C5(Q){try{return G5(Q),!1}catch(X){return!0}}function G5(Q){return""+Q}function w1(Q,X){if(C5(Q))return console.error("The provided `%s` attribute is an unsupported type %s. This value must be coerced to a string before using it here.",X,J5(Q)),G5(Q)}function m1(Q,X){if(C5(Q))return console.error("The provided `%s` CSS property is an unsupported type %s. This value must be coerced to a string before using it here.",X,J5(Q)),G5(Q)}function g0(Q){if(C5(Q))return console.error("Form field values (value, checked, defaultValue, or defaultChecked props) must be strings, not %s. This value must be coerced to a string before using it here.",J5(Q)),G5(Q)}function l1(Q){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u")return!1;var X=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(X.isDisabled)return!0;if(!X.supportsFiber)return console.error("The installed version of React DevTools is too old and will not work with the current version of React. Please update React DevTools. https://react.dev/link/react-devtools"),!0;try{xQ=X.inject(Q),k7=X}catch(B){console.error("React instrumentation encountered an error: %o.",B)}return X.checkDCE?!0:!1}function l0(Q){if(typeof fa==="function"&&ga(Q),k7&&typeof k7.setStrictMode==="function")try{k7.setStrictMode(xQ,Q)}catch(X){x3||(x3=!0,console.error("React instrumentation encountered an error: %o",X))}}function J2(Q){return Q>>>=0,Q===0?32:31-(ua(Q)/ha|0)|0}function U5(Q){var X=Q&42;if(X!==0)return X;switch(Q&-Q){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return Q&261888;case 262144:case 524288:case 1048576:case 2097152:return Q&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return Q&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return console.error("Should have found matching lanes. This is a bug in React."),Q}}function Q5(Q,X,B){var G=Q.pendingLanes;if(G===0)return 0;var w=0,N=Q.suspendedLanes,F=Q.pingedLanes;Q=Q.warmLanes;var V=G&134217727;return V!==0?(G=V&~N,G!==0?w=U5(G):(F&=V,F!==0?w=U5(F):B||(B=V&~Q,B!==0&&(w=U5(B))))):(V=G&~N,V!==0?w=U5(V):F!==0?w=U5(F):B||(B=G&~Q,B!==0&&(w=U5(B)))),w===0?0:X!==0&&X!==w&&(X&N)===0&&(N=w&-w,B=X&-X,N>=B||N===32&&(B&4194048)!==0)?X:w}function M5(Q,X){return(Q.pendingLanes&~(Q.suspendedLanes&~Q.pingedLanes)&X)===0}function M7(Q,X){switch(Q){case 1:case 2:case 4:case 8:case 64:return X+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return X+5000;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return console.error("Should have found matching lanes. This is a bug in React."),-1}}function w7(){var Q=CG;return CG<<=1,(CG&62914560)===0&&(CG=4194304),Q}function L2(Q){for(var X=[],B=0;31>B;B++)X.push(Q);return X}function F5(Q,X){Q.pendingLanes|=X,X!==268435456&&(Q.suspendedLanes=0,Q.pingedLanes=0,Q.warmLanes=0)}function e1(Q,X,B,G,w,N){var F=Q.pendingLanes;Q.pendingLanes=B,Q.suspendedLanes=0,Q.pingedLanes=0,Q.warmLanes=0,Q.expiredLanes&=B,Q.entangledLanes&=B,Q.errorRecoveryDisabledLanes&=B,Q.shellSuspendCounter=0;var{entanglements:V,expirationTimes:b,hiddenUpdates:I}=Q;for(B=F&~B;0"u")return null;try{return Q.activeElement||Q.body}catch(X){return Q.body}}function x0(Q){return Q.replace(la,function(X){return"\\"+X.charCodeAt(0).toString(16)+" "})}function a0(Q,X){X.checked===void 0||X.defaultChecked===void 0||FE||(console.error("%s contains an input of type %s with both checked and defaultChecked props. Input elements must be either controlled or uncontrolled (specify either the checked prop, or the defaultChecked prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://react.dev/link/controlled-components",M1()||"A component",X.type),FE=!0),X.value===void 0||X.defaultValue===void 0||AE||(console.error("%s contains an input of type %s with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://react.dev/link/controlled-components",M1()||"A component",X.type),AE=!0)}function o0(Q,X,B,G,w,N,F,V){if(Q.name="",F!=null&&typeof F!=="function"&&typeof F!=="symbol"&&typeof F!=="boolean"?(w1(F,"type"),Q.type=F):Q.removeAttribute("type"),X!=null)if(F==="number"){if(X===0&&Q.value===""||Q.value!=X)Q.value=""+e(X)}else Q.value!==""+e(X)&&(Q.value=""+e(X));else F!=="submit"&&F!=="reset"||Q.removeAttribute("value");X!=null?s0(Q,F,e(X)):B!=null?s0(Q,F,e(B)):G!=null&&Q.removeAttribute("value"),w==null&&N!=null&&(Q.defaultChecked=!!N),w!=null&&(Q.checked=w&&typeof w!=="function"&&typeof w!=="symbol"),V!=null&&typeof V!=="function"&&typeof V!=="symbol"&&typeof V!=="boolean"?(w1(V,"name"),Q.name=""+e(V)):Q.removeAttribute("name")}function Q1(Q,X,B,G,w,N,F,V){if(N!=null&&typeof N!=="function"&&typeof N!=="symbol"&&typeof N!=="boolean"&&(w1(N,"type"),Q.type=N),X!=null||B!=null){if(!(N!=="submit"&&N!=="reset"||X!==void 0&&X!==null)){V0(Q);return}B=B!=null?""+e(B):"",X=X!=null?""+e(X):B,V||X===Q.value||(Q.value=X),Q.defaultValue=X}G=G!=null?G:w,G=typeof G!=="function"&&typeof G!=="symbol"&&!!G,Q.checked=V?Q.checked:!!G,Q.defaultChecked=!!G,F!=null&&typeof F!=="function"&&typeof F!=="symbol"&&typeof F!=="boolean"&&(w1(F,"name"),Q.name=F),V0(Q)}function s0(Q,X,B){X==="number"&&S0(Q.ownerDocument)===Q||Q.defaultValue===""+B||(Q.defaultValue=""+B)}function r1(Q,X){X.value==null&&(typeof X.children==="object"&&X.children!==null?HX.Children.forEach(X.children,function(B){B==null||typeof B==="string"||typeof B==="number"||typeof B==="bigint"||VE||(VE=!0,console.error("Cannot infer the option value of complex children. Pass a `value` prop or use a plain string as children to