@burdenoff/microfe-bigconsole 2026.626.2 → 2026.630.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bigconsole/assistant/createSandboxAssistantTransport.js +1 -1
- package/dist/bigconsole/assistant/createSandboxAssistantTransport.js.map +1 -1
- package/dist/bigconsole/components/dashboard/LiveCanvasToggle.js +30 -0
- package/dist/bigconsole/components/dashboard/LiveCanvasToggle.js.map +1 -0
- package/dist/bigconsole/components/dashboard/index.js +1 -0
- package/dist/bigconsole/hooks/useDashboardOperations.js +5 -4
- package/dist/bigconsole/hooks/useDashboardOperations.js.map +1 -1
- package/dist/bigconsole/pages/DashboardViewPage.js +196 -167
- package/dist/bigconsole/pages/DashboardViewPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { gatherPageContext as c } from "./pageContext.js";
|
|
|
3
3
|
import { useCallback as l, useMemo as u, useRef as d } from "react";
|
|
4
4
|
import { useAuthToken as f } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
5
5
|
//#region src/bigconsole/assistant/createSandboxAssistantTransport.ts
|
|
6
|
-
var p = "
|
|
6
|
+
var p = "api-calls", m = 18e4, h = 1500, g = 3e4;
|
|
7
7
|
function _(e) {
|
|
8
8
|
try {
|
|
9
9
|
let t = e === "workspaceId" ? "burdenoff-active-context-workspace" : "burdenoff-active-context-organization", n = localStorage.getItem(t);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createSandboxAssistantTransport.js","names":[],"sources":["../../../src/bigconsole/assistant/createSandboxAssistantTransport.ts"],"sourcesContent":["/**\n * BigConsole adapter for the shared fe-libs AssistantWidget.\n *\n * The floating widget (fe-libs, Layer 1) is backend-agnostic — it calls an\n * injected `AssistantTransport`. This hook builds a transport that drives the\n * sandbox AI assistant (combined `assistant` mode = docs Q&A + api-calls): it\n * provisions/reuses a sandbox, opens a session, dispatches the prompt async,\n * then polls for the streamed answer. The agent introspects the GraphQL schema\n * and performs API calls on the user's behalf using their workspace token,\n * contextual to the current screen (via `gatherPageContext`).\n *\n * This is a faithful port of microfe-vibecontrols'\n * `services/createSandboxAssistantTransport.ts`; the only product-specific\n * difference lives in `assistantApi.createAssistantSandbox`\n * (`AI_ASSISTANT_PRODUCT=bigconsole`).\n */\n\nimport { useCallback, useMemo, useRef } from 'react';\nimport { useAuthToken } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n createAssistantSandbox,\n createAssistantSession,\n extendAssistantSandboxTTL,\n findExistingSandbox,\n getAssistantMessages,\n sendAssistantPromptAsync,\n waitForAssistantServiceReady,\n waitForSandboxReady,\n} from './assistantApi';\nimport { gatherPageContext } from './pageContext';\nimport type { AssistantMode, AssistantRawMessage, AssistantSandboxAuthContext } from './types';\n\n/**\n * Locally-defined mirror of the fe-libs `AssistantTransport` contract.\n *\n * Intentionally NOT imported from `@burdenoff/fe-libs`: microfe's tsconfig maps\n * `@burdenoff/fe-libs/*` to fe-libs *source*, so vite-plugin-dts would rewrite a\n * cross-package type used in this hook's public signature to a broken\n * source-relative path in the emitted `.d.ts`. Structural typing makes this\n * shape assignable to fe-libs' `AssistantTransport` at the call site\n * (bigconsole-app's AppShell), which is where compatibility is enforced.\n */\ninterface AssistantSendArgs {\n prompt: string;\n onProgress: (partialText: string) => void;\n signal: AbortSignal;\n}\n\nexport interface AssistantTransport {\n sendPrompt: (args: AssistantSendArgs) => Promise<{ text: string }>;\n}\n\nconst MODE: AssistantMode = 'assistant';\nconst STREAM_BUDGET_MS = 180_000;\nconst POLL_INTERVAL_MS = 1500;\nconst TTL_EXTEND_INTERVAL_MS = 30_000;\n\n// ── Context helpers (mirror vibecontrols' resolution) ────────────────\n\nfunction getProfileContextValue(key: 'workspaceId' | 'organizationId'): string {\n try {\n const activeContextKey =\n key === 'workspaceId' ? 'burdenoff-active-context-workspace' : 'burdenoff-active-context-organization';\n const activeContextValue = localStorage.getItem(activeContextKey);\n if (activeContextValue) return activeContextValue;\n\n const activeProfileId = sessionStorage.getItem('bf-active-profile');\n if (!activeProfileId) return '';\n const raw = localStorage.getItem(`bf-p-${activeProfileId}-context`);\n if (!raw) return '';\n const context = JSON.parse(raw) as { workspaceId?: string; organizationId?: string };\n return context[key] ?? '';\n } catch {\n return '';\n }\n}\n\nfunction getWorkspaceId(fallback: string | null): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('workspace') ?? getProfileContextValue('workspaceId') ?? fallback ?? '';\n}\n\nfunction getOrganizationId(): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('org') ?? getProfileContextValue('organizationId');\n}\n\n// ── Message-progress helpers (pure; mirror vibecontrols) ─────────────\n\nfunction getRawMessageCreatedAt(message: AssistantRawMessage): number {\n return message.info?.time?.created ?? 0;\n}\n\nfunction getAssistantText(parts: AssistantRawMessage['parts']): string {\n return (parts ?? [])\n .filter((part) => part.type === 'text' && typeof part.text === 'string')\n .map((part) => part.text?.trim() ?? '')\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction getToolProgress(parts: AssistantRawMessage['parts']): string[] {\n return (parts ?? [])\n .filter((part) => part.type === 'tool' && part.tool)\n .map((part) => {\n const status = part.state?.status ?? 'running';\n const tool = part.tool ?? 'tool';\n if (status === 'completed') return `Completed: ${tool}`;\n if (status === 'failed') return `Failed: ${tool}`;\n return `Running: ${tool}`;\n });\n}\n\nfunction buildProgress(messages: AssistantRawMessage[], sinceMs: number): { content: string; done: boolean } {\n const relevant = messages\n .filter((message) => message.info?.role === 'assistant' && getRawMessageCreatedAt(message) >= sinceMs)\n .sort((left, right) => getRawMessageCreatedAt(left) - getRawMessageCreatedAt(right));\n\n if (relevant.length === 0) return { content: 'Thinking…', done: false };\n\n const latest = relevant[relevant.length - 1]!;\n const text = getAssistantText(latest.parts);\n const toolProgress = getToolProgress(latest.parts);\n const content = [text, ...toolProgress].filter(Boolean).join('\\n\\n') || 'Thinking…';\n\n return {\n content,\n done: Boolean(latest.info?.time?.completed) && (Boolean(text) || toolProgress.length > 0),\n };\n}\n\nfunction sanitize(response: string): string {\n return response\n .replace(/(Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/(X-Workspace-Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9._-]+\\.[A-Za-z0-9._-]+\\b/g, '[REDACTED_JWT]')\n .replace(/\\bsk-ant-[A-Za-z0-9-]+\\b/g, '[REDACTED_API_KEY]');\n}\n\nfunction isRecoverable(error: unknown): boolean {\n const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n if (\n message.includes('rate limit exceeded') ||\n message.includes('unauthorized') ||\n message.includes('k8s api error 401')\n ) {\n return false;\n }\n return [\n 'sandbox not found',\n 'sandbox is not running',\n 'sandbox service not available yet',\n 'sandbox failed',\n 'sandbox startup timed out',\n 'assistant service did not become healthy',\n 'proxy error: 404',\n 'proxy error: 502',\n 'proxy error: 503',\n ].some((fragment) => message.includes(fragment));\n}\n\n/**\n * Returns a memoized `AssistantTransport` wired to the BigConsole sandbox\n * assistant agent. The sandbox + session are cached in refs so follow-up turns\n * reuse the warm environment for the lifetime of the host shell.\n */\nexport function useSandboxAssistantTransport(): AssistantTransport {\n const { getAccessToken, getWorkspaceToken, userId, workspaceId: ctxWorkspaceId } = useAuthToken();\n\n const sandboxIdRef = useRef<string | null>(null);\n const sessionIdRef = useRef<string | null>(null);\n\n const ensureRuntime = useCallback(\n async (\n workspaceId: string,\n authContext: AssistantSandboxAuthContext\n ): Promise<{ sandboxId: string; sessionId: string }> => {\n let sandboxId = sandboxIdRef.current;\n if (!sandboxId) {\n sandboxId = await findExistingSandbox(workspaceId, MODE, authContext);\n if (!sandboxId) {\n if (!getAccessToken()) {\n throw new Error('The assistant requires an authenticated session. Please sign in again.');\n }\n sandboxId = await createAssistantSandbox(MODE, workspaceId, authContext);\n }\n await waitForSandboxReady(sandboxId, workspaceId, authContext);\n await waitForAssistantServiceReady(sandboxId, workspaceId, authContext);\n sandboxIdRef.current = sandboxId;\n }\n\n let sessionId = sessionIdRef.current;\n if (!sessionId) {\n const result = await createAssistantSession(sandboxId, workspaceId, MODE, authContext);\n sessionId = result.sessionId;\n sessionIdRef.current = sessionId;\n }\n\n return { sandboxId, sessionId };\n },\n [getAccessToken]\n );\n\n const sendPrompt = useCallback(\n async ({ prompt, onProgress, signal }: AssistantSendArgs): Promise<{ text: string }> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId) {\n throw new Error('The assistant needs an active workspace. Open a workspace and try again.');\n }\n const authContext: AssistantSandboxAuthContext = {\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n userId,\n organizationId: getOrganizationId(),\n };\n\n const run = async (attempt: 1 | 2): Promise<{ text: string }> => {\n try {\n const { sandboxId, sessionId } = await ensureRuntime(workspaceId, authContext);\n\n const startedAt = Date.now();\n await sendAssistantPromptAsync(\n sandboxId,\n workspaceId,\n sessionId,\n prompt,\n MODE,\n gatherPageContext(),\n authContext\n );\n\n const timeoutAt = Date.now() + STREAM_BUDGET_MS;\n let lastTtlExtensionAt = Date.now();\n let progress = buildProgress([], startedAt);\n\n while (Date.now() < timeoutAt) {\n if (signal.aborted) throw new Error('Cancelled');\n\n const messages = await getAssistantMessages(sandboxId, workspaceId, sessionId, authContext, 50);\n progress = buildProgress(messages, startedAt);\n onProgress(sanitize(progress.content));\n if (progress.done) break;\n\n if (Date.now() - lastTtlExtensionAt > TTL_EXTEND_INTERVAL_MS) {\n await extendAssistantSandboxTTL(sandboxId, workspaceId, 600, authContext).catch(() => undefined);\n lastTtlExtensionAt = Date.now();\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (!progress.done) {\n throw new Error('The assistant timed out while responding. Please try again.');\n }\n\n return { text: sanitize(progress.content) };\n } catch (error) {\n // A stale/expired sandbox or session is recoverable — drop the warm\n // refs and retry once from a clean runtime.\n if (attempt === 1 && isRecoverable(error)) {\n sandboxIdRef.current = null;\n sessionIdRef.current = null;\n return run(2);\n }\n throw error;\n }\n };\n\n return run(1);\n },\n [ctxWorkspaceId, ensureRuntime, getAccessToken, getWorkspaceToken, userId]\n );\n\n return useMemo<AssistantTransport>(() => ({ sendPrompt }), [sendPrompt]);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createSandboxAssistantTransport.js","names":[],"sources":["../../../src/bigconsole/assistant/createSandboxAssistantTransport.ts"],"sourcesContent":["/**\n * BigConsole adapter for the shared fe-libs AssistantWidget.\n *\n * The floating widget (fe-libs, Layer 1) is backend-agnostic — it calls an\n * injected `AssistantTransport`. This hook builds a transport that drives the\n * sandbox AI assistant (combined `assistant` mode = docs Q&A + api-calls): it\n * provisions/reuses a sandbox, opens a session, dispatches the prompt async,\n * then polls for the streamed answer. The agent introspects the GraphQL schema\n * and performs API calls on the user's behalf using their workspace token,\n * contextual to the current screen (via `gatherPageContext`).\n *\n * This is a faithful port of microfe-vibecontrols'\n * `services/createSandboxAssistantTransport.ts`; the only product-specific\n * difference lives in `assistantApi.createAssistantSandbox`\n * (`AI_ASSISTANT_PRODUCT=bigconsole`).\n */\n\nimport { useCallback, useMemo, useRef } from 'react';\nimport { useAuthToken } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n createAssistantSandbox,\n createAssistantSession,\n extendAssistantSandboxTTL,\n findExistingSandbox,\n getAssistantMessages,\n sendAssistantPromptAsync,\n waitForAssistantServiceReady,\n waitForSandboxReady,\n} from './assistantApi';\nimport { gatherPageContext } from './pageContext';\nimport type { AssistantMode, AssistantRawMessage, AssistantSandboxAuthContext } from './types';\n\n/**\n * Locally-defined mirror of the fe-libs `AssistantTransport` contract.\n *\n * Intentionally NOT imported from `@burdenoff/fe-libs`: microfe's tsconfig maps\n * `@burdenoff/fe-libs/*` to fe-libs *source*, so vite-plugin-dts would rewrite a\n * cross-package type used in this hook's public signature to a broken\n * source-relative path in the emitted `.d.ts`. Structural typing makes this\n * shape assignable to fe-libs' `AssistantTransport` at the call site\n * (bigconsole-app's AppShell), which is where compatibility is enforced.\n */\ninterface AssistantSendArgs {\n prompt: string;\n onProgress: (partialText: string) => void;\n signal: AbortSignal;\n}\n\nexport interface AssistantTransport {\n sendPrompt: (args: AssistantSendArgs) => Promise<{ text: string }>;\n}\n\n// The sandbox runs the `ai-assistant-api-calls` image (see assistantApi\n// `getImageForMode` — `assistant` already maps to that image). That image's\n// runtime accepts `api-calls` / `general` / `vibe-plugins`, but NOT the combined\n// `assistant` mode — sending `assistant` makes the agent reject session creation\n// with \"Invalid mode: assistant\". `api-calls` is the same image and lets the\n// agent execute GraphQL on the user's behalf (build the DataSink→Parser→Widget→\n// Dashboard pipeline + answer). Switch back to `assistant` only once an agent\n// image that supports the combined mode is rolled out.\nconst MODE: AssistantMode = 'api-calls';\nconst STREAM_BUDGET_MS = 180_000;\nconst POLL_INTERVAL_MS = 1500;\nconst TTL_EXTEND_INTERVAL_MS = 30_000;\n\n// ── Context helpers (mirror vibecontrols' resolution) ────────────────\n\nfunction getProfileContextValue(key: 'workspaceId' | 'organizationId'): string {\n try {\n const activeContextKey =\n key === 'workspaceId' ? 'burdenoff-active-context-workspace' : 'burdenoff-active-context-organization';\n const activeContextValue = localStorage.getItem(activeContextKey);\n if (activeContextValue) return activeContextValue;\n\n const activeProfileId = sessionStorage.getItem('bf-active-profile');\n if (!activeProfileId) return '';\n const raw = localStorage.getItem(`bf-p-${activeProfileId}-context`);\n if (!raw) return '';\n const context = JSON.parse(raw) as { workspaceId?: string; organizationId?: string };\n return context[key] ?? '';\n } catch {\n return '';\n }\n}\n\nfunction getWorkspaceId(fallback: string | null): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('workspace') ?? getProfileContextValue('workspaceId') ?? fallback ?? '';\n}\n\nfunction getOrganizationId(): string {\n const params = new URLSearchParams(window.location.search);\n return params.get('org') ?? getProfileContextValue('organizationId');\n}\n\n// ── Message-progress helpers (pure; mirror vibecontrols) ─────────────\n\nfunction getRawMessageCreatedAt(message: AssistantRawMessage): number {\n return message.info?.time?.created ?? 0;\n}\n\nfunction getAssistantText(parts: AssistantRawMessage['parts']): string {\n return (parts ?? [])\n .filter((part) => part.type === 'text' && typeof part.text === 'string')\n .map((part) => part.text?.trim() ?? '')\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction getToolProgress(parts: AssistantRawMessage['parts']): string[] {\n return (parts ?? [])\n .filter((part) => part.type === 'tool' && part.tool)\n .map((part) => {\n const status = part.state?.status ?? 'running';\n const tool = part.tool ?? 'tool';\n if (status === 'completed') return `Completed: ${tool}`;\n if (status === 'failed') return `Failed: ${tool}`;\n return `Running: ${tool}`;\n });\n}\n\nfunction buildProgress(messages: AssistantRawMessage[], sinceMs: number): { content: string; done: boolean } {\n const relevant = messages\n .filter((message) => message.info?.role === 'assistant' && getRawMessageCreatedAt(message) >= sinceMs)\n .sort((left, right) => getRawMessageCreatedAt(left) - getRawMessageCreatedAt(right));\n\n if (relevant.length === 0) return { content: 'Thinking…', done: false };\n\n const latest = relevant[relevant.length - 1]!;\n const text = getAssistantText(latest.parts);\n const toolProgress = getToolProgress(latest.parts);\n const content = [text, ...toolProgress].filter(Boolean).join('\\n\\n') || 'Thinking…';\n\n return {\n content,\n done: Boolean(latest.info?.time?.completed) && (Boolean(text) || toolProgress.length > 0),\n };\n}\n\nfunction sanitize(response: string): string {\n return response\n .replace(/(Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/(X-Workspace-Authorization\\s*:\\s*Bearer\\s+)[^\\s\\n]+/gi, '$1[REDACTED]')\n .replace(/\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9._-]+\\.[A-Za-z0-9._-]+\\b/g, '[REDACTED_JWT]')\n .replace(/\\bsk-ant-[A-Za-z0-9-]+\\b/g, '[REDACTED_API_KEY]');\n}\n\nfunction isRecoverable(error: unknown): boolean {\n const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n if (\n message.includes('rate limit exceeded') ||\n message.includes('unauthorized') ||\n message.includes('k8s api error 401')\n ) {\n return false;\n }\n return [\n 'sandbox not found',\n 'sandbox is not running',\n 'sandbox service not available yet',\n 'sandbox failed',\n 'sandbox startup timed out',\n 'assistant service did not become healthy',\n 'proxy error: 404',\n 'proxy error: 502',\n 'proxy error: 503',\n ].some((fragment) => message.includes(fragment));\n}\n\n/**\n * Returns a memoized `AssistantTransport` wired to the BigConsole sandbox\n * assistant agent. The sandbox + session are cached in refs so follow-up turns\n * reuse the warm environment for the lifetime of the host shell.\n */\nexport function useSandboxAssistantTransport(): AssistantTransport {\n const { getAccessToken, getWorkspaceToken, userId, workspaceId: ctxWorkspaceId } = useAuthToken();\n\n const sandboxIdRef = useRef<string | null>(null);\n const sessionIdRef = useRef<string | null>(null);\n\n const ensureRuntime = useCallback(\n async (\n workspaceId: string,\n authContext: AssistantSandboxAuthContext\n ): Promise<{ sandboxId: string; sessionId: string }> => {\n let sandboxId = sandboxIdRef.current;\n if (!sandboxId) {\n sandboxId = await findExistingSandbox(workspaceId, MODE, authContext);\n if (!sandboxId) {\n if (!getAccessToken()) {\n throw new Error('The assistant requires an authenticated session. Please sign in again.');\n }\n sandboxId = await createAssistantSandbox(MODE, workspaceId, authContext);\n }\n await waitForSandboxReady(sandboxId, workspaceId, authContext);\n await waitForAssistantServiceReady(sandboxId, workspaceId, authContext);\n sandboxIdRef.current = sandboxId;\n }\n\n let sessionId = sessionIdRef.current;\n if (!sessionId) {\n const result = await createAssistantSession(sandboxId, workspaceId, MODE, authContext);\n sessionId = result.sessionId;\n sessionIdRef.current = sessionId;\n }\n\n return { sandboxId, sessionId };\n },\n [getAccessToken]\n );\n\n const sendPrompt = useCallback(\n async ({ prompt, onProgress, signal }: AssistantSendArgs): Promise<{ text: string }> => {\n const workspaceId = getWorkspaceId(ctxWorkspaceId);\n if (!workspaceId) {\n throw new Error('The assistant needs an active workspace. Open a workspace and try again.');\n }\n const authContext: AssistantSandboxAuthContext = {\n accessToken: getAccessToken(),\n workspaceToken: getWorkspaceToken(),\n userId,\n organizationId: getOrganizationId(),\n };\n\n const run = async (attempt: 1 | 2): Promise<{ text: string }> => {\n try {\n const { sandboxId, sessionId } = await ensureRuntime(workspaceId, authContext);\n\n const startedAt = Date.now();\n await sendAssistantPromptAsync(\n sandboxId,\n workspaceId,\n sessionId,\n prompt,\n MODE,\n gatherPageContext(),\n authContext\n );\n\n const timeoutAt = Date.now() + STREAM_BUDGET_MS;\n let lastTtlExtensionAt = Date.now();\n let progress = buildProgress([], startedAt);\n\n while (Date.now() < timeoutAt) {\n if (signal.aborted) throw new Error('Cancelled');\n\n const messages = await getAssistantMessages(sandboxId, workspaceId, sessionId, authContext, 50);\n progress = buildProgress(messages, startedAt);\n onProgress(sanitize(progress.content));\n if (progress.done) break;\n\n if (Date.now() - lastTtlExtensionAt > TTL_EXTEND_INTERVAL_MS) {\n await extendAssistantSandboxTTL(sandboxId, workspaceId, 600, authContext).catch(() => undefined);\n lastTtlExtensionAt = Date.now();\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (!progress.done) {\n throw new Error('The assistant timed out while responding. Please try again.');\n }\n\n return { text: sanitize(progress.content) };\n } catch (error) {\n // A stale/expired sandbox or session is recoverable — drop the warm\n // refs and retry once from a clean runtime.\n if (attempt === 1 && isRecoverable(error)) {\n sandboxIdRef.current = null;\n sessionIdRef.current = null;\n return run(2);\n }\n throw error;\n }\n };\n\n return run(1);\n },\n [ctxWorkspaceId, ensureRuntime, getAccessToken, getWorkspaceToken, userId]\n );\n\n return useMemo<AssistantTransport>(() => ({ sendPrompt }), [sendPrompt]);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { memo as e } from "react";
|
|
2
|
+
import { Radio as t } from "lucide-react";
|
|
3
|
+
import { jsx as n, jsxs as r } from "react/jsx-runtime";
|
|
4
|
+
//#region src/bigconsole/components/dashboard/LiveCanvasToggle.tsx
|
|
5
|
+
var i = e(function({ isLive: e, onToggle: i }) {
|
|
6
|
+
return /* @__PURE__ */ r("button", {
|
|
7
|
+
type: "button",
|
|
8
|
+
onClick: i,
|
|
9
|
+
"aria-pressed": e,
|
|
10
|
+
"data-testid": "live-canvas-toggle",
|
|
11
|
+
"data-live": e ? "on" : "off",
|
|
12
|
+
title: e ? "Live canvas on — new widgets and pages appear automatically, no reload needed" : "Turn on live canvas to watch the board update as the pipeline builds",
|
|
13
|
+
className: ["inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors", e ? "bg-status-success-bg text-status-success-text border border-status-success-border" : "text-text-secondary border border-border-default hover:bg-bg-sunken"].join(" "),
|
|
14
|
+
children: [e ? /* @__PURE__ */ r("span", {
|
|
15
|
+
className: "relative flex h-2.5 w-2.5 flex-shrink-0",
|
|
16
|
+
"aria-hidden": "true",
|
|
17
|
+
children: [/* @__PURE__ */ n("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-status-success-text opacity-60" }), /* @__PURE__ */ n("span", { className: "relative inline-flex h-2.5 w-2.5 rounded-full bg-status-success-text" })]
|
|
18
|
+
}) : /* @__PURE__ */ n(t, {
|
|
19
|
+
className: "h-3.5 w-3.5 flex-shrink-0",
|
|
20
|
+
"aria-hidden": "true"
|
|
21
|
+
}), /* @__PURE__ */ n("span", {
|
|
22
|
+
className: "uppercase tracking-wide",
|
|
23
|
+
children: e ? "Live" : "Go live"
|
|
24
|
+
})]
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
//#endregion
|
|
28
|
+
export { i as default };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=LiveCanvasToggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveCanvasToggle.js","names":[],"sources":["../../../../src/bigconsole/components/dashboard/LiveCanvasToggle.tsx"],"sourcesContent":["/**\n * LiveCanvasToggle\n *\n * Operator control that turns the dashboard into a \"live canvas\": while it is\n * on, the dashboard view polls the dashboard + widget STRUCTURE on a short\n * cadence so that pipeline changes made server-side — e.g. the AI assistant's\n * api-calls agent creating DataSink → Parser → Widget → Dashboard, or any other\n * writer — appear on the board in (near) real time WITHOUT a manual reload.\n *\n * This complements `LiveBoardBanner` (UC4), which only surfaces per-widget DATA\n * polling (`refreshInterval`). Nothing in that path refetches the board's\n * structure, so a newly-added widget would never show up live. This toggle is\n * the structure-level counterpart.\n *\n * WHY POLLING (not a subscription): identical to the LiveBoardBanner rationale —\n * the backend GraphQL server is HTTP-only with no graphql-ws transport / PubSub,\n * so there is no push channel. The live canvas runs on the same fallback path:\n * a visibility-gated interval refetch.\n *\n * Semantic status tokens only — no raw colors — so it themes correctly on\n * light / dark / high-contrast displays.\n */\n\nimport { type FC, memo } from 'react';\nimport { Radio } from 'lucide-react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface LiveCanvasToggleProps {\n /** Whether live structure polling is currently active. */\n isLive: boolean;\n /** Toggle live structure polling on/off. */\n onToggle: () => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const LiveCanvasToggle: FC<LiveCanvasToggleProps> = memo(function LiveCanvasToggle({ isLive, onToggle }) {\n return (\n <button\n type=\"button\"\n onClick={onToggle}\n aria-pressed={isLive}\n data-testid=\"live-canvas-toggle\"\n data-live={isLive ? 'on' : 'off'}\n title={\n isLive\n ? 'Live canvas on — new widgets and pages appear automatically, no reload needed'\n : 'Turn on live canvas to watch the board update as the pipeline builds'\n }\n className={[\n 'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors',\n isLive\n ? 'bg-status-success-bg text-status-success-text border border-status-success-border'\n : 'text-text-secondary border border-border-default hover:bg-bg-sunken',\n ].join(' ')}\n >\n {isLive ? (\n <span className=\"relative flex h-2.5 w-2.5 flex-shrink-0\" aria-hidden=\"true\">\n <span className=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-status-success-text opacity-60\" />\n <span className=\"relative inline-flex h-2.5 w-2.5 rounded-full bg-status-success-text\" />\n </span>\n ) : (\n <Radio className=\"h-3.5 w-3.5 flex-shrink-0\" aria-hidden=\"true\" />\n )}\n <span className=\"uppercase tracking-wide\">{isLive ? 'Live' : 'Go live'}</span>\n </button>\n );\n});\n\nexport default LiveCanvasToggle;\n"],"mappings":";;;;AAyCA,IAAa,IAA8C,EAAK,SAA0B,EAAE,WAAQ,eAAY;AAC9G,QACE,kBAAC,UAAD;EACE,MAAK;EACL,SAAS;EACT,gBAAc;EACd,eAAY;EACZ,aAAW,IAAS,OAAO;EAC3B,OACE,IACI,kFACA;EAEN,WAAW,CACT,iGACA,IACI,sFACA,sEACL,CAAC,KAAK,IAAI;YAhBb,CAkBG,IACC,kBAAC,QAAD;GAAM,WAAU;GAA0C,eAAY;aAAtE,CACE,kBAAC,QAAD,EAAM,WAAU,kGAAmG,CAAA,EACnH,kBAAC,QAAD,EAAM,WAAU,wEAAyE,CAAA,CACpF;OAEP,kBAAC,GAAD;GAAO,WAAU;GAA4B,eAAY;GAAS,CAAA,EAEpE,kBAAC,QAAD;GAAM,WAAU;aAA2B,IAAS,SAAS;GAAiB,CAAA,CACvE;;EAEX"}
|
|
@@ -13,6 +13,7 @@ import "./ShareDialog/ShareDialog.js";
|
|
|
13
13
|
import "./ShareDialog/index.js";
|
|
14
14
|
import "./WidgetPalette.js";
|
|
15
15
|
import "./DashboardCanvas.js";
|
|
16
|
+
import "./LiveCanvasToggle.js";
|
|
16
17
|
import "./TemplatePicker/TemplatePicker.js";
|
|
17
18
|
import "./TemplatePicker/index.js";
|
|
18
19
|
import "./CreateDashboardDialog.js";
|
|
@@ -70,9 +70,10 @@ function p() {
|
|
|
70
70
|
} finally {
|
|
71
71
|
m(!1);
|
|
72
72
|
}
|
|
73
|
-
}, [y]), C = s(async (e) => {
|
|
73
|
+
}, [y]), C = s(async (e, t) => {
|
|
74
|
+
let n = t?.silent ?? !1;
|
|
74
75
|
try {
|
|
75
|
-
m(!0), g(null);
|
|
76
|
+
n || (m(!0), g(null));
|
|
76
77
|
let t = await y.query({
|
|
77
78
|
query: i,
|
|
78
79
|
variables: { id: e },
|
|
@@ -84,9 +85,9 @@ function p() {
|
|
|
84
85
|
}
|
|
85
86
|
return null;
|
|
86
87
|
} catch (e) {
|
|
87
|
-
return g(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to fetch dashboard")), null;
|
|
88
|
+
return n || g(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to fetch dashboard")), null;
|
|
88
89
|
} finally {
|
|
89
|
-
m(!1);
|
|
90
|
+
n || m(!1);
|
|
90
91
|
}
|
|
91
92
|
}, [y, x]), w = s(async (e) => {
|
|
92
93
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDashboardOperations.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardOperations.ts"],"sourcesContent":["/**\n * useDashboardOperations Hook\n *\n * Provides CRUD operations for dashboards using GraphQL.\n */\n\nimport { useCallback, useState, useEffect } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport type { Dashboard, DashboardLayoutConfig } from '../types';\nimport { useDashboardStore } from '../store';\nimport {\n ListDashboardsDocument,\n GetDashboardDocument,\n CreateDashboardDocument,\n UpdateDashboardDocument,\n DeleteDashboardDocument,\n CloneDashboardDocument,\n} from '../../generated/wspace-operations';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DashboardListItem {\n id: string;\n name: string;\n description?: string;\n category?: string;\n isPublic?: boolean;\n viewCount?: number;\n widgetCount?: number;\n createdAt: string;\n updatedAt: string;\n pageCount: number;\n}\n\ninterface CreateDashboardInput {\n name: string;\n description?: string;\n category?: string;\n layoutType?: string;\n tags?: string[];\n}\n\ninterface UpdateDashboardInput {\n id: string;\n name?: string;\n description?: string;\n category?: string;\n isPublic?: boolean;\n}\n\ninterface DashboardOperationsResult {\n loading: boolean;\n error: Error | null;\n dashboards: DashboardListItem[];\n currentDashboard: Dashboard | null;\n\n // Operations\n fetchDashboards: (search?: string) => Promise<void>;\n fetchDashboard: (id: string) => Promise<Dashboard | null>;\n createDashboard: (input: CreateDashboardInput) => Promise<Dashboard | null>;\n updateDashboard: (input: UpdateDashboardInput) => Promise<Dashboard | null>;\n deleteDashboard: (id: string) => Promise<boolean>;\n cloneDashboard: (sourceDashboardId: string, name: string) => Promise<Dashboard | null>;\n refetch: () => Promise<void>;\n}\n\n// ============================================================================\n// Helper: Normalize dashboard from GraphQL response\n// ============================================================================\n\n// Raw GraphQL response shape for dashboard data\ninterface RawDashboardData {\n id: string;\n name?: string;\n description?: string;\n workspaceId?: string;\n isPublic?: boolean;\n isTemplate?: boolean;\n layout?: Record<string, unknown>;\n category?: string;\n viewCount?: number;\n widgetCount?: number;\n createdBy?: string;\n ownerId?: string;\n createdAt?: string;\n updatedAt?: string;\n pages?: {\n edges?: Array<{\n node: {\n id: string;\n dashboardId?: string;\n name?: string;\n order?: number;\n layout?: Record<string, unknown>;\n gridColumns?: number;\n createdAt?: string;\n updatedAt?: string;\n };\n }>;\n totalCount?: number;\n };\n}\n\nfunction normalizeDashboard(data: RawDashboardData): Dashboard {\n const pages =\n data.pages?.edges?.map((edge) => ({\n id: edge.node.id,\n dashboardId: edge.node.dashboardId || data.id,\n name: edge.node.name,\n slug: edge.node.name?.toLowerCase().replace(/\\s+/g, '-') || '',\n order: edge.node.order || 0,\n layoutConfig: edge.node.layout || {},\n gridColumns: edge.node.gridColumns || 12,\n widgets: [], // Widgets loaded separately via useWidgetOperations\n createdAt: edge.node.createdAt || new Date().toISOString(),\n updatedAt: edge.node.updatedAt || new Date().toISOString(),\n })) || [];\n\n // GetDashboard caps pages at first:10 to stay under the gateway cost-limit\n // (see DashboardFullFields fragment / BOFF-2720). Surface a warning instead of\n // silently dropping pages if a dashboard ever exceeds the cap.\n const totalPages = data.pages?.totalCount ?? pages.length;\n if (totalPages > pages.length) {\n console.warn(\n `[bigconsole] Dashboard ${data.id} has ${totalPages} pages but only ${pages.length} were loaded ` +\n `(GetDashboard pages cap). Pages beyond the cap are not shown — see BOFF-2720.`\n );\n }\n\n return {\n id: data.id,\n workspaceId: data.workspaceId || '',\n name: data.name || 'Untitled Dashboard',\n description: data.description,\n slug: data.name?.toLowerCase().replace(/\\s+/g, '-') || '',\n isPublished: data.isPublic || false,\n isTemplate: data.isTemplate || false,\n layoutConfig: (data.layout as unknown as DashboardLayoutConfig) || {\n columns: 12,\n rowHeight: 100,\n gap: 10,\n margin: [10, 10],\n containerPadding: [10, 10],\n },\n pages,\n translations: [],\n createdBy: data.createdBy || data.ownerId || '',\n createdAt: data.createdAt || new Date().toISOString(),\n updatedAt: data.updatedAt || new Date().toISOString(),\n };\n}\n\nfunction normalizeDashboardListItem(data: RawDashboardData): DashboardListItem {\n // The list query selects only pages.totalCount (no edges) to stay under the\n // gateway Armor cost-limit — see DashboardListItem fragment / BOFF-2719.\n return {\n id: data.id,\n name: data.name || 'Untitled Dashboard',\n description: data.description,\n category: data.category,\n isPublic: data.isPublic,\n viewCount: data.viewCount,\n widgetCount: data.widgetCount,\n createdAt: data.createdAt || new Date().toISOString(),\n updatedAt: data.updatedAt || new Date().toISOString(),\n pageCount: data.pages?.totalCount ?? 0,\n };\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useDashboardOperations(): DashboardOperationsResult {\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [dashboards, setDashboards] = useState<DashboardListItem[]>([]);\n\n // Get Apollo Client - will throw if not in ApolloProvider context\n const apolloClient = useApolloClient();\n\n // Store actions\n const currentDashboard = useDashboardStore((state) => state.currentDashboard);\n const setCurrentDashboard = useDashboardStore((state) => state.setCurrentDashboard);\n\n // Fetch dashboards list\n const fetchDashboards = useCallback(\n async (search?: string) => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.query<{ listDashboards?: { edges?: Array<{ node: RawDashboardData }> } }>({\n query: ListDashboardsDocument,\n variables: {\n first: 50,\n search,\n includeArchived: false,\n },\n fetchPolicy: 'network-only',\n });\n\n const edges = result.data?.listDashboards?.edges || [];\n const dashboardsList = edges.map((edge) => normalizeDashboardListItem(edge.node));\n setDashboards(dashboardsList);\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to fetch dashboards');\n setError(error);\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n // Fetch single dashboard\n const fetchDashboard = useCallback(\n async (id: string): Promise<Dashboard | null> => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.query<{ getDashboard?: RawDashboardData }>({\n query: GetDashboardDocument,\n variables: { id },\n fetchPolicy: 'network-only',\n });\n\n if (result.data?.getDashboard) {\n const dashboard = normalizeDashboard(result.data.getDashboard);\n setCurrentDashboard(dashboard);\n return dashboard;\n }\n\n return null;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to fetch dashboard');\n setError(error);\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, setCurrentDashboard]\n );\n\n // Create dashboard\n const createDashboard = useCallback(\n async (input: CreateDashboardInput): Promise<Dashboard | null> => {\n try {\n setLoading(true);\n setError(null);\n\n // Build mutation input\n // Note: Tags require Tag IDs from the Tags service. For now, tags are stored in metadata.\n // Full tag integration would require querying/creating tags via the Tags service first.\n const mutationInput: Record<string, unknown> = {\n name: input.name,\n description: input.description,\n category: input.category || 'CUSTOM',\n layout: input.layoutType || 'GRID',\n createDefaultPage: true,\n };\n\n // Store tags in metadata until full Tags service integration\n if (input.tags && input.tags.length > 0) {\n mutationInput.metadata = {\n tags: input.tags,\n };\n }\n\n const result = await apolloClient.mutate<{ createBigConsoleDashboard?: RawDashboardData }>({\n mutation: CreateDashboardDocument,\n variables: {\n input: mutationInput,\n },\n });\n\n if (result.data?.createBigConsoleDashboard) {\n const newDashboard = normalizeDashboard(result.data.createBigConsoleDashboard);\n await fetchDashboards();\n return newDashboard;\n }\n\n return null;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to create dashboard');\n setError(error);\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, fetchDashboards]\n );\n\n // Update dashboard\n const updateDashboard = useCallback(\n async (input: UpdateDashboardInput): Promise<Dashboard | null> => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.mutate<{ updateBigConsoleDashboard?: RawDashboardData }>({\n mutation: UpdateDashboardDocument,\n variables: { input },\n });\n\n if (result.data?.updateBigConsoleDashboard) {\n const updatedDashboard = normalizeDashboard(result.data.updateBigConsoleDashboard);\n if (currentDashboard?.id === input.id) {\n setCurrentDashboard(updatedDashboard);\n }\n await fetchDashboards();\n return updatedDashboard;\n }\n\n return null;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to update dashboard');\n setError(error);\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, currentDashboard?.id, setCurrentDashboard, fetchDashboards]\n );\n\n // Delete dashboard\n const deleteDashboard = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.mutate<{ deleteBigConsoleDashboard?: boolean }>({\n mutation: DeleteDashboardDocument,\n variables: { id },\n });\n\n if (result.data?.deleteBigConsoleDashboard) {\n if (currentDashboard?.id === id) {\n setCurrentDashboard(null);\n }\n await fetchDashboards();\n return true;\n }\n\n return false;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to delete dashboard');\n setError(error);\n return false;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, currentDashboard?.id, setCurrentDashboard, fetchDashboards]\n );\n\n // Clone dashboard (duplicate including widgets)\n const cloneDashboard = useCallback(\n async (sourceDashboardId: string, name: string): Promise<Dashboard | null> => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.mutate<{ cloneDashboard?: RawDashboardData }>({\n mutation: CloneDashboardDocument,\n variables: {\n input: { sourceDashboardId, name, includeWidgets: true },\n },\n });\n\n if (result.data?.cloneDashboard) {\n const cloned = normalizeDashboard(result.data.cloneDashboard);\n await fetchDashboards();\n return cloned;\n }\n\n return null;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to clone dashboard');\n setError(error);\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, fetchDashboards]\n );\n\n // Refetch\n const refetch = useCallback(async () => {\n await fetchDashboards();\n }, [fetchDashboards]);\n\n // Fetch on mount\n useEffect(() => {\n fetchDashboards();\n }, [fetchDashboards]);\n\n return {\n loading,\n error,\n dashboards,\n currentDashboard,\n fetchDashboards,\n fetchDashboard,\n createDashboard,\n updateDashboard,\n deleteDashboard,\n cloneDashboard,\n refetch,\n };\n}\n\nexport default useDashboardOperations;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useDashboardOperations.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardOperations.ts"],"sourcesContent":["/**\n * useDashboardOperations Hook\n *\n * Provides CRUD operations for dashboards using GraphQL.\n */\n\nimport { useCallback, useState, useEffect } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport type { Dashboard, DashboardLayoutConfig } from '../types';\nimport { useDashboardStore } from '../store';\nimport {\n ListDashboardsDocument,\n GetDashboardDocument,\n CreateDashboardDocument,\n UpdateDashboardDocument,\n DeleteDashboardDocument,\n CloneDashboardDocument,\n} from '../../generated/wspace-operations';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DashboardListItem {\n id: string;\n name: string;\n description?: string;\n category?: string;\n isPublic?: boolean;\n viewCount?: number;\n widgetCount?: number;\n createdAt: string;\n updatedAt: string;\n pageCount: number;\n}\n\ninterface CreateDashboardInput {\n name: string;\n description?: string;\n category?: string;\n layoutType?: string;\n tags?: string[];\n}\n\ninterface UpdateDashboardInput {\n id: string;\n name?: string;\n description?: string;\n category?: string;\n isPublic?: boolean;\n}\n\ninterface FetchDashboardOptions {\n /**\n * Silent refetch for live-canvas polling: do NOT toggle the global `loading`\n * flag (which would flash the page-level spinner) and do NOT surface a\n * transient fetch error (which would replace the board with the error screen).\n * The last good dashboard stays on screen if a poll fails.\n */\n silent?: boolean;\n}\n\ninterface DashboardOperationsResult {\n loading: boolean;\n error: Error | null;\n dashboards: DashboardListItem[];\n currentDashboard: Dashboard | null;\n\n // Operations\n fetchDashboards: (search?: string) => Promise<void>;\n fetchDashboard: (id: string, options?: FetchDashboardOptions) => Promise<Dashboard | null>;\n createDashboard: (input: CreateDashboardInput) => Promise<Dashboard | null>;\n updateDashboard: (input: UpdateDashboardInput) => Promise<Dashboard | null>;\n deleteDashboard: (id: string) => Promise<boolean>;\n cloneDashboard: (sourceDashboardId: string, name: string) => Promise<Dashboard | null>;\n refetch: () => Promise<void>;\n}\n\n// ============================================================================\n// Helper: Normalize dashboard from GraphQL response\n// ============================================================================\n\n// Raw GraphQL response shape for dashboard data\ninterface RawDashboardData {\n id: string;\n name?: string;\n description?: string;\n workspaceId?: string;\n isPublic?: boolean;\n isTemplate?: boolean;\n layout?: Record<string, unknown>;\n category?: string;\n viewCount?: number;\n widgetCount?: number;\n createdBy?: string;\n ownerId?: string;\n createdAt?: string;\n updatedAt?: string;\n pages?: {\n edges?: Array<{\n node: {\n id: string;\n dashboardId?: string;\n name?: string;\n order?: number;\n layout?: Record<string, unknown>;\n gridColumns?: number;\n createdAt?: string;\n updatedAt?: string;\n };\n }>;\n totalCount?: number;\n };\n}\n\nfunction normalizeDashboard(data: RawDashboardData): Dashboard {\n const pages =\n data.pages?.edges?.map((edge) => ({\n id: edge.node.id,\n dashboardId: edge.node.dashboardId || data.id,\n name: edge.node.name,\n slug: edge.node.name?.toLowerCase().replace(/\\s+/g, '-') || '',\n order: edge.node.order || 0,\n layoutConfig: edge.node.layout || {},\n gridColumns: edge.node.gridColumns || 12,\n widgets: [], // Widgets loaded separately via useWidgetOperations\n createdAt: edge.node.createdAt || new Date().toISOString(),\n updatedAt: edge.node.updatedAt || new Date().toISOString(),\n })) || [];\n\n // GetDashboard caps pages at first:10 to stay under the gateway cost-limit\n // (see DashboardFullFields fragment / BOFF-2720). Surface a warning instead of\n // silently dropping pages if a dashboard ever exceeds the cap.\n const totalPages = data.pages?.totalCount ?? pages.length;\n if (totalPages > pages.length) {\n console.warn(\n `[bigconsole] Dashboard ${data.id} has ${totalPages} pages but only ${pages.length} were loaded ` +\n `(GetDashboard pages cap). Pages beyond the cap are not shown — see BOFF-2720.`\n );\n }\n\n return {\n id: data.id,\n workspaceId: data.workspaceId || '',\n name: data.name || 'Untitled Dashboard',\n description: data.description,\n slug: data.name?.toLowerCase().replace(/\\s+/g, '-') || '',\n isPublished: data.isPublic || false,\n isTemplate: data.isTemplate || false,\n layoutConfig: (data.layout as unknown as DashboardLayoutConfig) || {\n columns: 12,\n rowHeight: 100,\n gap: 10,\n margin: [10, 10],\n containerPadding: [10, 10],\n },\n pages,\n translations: [],\n createdBy: data.createdBy || data.ownerId || '',\n createdAt: data.createdAt || new Date().toISOString(),\n updatedAt: data.updatedAt || new Date().toISOString(),\n };\n}\n\nfunction normalizeDashboardListItem(data: RawDashboardData): DashboardListItem {\n // The list query selects only pages.totalCount (no edges) to stay under the\n // gateway Armor cost-limit — see DashboardListItem fragment / BOFF-2719.\n return {\n id: data.id,\n name: data.name || 'Untitled Dashboard',\n description: data.description,\n category: data.category,\n isPublic: data.isPublic,\n viewCount: data.viewCount,\n widgetCount: data.widgetCount,\n createdAt: data.createdAt || new Date().toISOString(),\n updatedAt: data.updatedAt || new Date().toISOString(),\n pageCount: data.pages?.totalCount ?? 0,\n };\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useDashboardOperations(): DashboardOperationsResult {\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [dashboards, setDashboards] = useState<DashboardListItem[]>([]);\n\n // Get Apollo Client - will throw if not in ApolloProvider context\n const apolloClient = useApolloClient();\n\n // Store actions\n const currentDashboard = useDashboardStore((state) => state.currentDashboard);\n const setCurrentDashboard = useDashboardStore((state) => state.setCurrentDashboard);\n\n // Fetch dashboards list\n const fetchDashboards = useCallback(\n async (search?: string) => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.query<{ listDashboards?: { edges?: Array<{ node: RawDashboardData }> } }>({\n query: ListDashboardsDocument,\n variables: {\n first: 50,\n search,\n includeArchived: false,\n },\n fetchPolicy: 'network-only',\n });\n\n const edges = result.data?.listDashboards?.edges || [];\n const dashboardsList = edges.map((edge) => normalizeDashboardListItem(edge.node));\n setDashboards(dashboardsList);\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to fetch dashboards');\n setError(error);\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n // Fetch single dashboard\n const fetchDashboard = useCallback(\n async (id: string, options?: FetchDashboardOptions): Promise<Dashboard | null> => {\n const silent = options?.silent ?? false;\n try {\n if (!silent) {\n setLoading(true);\n setError(null);\n }\n\n const result = await apolloClient.query<{ getDashboard?: RawDashboardData }>({\n query: GetDashboardDocument,\n variables: { id },\n fetchPolicy: 'network-only',\n });\n\n if (result.data?.getDashboard) {\n const dashboard = normalizeDashboard(result.data.getDashboard);\n setCurrentDashboard(dashboard);\n return dashboard;\n }\n\n return null;\n } catch (err) {\n // Silent (live-poll) failures must not wipe the board or flip the page to\n // the error screen — keep the last good dashboard on screen.\n if (!silent) {\n const error = err instanceof Error ? err : new Error('Failed to fetch dashboard');\n setError(error);\n }\n return null;\n } finally {\n if (!silent) {\n setLoading(false);\n }\n }\n },\n [apolloClient, setCurrentDashboard]\n );\n\n // Create dashboard\n const createDashboard = useCallback(\n async (input: CreateDashboardInput): Promise<Dashboard | null> => {\n try {\n setLoading(true);\n setError(null);\n\n // Build mutation input\n // Note: Tags require Tag IDs from the Tags service. For now, tags are stored in metadata.\n // Full tag integration would require querying/creating tags via the Tags service first.\n const mutationInput: Record<string, unknown> = {\n name: input.name,\n description: input.description,\n category: input.category || 'CUSTOM',\n layout: input.layoutType || 'GRID',\n createDefaultPage: true,\n };\n\n // Store tags in metadata until full Tags service integration\n if (input.tags && input.tags.length > 0) {\n mutationInput.metadata = {\n tags: input.tags,\n };\n }\n\n const result = await apolloClient.mutate<{ createBigConsoleDashboard?: RawDashboardData }>({\n mutation: CreateDashboardDocument,\n variables: {\n input: mutationInput,\n },\n });\n\n if (result.data?.createBigConsoleDashboard) {\n const newDashboard = normalizeDashboard(result.data.createBigConsoleDashboard);\n await fetchDashboards();\n return newDashboard;\n }\n\n return null;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to create dashboard');\n setError(error);\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, fetchDashboards]\n );\n\n // Update dashboard\n const updateDashboard = useCallback(\n async (input: UpdateDashboardInput): Promise<Dashboard | null> => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.mutate<{ updateBigConsoleDashboard?: RawDashboardData }>({\n mutation: UpdateDashboardDocument,\n variables: { input },\n });\n\n if (result.data?.updateBigConsoleDashboard) {\n const updatedDashboard = normalizeDashboard(result.data.updateBigConsoleDashboard);\n if (currentDashboard?.id === input.id) {\n setCurrentDashboard(updatedDashboard);\n }\n await fetchDashboards();\n return updatedDashboard;\n }\n\n return null;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to update dashboard');\n setError(error);\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, currentDashboard?.id, setCurrentDashboard, fetchDashboards]\n );\n\n // Delete dashboard\n const deleteDashboard = useCallback(\n async (id: string): Promise<boolean> => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.mutate<{ deleteBigConsoleDashboard?: boolean }>({\n mutation: DeleteDashboardDocument,\n variables: { id },\n });\n\n if (result.data?.deleteBigConsoleDashboard) {\n if (currentDashboard?.id === id) {\n setCurrentDashboard(null);\n }\n await fetchDashboards();\n return true;\n }\n\n return false;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to delete dashboard');\n setError(error);\n return false;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, currentDashboard?.id, setCurrentDashboard, fetchDashboards]\n );\n\n // Clone dashboard (duplicate including widgets)\n const cloneDashboard = useCallback(\n async (sourceDashboardId: string, name: string): Promise<Dashboard | null> => {\n try {\n setLoading(true);\n setError(null);\n\n const result = await apolloClient.mutate<{ cloneDashboard?: RawDashboardData }>({\n mutation: CloneDashboardDocument,\n variables: {\n input: { sourceDashboardId, name, includeWidgets: true },\n },\n });\n\n if (result.data?.cloneDashboard) {\n const cloned = normalizeDashboard(result.data.cloneDashboard);\n await fetchDashboards();\n return cloned;\n }\n\n return null;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to clone dashboard');\n setError(error);\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient, fetchDashboards]\n );\n\n // Refetch\n const refetch = useCallback(async () => {\n await fetchDashboards();\n }, [fetchDashboards]);\n\n // Fetch on mount\n useEffect(() => {\n fetchDashboards();\n }, [fetchDashboards]);\n\n return {\n loading,\n error,\n dashboards,\n currentDashboard,\n fetchDashboards,\n fetchDashboard,\n createDashboard,\n updateDashboard,\n deleteDashboard,\n cloneDashboard,\n refetch,\n };\n}\n\nexport default useDashboardOperations;\n"],"mappings":"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"}
|
|
@@ -6,99 +6,118 @@ import "../context/index.js";
|
|
|
6
6
|
import r from "../hooks/useWidgetOperations.js";
|
|
7
7
|
import i from "../hooks/useDashboardOperations.js";
|
|
8
8
|
import a from "../hooks/useFilterUrlSync.js";
|
|
9
|
-
import
|
|
9
|
+
import ee from "../hooks/usePageOperations.js";
|
|
10
10
|
import "../hooks/index.js";
|
|
11
|
-
import
|
|
11
|
+
import o from "../components/dashboard/DashboardCanvas.js";
|
|
12
|
+
import te from "../components/dashboard/LiveCanvasToggle.js";
|
|
12
13
|
import s from "../components/dashboard/ExportDashboardDialog.js";
|
|
13
14
|
import c from "../components/dashboard/DrilldownBreadcrumb.js";
|
|
14
15
|
import "../components/dashboard/index.js";
|
|
15
|
-
import { memo as l, useCallback as u, useEffect as d,
|
|
16
|
-
import { useParams as
|
|
17
|
-
import { ArrowLeft as
|
|
18
|
-
import { Button as
|
|
19
|
-
import { Fragment as
|
|
16
|
+
import { memo as l, useCallback as u, useEffect as d, useRef as f, useState as p } from "react";
|
|
17
|
+
import { useParams as m } from "react-router-dom";
|
|
18
|
+
import { ArrowLeft as ne } from "lucide-react";
|
|
19
|
+
import { Button as re, IllustratedEmptyState as h } from "@burdenoff/fe-libs/ui";
|
|
20
|
+
import { Fragment as ie, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
20
21
|
//#region src/bigconsole/pages/DashboardViewPage.tsx
|
|
21
|
-
var
|
|
22
|
-
let { dashboardId: l } =
|
|
22
|
+
var v = l(function() {
|
|
23
|
+
let { dashboardId: l } = m(), v = n(), [y, b] = p(null), [ae, x] = p(!1), [S, C] = p(!1), [oe, w] = p(!1), [T, E] = p(!1), { isInDrilldown: D, getContextFromUrl: O, getDrilldownMetadata: k } = a(), A = t((e) => e.currentDashboard), j = t((e) => e.setCurrentDashboard), M = t((e) => e.setGlobalFilterValue), N = t((e) => e.clearAllGlobalFilters), P = e((e) => e.widgets), F = e((e) => e.clipboardWidgetId), I = e((e) => e.clipboardAction), L = e((e) => e.clearClipboard), R = e((e) => e.selectWidget), { loading: z, error: B, fetchDashboard: V, updateDashboard: H } = i(), { createWidget: U, deleteWidget: W, duplicateWidget: G, batchUpdatePositions: K, refetch: q } = r(y || void 0, l), { createPage: J, updatePage: Y, deletePage: X, reorderPages: Z } = ee();
|
|
23
24
|
d(() => {
|
|
24
25
|
if (!l) {
|
|
25
|
-
|
|
26
|
+
v("/dashboards");
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
|
-
return
|
|
29
|
-
e ? e.pages && e.pages.length > 0 &&
|
|
29
|
+
return N(), C(!1), V(l).then((e) => {
|
|
30
|
+
e ? e.pages && e.pages.length > 0 && b(e.pages[0].id) : C(!0);
|
|
30
31
|
}), () => {
|
|
31
|
-
|
|
32
|
+
j(null), N(), C(!1);
|
|
32
33
|
};
|
|
33
34
|
}, [
|
|
34
35
|
l,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
v,
|
|
37
|
+
V,
|
|
38
|
+
j,
|
|
39
|
+
N
|
|
39
40
|
]), d(() => {
|
|
40
|
-
if (!
|
|
41
|
-
let e =
|
|
41
|
+
if (!D) return;
|
|
42
|
+
let e = O(), t = k();
|
|
42
43
|
Object.entries(e).forEach(([e, t]) => {
|
|
43
|
-
|
|
44
|
-
}), t.depth > 0 && (
|
|
44
|
+
M(`ctx_${e}`, t);
|
|
45
|
+
}), t.depth > 0 && (M("__drilldown_depth", t.depth), M("__drilldown_parent", t.parent), M("__drilldown_path", t.path)), Object.keys(e).length > 0 && console.debug("[DashboardViewPage] Loaded drilldown context:", {
|
|
45
46
|
context: e,
|
|
46
47
|
metadata: t
|
|
47
48
|
});
|
|
48
49
|
}, [
|
|
50
|
+
D,
|
|
51
|
+
O,
|
|
49
52
|
k,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
M
|
|
54
|
+
]);
|
|
55
|
+
let Q = f(q);
|
|
56
|
+
Q.current = q, d(() => {
|
|
57
|
+
if (!T || !l) return;
|
|
58
|
+
let e = () => {
|
|
59
|
+
typeof document < "u" && document.visibilityState !== "visible" || t.getState().viewMode !== "edit" && (V(l, { silent: !0 }), Q.current());
|
|
60
|
+
};
|
|
61
|
+
e();
|
|
62
|
+
let n = window.setInterval(e, 3500), r = () => {
|
|
63
|
+
document.visibilityState === "visible" && e();
|
|
64
|
+
};
|
|
65
|
+
return document.addEventListener("visibilitychange", r), () => {
|
|
66
|
+
window.clearInterval(n), document.removeEventListener("visibilitychange", r);
|
|
67
|
+
};
|
|
68
|
+
}, [
|
|
69
|
+
T,
|
|
70
|
+
l,
|
|
71
|
+
V
|
|
53
72
|
]);
|
|
54
|
-
let
|
|
73
|
+
let se = u(async () => {
|
|
55
74
|
if (l) {
|
|
56
|
-
|
|
75
|
+
x(!0);
|
|
57
76
|
try {
|
|
58
|
-
let e = Array.from(
|
|
59
|
-
e.length > 0 && await
|
|
77
|
+
let e = Array.from(P.values());
|
|
78
|
+
e.length > 0 && await K(e.map((e) => ({
|
|
60
79
|
id: e.id,
|
|
61
80
|
position: e.position
|
|
62
|
-
}))),
|
|
81
|
+
}))), A && await H({
|
|
63
82
|
id: l,
|
|
64
|
-
name:
|
|
65
|
-
description:
|
|
83
|
+
name: A.name,
|
|
84
|
+
description: A.description
|
|
66
85
|
});
|
|
67
86
|
} catch {} finally {
|
|
68
|
-
|
|
87
|
+
x(!1);
|
|
69
88
|
}
|
|
70
89
|
}
|
|
71
90
|
}, [
|
|
72
91
|
l,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
]),
|
|
78
|
-
|
|
79
|
-
}, [
|
|
80
|
-
|
|
81
|
-
}, []),
|
|
82
|
-
let e =
|
|
83
|
-
if (!
|
|
84
|
-
let t =
|
|
92
|
+
P,
|
|
93
|
+
A,
|
|
94
|
+
K,
|
|
95
|
+
H
|
|
96
|
+
]), ce = u(() => {
|
|
97
|
+
v("/dashboards");
|
|
98
|
+
}, [v]), le = u((e) => {
|
|
99
|
+
b(e);
|
|
100
|
+
}, []), $ = u(async () => {
|
|
101
|
+
let e = y || A?.pages?.[0]?.id;
|
|
102
|
+
if (!F || !l || !e) return;
|
|
103
|
+
let t = P.get(F);
|
|
85
104
|
if (!t) {
|
|
86
|
-
|
|
105
|
+
L();
|
|
87
106
|
return;
|
|
88
107
|
}
|
|
89
108
|
let n = {
|
|
90
109
|
x: 0,
|
|
91
|
-
y: Array.from(
|
|
110
|
+
y: Array.from(P.values()).reduce((e, t) => {
|
|
92
111
|
let n = (t.positionY ?? 0) + (t.positionHeight ?? 4);
|
|
93
112
|
return Math.max(e, n);
|
|
94
113
|
}, 0),
|
|
95
114
|
width: t.positionWidth ?? 4,
|
|
96
115
|
height: t.positionHeight ?? 4
|
|
97
|
-
}, r =
|
|
116
|
+
}, r = I === "copy" ? `${t.title} (Copy)` : t.title, i = null;
|
|
98
117
|
try {
|
|
99
|
-
i = await
|
|
118
|
+
i = await G(F, n, r);
|
|
100
119
|
} catch {}
|
|
101
|
-
i ||= await
|
|
120
|
+
i ||= await U({
|
|
102
121
|
pageId: e,
|
|
103
122
|
dashboardId: l,
|
|
104
123
|
type: t.type,
|
|
@@ -112,213 +131,223 @@ var b = l(function() {
|
|
|
112
131
|
position: n,
|
|
113
132
|
refreshInterval: t.refreshInterval ?? void 0,
|
|
114
133
|
metadata: t.metadata
|
|
115
|
-
}), i && (
|
|
134
|
+
}), i && (R(i.id), I === "cut" && await W(F)), L();
|
|
116
135
|
}, [
|
|
117
|
-
|
|
118
|
-
R,
|
|
119
|
-
l,
|
|
120
|
-
x,
|
|
121
|
-
M,
|
|
136
|
+
F,
|
|
122
137
|
I,
|
|
123
|
-
|
|
138
|
+
l,
|
|
139
|
+
y,
|
|
140
|
+
A,
|
|
141
|
+
P,
|
|
124
142
|
G,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
U,
|
|
144
|
+
W,
|
|
145
|
+
R,
|
|
146
|
+
L
|
|
147
|
+
]), ue = u(() => {
|
|
148
|
+
w(!0);
|
|
149
|
+
}, []), de = u(() => {
|
|
150
|
+
w(!1);
|
|
151
|
+
}, []), fe = u(async () => {
|
|
152
|
+
if (!A || !l) return;
|
|
153
|
+
let e = await J({
|
|
135
154
|
dashboardId: l,
|
|
136
|
-
name: `Page ${(
|
|
155
|
+
name: `Page ${(A.pages?.length || 0) + 1}`
|
|
137
156
|
});
|
|
138
|
-
e && (
|
|
139
|
-
...
|
|
140
|
-
pages: [...
|
|
141
|
-
}),
|
|
157
|
+
e && (j({
|
|
158
|
+
...A,
|
|
159
|
+
pages: [...A.pages || [], e]
|
|
160
|
+
}), b(e.id));
|
|
142
161
|
}, [
|
|
143
|
-
|
|
162
|
+
A,
|
|
144
163
|
l,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
]),
|
|
148
|
-
|
|
164
|
+
j,
|
|
165
|
+
J
|
|
166
|
+
]), pe = u(async (e, t) => {
|
|
167
|
+
A && await Y({
|
|
149
168
|
id: e,
|
|
150
169
|
name: t
|
|
151
|
-
}) &&
|
|
152
|
-
...
|
|
153
|
-
pages:
|
|
170
|
+
}) && j({
|
|
171
|
+
...A,
|
|
172
|
+
pages: A.pages?.map((n) => n.id === e ? {
|
|
154
173
|
...n,
|
|
155
174
|
name: t
|
|
156
175
|
} : n)
|
|
157
176
|
});
|
|
158
177
|
}, [
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
]),
|
|
163
|
-
if (!
|
|
164
|
-
let t =
|
|
178
|
+
A,
|
|
179
|
+
j,
|
|
180
|
+
Y
|
|
181
|
+
]), me = u(async (e) => {
|
|
182
|
+
if (!A || !l) return;
|
|
183
|
+
let t = A.pages?.find((t) => t.id === e);
|
|
165
184
|
if (!t) return;
|
|
166
|
-
let n = await
|
|
185
|
+
let n = await J({
|
|
167
186
|
dashboardId: l,
|
|
168
187
|
name: `${t.name} (Copy)`
|
|
169
188
|
});
|
|
170
|
-
n && (
|
|
171
|
-
...
|
|
172
|
-
pages: [...
|
|
173
|
-
}),
|
|
189
|
+
n && (j({
|
|
190
|
+
...A,
|
|
191
|
+
pages: [...A.pages || [], n]
|
|
192
|
+
}), b(n.id));
|
|
174
193
|
}, [
|
|
175
|
-
|
|
194
|
+
A,
|
|
176
195
|
l,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
]),
|
|
180
|
-
if (!(!
|
|
181
|
-
let t =
|
|
196
|
+
j,
|
|
197
|
+
J
|
|
198
|
+
]), he = u(async (e) => {
|
|
199
|
+
if (!(!A || (A.pages?.length || 0) <= 1) && await X(e)) {
|
|
200
|
+
let t = A.pages?.filter((t) => t.id !== e).map((e, t) => ({
|
|
182
201
|
...e,
|
|
183
202
|
order: t
|
|
184
203
|
}));
|
|
185
|
-
|
|
186
|
-
...
|
|
204
|
+
j({
|
|
205
|
+
...A,
|
|
187
206
|
pages: t
|
|
188
|
-
}),
|
|
207
|
+
}), y === e && b(t?.[0]?.id || null);
|
|
189
208
|
}
|
|
190
209
|
}, [
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
]),
|
|
196
|
-
!
|
|
197
|
-
...
|
|
210
|
+
A,
|
|
211
|
+
j,
|
|
212
|
+
y,
|
|
213
|
+
X
|
|
214
|
+
]), ge = u(async (e) => {
|
|
215
|
+
!A || !l || (j({
|
|
216
|
+
...A,
|
|
198
217
|
pages: e.map((e, t) => ({
|
|
199
218
|
...e,
|
|
200
219
|
order: t
|
|
201
220
|
}))
|
|
202
|
-
}), await
|
|
221
|
+
}), await Z(l, e.map((e) => e.id)));
|
|
203
222
|
}, [
|
|
204
|
-
|
|
223
|
+
A,
|
|
205
224
|
l,
|
|
206
|
-
|
|
207
|
-
|
|
225
|
+
j,
|
|
226
|
+
Z
|
|
208
227
|
]);
|
|
209
|
-
return
|
|
228
|
+
return B ? /* @__PURE__ */ g("div", {
|
|
210
229
|
className: "flex flex-col items-center justify-center h-full",
|
|
211
230
|
style: { padding: "var(--space-pagePadding)" },
|
|
212
|
-
children: /* @__PURE__ */
|
|
231
|
+
children: /* @__PURE__ */ _("div", {
|
|
213
232
|
className: "bg-status-error-bg border border-status-error-border rounded-lg p-6 w-full max-w-md",
|
|
214
233
|
children: [
|
|
215
|
-
/* @__PURE__ */
|
|
234
|
+
/* @__PURE__ */ g("h3", {
|
|
216
235
|
className: "text-lg font-medium text-status-error-text mb-2",
|
|
217
236
|
children: "Error loading dashboard"
|
|
218
237
|
}),
|
|
219
|
-
/* @__PURE__ */
|
|
238
|
+
/* @__PURE__ */ g("p", {
|
|
220
239
|
className: "text-sm text-text-secondary mb-4 break-words",
|
|
221
|
-
children:
|
|
240
|
+
children: B.message
|
|
222
241
|
}),
|
|
223
|
-
/* @__PURE__ */
|
|
242
|
+
/* @__PURE__ */ _("div", {
|
|
224
243
|
className: "flex flex-wrap gap-3",
|
|
225
|
-
children: [/* @__PURE__ */
|
|
244
|
+
children: [/* @__PURE__ */ g("button", {
|
|
226
245
|
type: "button",
|
|
227
|
-
onClick: () =>
|
|
246
|
+
onClick: () => V(l),
|
|
228
247
|
className: "px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md",
|
|
229
248
|
children: "Retry"
|
|
230
|
-
}), /* @__PURE__ */
|
|
249
|
+
}), /* @__PURE__ */ g("button", {
|
|
231
250
|
type: "button",
|
|
232
|
-
onClick: () =>
|
|
251
|
+
onClick: () => v("/dashboards"),
|
|
233
252
|
className: "px-4 py-2 text-sm font-medium text-text-secondary bg-bg-sunken hover:bg-bg-sunken/80 rounded-md",
|
|
234
253
|
children: "Back to Dashboards"
|
|
235
254
|
})]
|
|
236
255
|
})
|
|
237
256
|
]
|
|
238
257
|
})
|
|
239
|
-
}) :
|
|
258
|
+
}) : S ? /* @__PURE__ */ g("div", {
|
|
240
259
|
className: "flex flex-col items-center justify-center h-full",
|
|
241
260
|
style: { padding: "var(--space-pagePadding)" },
|
|
242
|
-
children: /* @__PURE__ */
|
|
261
|
+
children: /* @__PURE__ */ g(h, {
|
|
243
262
|
illustration: "empty-data",
|
|
244
263
|
title: "Dashboard not found",
|
|
245
|
-
description: /* @__PURE__ */
|
|
264
|
+
description: /* @__PURE__ */ _(ie, { children: ["The dashboard you're looking for doesn't exist or has been deleted.", /* @__PURE__ */ _("span", {
|
|
246
265
|
className: "mt-2 block font-mono text-xs text-text-tertiary break-all",
|
|
247
266
|
children: ["ID: ", l]
|
|
248
267
|
})] }),
|
|
249
|
-
action: /* @__PURE__ */
|
|
268
|
+
action: /* @__PURE__ */ g("button", {
|
|
250
269
|
type: "button",
|
|
251
|
-
onClick: () =>
|
|
270
|
+
onClick: () => v("/dashboards"),
|
|
252
271
|
className: "px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md",
|
|
253
272
|
children: "Back to Dashboards"
|
|
254
273
|
})
|
|
255
274
|
})
|
|
256
|
-
}) :
|
|
275
|
+
}) : z || !A ? /* @__PURE__ */ g("div", {
|
|
257
276
|
className: "flex items-center justify-center h-full",
|
|
258
|
-
children: /* @__PURE__ */
|
|
277
|
+
children: /* @__PURE__ */ _("div", {
|
|
259
278
|
className: "flex flex-col items-center gap-4",
|
|
260
|
-
children: [/* @__PURE__ */
|
|
279
|
+
children: [/* @__PURE__ */ g("div", { className: "animate-spin w-8 h-8 border-4 border-action-primary-bg border-t-transparent rounded-full" }), /* @__PURE__ */ g("p", {
|
|
261
280
|
className: "text-sm text-text-secondary",
|
|
262
281
|
children: "Loading dashboard..."
|
|
263
282
|
})]
|
|
264
283
|
})
|
|
265
|
-
}) : /* @__PURE__ */
|
|
284
|
+
}) : /* @__PURE__ */ _("div", {
|
|
266
285
|
className: "h-full flex flex-col overflow-hidden",
|
|
267
286
|
children: [
|
|
268
|
-
/* @__PURE__ */
|
|
287
|
+
/* @__PURE__ */ _("div", {
|
|
269
288
|
className: "flex items-center gap-3 px-3 py-2 border-b border-border-default bg-bg-surface flex-shrink-0",
|
|
270
|
-
children: [
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
289
|
+
children: [
|
|
290
|
+
/* @__PURE__ */ _(re, {
|
|
291
|
+
onClick: ce,
|
|
292
|
+
variant: "ghost",
|
|
293
|
+
size: "sm",
|
|
294
|
+
title: "Back to Dashboards",
|
|
295
|
+
"aria-label": "Back to Dashboards",
|
|
296
|
+
children: [/* @__PURE__ */ g(ne, { className: "w-4 h-4 mr-2" }), "Back"]
|
|
297
|
+
}),
|
|
298
|
+
/* @__PURE__ */ g("h2", {
|
|
299
|
+
className: "text-sm font-semibold text-text-primary truncate",
|
|
300
|
+
children: A.name
|
|
301
|
+
}),
|
|
302
|
+
/* @__PURE__ */ g("div", {
|
|
303
|
+
className: "ml-auto flex-shrink-0",
|
|
304
|
+
children: /* @__PURE__ */ g(te, {
|
|
305
|
+
isLive: T,
|
|
306
|
+
onToggle: () => E((e) => !e)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
]
|
|
281
310
|
}),
|
|
282
|
-
|
|
283
|
-
currentDashboardId:
|
|
284
|
-
currentDashboardName:
|
|
311
|
+
D && /* @__PURE__ */ g(c, {
|
|
312
|
+
currentDashboardId: A.id,
|
|
313
|
+
currentDashboardName: A.name,
|
|
285
314
|
basePath: "/bigconsole/dashboards"
|
|
286
315
|
}),
|
|
287
|
-
/* @__PURE__ */
|
|
316
|
+
/* @__PURE__ */ g("div", {
|
|
288
317
|
className: "flex-1 min-w-0 overflow-hidden",
|
|
289
|
-
children: /* @__PURE__ */
|
|
290
|
-
dashboardId:
|
|
291
|
-
pageId:
|
|
292
|
-
title:
|
|
293
|
-
onPaste:
|
|
294
|
-
onSave:
|
|
295
|
-
isSaving:
|
|
296
|
-
onExport:
|
|
297
|
-
pages: (
|
|
318
|
+
children: /* @__PURE__ */ g(o, {
|
|
319
|
+
dashboardId: A.id,
|
|
320
|
+
pageId: y || A.pages?.[0]?.id,
|
|
321
|
+
title: A.name,
|
|
322
|
+
onPaste: $,
|
|
323
|
+
onSave: se,
|
|
324
|
+
isSaving: ae,
|
|
325
|
+
onExport: ue,
|
|
326
|
+
pages: (A.pages || []).map((e, t) => ({
|
|
298
327
|
id: e.id,
|
|
299
328
|
name: e.name || `Page ${t + 1}`,
|
|
300
329
|
order: e.order ?? 0
|
|
301
330
|
})),
|
|
302
|
-
activePageId:
|
|
303
|
-
onPageChange:
|
|
304
|
-
onPageAdd:
|
|
305
|
-
onPageRename:
|
|
306
|
-
onPageDuplicate:
|
|
307
|
-
onPageDelete:
|
|
308
|
-
onPagesReorder:
|
|
331
|
+
activePageId: y || A.pages?.[0]?.id,
|
|
332
|
+
onPageChange: le,
|
|
333
|
+
onPageAdd: fe,
|
|
334
|
+
onPageRename: pe,
|
|
335
|
+
onPageDuplicate: me,
|
|
336
|
+
onPageDelete: he,
|
|
337
|
+
onPagesReorder: ge,
|
|
309
338
|
showPageTabs: !0
|
|
310
339
|
})
|
|
311
340
|
}),
|
|
312
|
-
/* @__PURE__ */
|
|
313
|
-
isOpen:
|
|
314
|
-
dashboardId:
|
|
315
|
-
dashboardName:
|
|
316
|
-
onClose:
|
|
341
|
+
/* @__PURE__ */ g(s, {
|
|
342
|
+
isOpen: oe,
|
|
343
|
+
dashboardId: A.id,
|
|
344
|
+
dashboardName: A.name,
|
|
345
|
+
onClose: de
|
|
317
346
|
})
|
|
318
347
|
]
|
|
319
348
|
});
|
|
320
349
|
});
|
|
321
350
|
//#endregion
|
|
322
|
-
export {
|
|
351
|
+
export { v as DashboardViewPage, v as default };
|
|
323
352
|
|
|
324
353
|
//# sourceMappingURL=DashboardViewPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardViewPage.js","names":[],"sources":["../../../src/bigconsole/pages/DashboardViewPage.tsx"],"sourcesContent":["/**\n * DashboardViewPage\n *\n * View/edit a single dashboard - fetches data from real backend.\n * Supports drilldown context propagation via URL parameters.\n */\n\nimport { type FC, memo, useEffect, useCallback, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft } from 'lucide-react';\nimport { Button, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useBigConsoleNavigate } from '../context';\nimport { DashboardCanvas, ExportDashboardDialog, DrilldownBreadcrumb } from '../components/dashboard';\nimport { useDashboardStore, useWidgetStore } from '../store';\nimport { useDashboardOperations, useWidgetOperations, usePageOperations } from '../hooks';\nimport { useFilterUrlSync } from '../hooks/useFilterUrlSync';\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardViewPage: FC = memo(function DashboardViewPage() {\n const { dashboardId } = useParams<{ dashboardId: string }>();\n const navigate = useBigConsoleNavigate();\n const [currentPageId, setCurrentPageId] = useState<string | null>(null);\n const [isSaving, setIsSaving] = useState(false);\n const [notFound, setNotFound] = useState(false);\n const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);\n\n // Drilldown context from URL\n const { isInDrilldown, getContextFromUrl, getDrilldownMetadata } = useFilterUrlSync();\n\n // Store actions\n const currentDashboard = useDashboardStore((state) => state.currentDashboard);\n const setCurrentDashboard = useDashboardStore((state) => state.setCurrentDashboard);\n const setGlobalFilterValue = useDashboardStore((state) => state.setGlobalFilterValue);\n const clearAllGlobalFilters = useDashboardStore((state) => state.clearAllGlobalFilters);\n const widgets = useWidgetStore((state) => state.widgets);\n const clipboardWidgetId = useWidgetStore((state) => state.clipboardWidgetId);\n const clipboardAction = useWidgetStore((state) => state.clipboardAction);\n const clearClipboard = useWidgetStore((state) => state.clearClipboard);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n\n // Dashboard operations hook\n const { loading, error, fetchDashboard, updateDashboard } = useDashboardOperations();\n\n // Widget operations hook\n const {\n createWidget: createWidgetMutation,\n deleteWidget: deleteWidgetMutation,\n duplicateWidget: duplicateWidgetMutation,\n batchUpdatePositions,\n } = useWidgetOperations(currentPageId || undefined, dashboardId);\n\n // Page operations hook\n const {\n createPage: createPageMutation,\n updatePage: updatePageMutation,\n deletePage: deletePageMutation,\n reorderPages: reorderPagesMutation,\n } = usePageOperations();\n\n // Load dashboard data\n useEffect(() => {\n if (!dashboardId) {\n navigate('/dashboards');\n return;\n }\n\n // Clear stale global filters from previous dashboard before loading new one.\n // This prevents drilldown context filters (ctx_*) from the source dashboard\n // from persisting and incorrectly filtering data on the target dashboard.\n clearAllGlobalFilters();\n\n setNotFound(false);\n\n // Fetch dashboard from backend\n fetchDashboard(dashboardId).then((dashboard) => {\n if (dashboard) {\n // Set the first page as current page\n if (dashboard.pages && dashboard.pages.length > 0) {\n setCurrentPageId(dashboard.pages[0].id);\n }\n } else {\n // Dashboard not found\n setNotFound(true);\n }\n });\n\n // Cleanup\n return () => {\n setCurrentDashboard(null);\n clearAllGlobalFilters();\n setNotFound(false);\n };\n }, [dashboardId, navigate, fetchDashboard, setCurrentDashboard, clearAllGlobalFilters]);\n\n // Load drilldown context from URL and apply to global filters\n useEffect(() => {\n if (!isInDrilldown) return;\n\n // Get context params from URL (ctx_* params)\n const contextParams = getContextFromUrl();\n const drilldownMeta = getDrilldownMetadata();\n\n // Apply context to dashboard state (available for widgets to use)\n // Store as special filter values that widgets can reference\n Object.entries(contextParams).forEach(([key, value]) => {\n // Store context params as global filter values with ctx_ prefix for clarity\n setGlobalFilterValue(`ctx_${key}`, value);\n });\n\n // Store drilldown metadata as special values\n if (drilldownMeta.depth > 0) {\n setGlobalFilterValue('__drilldown_depth', drilldownMeta.depth);\n setGlobalFilterValue('__drilldown_parent', drilldownMeta.parent);\n setGlobalFilterValue('__drilldown_path', drilldownMeta.path);\n }\n\n // Log for debugging\n if (Object.keys(contextParams).length > 0) {\n console.debug('[DashboardViewPage] Loaded drilldown context:', {\n context: contextParams,\n metadata: drilldownMeta,\n });\n }\n }, [isInDrilldown, getContextFromUrl, getDrilldownMetadata, setGlobalFilterValue]);\n\n // Handle save - batch update widget positions and dashboard metadata\n const handleSave = useCallback(async () => {\n if (!dashboardId) return;\n\n setIsSaving(true);\n try {\n // Get all widgets and their positions\n const widgetList = Array.from(widgets.values());\n if (widgetList.length > 0) {\n const positionUpdates = widgetList.map((widget) => ({\n id: widget.id,\n position: widget.position,\n }));\n\n await batchUpdatePositions(positionUpdates);\n }\n\n // Update dashboard metadata if needed\n if (currentDashboard) {\n await updateDashboard({\n id: dashboardId,\n name: currentDashboard.name,\n description: currentDashboard.description,\n });\n }\n } catch (error) {\n } finally {\n setIsSaving(false);\n }\n }, [dashboardId, widgets, currentDashboard, batchUpdatePositions, updateDashboard]);\n\n // Navigate back to the dashboards list\n const handleBack = useCallback(() => {\n navigate('/dashboards');\n }, [navigate]);\n\n // Handle page change\n const handlePageChange = useCallback((pageId: string) => {\n setCurrentPageId(pageId);\n }, []);\n\n // Handle paste widget from clipboard\n const handlePaste = useCallback(async () => {\n const pageId = currentPageId || currentDashboard?.pages?.[0]?.id;\n if (!clipboardWidgetId || !dashboardId || !pageId) {\n return;\n }\n\n const sourceWidget = widgets.get(clipboardWidgetId);\n if (!sourceWidget) {\n clearClipboard();\n return;\n }\n\n // Calculate new position (place at bottom of existing widgets)\n const widgetsArray = Array.from(widgets.values());\n const maxY = widgetsArray.reduce((max, w) => {\n const widgetBottom = (w.positionY ?? 0) + (w.positionHeight ?? 4);\n return Math.max(max, widgetBottom);\n }, 0);\n\n const newPosition = {\n x: 0,\n y: maxY,\n width: sourceWidget.positionWidth ?? 4,\n height: sourceWidget.positionHeight ?? 4,\n };\n\n const newTitle = clipboardAction === 'copy' ? `${sourceWidget.title} (Copy)` : sourceWidget.title;\n\n let newWidget = null;\n\n // Try duplicateWidget API first\n try {\n newWidget = await duplicateWidgetMutation(clipboardWidgetId, newPosition, newTitle);\n } catch (err) {}\n\n // Fallback: Create new widget with source widget's data\n if (!newWidget) {\n newWidget = await createWidgetMutation({\n pageId,\n dashboardId,\n type: sourceWidget.type,\n title: newTitle,\n description: sourceWidget.description,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: (sourceWidget as unknown as Record<string, string | undefined>).dataSinkId,\n datasetId: (sourceWidget as unknown as Record<string, string | undefined>).datasetId,\n parserId: (sourceWidget as unknown as Record<string, string | undefined>).parserId,\n parserRules: (sourceWidget as unknown as Record<string, string | undefined>).parserRules,\n config: sourceWidget.config || {},\n position: newPosition,\n refreshInterval: sourceWidget.refreshInterval ?? undefined,\n metadata: sourceWidget.metadata,\n });\n }\n\n if (newWidget) {\n selectWidget(newWidget.id);\n\n // If cut action, delete the original widget from backend\n if (clipboardAction === 'cut') {\n await deleteWidgetMutation(clipboardWidgetId);\n }\n }\n\n clearClipboard();\n }, [\n clipboardWidgetId,\n clipboardAction,\n dashboardId,\n currentPageId,\n currentDashboard,\n widgets,\n duplicateWidgetMutation,\n createWidgetMutation,\n deleteWidgetMutation,\n selectWidget,\n clearClipboard,\n ]);\n\n // Handle export\n const handleExport = useCallback(() => {\n setIsExportDialogOpen(true);\n }, []);\n\n const handleExportClose = useCallback(() => {\n setIsExportDialogOpen(false);\n }, []);\n\n // ============================================================================\n // Page Management Callbacks\n // ============================================================================\n\n // Handle adding a new page\n const handlePageAdd = useCallback(async () => {\n if (!currentDashboard || !dashboardId) return;\n\n const newPageCount = (currentDashboard.pages?.length || 0) + 1;\n const newPageName = `Page ${newPageCount}`;\n\n // Call GraphQL mutation to create page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: newPageName,\n });\n\n if (newPage) {\n // Update local state with the created page\n setCurrentDashboard({\n ...currentDashboard,\n pages: [...(currentDashboard.pages || []), newPage],\n });\n\n // Switch to the new page\n setCurrentPageId(newPage.id);\n }\n }, [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]);\n\n // Handle renaming a page\n const handlePageRename = useCallback(\n async (pageId: string, newName: string) => {\n if (!currentDashboard) return;\n\n // Call GraphQL mutation to update page\n const updatedPage = await updatePageMutation({\n id: pageId,\n name: newName,\n });\n\n if (updatedPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: currentDashboard.pages?.map((page) => (page.id === pageId ? { ...page, name: newName } : page)),\n });\n }\n },\n [currentDashboard, setCurrentDashboard, updatePageMutation]\n );\n\n // Handle duplicating a page\n const handlePageDuplicate = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || !dashboardId) return;\n\n const sourcePage = currentDashboard.pages?.find((p) => p.id === pageId);\n if (!sourcePage) return;\n\n // Call GraphQL mutation to create a copy of the page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: `${sourcePage.name} (Copy)`,\n });\n\n if (newPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: [...(currentDashboard.pages || []), newPage],\n });\n\n setCurrentPageId(newPage.id);\n }\n // Note: Widget duplication would require additional backend support\n },\n [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]\n );\n\n // Handle deleting a page\n const handlePageDelete = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || (currentDashboard.pages?.length || 0) <= 1) return;\n\n // Call GraphQL mutation to delete page\n const success = await deletePageMutation(pageId);\n\n if (success) {\n const updatedPages = currentDashboard.pages\n ?.filter((p) => p.id !== pageId)\n .map((p, index) => ({ ...p, order: index }));\n\n setCurrentDashboard({\n ...currentDashboard,\n pages: updatedPages,\n });\n\n // Switch to first page if current page was deleted\n if (currentPageId === pageId) {\n setCurrentPageId(updatedPages?.[0]?.id || null);\n }\n }\n },\n [currentDashboard, setCurrentDashboard, currentPageId, deletePageMutation]\n );\n\n // Handle reordering pages\n const handlePagesReorder = useCallback(\n async (reorderedPages: Array<{ id: string; name?: string; order?: number }>) => {\n if (!currentDashboard || !dashboardId) return;\n\n // Update local state optimistically\n setCurrentDashboard({\n ...currentDashboard,\n pages: reorderedPages.map((page, index) => ({\n ...page,\n order: index,\n })),\n });\n\n // Call GraphQL mutation to persist the reorder\n const pageIds = reorderedPages.map((p) => p.id);\n await reorderPagesMutation(dashboardId, pageIds);\n },\n [currentDashboard, dashboardId, setCurrentDashboard, reorderPagesMutation]\n );\n\n // Error state\n if (error) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full\" style={{ padding: 'var(--space-pagePadding)' }}>\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-6 w-full max-w-md\">\n <h3 className=\"text-lg font-medium text-status-error-text mb-2\">Error loading dashboard</h3>\n <p className=\"text-sm text-text-secondary mb-4 break-words\">{error.message}</p>\n <div className=\"flex flex-wrap gap-3\">\n <button\n type=\"button\"\n onClick={() => fetchDashboard(dashboardId!)}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary bg-bg-sunken hover:bg-bg-sunken/80 rounded-md\"\n >\n Back to Dashboards\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n // Not found state\n if (notFound) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full\" style={{ padding: 'var(--space-pagePadding)' }}>\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Dashboard not found\"\n description={\n <>\n The dashboard you're looking for doesn't exist or has been deleted.\n <span className=\"mt-2 block font-mono text-xs text-text-tertiary break-all\">ID: {dashboardId}</span>\n </>\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Back to Dashboards\n </button>\n }\n />\n </div>\n );\n }\n\n // Loading state\n if (loading || !currentDashboard) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"flex flex-col items-center gap-4\">\n <div className=\"animate-spin w-8 h-8 border-4 border-action-primary-bg border-t-transparent rounded-full\" />\n <p className=\"text-sm text-text-secondary\">Loading dashboard...</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full flex flex-col overflow-hidden\">\n {/* Page header with back navigation - visible in both view and edit modes */}\n <div className=\"flex items-center gap-3 px-3 py-2 border-b border-border-default bg-bg-surface flex-shrink-0\">\n <Button\n onClick={handleBack}\n variant=\"ghost\"\n size=\"sm\"\n title=\"Back to Dashboards\"\n aria-label=\"Back to Dashboards\"\n >\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n Back\n </Button>\n <h2 className=\"text-sm font-semibold text-text-primary truncate\">{currentDashboard.name}</h2>\n </div>\n\n {/* Drilldown Breadcrumb - shown when navigating via drilldown */}\n {isInDrilldown && (\n <DrilldownBreadcrumb\n currentDashboardId={currentDashboard.id}\n currentDashboardName={currentDashboard.name}\n basePath=\"/bigconsole/dashboards\"\n />\n )}\n\n {/* Dashboard Canvas with Page Tabs */}\n <div className=\"flex-1 min-w-0 overflow-hidden\">\n <DashboardCanvas\n dashboardId={currentDashboard.id}\n pageId={currentPageId || currentDashboard.pages?.[0]?.id}\n title={currentDashboard.name}\n onPaste={handlePaste}\n onSave={handleSave}\n isSaving={isSaving}\n onExport={handleExport}\n // Page tabs props\n pages={(currentDashboard.pages || []).map((page, index) => ({\n id: page.id,\n name: page.name || `Page ${index + 1}`,\n order: page.order ?? 0,\n }))}\n activePageId={currentPageId || currentDashboard.pages?.[0]?.id}\n onPageChange={handlePageChange}\n onPageAdd={handlePageAdd}\n onPageRename={handlePageRename}\n onPageDuplicate={handlePageDuplicate}\n onPageDelete={handlePageDelete}\n onPagesReorder={handlePagesReorder}\n showPageTabs={true}\n />\n </div>\n\n {/* Export Dashboard Dialog */}\n <ExportDashboardDialog\n isOpen={isExportDialogOpen}\n dashboardId={currentDashboard.id}\n dashboardName={currentDashboard.name}\n onClose={handleExportClose}\n />\n </div>\n );\n});\n\nexport default DashboardViewPage;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardViewPage.js","names":[],"sources":["../../../src/bigconsole/pages/DashboardViewPage.tsx"],"sourcesContent":["/**\n * DashboardViewPage\n *\n * View/edit a single dashboard - fetches data from real backend.\n * Supports drilldown context propagation via URL parameters.\n */\n\nimport { type FC, memo, useEffect, useCallback, useState, useRef } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft } from 'lucide-react';\nimport { Button, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useBigConsoleNavigate } from '../context';\nimport { DashboardCanvas, ExportDashboardDialog, DrilldownBreadcrumb, LiveCanvasToggle } from '../components/dashboard';\nimport { useDashboardStore, useWidgetStore } from '../store';\nimport { useDashboardOperations, useWidgetOperations, usePageOperations } from '../hooks';\nimport { useFilterUrlSync } from '../hooks/useFilterUrlSync';\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const DashboardViewPage: FC = memo(function DashboardViewPage() {\n const { dashboardId } = useParams<{ dashboardId: string }>();\n const navigate = useBigConsoleNavigate();\n const [currentPageId, setCurrentPageId] = useState<string | null>(null);\n const [isSaving, setIsSaving] = useState(false);\n const [notFound, setNotFound] = useState(false);\n const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);\n // Live canvas: when on, poll the dashboard + widget STRUCTURE so server-side\n // pipeline builds (AI assistant agent or any other writer) appear without a reload.\n const [isLive, setIsLive] = useState(false);\n\n // Drilldown context from URL\n const { isInDrilldown, getContextFromUrl, getDrilldownMetadata } = useFilterUrlSync();\n\n // Store actions\n const currentDashboard = useDashboardStore((state) => state.currentDashboard);\n const setCurrentDashboard = useDashboardStore((state) => state.setCurrentDashboard);\n const setGlobalFilterValue = useDashboardStore((state) => state.setGlobalFilterValue);\n const clearAllGlobalFilters = useDashboardStore((state) => state.clearAllGlobalFilters);\n const widgets = useWidgetStore((state) => state.widgets);\n const clipboardWidgetId = useWidgetStore((state) => state.clipboardWidgetId);\n const clipboardAction = useWidgetStore((state) => state.clipboardAction);\n const clearClipboard = useWidgetStore((state) => state.clearClipboard);\n const selectWidget = useWidgetStore((state) => state.selectWidget);\n\n // Dashboard operations hook\n const { loading, error, fetchDashboard, updateDashboard } = useDashboardOperations();\n\n // Widget operations hook\n const {\n createWidget: createWidgetMutation,\n deleteWidget: deleteWidgetMutation,\n duplicateWidget: duplicateWidgetMutation,\n batchUpdatePositions,\n refetch: refetchWidgets,\n } = useWidgetOperations(currentPageId || undefined, dashboardId);\n\n // Page operations hook\n const {\n createPage: createPageMutation,\n updatePage: updatePageMutation,\n deletePage: deletePageMutation,\n reorderPages: reorderPagesMutation,\n } = usePageOperations();\n\n // Load dashboard data\n useEffect(() => {\n if (!dashboardId) {\n navigate('/dashboards');\n return;\n }\n\n // Clear stale global filters from previous dashboard before loading new one.\n // This prevents drilldown context filters (ctx_*) from the source dashboard\n // from persisting and incorrectly filtering data on the target dashboard.\n clearAllGlobalFilters();\n\n setNotFound(false);\n\n // Fetch dashboard from backend\n fetchDashboard(dashboardId).then((dashboard) => {\n if (dashboard) {\n // Set the first page as current page\n if (dashboard.pages && dashboard.pages.length > 0) {\n setCurrentPageId(dashboard.pages[0].id);\n }\n } else {\n // Dashboard not found\n setNotFound(true);\n }\n });\n\n // Cleanup\n return () => {\n setCurrentDashboard(null);\n clearAllGlobalFilters();\n setNotFound(false);\n };\n }, [dashboardId, navigate, fetchDashboard, setCurrentDashboard, clearAllGlobalFilters]);\n\n // Load drilldown context from URL and apply to global filters\n useEffect(() => {\n if (!isInDrilldown) return;\n\n // Get context params from URL (ctx_* params)\n const contextParams = getContextFromUrl();\n const drilldownMeta = getDrilldownMetadata();\n\n // Apply context to dashboard state (available for widgets to use)\n // Store as special filter values that widgets can reference\n Object.entries(contextParams).forEach(([key, value]) => {\n // Store context params as global filter values with ctx_ prefix for clarity\n setGlobalFilterValue(`ctx_${key}`, value);\n });\n\n // Store drilldown metadata as special values\n if (drilldownMeta.depth > 0) {\n setGlobalFilterValue('__drilldown_depth', drilldownMeta.depth);\n setGlobalFilterValue('__drilldown_parent', drilldownMeta.parent);\n setGlobalFilterValue('__drilldown_path', drilldownMeta.path);\n }\n\n // Log for debugging\n if (Object.keys(contextParams).length > 0) {\n console.debug('[DashboardViewPage] Loaded drilldown context:', {\n context: contextParams,\n metadata: drilldownMeta,\n });\n }\n }, [isInDrilldown, getContextFromUrl, getDrilldownMetadata, setGlobalFilterValue]);\n\n // ---------------------------------------------------------------------------\n // Live canvas: poll dashboard + widget STRUCTURE while live mode is on.\n //\n // The per-widget DATA poll (WidgetWrapper → refreshInterval) already keeps\n // numbers fresh, but nothing refetches the board's STRUCTURE, so a widget the\n // AI assistant's api-calls agent (DataSink → Parser → Widget) — or any other\n // server-side writer — adds would never appear without a manual reload. This\n // interval closes that gap: it silently refetches the dashboard (pages) and\n // the widget list, so new widgets/pages materialise on the canvas live.\n //\n // Efficiency: it runs ONLY while live mode is on AND the tab is visible, and\n // pauses while the board is in edit mode so a poll cannot clobber an in-flight\n // drag/resize. Polling, not subscriptions — the backend GraphQL server is\n // HTTP-only with no graphql-ws/PubSub transport (see LiveBoardBanner).\n //\n // Latency note: the floor on how quickly a change shows up is NOT this poll\n // interval but fe-libs' shared GraphQL read cache (RECENT_QUERY_CACHE_TTL_MS,\n // ~15s) — `network-only` is served from that short-lived client cache, so a\n // newly-created widget surfaces on the next poll AFTER its cache entry\n // expires. No manual reload is ever needed; worst-case freshness is ~15s.\n // ---------------------------------------------------------------------------\n const refetchWidgetsRef = useRef(refetchWidgets);\n refetchWidgetsRef.current = refetchWidgets;\n\n useEffect(() => {\n if (!isLive || !dashboardId) return;\n\n const LIVE_POLL_MS = 3500;\n\n const pollStructure = () => {\n // Skip work when the tab is backgrounded or the board is being edited.\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n if (useDashboardStore.getState().viewMode === 'edit') return;\n void fetchDashboard(dashboardId, { silent: true });\n void refetchWidgetsRef.current();\n };\n\n // Refetch immediately on enable so newly-built widgets show up at once,\n // then settle into the cadence.\n pollStructure();\n const intervalId = window.setInterval(pollStructure, LIVE_POLL_MS);\n\n // Catch up the moment the operator returns to the tab.\n const handleVisibility = () => {\n if (document.visibilityState === 'visible') pollStructure();\n };\n document.addEventListener('visibilitychange', handleVisibility);\n\n return () => {\n window.clearInterval(intervalId);\n document.removeEventListener('visibilitychange', handleVisibility);\n };\n }, [isLive, dashboardId, fetchDashboard]);\n\n // Handle save - batch update widget positions and dashboard metadata\n const handleSave = useCallback(async () => {\n if (!dashboardId) return;\n\n setIsSaving(true);\n try {\n // Get all widgets and their positions\n const widgetList = Array.from(widgets.values());\n if (widgetList.length > 0) {\n const positionUpdates = widgetList.map((widget) => ({\n id: widget.id,\n position: widget.position,\n }));\n\n await batchUpdatePositions(positionUpdates);\n }\n\n // Update dashboard metadata if needed\n if (currentDashboard) {\n await updateDashboard({\n id: dashboardId,\n name: currentDashboard.name,\n description: currentDashboard.description,\n });\n }\n } catch (error) {\n } finally {\n setIsSaving(false);\n }\n }, [dashboardId, widgets, currentDashboard, batchUpdatePositions, updateDashboard]);\n\n // Navigate back to the dashboards list\n const handleBack = useCallback(() => {\n navigate('/dashboards');\n }, [navigate]);\n\n // Handle page change\n const handlePageChange = useCallback((pageId: string) => {\n setCurrentPageId(pageId);\n }, []);\n\n // Handle paste widget from clipboard\n const handlePaste = useCallback(async () => {\n const pageId = currentPageId || currentDashboard?.pages?.[0]?.id;\n if (!clipboardWidgetId || !dashboardId || !pageId) {\n return;\n }\n\n const sourceWidget = widgets.get(clipboardWidgetId);\n if (!sourceWidget) {\n clearClipboard();\n return;\n }\n\n // Calculate new position (place at bottom of existing widgets)\n const widgetsArray = Array.from(widgets.values());\n const maxY = widgetsArray.reduce((max, w) => {\n const widgetBottom = (w.positionY ?? 0) + (w.positionHeight ?? 4);\n return Math.max(max, widgetBottom);\n }, 0);\n\n const newPosition = {\n x: 0,\n y: maxY,\n width: sourceWidget.positionWidth ?? 4,\n height: sourceWidget.positionHeight ?? 4,\n };\n\n const newTitle = clipboardAction === 'copy' ? `${sourceWidget.title} (Copy)` : sourceWidget.title;\n\n let newWidget = null;\n\n // Try duplicateWidget API first\n try {\n newWidget = await duplicateWidgetMutation(clipboardWidgetId, newPosition, newTitle);\n } catch (err) {}\n\n // Fallback: Create new widget with source widget's data\n if (!newWidget) {\n newWidget = await createWidgetMutation({\n pageId,\n dashboardId,\n type: sourceWidget.type,\n title: newTitle,\n description: sourceWidget.description,\n // v2.0: Data source fields (DataSink → Parser → Widget)\n dataSinkId: (sourceWidget as unknown as Record<string, string | undefined>).dataSinkId,\n datasetId: (sourceWidget as unknown as Record<string, string | undefined>).datasetId,\n parserId: (sourceWidget as unknown as Record<string, string | undefined>).parserId,\n parserRules: (sourceWidget as unknown as Record<string, string | undefined>).parserRules,\n config: sourceWidget.config || {},\n position: newPosition,\n refreshInterval: sourceWidget.refreshInterval ?? undefined,\n metadata: sourceWidget.metadata,\n });\n }\n\n if (newWidget) {\n selectWidget(newWidget.id);\n\n // If cut action, delete the original widget from backend\n if (clipboardAction === 'cut') {\n await deleteWidgetMutation(clipboardWidgetId);\n }\n }\n\n clearClipboard();\n }, [\n clipboardWidgetId,\n clipboardAction,\n dashboardId,\n currentPageId,\n currentDashboard,\n widgets,\n duplicateWidgetMutation,\n createWidgetMutation,\n deleteWidgetMutation,\n selectWidget,\n clearClipboard,\n ]);\n\n // Handle export\n const handleExport = useCallback(() => {\n setIsExportDialogOpen(true);\n }, []);\n\n const handleExportClose = useCallback(() => {\n setIsExportDialogOpen(false);\n }, []);\n\n // ============================================================================\n // Page Management Callbacks\n // ============================================================================\n\n // Handle adding a new page\n const handlePageAdd = useCallback(async () => {\n if (!currentDashboard || !dashboardId) return;\n\n const newPageCount = (currentDashboard.pages?.length || 0) + 1;\n const newPageName = `Page ${newPageCount}`;\n\n // Call GraphQL mutation to create page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: newPageName,\n });\n\n if (newPage) {\n // Update local state with the created page\n setCurrentDashboard({\n ...currentDashboard,\n pages: [...(currentDashboard.pages || []), newPage],\n });\n\n // Switch to the new page\n setCurrentPageId(newPage.id);\n }\n }, [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]);\n\n // Handle renaming a page\n const handlePageRename = useCallback(\n async (pageId: string, newName: string) => {\n if (!currentDashboard) return;\n\n // Call GraphQL mutation to update page\n const updatedPage = await updatePageMutation({\n id: pageId,\n name: newName,\n });\n\n if (updatedPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: currentDashboard.pages?.map((page) => (page.id === pageId ? { ...page, name: newName } : page)),\n });\n }\n },\n [currentDashboard, setCurrentDashboard, updatePageMutation]\n );\n\n // Handle duplicating a page\n const handlePageDuplicate = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || !dashboardId) return;\n\n const sourcePage = currentDashboard.pages?.find((p) => p.id === pageId);\n if (!sourcePage) return;\n\n // Call GraphQL mutation to create a copy of the page\n // Note: 'order' is not available for create - backend auto-assigns order\n const newPage = await createPageMutation({\n dashboardId,\n name: `${sourcePage.name} (Copy)`,\n });\n\n if (newPage) {\n setCurrentDashboard({\n ...currentDashboard,\n pages: [...(currentDashboard.pages || []), newPage],\n });\n\n setCurrentPageId(newPage.id);\n }\n // Note: Widget duplication would require additional backend support\n },\n [currentDashboard, dashboardId, setCurrentDashboard, createPageMutation]\n );\n\n // Handle deleting a page\n const handlePageDelete = useCallback(\n async (pageId: string) => {\n if (!currentDashboard || (currentDashboard.pages?.length || 0) <= 1) return;\n\n // Call GraphQL mutation to delete page\n const success = await deletePageMutation(pageId);\n\n if (success) {\n const updatedPages = currentDashboard.pages\n ?.filter((p) => p.id !== pageId)\n .map((p, index) => ({ ...p, order: index }));\n\n setCurrentDashboard({\n ...currentDashboard,\n pages: updatedPages,\n });\n\n // Switch to first page if current page was deleted\n if (currentPageId === pageId) {\n setCurrentPageId(updatedPages?.[0]?.id || null);\n }\n }\n },\n [currentDashboard, setCurrentDashboard, currentPageId, deletePageMutation]\n );\n\n // Handle reordering pages\n const handlePagesReorder = useCallback(\n async (reorderedPages: Array<{ id: string; name?: string; order?: number }>) => {\n if (!currentDashboard || !dashboardId) return;\n\n // Update local state optimistically\n setCurrentDashboard({\n ...currentDashboard,\n pages: reorderedPages.map((page, index) => ({\n ...page,\n order: index,\n })),\n });\n\n // Call GraphQL mutation to persist the reorder\n const pageIds = reorderedPages.map((p) => p.id);\n await reorderPagesMutation(dashboardId, pageIds);\n },\n [currentDashboard, dashboardId, setCurrentDashboard, reorderPagesMutation]\n );\n\n // Error state\n if (error) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full\" style={{ padding: 'var(--space-pagePadding)' }}>\n <div className=\"bg-status-error-bg border border-status-error-border rounded-lg p-6 w-full max-w-md\">\n <h3 className=\"text-lg font-medium text-status-error-text mb-2\">Error loading dashboard</h3>\n <p className=\"text-sm text-text-secondary mb-4 break-words\">{error.message}</p>\n <div className=\"flex flex-wrap gap-3\">\n <button\n type=\"button\"\n onClick={() => fetchDashboard(dashboardId!)}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary bg-bg-sunken hover:bg-bg-sunken/80 rounded-md\"\n >\n Back to Dashboards\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n // Not found state\n if (notFound) {\n return (\n <div className=\"flex flex-col items-center justify-center h-full\" style={{ padding: 'var(--space-pagePadding)' }}>\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Dashboard not found\"\n description={\n <>\n The dashboard you're looking for doesn't exist or has been deleted.\n <span className=\"mt-2 block font-mono text-xs text-text-tertiary break-all\">ID: {dashboardId}</span>\n </>\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigate('/dashboards')}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-fg bg-action-primary-bg hover:bg-action-primary-bgHover rounded-md\"\n >\n Back to Dashboards\n </button>\n }\n />\n </div>\n );\n }\n\n // Loading state\n if (loading || !currentDashboard) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"flex flex-col items-center gap-4\">\n <div className=\"animate-spin w-8 h-8 border-4 border-action-primary-bg border-t-transparent rounded-full\" />\n <p className=\"text-sm text-text-secondary\">Loading dashboard...</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full flex flex-col overflow-hidden\">\n {/* Page header with back navigation - visible in both view and edit modes */}\n <div className=\"flex items-center gap-3 px-3 py-2 border-b border-border-default bg-bg-surface flex-shrink-0\">\n <Button\n onClick={handleBack}\n variant=\"ghost\"\n size=\"sm\"\n title=\"Back to Dashboards\"\n aria-label=\"Back to Dashboards\"\n >\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n Back\n </Button>\n <h2 className=\"text-sm font-semibold text-text-primary truncate\">{currentDashboard.name}</h2>\n {/* Live canvas toggle — watch server-side pipeline builds appear without a reload */}\n <div className=\"ml-auto flex-shrink-0\">\n <LiveCanvasToggle isLive={isLive} onToggle={() => setIsLive((prev) => !prev)} />\n </div>\n </div>\n\n {/* Drilldown Breadcrumb - shown when navigating via drilldown */}\n {isInDrilldown && (\n <DrilldownBreadcrumb\n currentDashboardId={currentDashboard.id}\n currentDashboardName={currentDashboard.name}\n basePath=\"/bigconsole/dashboards\"\n />\n )}\n\n {/* Dashboard Canvas with Page Tabs */}\n <div className=\"flex-1 min-w-0 overflow-hidden\">\n <DashboardCanvas\n dashboardId={currentDashboard.id}\n pageId={currentPageId || currentDashboard.pages?.[0]?.id}\n title={currentDashboard.name}\n onPaste={handlePaste}\n onSave={handleSave}\n isSaving={isSaving}\n onExport={handleExport}\n // Page tabs props\n pages={(currentDashboard.pages || []).map((page, index) => ({\n id: page.id,\n name: page.name || `Page ${index + 1}`,\n order: page.order ?? 0,\n }))}\n activePageId={currentPageId || currentDashboard.pages?.[0]?.id}\n onPageChange={handlePageChange}\n onPageAdd={handlePageAdd}\n onPageRename={handlePageRename}\n onPageDuplicate={handlePageDuplicate}\n onPageDelete={handlePageDelete}\n onPagesReorder={handlePagesReorder}\n showPageTabs={true}\n />\n </div>\n\n {/* Export Dashboard Dialog */}\n <ExportDashboardDialog\n isOpen={isExportDialogOpen}\n dashboardId={currentDashboard.id}\n dashboardName={currentDashboard.name}\n onClose={handleExportClose}\n />\n </div>\n );\n});\n\nexport default DashboardViewPage;\n"],"mappings":"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"}
|