@burdenoff/microfe-bigconsole 2026.803.2 → 2026.803.3
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/components/dashboard/ShareDialog/EmbedTab.js +268 -168
- package/dist/bigconsole/components/dashboard/ShareDialog/EmbedTab.js.map +1 -1
- package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js +63 -24
- package/dist/bigconsole/components/embed/ReadOnlyDashboardRenderer.js.map +1 -1
- package/dist/bigconsole/components/embed/types.js.map +1 -1
- package/dist/bigconsole/hooks/useDashboardEmbedPolicy.js +87 -50
- package/dist/bigconsole/hooks/useDashboardEmbedPolicy.js.map +1 -1
- package/dist/generated/wspace-operations.js +3 -0
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js +8 -0
- package/dist/generated/wspace-types.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmbedTab.js","names":[],"sources":["../../../../../src/bigconsole/components/dashboard/ShareDialog/EmbedTab.tsx"],"sourcesContent":["/**\n * EmbedTab — \"Publish to web\" section of the dashboard Share dialog.\n *\n * Owner-facing UI to publish a dashboard as an anonymous-link embed serving LIVE\n * data, manage the allowed parent domains, copy the iframe snippet, rotate the\n * link key, and unpublish. Backed by {@link useDashboardEmbedPolicy}; all\n * mutations are gateway-enforced by `@rbac(manage_embed)` (owner-only).\n *\n * Self-contained (inline styles driven by the dialog's `colors`) so it slots into\n * the existing hand-styled ShareDialog without a broader refactor.\n */\n\nimport React, { useEffect, useMemo, useState } from 'react';\nimport { useDashboardEmbedPolicy } from '../../../hooks/useDashboardEmbedPolicy';\n\ninterface DialogColors {\n bgPrimary: string;\n bgSecondary: string;\n bgTertiary: string;\n textPrimary: string;\n textSecondary: string;\n textTertiary: string;\n borderDefault: string;\n actionPrimaryBg: string;\n actionPrimaryBgHover: string;\n statusSuccessBg: string;\n statusErrorText: string;\n}\n\nexport interface EmbedTabProps {\n dashboardId: string;\n colors: DialogColors;\n}\n\n/** Embed color-mode baked into the iframe URL (BOFF-5734). */\ntype EmbedSnippetTheme = 'light' | 'dark' | 'auto';\n\nconst THEME_OPTIONS: ReadonlyArray<{ value: EmbedSnippetTheme; label: string }> = [\n { value: 'light', label: 'Light' },\n { value: 'dark', label: 'Dark' },\n { value: 'auto', label: 'Auto (match viewer)' },\n];\n\n/**\n * Bake the chosen color mode into an iframe snippet by adding `?theme=` to its\n * `src` URL. `light` is the default and is left implicit so existing/plain\n * snippets stay unchanged. The secret `#key=` fragment is preserved untouched.\n *\n * Inserted via string splicing (before the fragment) rather than `URL`/\n * `URLSearchParams`, which re-serialize ALL params and could subtly re-encode\n * existing ones (e.g. `%20` → `+`). `theme` is a fixed enum, so no encoding is\n * needed; every other part of the URL is passed through byte-for-byte.\n */\nexport function withThemeInSnippet(snippet: string, theme: EmbedSnippetTheme): string {\n if (theme === 'light') return snippet;\n // Require whitespace before `src` so we match the real attribute, never a\n // substring like `data-src`/`srcdoc` (the snippet is backend-generated with\n // each attribute on its own indented line).\n return snippet.replace(/(?<=\\s)src=\"([^\"]*)\"/, (_match, url: string) => {\n const hashIdx = url.indexOf('#');\n const base = hashIdx === -1 ? url : url.slice(0, hashIdx);\n const fragment = hashIdx === -1 ? '' : url.slice(hashIdx);\n const sep = base.includes('?') ? '&' : '?';\n return `src=\"${base}${sep}theme=${theme}${fragment}\"`;\n });\n}\n\n/** Validate an exact https origin (matches the backend allowlist rules). */\nfunction normalizeOrigin(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const url = new URL(trimmed);\n if (url.protocol !== 'https:') return null;\n if ((url.pathname !== '' && url.pathname !== '/') || url.search || url.hash) return null;\n return url.origin;\n } catch {\n return null;\n }\n}\n\nexport function EmbedTab({ dashboardId, colors }: EmbedTabProps) {\n const { policy, secret, loading, error, fetchPolicy, publishToWeb, setAllowedOrigins, rotateKey, unpublish } =\n useDashboardEmbedPolicy();\n\n const [domainInput, setDomainInput] = useState('');\n const [pendingDomains, setPendingDomains] = useState<string[]>([]);\n const [attested, setAttested] = useState(false);\n const [copied, setCopied] = useState(false);\n const [theme, setTheme] = useState<EmbedSnippetTheme>('light');\n const [localError, setLocalError] = useState<string | null>(null);\n\n useEffect(() => {\n if (dashboardId) fetchPolicy(dashboardId);\n }, [dashboardId, fetchPolicy]);\n\n const isPublished = policy?.status === 'ACTIVE';\n const domains = isPublished ? (policy?.allowedParentOrigins ?? []) : pendingDomains;\n\n // The copyable iframe snippet: from the one-time secret if present (fresh\n // publish/rotate), else reconstructed from the locator for an already-published\n // policy (the fragment key is only available in the one-time secret).\n const iframeSnippet = useMemo(() => {\n if (secret?.iframeSnippet) return withThemeInSnippet(secret.iframeSnippet, theme);\n return null;\n }, [secret, theme]);\n\n const addDomain = () => {\n setLocalError(null);\n const origin = normalizeOrigin(domainInput);\n if (!origin) {\n setLocalError('Enter a valid https origin, e.g. https://example.com');\n return;\n }\n if (domains.includes(origin)) {\n setDomainInput('');\n return;\n }\n if (isPublished && policy) {\n void setAllowedOrigins(policy.id, [...domains, origin]);\n } else {\n setPendingDomains((prev) => [...prev, origin]);\n }\n setDomainInput('');\n };\n\n const removeDomain = (origin: string) => {\n if (isPublished && policy) {\n void setAllowedOrigins(\n policy.id,\n domains.filter((d) => d !== origin)\n );\n } else {\n setPendingDomains((prev) => prev.filter((d) => d !== origin));\n }\n };\n\n const handlePublish = async () => {\n setLocalError(null);\n if (pendingDomains.length === 0) {\n setLocalError('Add at least one allowed website domain before publishing.');\n return;\n }\n if (!attested) {\n setLocalError('Please confirm this dashboard is safe to be public.');\n return;\n }\n await publishToWeb(dashboardId, pendingDomains, true);\n };\n\n const handleCopy = async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n } catch {\n setLocalError('Could not copy — select and copy manually.');\n }\n };\n\n const primaryBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: 'none',\n background: colors.actionPrimaryBg,\n color: '#fff',\n fontSize: '13px',\n fontWeight: 600,\n cursor: 'pointer',\n };\n const ghostBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: 'transparent',\n color: colors.textPrimary,\n fontSize: '13px',\n cursor: 'pointer',\n };\n const inputStyle: React.CSSProperties = {\n flex: 1,\n padding: '8px 10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '13px',\n };\n\n return (\n <div style={{ padding: '20px 24px' }}>\n {/* Status banner */}\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n borderRadius: '10px',\n background: colors.bgSecondary,\n marginBottom: '16px',\n }}\n >\n <div>\n <div style={{ fontSize: '14px', fontWeight: 600, color: colors.textPrimary }}>Publish to the web</div>\n <div style={{ fontSize: '12px', color: colors.textSecondary, marginTop: '2px' }}>\n {isPublished\n ? 'This dashboard is live. Anyone with the link on an allowed website sees its live data.'\n : 'Embed this dashboard on your website. Viewers see live data — no login required.'}\n </div>\n </div>\n <span\n style={{\n fontSize: '11px',\n fontWeight: 700,\n padding: '3px 8px',\n borderRadius: '999px',\n color: '#fff',\n background: isPublished ? colors.statusSuccessBg : colors.textTertiary,\n }}\n >\n {isPublished ? 'PUBLISHED' : 'PRIVATE'}\n </span>\n </div>\n\n {/* Allowed domains */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '6px' }}>\n Allowed website domains\n </div>\n <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>\n <input\n style={inputStyle}\n placeholder=\"https://yoursite.com\"\n value={domainInput}\n onChange={(e) => setDomainInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') addDomain();\n }}\n />\n <button style={ghostBtn} onClick={addDomain} type=\"button\">\n Add\n </button>\n </div>\n {domains.length === 0 ? (\n <div style={{ fontSize: '12px', color: colors.textTertiary }}>\n No domains yet. Only listed https domains may frame this dashboard.\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>\n {domains.map((d) => (\n <div\n key={d}\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '6px 10px',\n borderRadius: '8px',\n background: colors.bgTertiary,\n }}\n >\n <span style={{ fontSize: '13px', color: colors.textPrimary }}>{d}</span>\n <button\n style={{ ...ghostBtn, padding: '2px 8px', color: colors.statusErrorText }}\n onClick={() => removeDomain(d)}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Publish CTA (unpublished) */}\n {!isPublished && (\n <div style={{ marginBottom: '8px' }}>\n <label style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>\n <input type=\"checkbox\" checked={attested} onChange={(e) => setAttested(e.target.checked)} />\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>\n I understand this makes the dashboard's live data visible to anyone with the link, and I confirm it\n is safe to be public.\n </span>\n </label>\n <button style={primaryBtn} onClick={handlePublish} disabled={loading} type=\"button\">\n {loading ? 'Publishing…' : 'Publish to web'}\n </button>\n </div>\n )}\n\n {/* Embed snippet + management (published) */}\n {isPublished && (\n <>\n {iframeSnippet ? (\n <div style={{ marginBottom: '16px' }}>\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginBottom: '6px',\n }}\n >\n <span style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary }}>Embed snippet</span>\n <label style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>\n <span style={{ fontSize: '12px', color: colors.textTertiary }}>Theme</span>\n <select\n value={theme}\n onChange={(e) => {\n // The options are fixed, but narrow instead of casting so an\n // unexpected value can never slip into state.\n const value = e.target.value;\n if (value === 'light' || value === 'dark' || value === 'auto') {\n setTheme(value);\n }\n }}\n style={{\n padding: '4px 8px',\n borderRadius: '6px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '12px',\n cursor: 'pointer',\n }}\n >\n {THEME_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n </div>\n <textarea\n readOnly\n value={iframeSnippet}\n style={{\n width: '100%',\n minHeight: '96px',\n padding: '10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontFamily: 'monospace',\n fontSize: '12px',\n resize: 'vertical',\n }}\n />\n <button style={{ ...ghostBtn, marginTop: '8px' }} onClick={() => handleCopy(iframeSnippet)} type=\"button\">\n {copied ? 'Copied ✓' : 'Copy snippet'}\n </button>\n </div>\n ) : (\n <div\n style={{\n fontSize: '12px',\n color: colors.textSecondary,\n background: colors.bgSecondary,\n borderRadius: '8px',\n padding: '10px 12px',\n marginBottom: '16px',\n }}\n >\n The embed link is already active. For security the snippet (which contains the secret key) is only shown\n once at publish time — use <strong>Rotate link</strong> to generate a fresh snippet.\n </div>\n )}\n\n <div style={{ display: 'flex', gap: '8px' }}>\n <button style={ghostBtn} onClick={() => policy && rotateKey(policy.id)} disabled={loading} type=\"button\">\n Rotate link\n </button>\n <button\n style={{ ...ghostBtn, color: colors.statusErrorText, borderColor: colors.statusErrorText }}\n onClick={() => {\n if (\n policy &&\n window.confirm(\n 'Unpublish this dashboard? The embed will stop working immediately for anyone using the link.'\n )\n ) {\n void unpublish(policy.id);\n }\n }}\n disabled={loading}\n type=\"button\"\n >\n Unpublish\n </button>\n </div>\n </>\n )}\n\n {(localError || error) && (\n <div style={{ marginTop: '12px', fontSize: '12px', color: colors.statusErrorText }}>\n {localError ?? error?.message}\n </div>\n )}\n </div>\n );\n}\n\nexport default EmbedTab;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EmbedTab.js","names":[],"sources":["../../../../../src/bigconsole/components/dashboard/ShareDialog/EmbedTab.tsx"],"sourcesContent":["/**\n * EmbedTab — \"Publish to web\" section of the dashboard Share dialog.\n *\n * Owner-facing UI to publish a dashboard as an anonymous-link embed serving LIVE\n * data, manage the allowed parent domains, copy the iframe snippet, rotate the\n * link key, and unpublish. Backed by {@link useDashboardEmbedPolicy}; all\n * mutations are gateway-enforced by `@rbac(manage_embed)` (owner-only).\n *\n * Self-contained (inline styles driven by the dialog's `colors`) so it slots into\n * the existing hand-styled ShareDialog without a broader refactor.\n */\n\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { useDashboardEmbedPolicy, themeChoiceFromPolicy } from '../../../hooks/useDashboardEmbedPolicy';\n\ninterface DialogColors {\n bgPrimary: string;\n bgSecondary: string;\n bgTertiary: string;\n textPrimary: string;\n textSecondary: string;\n textTertiary: string;\n borderDefault: string;\n actionPrimaryBg: string;\n actionPrimaryBgHover: string;\n statusSuccessBg: string;\n statusErrorText: string;\n}\n\nexport interface EmbedTabProps {\n dashboardId: string;\n colors: DialogColors;\n}\n\n/** Embed color-mode baked into the iframe URL (BOFF-5734). */\ntype EmbedSnippetTheme = 'light' | 'dark' | 'auto';\n\nconst THEME_OPTIONS: ReadonlyArray<{ value: EmbedSnippetTheme; label: string }> = [\n { value: 'light', label: 'Light' },\n { value: 'dark', label: 'Dark' },\n { value: 'auto', label: 'Auto (match viewer)' },\n];\n\n/**\n * Bake the chosen color mode into an iframe snippet by adding `?theme=` to its\n * `src` URL. `light` is the default and is left implicit so existing/plain\n * snippets stay unchanged. The secret `#key=` fragment is preserved untouched.\n *\n * Inserted via string splicing (before the fragment) rather than `URL`/\n * `URLSearchParams`, which re-serialize ALL params and could subtly re-encode\n * existing ones (e.g. `%20` → `+`). `theme` is a fixed enum, so no encoding is\n * needed; every other part of the URL is passed through byte-for-byte.\n */\nexport function withThemeInSnippet(snippet: string, theme: EmbedSnippetTheme): string {\n if (theme === 'light') return snippet;\n // Require whitespace before `src` so we match the real attribute, never a\n // substring like `data-src`/`srcdoc` (the snippet is backend-generated with\n // each attribute on its own indented line).\n return snippet.replace(/(?<=\\s)src=\"([^\"]*)\"/, (_match, url: string) => {\n const hashIdx = url.indexOf('#');\n const base = hashIdx === -1 ? url : url.slice(0, hashIdx);\n const fragment = hashIdx === -1 ? '' : url.slice(hashIdx);\n const sep = base.includes('?') ? '&' : '?';\n return `src=\"${base}${sep}theme=${theme}${fragment}\"`;\n });\n}\n\n/** Validate an exact https origin (matches the backend allowlist rules). */\nfunction normalizeOrigin(raw: string): string | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try {\n const url = new URL(trimmed);\n if (url.protocol !== 'https:') return null;\n if ((url.pathname !== '' && url.pathname !== '/') || url.search || url.hash) return null;\n return url.origin;\n } catch {\n return null;\n }\n}\n\nexport function EmbedTab({ dashboardId, colors }: EmbedTabProps) {\n const {\n policy,\n secret,\n loading,\n error,\n fetchPolicy,\n publishToWeb,\n applyPresentation,\n setAllowedOrigins,\n rotateKey,\n unpublish,\n } = useDashboardEmbedPolicy();\n\n const [domainInput, setDomainInput] = useState('');\n const [pendingDomains, setPendingDomains] = useState<string[]>([]);\n const [attested, setAttested] = useState(false);\n const [copied, setCopied] = useState(false);\n // Presentation options (BOFF-5735). Defaults mirror the backend column defaults\n // so a fresh publish captures a sensible look without extra clicks.\n const [theme, setTheme] = useState<EmbedSnippetTheme>('light');\n const [showTitle, setShowTitle] = useState(false);\n const [showBranding, setShowBranding] = useState(true);\n const [presentationSaved, setPresentationSaved] = useState(false);\n const [localError, setLocalError] = useState<string | null>(null);\n // Only seed the presentation controls once per policy identity — NOT on every\n // `policy` object change. Otherwise an unrelated mutation (e.g. adding a domain\n // via setAllowedOrigins) would return a fresh policy object and clobber the\n // owner's in-progress, not-yet-applied theme/title/branding edits.\n const syncedPolicyIdRef = useRef<string | null>(null);\n const savedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n if (dashboardId) fetchPolicy(dashboardId);\n }, [dashboardId, fetchPolicy]);\n\n // Reflect the persisted presentation the first time each policy loads.\n useEffect(() => {\n if (!policy || syncedPolicyIdRef.current === policy.id) return;\n syncedPolicyIdRef.current = policy.id;\n setTheme(themeChoiceFromPolicy(policy.presentationTheme));\n setShowTitle(policy.showDashboardTitle);\n setShowBranding(policy.showBrandingFooter);\n }, [policy]);\n\n // Clear any pending \"Updated ✓\" timer on unmount.\n useEffect(\n () => () => {\n if (savedTimerRef.current) clearTimeout(savedTimerRef.current);\n },\n []\n );\n\n const isPublished = policy?.status === 'ACTIVE';\n const domains = isPublished ? (policy?.allowedParentOrigins ?? []) : pendingDomains;\n\n // The copyable iframe snippet: from the one-time secret if present (fresh\n // publish/rotate), else reconstructed from the locator for an already-published\n // policy (the fragment key is only available in the one-time secret).\n const iframeSnippet = useMemo(() => {\n if (secret?.iframeSnippet) return withThemeInSnippet(secret.iframeSnippet, theme);\n return null;\n }, [secret, theme]);\n\n const addDomain = () => {\n setLocalError(null);\n const origin = normalizeOrigin(domainInput);\n if (!origin) {\n setLocalError('Enter a valid https origin, e.g. https://example.com');\n return;\n }\n if (domains.includes(origin)) {\n setDomainInput('');\n return;\n }\n if (isPublished && policy) {\n void setAllowedOrigins(policy.id, [...domains, origin]);\n } else {\n setPendingDomains((prev) => [...prev, origin]);\n }\n setDomainInput('');\n };\n\n const removeDomain = (origin: string) => {\n if (isPublished && policy) {\n void setAllowedOrigins(\n policy.id,\n domains.filter((d) => d !== origin)\n );\n } else {\n setPendingDomains((prev) => prev.filter((d) => d !== origin));\n }\n };\n\n const handlePublish = async () => {\n setLocalError(null);\n if (pendingDomains.length === 0) {\n setLocalError('Add at least one allowed website domain before publishing.');\n return;\n }\n if (!attested) {\n setLocalError('Please confirm this dashboard is safe to be public.');\n return;\n }\n await publishToWeb(dashboardId, pendingDomains, true, { theme, showTitle, showBranding });\n };\n\n const handleApplyPresentation = async () => {\n if (!policy) return;\n setLocalError(null);\n const updated = await applyPresentation(policy.id, { theme, showTitle, showBranding });\n if (updated) {\n setPresentationSaved(true);\n // Clear any in-flight timer so rapid re-clicks don't hide the confirmation early.\n if (savedTimerRef.current) clearTimeout(savedTimerRef.current);\n savedTimerRef.current = setTimeout(() => setPresentationSaved(false), 1500);\n }\n };\n\n const handleCopy = async (text: string) => {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n } catch {\n setLocalError('Could not copy — select and copy manually.');\n }\n };\n\n const primaryBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: 'none',\n background: colors.actionPrimaryBg,\n color: '#fff',\n fontSize: '13px',\n fontWeight: 600,\n cursor: 'pointer',\n };\n const ghostBtn: React.CSSProperties = {\n padding: '8px 14px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: 'transparent',\n color: colors.textPrimary,\n fontSize: '13px',\n cursor: 'pointer',\n };\n const inputStyle: React.CSSProperties = {\n flex: 1,\n padding: '8px 10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '13px',\n };\n const selectStyle: React.CSSProperties = {\n padding: '4px 8px',\n borderRadius: '6px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontSize: '12px',\n cursor: 'pointer',\n };\n const toggleRow: React.CSSProperties = {\n display: 'flex',\n gap: '8px',\n alignItems: 'flex-start',\n fontSize: '12px',\n color: colors.textSecondary,\n };\n\n // Presentation controls (BOFF-5735) — shared by the pre-publish and published\n // states. Local state drives both the persisted policy and the snippet `?theme=`.\n const presentationControls = (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>\n <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>Theme</span>\n <select\n value={theme}\n onChange={(e) => {\n // The options are fixed, but narrow instead of casting so an\n // unexpected value can never slip into state.\n const value = e.target.value;\n if (value === 'light' || value === 'dark' || value === 'auto') {\n setTheme(value);\n }\n }}\n style={selectStyle}\n >\n {THEME_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </label>\n <label style={toggleRow}>\n <input type=\"checkbox\" checked={showTitle} onChange={(e) => setShowTitle(e.target.checked)} />\n <span>Show the dashboard title as a header</span>\n </label>\n <label style={toggleRow}>\n <input type=\"checkbox\" checked={showBranding} onChange={(e) => setShowBranding(e.target.checked)} />\n <span>Show a subtle “Powered by BigConsole” footer</span>\n </label>\n </div>\n );\n\n return (\n <div style={{ padding: '20px 24px' }}>\n {/* Status banner */}\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 14px',\n borderRadius: '10px',\n background: colors.bgSecondary,\n marginBottom: '16px',\n }}\n >\n <div>\n <div style={{ fontSize: '14px', fontWeight: 600, color: colors.textPrimary }}>Publish to the web</div>\n <div style={{ fontSize: '12px', color: colors.textSecondary, marginTop: '2px' }}>\n {isPublished\n ? 'This dashboard is live. Anyone with the link on an allowed website sees its live data.'\n : 'Embed this dashboard on your website. Viewers see live data — no login required.'}\n </div>\n </div>\n <span\n style={{\n fontSize: '11px',\n fontWeight: 700,\n padding: '3px 8px',\n borderRadius: '999px',\n color: '#fff',\n background: isPublished ? colors.statusSuccessBg : colors.textTertiary,\n }}\n >\n {isPublished ? 'PUBLISHED' : 'PRIVATE'}\n </span>\n </div>\n\n {/* Allowed domains */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '6px' }}>\n Allowed website domains\n </div>\n <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>\n <input\n style={inputStyle}\n placeholder=\"https://yoursite.com\"\n value={domainInput}\n onChange={(e) => setDomainInput(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') addDomain();\n }}\n />\n <button style={ghostBtn} onClick={addDomain} type=\"button\">\n Add\n </button>\n </div>\n {domains.length === 0 ? (\n <div style={{ fontSize: '12px', color: colors.textTertiary }}>\n No domains yet. Only listed https domains may frame this dashboard.\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>\n {domains.map((d) => (\n <div\n key={d}\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '6px 10px',\n borderRadius: '8px',\n background: colors.bgTertiary,\n }}\n >\n <span style={{ fontSize: '13px', color: colors.textPrimary }}>{d}</span>\n <button\n style={{ ...ghostBtn, padding: '2px 8px', color: colors.statusErrorText }}\n onClick={() => removeDomain(d)}\n type=\"button\"\n >\n Remove\n </button>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Presentation (unpublished — captured into the publish) */}\n {!isPublished && (\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '8px' }}>\n Presentation\n </div>\n {presentationControls}\n </div>\n )}\n\n {/* Publish CTA (unpublished) */}\n {!isPublished && (\n <div style={{ marginBottom: '8px' }}>\n <label style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '12px' }}>\n <input type=\"checkbox\" checked={attested} onChange={(e) => setAttested(e.target.checked)} />\n <span style={{ fontSize: '12px', color: colors.textSecondary }}>\n I understand this makes the dashboard's live data visible to anyone with the link, and I confirm it\n is safe to be public.\n </span>\n </label>\n <button style={primaryBtn} onClick={handlePublish} disabled={loading} type=\"button\">\n {loading ? 'Publishing…' : 'Publish to web'}\n </button>\n </div>\n )}\n\n {/* Embed snippet + management (published) */}\n {isPublished && (\n <>\n {iframeSnippet ? (\n <div style={{ marginBottom: '16px' }}>\n <div style={{ marginBottom: '6px' }}>\n <span style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary }}>Embed snippet</span>\n </div>\n <textarea\n readOnly\n value={iframeSnippet}\n style={{\n width: '100%',\n minHeight: '96px',\n padding: '10px',\n borderRadius: '8px',\n border: `1px solid ${colors.borderDefault}`,\n background: colors.bgSecondary,\n color: colors.textPrimary,\n fontFamily: 'monospace',\n fontSize: '12px',\n resize: 'vertical',\n }}\n />\n <button style={{ ...ghostBtn, marginTop: '8px' }} onClick={() => handleCopy(iframeSnippet)} type=\"button\">\n {copied ? 'Copied ✓' : 'Copy snippet'}\n </button>\n </div>\n ) : (\n <div\n style={{\n fontSize: '12px',\n color: colors.textSecondary,\n background: colors.bgSecondary,\n borderRadius: '8px',\n padding: '10px 12px',\n marginBottom: '16px',\n }}\n >\n The embed link is already active. For security the snippet (which contains the secret key) is only shown\n once at publish time — use <strong>Rotate link</strong> to generate a fresh snippet.\n </div>\n )}\n\n {/* Presentation (published — persisted + re-published to take effect) */}\n <div style={{ marginBottom: '16px' }}>\n <div style={{ fontSize: '12px', fontWeight: 600, color: colors.textSecondary, marginBottom: '8px' }}>\n Presentation\n </div>\n {presentationControls}\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginTop: '10px' }}>\n <button style={ghostBtn} onClick={handleApplyPresentation} disabled={loading} type=\"button\">\n {loading ? 'Applying…' : 'Apply changes'}\n </button>\n {presentationSaved && <span style={{ fontSize: '12px', color: colors.textTertiary }}>Updated ✓</span>}\n </div>\n <div style={{ fontSize: '11px', color: colors.textTertiary, marginTop: '6px' }}>\n Changes apply to live embeds within a few seconds — no re-publish needed, and the embed link stays the\n same.\n </div>\n </div>\n\n <div style={{ display: 'flex', gap: '8px' }}>\n <button style={ghostBtn} onClick={() => policy && rotateKey(policy.id)} disabled={loading} type=\"button\">\n Rotate link\n </button>\n <button\n style={{ ...ghostBtn, color: colors.statusErrorText, borderColor: colors.statusErrorText }}\n onClick={() => {\n if (\n policy &&\n window.confirm(\n 'Unpublish this dashboard? The embed will stop working immediately for anyone using the link.'\n )\n ) {\n void unpublish(policy.id);\n }\n }}\n disabled={loading}\n type=\"button\"\n >\n Unpublish\n </button>\n </div>\n </>\n )}\n\n {(localError || error) && (\n <div style={{ marginTop: '12px', fontSize: '12px', color: colors.statusErrorText }}>\n {localError ?? error?.message}\n </div>\n )}\n </div>\n );\n}\n\nexport default EmbedTab;\n"],"mappings":"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"}
|
|
@@ -27,36 +27,75 @@ function o({ page: e, capabilities: n }) {
|
|
|
27
27
|
}, e.id))
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
function s(
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
function s() {
|
|
31
|
+
return /* @__PURE__ */ r("div", {
|
|
32
|
+
className: "flex shrink-0 items-center justify-end border-t border-border-default bg-bg-surface px-4 py-2",
|
|
33
|
+
children: /* @__PURE__ */ i("a", {
|
|
34
|
+
href: "https://bigconsole.com",
|
|
35
|
+
target: "_blank",
|
|
36
|
+
rel: "noopener noreferrer",
|
|
37
|
+
className: "text-xs text-text-tertiary transition-colors hover:text-text-secondary",
|
|
38
|
+
children: ["Powered by ", /* @__PURE__ */ r("span", {
|
|
39
|
+
className: "font-semibold text-text-secondary",
|
|
40
|
+
children: "BigConsole"
|
|
41
|
+
})]
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function c({ model: t }) {
|
|
46
|
+
let [a, c] = n(0), l = t.pages, u = t.presentation, d = u?.showTitle ? (u.title ?? "").trim() : "", f = u?.showBranding === !0, p = l[Math.min(a, l.length - 1)];
|
|
47
|
+
return p ? /* @__PURE__ */ r(e, {
|
|
33
48
|
label: "dashboard",
|
|
34
49
|
children: /* @__PURE__ */ i("div", {
|
|
35
50
|
className: "flex h-full w-full flex-col bg-bg-canvas",
|
|
36
|
-
children: [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
children: [
|
|
52
|
+
d && /* @__PURE__ */ r("header", {
|
|
53
|
+
className: "shrink-0 border-b border-border-default bg-bg-surface px-4 py-3",
|
|
54
|
+
children: /* @__PURE__ */ r("h1", {
|
|
55
|
+
className: "truncate text-base font-semibold text-text-primary",
|
|
56
|
+
children: d
|
|
57
|
+
})
|
|
58
|
+
}),
|
|
59
|
+
l.length > 1 && /* @__PURE__ */ r("div", {
|
|
60
|
+
role: "tablist",
|
|
61
|
+
className: "flex gap-1 border-b border-border-default bg-bg-surface px-2",
|
|
62
|
+
children: l.map((e, t) => /* @__PURE__ */ r("button", {
|
|
63
|
+
role: "tab",
|
|
64
|
+
"aria-selected": t === a,
|
|
65
|
+
onClick: () => c(t),
|
|
66
|
+
className: t === a ? "-mb-px border-b-2 border-action-primary-bg px-3 py-2 text-sm font-medium text-text-primary" : "border-b-2 border-transparent px-3 py-2 text-sm text-text-secondary hover:text-text-primary",
|
|
67
|
+
children: e.title ?? `Page ${t + 1}`
|
|
68
|
+
}, e.id))
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ r("div", {
|
|
71
|
+
className: "min-h-0 flex-1 overflow-auto",
|
|
72
|
+
children: /* @__PURE__ */ r(o, {
|
|
73
|
+
page: p,
|
|
74
|
+
capabilities: t.capabilities
|
|
75
|
+
})
|
|
76
|
+
}),
|
|
77
|
+
f && /* @__PURE__ */ r(s, {})
|
|
78
|
+
]
|
|
53
79
|
})
|
|
54
|
-
}) : /* @__PURE__ */
|
|
55
|
-
className: "flex h-full
|
|
56
|
-
children:
|
|
80
|
+
}) : /* @__PURE__ */ i("div", {
|
|
81
|
+
className: "flex h-full flex-col bg-bg-canvas",
|
|
82
|
+
children: [
|
|
83
|
+
d && /* @__PURE__ */ r("header", {
|
|
84
|
+
className: "shrink-0 border-b border-border-default bg-bg-surface px-4 py-3",
|
|
85
|
+
children: /* @__PURE__ */ r("h1", {
|
|
86
|
+
className: "truncate text-base font-semibold text-text-primary",
|
|
87
|
+
children: d
|
|
88
|
+
})
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ r("div", {
|
|
91
|
+
className: "flex flex-1 items-center justify-center text-sm text-text-secondary",
|
|
92
|
+
children: "This dashboard has no pages."
|
|
93
|
+
}),
|
|
94
|
+
f && /* @__PURE__ */ r(s, {})
|
|
95
|
+
]
|
|
57
96
|
});
|
|
58
97
|
}
|
|
59
98
|
//#endregion
|
|
60
|
-
export {
|
|
99
|
+
export { c as ReadOnlyDashboardRenderer };
|
|
61
100
|
|
|
62
101
|
//# sourceMappingURL=ReadOnlyDashboardRenderer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReadOnlyDashboardRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/ReadOnlyDashboardRenderer.tsx"],"sourcesContent":["/**\n * Read-only embedded dashboard renderer (BOFF-2986).\n *\n * Renders the sanitized render model's pages + widgets on a simple responsive\n * grid. It is STRICTLY view-only: no builder toolbar, edit toggle, sharing,\n * save, palette, inspector or comments — those authenticated affordances do not\n * exist in this component tree at all.\n */\n\nimport { useState } from 'react';\nimport type { EmbedCapabilities, EmbedPageModel, EmbedRenderModel } from './types';\nimport { EmbedWidgetRenderer } from './EmbedWidgetRenderer';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\n\nconst GRID_COLUMNS = 12;\n\ninterface Props {\n readonly model: EmbedRenderModel;\n}\n\nfunction PageGrid({ page, capabilities }: { page: EmbedPageModel; capabilities: EmbedCapabilities }) {\n if (page.widgets.length === 0) {\n return (\n <div className=\"flex h-40 items-center justify-center text-sm text-text-secondary\">This page has no widgets.</div>\n );\n }\n return (\n <div\n className=\"grid w-full gap-4 p-4\"\n style={{ gridTemplateColumns: `repeat(${GRID_COLUMNS}, minmax(0, 1fr))`, gridAutoRows: 'minmax(80px, auto)' }}\n >\n {page.widgets.map((widget) => (\n <div\n key={widget.id}\n className=\"overflow-hidden rounded-lg border border-border-default bg-bg-surface shadow-sm\"\n style={{\n // Clamp both ends: negative/oversized coordinates from malformed\n // model data must never produce an invalid CSS grid line.\n gridColumn: `${Math.min(Math.max(widget.layout.x, 0), GRID_COLUMNS - 1) + 1} / span ${Math.min(\n Math.max(widget.layout.w, 1),\n GRID_COLUMNS\n )}`,\n gridRow: `${Math.max(widget.layout.y, 0) + 1} / span ${Math.max(widget.layout.h, 1)}`,\n }}\n >\n <EmbedWidgetRenderer widget={widget} capabilities={capabilities} />\n </div>\n ))}\n </div>\n );\n}\n\nexport function ReadOnlyDashboardRenderer({ model }: Props) {\n const [activePageIndex, setActivePageIndex] = useState(0);\n const pages = model.pages;\n const activePage = pages[Math.min(activePageIndex, pages.length - 1)];\n\n if (!activePage) {\n return (\n <div className=\"flex h-full
|
|
1
|
+
{"version":3,"file":"ReadOnlyDashboardRenderer.js","names":[],"sources":["../../../../src/bigconsole/components/embed/ReadOnlyDashboardRenderer.tsx"],"sourcesContent":["/**\n * Read-only embedded dashboard renderer (BOFF-2986).\n *\n * Renders the sanitized render model's pages + widgets on a simple responsive\n * grid. It is STRICTLY view-only: no builder toolbar, edit toggle, sharing,\n * save, palette, inspector or comments — those authenticated affordances do not\n * exist in this component tree at all.\n */\n\nimport { useState } from 'react';\nimport type { EmbedCapabilities, EmbedPageModel, EmbedRenderModel } from './types';\nimport { EmbedWidgetRenderer } from './EmbedWidgetRenderer';\nimport { EmbedErrorBoundary } from './EmbedErrorBoundary';\n\nconst GRID_COLUMNS = 12;\n\ninterface Props {\n readonly model: EmbedRenderModel;\n}\n\nfunction PageGrid({ page, capabilities }: { page: EmbedPageModel; capabilities: EmbedCapabilities }) {\n if (page.widgets.length === 0) {\n return (\n <div className=\"flex h-40 items-center justify-center text-sm text-text-secondary\">This page has no widgets.</div>\n );\n }\n return (\n <div\n className=\"grid w-full gap-4 p-4\"\n style={{ gridTemplateColumns: `repeat(${GRID_COLUMNS}, minmax(0, 1fr))`, gridAutoRows: 'minmax(80px, auto)' }}\n >\n {page.widgets.map((widget) => (\n <div\n key={widget.id}\n className=\"overflow-hidden rounded-lg border border-border-default bg-bg-surface shadow-sm\"\n style={{\n // Clamp both ends: negative/oversized coordinates from malformed\n // model data must never produce an invalid CSS grid line.\n gridColumn: `${Math.min(Math.max(widget.layout.x, 0), GRID_COLUMNS - 1) + 1} / span ${Math.min(\n Math.max(widget.layout.w, 1),\n GRID_COLUMNS\n )}`,\n gridRow: `${Math.max(widget.layout.y, 0) + 1} / span ${Math.max(widget.layout.h, 1)}`,\n }}\n >\n <EmbedWidgetRenderer widget={widget} capabilities={capabilities} />\n </div>\n ))}\n </div>\n );\n}\n\n/** Subtle attribution/upsell footer (BOFF-5735), shown only when the owner opts in. */\nfunction BrandingFooter() {\n return (\n <div className=\"flex shrink-0 items-center justify-end border-t border-border-default bg-bg-surface px-4 py-2\">\n <a\n href=\"https://bigconsole.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-xs text-text-tertiary transition-colors hover:text-text-secondary\"\n >\n Powered by <span className=\"font-semibold text-text-secondary\">BigConsole</span>\n </a>\n </div>\n );\n}\n\nexport function ReadOnlyDashboardRenderer({ model }: Props) {\n const [activePageIndex, setActivePageIndex] = useState(0);\n const pages = model.pages;\n const presentation = model.presentation;\n // Only show a header when the owner enabled it AND there's a non-empty title.\n const headerTitle = presentation?.showTitle ? (presentation.title ?? '').trim() : '';\n const showBranding = presentation?.showBranding === true;\n const activePage = pages[Math.min(activePageIndex, pages.length - 1)];\n\n if (!activePage) {\n return (\n <div className=\"flex h-full flex-col bg-bg-canvas\">\n {headerTitle && (\n <header className=\"shrink-0 border-b border-border-default bg-bg-surface px-4 py-3\">\n <h1 className=\"truncate text-base font-semibold text-text-primary\">{headerTitle}</h1>\n </header>\n )}\n <div className=\"flex flex-1 items-center justify-center text-sm text-text-secondary\">\n This dashboard has no pages.\n </div>\n {showBranding && <BrandingFooter />}\n </div>\n );\n }\n\n return (\n <EmbedErrorBoundary label=\"dashboard\">\n <div className=\"flex h-full w-full flex-col bg-bg-canvas\">\n {headerTitle && (\n <header className=\"shrink-0 border-b border-border-default bg-bg-surface px-4 py-3\">\n <h1 className=\"truncate text-base font-semibold text-text-primary\">{headerTitle}</h1>\n </header>\n )}\n {pages.length > 1 && (\n <div role=\"tablist\" className=\"flex gap-1 border-b border-border-default bg-bg-surface px-2\">\n {pages.map((page, i) => (\n <button\n key={page.id}\n role=\"tab\"\n aria-selected={i === activePageIndex}\n onClick={() => setActivePageIndex(i)}\n className={\n i === activePageIndex\n ? '-mb-px border-b-2 border-action-primary-bg px-3 py-2 text-sm font-medium text-text-primary'\n : 'border-b-2 border-transparent px-3 py-2 text-sm text-text-secondary hover:text-text-primary'\n }\n >\n {page.title ?? `Page ${i + 1}`}\n </button>\n ))}\n </div>\n )}\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <PageGrid page={activePage} capabilities={model.capabilities} />\n </div>\n {showBranding && <BrandingFooter />}\n </div>\n </EmbedErrorBoundary>\n );\n}\n"],"mappings":";;;;;AAcA,IAAM,IAAe;AAMrB,SAAS,EAAS,EAAE,SAAM,mBAA2E;AAMnG,QALI,EAAK,QAAQ,WAAW,IAExB,kBAAC,OAAD;EAAK,WAAU;YAAoE;EAA+B,CAAA,GAIpH,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GAAE,qBAAqB,UAAU,EAAa;GAAoB,cAAc;GAAsB;YAE5G,EAAK,QAAQ,KAAK,MACjB,kBAAC,OAAD;GAEE,WAAU;GACV,OAAO;IAGL,YAAY,GAAG,KAAK,IAAI,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,EAAE,IAAe,EAAE,GAAG,EAAE,UAAU,KAAK,IACzF,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,EAC5B,EACD;IACD,SAAS,GAAG,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE,GAAG,EAAE,UAAU,KAAK,IAAI,EAAO,OAAO,GAAG,EAAE;IACpF;aAED,kBAAC,GAAD;IAA6B;IAAsB;IAAgB,CAAA;GAC/D,EAbC,EAAO,GAaR,CACN;EACE,CAAA;;AAKV,SAAS,IAAiB;AACxB,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,KAAD;GACE,MAAK;GACL,QAAO;GACP,KAAI;GACJ,WAAU;aAJZ,CAKC,eACY,kBAAC,QAAD;IAAM,WAAU;cAAoC;IAAiB,CAAA,CAC9E;;EACA,CAAA;;AAIV,SAAgB,EAA0B,EAAE,YAAgB;CAC1D,IAAM,CAAC,GAAiB,KAAsB,EAAS,EAAE,EACnD,IAAQ,EAAM,OACd,IAAe,EAAM,cAErB,IAAc,GAAc,aAAa,EAAa,SAAS,IAAI,MAAM,GAAG,IAC5E,IAAe,GAAc,iBAAiB,IAC9C,IAAa,EAAM,KAAK,IAAI,GAAiB,EAAM,SAAS,EAAE;AAkBpE,QAhBK,IAiBH,kBAAC,GAAD;EAAoB,OAAM;YACxB,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,KACC,kBAAC,UAAD;KAAQ,WAAU;eAChB,kBAAC,MAAD;MAAI,WAAU;gBAAsD;MAAiB,CAAA;KAC9E,CAAA;IAEV,EAAM,SAAS,KACd,kBAAC,OAAD;KAAK,MAAK;KAAU,WAAU;eAC3B,EAAM,KAAK,GAAM,MAChB,kBAAC,UAAD;MAEE,MAAK;MACL,iBAAe,MAAM;MACrB,eAAe,EAAmB,EAAE;MACpC,WACE,MAAM,IACF,+FACA;gBAGL,EAAK,SAAS,QAAQ,IAAI;MACpB,EAXF,EAAK,GAWH,CACT;KACE,CAAA;IAER,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAU,MAAM;MAAY,cAAc,EAAM;MAAgB,CAAA;KAC5D,CAAA;IACL,KAAgB,kBAAC,GAAD,EAAkB,CAAA;IAC/B;;EACa,CAAA,GA9CnB,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,KACC,kBAAC,UAAD;IAAQ,WAAU;cAChB,kBAAC,MAAD;KAAI,WAAU;eAAsD;KAAiB,CAAA;IAC9E,CAAA;GAEX,kBAAC,OAAD;IAAK,WAAU;cAAsE;IAE/E,CAAA;GACL,KAAgB,kBAAC,GAAD,EAAkB,CAAA;GAC/B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":[],"sources":["../../../../src/bigconsole/components/embed/types.ts"],"sourcesContent":["/**\n * Stable, versioned embed render-model contract (BOFF-2986).\n *\n * This is the ONLY shape the read-only embed renderer consumes. It is a\n * sanitized DTO produced by the BigConsole publication builder — it deliberately\n * carries NO authenticated stores, Apollo clients, data-source configs,\n * credentials or internal ids. The renderer never fetches data itself; all\n * values are pre-projected into the model or fetched later through the\n * publication-scoped interaction controller using opaque aliases.\n *\n * Backend (publication builder) and frontend (renderer) MUST agree on\n * `EMBED_RENDER_MODEL_VERSION`. A mismatch fails closed at load time rather than\n * rendering a stale/incompatible view.\n */\n\nimport type { WidgetType } from '../../types';\n\n/** Bump on any breaking change to the DTO shapes below. */\nexport const EMBED_RENDER_MODEL_VERSION = 1 as const;\n\n/** Pure layout box (grid units); no store, no event emission. */\nexport interface EmbedWidgetLayout {\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n}\n\n/**\n * A single widget as projected for embedding. `config` and `data` are already\n * sanitized and schema-narrowed per widget type by the publication builder; the\n * renderer validates them again against its per-type schema before rendering.\n */\nexport interface EmbedWidgetModel {\n /** Opaque, publication-scoped widget id (not the internal DB id). */\n readonly id: string;\n readonly type: WidgetType;\n readonly title?: string;\n readonly layout: EmbedWidgetLayout;\n /** Sanitized, type-narrowed presentation config. */\n readonly config: Readonly<Record<string, unknown>>;\n /** Sanitized, pre-projected data payload (or null for interaction-fetched). */\n readonly data: Readonly<Record<string, unknown>> | null;\n}\n\nexport interface EmbedPageModel {\n readonly id: string;\n readonly title?: string;\n readonly widgets: readonly EmbedWidgetModel[];\n}\n\n/**\n * Publication-approved runtime capabilities. Everything defaults OFF; the\n * renderer must never enable an interaction the publication did not grant.\n */\nexport interface EmbedCapabilities {\n readonly filters: boolean;\n readonly drilldown: boolean;\n readonly downloads: boolean;\n /** Side-effect actions (forms/webhooks/workflows) — out of scope for read-only MVP. */\n readonly actions: boolean;\n}\n\nexport interface EmbedRenderModel {\n readonly renderModelVersion: number;\n readonly clientContractVersion: number;\n readonly title?: string;\n readonly pages: readonly EmbedPageModel[];\n readonly capabilities: EmbedCapabilities;\n /** ISO timestamp the immutable publication was frozen. */\n readonly definitionPublishedAt: string;\n}\n\n/** Result of validating an inbound render model before rendering. */\nexport type EmbedModelValidation =\n { readonly ok: true; readonly model: EmbedRenderModel } | { readonly ok: false; readonly reason: string };\n\n/**\n * Shape every embed widget renderer component receives. It gets the sanitized\n * model plus a controller for publication-scoped interactions — never raw hooks.\n */\nexport interface EmbedWidgetRenderProps {\n readonly widget: EmbedWidgetModel;\n readonly capabilities: EmbedCapabilities;\n}\n"],"mappings":";AAkBA,IAAa,IAA6B"}
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../../../src/bigconsole/components/embed/types.ts"],"sourcesContent":["/**\n * Stable, versioned embed render-model contract (BOFF-2986).\n *\n * This is the ONLY shape the read-only embed renderer consumes. It is a\n * sanitized DTO produced by the BigConsole publication builder — it deliberately\n * carries NO authenticated stores, Apollo clients, data-source configs,\n * credentials or internal ids. The renderer never fetches data itself; all\n * values are pre-projected into the model or fetched later through the\n * publication-scoped interaction controller using opaque aliases.\n *\n * Backend (publication builder) and frontend (renderer) MUST agree on\n * `EMBED_RENDER_MODEL_VERSION`. A mismatch fails closed at load time rather than\n * rendering a stale/incompatible view.\n */\n\nimport type { WidgetType } from '../../types';\n\n/** Bump on any breaking change to the DTO shapes below. */\nexport const EMBED_RENDER_MODEL_VERSION = 1 as const;\n\n/** Pure layout box (grid units); no store, no event emission. */\nexport interface EmbedWidgetLayout {\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n}\n\n/**\n * A single widget as projected for embedding. `config` and `data` are already\n * sanitized and schema-narrowed per widget type by the publication builder; the\n * renderer validates them again against its per-type schema before rendering.\n */\nexport interface EmbedWidgetModel {\n /** Opaque, publication-scoped widget id (not the internal DB id). */\n readonly id: string;\n readonly type: WidgetType;\n readonly title?: string;\n readonly layout: EmbedWidgetLayout;\n /** Sanitized, type-narrowed presentation config. */\n readonly config: Readonly<Record<string, unknown>>;\n /** Sanitized, pre-projected data payload (or null for interaction-fetched). */\n readonly data: Readonly<Record<string, unknown>> | null;\n}\n\nexport interface EmbedPageModel {\n readonly id: string;\n readonly title?: string;\n readonly widgets: readonly EmbedWidgetModel[];\n}\n\n/**\n * Publication-approved runtime capabilities. Everything defaults OFF; the\n * renderer must never enable an interaction the publication did not grant.\n */\nexport interface EmbedCapabilities {\n readonly filters: boolean;\n readonly drilldown: boolean;\n readonly downloads: boolean;\n /** Side-effect actions (forms/webhooks/workflows) — out of scope for read-only MVP. */\n readonly actions: boolean;\n}\n\n/** Publish-time color mode chosen by the owner (BOFF-5735). */\nexport type EmbedThemeMode = 'light' | 'dark' | 'auto';\n\n/**\n * Owner-chosen, publish-time presentation options (BOFF-5735), attached to the\n * render model (resolved live from the policy). Optional: pre-5735 render models\n * carry no `presentation` block, so the renderer must default gracefully (light,\n * no header, no footer) when absent.\n */\nexport interface EmbedPresentation {\n readonly theme: EmbedThemeMode;\n readonly showTitle: boolean;\n /** Dashboard name for the optional header, or null when hidden/empty. */\n readonly title: string | null;\n readonly showBranding: boolean;\n}\n\nexport interface EmbedRenderModel {\n readonly renderModelVersion: number;\n readonly clientContractVersion: number;\n readonly title?: string;\n readonly pages: readonly EmbedPageModel[];\n readonly capabilities: EmbedCapabilities;\n readonly presentation?: EmbedPresentation;\n /** ISO timestamp the immutable publication was frozen. */\n readonly definitionPublishedAt: string;\n}\n\n/** Result of validating an inbound render model before rendering. */\nexport type EmbedModelValidation =\n { readonly ok: true; readonly model: EmbedRenderModel } | { readonly ok: false; readonly reason: string };\n\n/**\n * Shape every embed widget renderer component receives. It gets the sanitized\n * model plus a controller for publication-scoped interactions — never raw hooks.\n */\nexport interface EmbedWidgetRenderProps {\n readonly widget: EmbedWidgetModel;\n readonly capabilities: EmbedCapabilities;\n}\n"],"mappings":";AAkBA,IAAa,IAA6B"}
|
|
@@ -1,64 +1,101 @@
|
|
|
1
1
|
import { CreateDashboardEmbedPolicyDocument as e, ListDashboardEmbedPoliciesDocument as t, PublishDashboardEmbedPolicyDocument as n, RevokeDashboardEmbedPolicyDocument as r, RotateDashboardEmbedPolicyDocument as i, UpdateDashboardEmbedPolicyDocument as a } from "../../generated/wspace-operations.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { DashboardEmbedTheme as o } from "../../generated/wspace-types.js";
|
|
3
|
+
import { useCallback as s, useState as c } from "react";
|
|
4
|
+
import { useApolloClient as l } from "@apollo/client/react";
|
|
4
5
|
//#region src/bigconsole/hooks/useDashboardEmbedPolicy.ts
|
|
5
|
-
|
|
6
|
+
var u = {
|
|
7
|
+
light: o.Light,
|
|
8
|
+
dark: o.Dark,
|
|
9
|
+
auto: o.Auto
|
|
10
|
+
};
|
|
11
|
+
function d(e) {
|
|
12
|
+
switch (e) {
|
|
13
|
+
case o.Dark: return "dark";
|
|
14
|
+
case o.Auto: return "auto";
|
|
15
|
+
default: return "light";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function f(e) {
|
|
6
19
|
return e.length === 0 ? null : e.find((e) => e.status === "ACTIVE") || ([...e].sort((e, t) => Date.parse(t.updatedAt) - Date.parse(e.updatedAt))[0] ?? null);
|
|
7
20
|
}
|
|
8
|
-
function
|
|
9
|
-
let [
|
|
21
|
+
function p() {
|
|
22
|
+
let [o, d] = c(null), [p, m] = c(null), [h, g] = c(!1), [_, v] = c(null), y = l();
|
|
10
23
|
return {
|
|
11
|
-
policy:
|
|
12
|
-
secret:
|
|
13
|
-
loading:
|
|
14
|
-
error:
|
|
15
|
-
fetchPolicy:
|
|
24
|
+
policy: o,
|
|
25
|
+
secret: p,
|
|
26
|
+
loading: h,
|
|
27
|
+
error: _,
|
|
28
|
+
fetchPolicy: s(async (e) => {
|
|
16
29
|
try {
|
|
17
|
-
|
|
18
|
-
let n =
|
|
30
|
+
g(!0), v(null);
|
|
31
|
+
let n = f((await y.query({
|
|
19
32
|
query: t,
|
|
20
33
|
variables: { dashboardId: e },
|
|
21
34
|
fetchPolicy: "network-only"
|
|
22
35
|
})).data?.listDashboardEmbedPolicies?.nodes ?? []);
|
|
23
36
|
return d(n), n;
|
|
24
37
|
} catch (e) {
|
|
25
|
-
return
|
|
38
|
+
return v(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to load embed policy")), null;
|
|
26
39
|
} finally {
|
|
27
|
-
|
|
40
|
+
g(!1);
|
|
28
41
|
}
|
|
29
|
-
}, [
|
|
30
|
-
publishToWeb:
|
|
42
|
+
}, [y]),
|
|
43
|
+
publishToWeb: s(async (t, r, i, a) => {
|
|
31
44
|
try {
|
|
32
|
-
|
|
33
|
-
let
|
|
45
|
+
g(!0), v(null);
|
|
46
|
+
let o = (await y.mutate({
|
|
34
47
|
mutation: e,
|
|
35
48
|
variables: { input: {
|
|
36
49
|
dashboardId: t,
|
|
37
|
-
allowedParentOrigins: r
|
|
50
|
+
allowedParentOrigins: r,
|
|
51
|
+
...a ? {
|
|
52
|
+
presentationTheme: u[a.theme],
|
|
53
|
+
showDashboardTitle: a.showTitle,
|
|
54
|
+
showBrandingFooter: a.showBranding
|
|
55
|
+
} : {}
|
|
38
56
|
} }
|
|
39
57
|
})).data?.createDashboardEmbedPolicy;
|
|
40
|
-
if (!
|
|
41
|
-
let
|
|
58
|
+
if (!o) return null;
|
|
59
|
+
let s = (await y.mutate({
|
|
42
60
|
mutation: n,
|
|
43
61
|
variables: {
|
|
44
|
-
policyId:
|
|
62
|
+
policyId: o.policy.id,
|
|
45
63
|
contentSafetyAttested: i
|
|
46
64
|
}
|
|
47
|
-
})).data?.publishDashboardEmbedPolicy ??
|
|
48
|
-
...
|
|
49
|
-
policy:
|
|
65
|
+
})).data?.publishDashboardEmbedPolicy ?? o.policy, c = {
|
|
66
|
+
...o,
|
|
67
|
+
policy: s
|
|
50
68
|
};
|
|
51
|
-
return
|
|
69
|
+
return m(c), d(s), c;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return v(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to publish to web")), null;
|
|
72
|
+
} finally {
|
|
73
|
+
g(!1);
|
|
74
|
+
}
|
|
75
|
+
}, [y]),
|
|
76
|
+
applyPresentation: s(async (e, t) => {
|
|
77
|
+
try {
|
|
78
|
+
g(!0), v(null);
|
|
79
|
+
let n = (await y.mutate({
|
|
80
|
+
mutation: a,
|
|
81
|
+
variables: { input: {
|
|
82
|
+
policyId: e,
|
|
83
|
+
presentationTheme: u[t.theme],
|
|
84
|
+
showDashboardTitle: t.showTitle,
|
|
85
|
+
showBrandingFooter: t.showBranding
|
|
86
|
+
} }
|
|
87
|
+
})).data?.updateDashboardEmbedPolicy ?? null;
|
|
88
|
+
return n && d(n), n;
|
|
52
89
|
} catch (e) {
|
|
53
|
-
return
|
|
90
|
+
return v(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to update presentation")), null;
|
|
54
91
|
} finally {
|
|
55
|
-
|
|
92
|
+
g(!1);
|
|
56
93
|
}
|
|
57
|
-
}, [
|
|
58
|
-
setAllowedOrigins:
|
|
94
|
+
}, [y]),
|
|
95
|
+
setAllowedOrigins: s(async (e, t) => {
|
|
59
96
|
try {
|
|
60
|
-
|
|
61
|
-
let n = (await
|
|
97
|
+
g(!0), v(null);
|
|
98
|
+
let n = (await y.mutate({
|
|
62
99
|
mutation: a,
|
|
63
100
|
variables: { input: {
|
|
64
101
|
policyId: e,
|
|
@@ -67,42 +104,42 @@ function u() {
|
|
|
67
104
|
})).data?.updateDashboardEmbedPolicy ?? null;
|
|
68
105
|
return n && d(n), n;
|
|
69
106
|
} catch (e) {
|
|
70
|
-
return
|
|
107
|
+
return v(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to update allowed domains")), null;
|
|
71
108
|
} finally {
|
|
72
|
-
|
|
109
|
+
g(!1);
|
|
73
110
|
}
|
|
74
|
-
}, [
|
|
75
|
-
rotateKey:
|
|
111
|
+
}, [y]),
|
|
112
|
+
rotateKey: s(async (e) => {
|
|
76
113
|
try {
|
|
77
|
-
|
|
78
|
-
let t = (await
|
|
114
|
+
g(!0), v(null);
|
|
115
|
+
let t = (await y.mutate({
|
|
79
116
|
mutation: i,
|
|
80
117
|
variables: { policyId: e }
|
|
81
118
|
})).data?.rotateDashboardEmbedPolicy ?? null;
|
|
82
|
-
return t && (
|
|
119
|
+
return t && (m(t), d(t.policy)), t;
|
|
83
120
|
} catch (e) {
|
|
84
|
-
return
|
|
121
|
+
return v(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to rotate embed link")), null;
|
|
85
122
|
} finally {
|
|
86
|
-
|
|
123
|
+
g(!1);
|
|
87
124
|
}
|
|
88
|
-
}, [
|
|
89
|
-
unpublish:
|
|
125
|
+
}, [y]),
|
|
126
|
+
unpublish: s(async (e) => {
|
|
90
127
|
try {
|
|
91
|
-
|
|
92
|
-
let t = (await
|
|
128
|
+
g(!0), v(null);
|
|
129
|
+
let t = (await y.mutate({
|
|
93
130
|
mutation: r,
|
|
94
131
|
variables: { policyId: e }
|
|
95
132
|
})).data?.revokeDashboardEmbedPolicy ?? null;
|
|
96
|
-
return t ? (d(t),
|
|
133
|
+
return t ? (d(t), m(null), !0) : !1;
|
|
97
134
|
} catch (e) {
|
|
98
|
-
return
|
|
135
|
+
return v(e instanceof Error ? e : /* @__PURE__ */ Error("Failed to unpublish")), !1;
|
|
99
136
|
} finally {
|
|
100
|
-
|
|
137
|
+
g(!1);
|
|
101
138
|
}
|
|
102
|
-
}, [
|
|
139
|
+
}, [y])
|
|
103
140
|
};
|
|
104
141
|
}
|
|
105
142
|
//#endregion
|
|
106
|
-
export {
|
|
143
|
+
export { p as default, d as themeChoiceFromPolicy };
|
|
107
144
|
|
|
108
145
|
//# sourceMappingURL=useDashboardEmbedPolicy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDashboardEmbedPolicy.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardEmbedPolicy.ts"],"sourcesContent":["/**\n * useDashboardEmbedPolicy Hook\n *\n * Owner-facing operations for publishing a dashboard to the web (anonymous-link\n * embed). Mirrors {@link useDashboardSharing}. All mutations are gateway-enforced\n * by `@rbac(manage_embed)` (owner-only). Publishing serves the dashboard's LIVE\n * data anonymously — the caller confirms content-safety before enabling.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport {\n ListDashboardEmbedPoliciesDocument,\n CreateDashboardEmbedPolicyDocument,\n PublishDashboardEmbedPolicyDocument,\n UpdateDashboardEmbedPolicyDocument,\n RotateDashboardEmbedPolicyDocument,\n RevokeDashboardEmbedPolicyDocument,\n} from '../../generated/wspace-operations';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type EmbedPolicyStatus = 'DRAFT' | 'ACTIVE' | 'NEEDS_REVIEW' | 'BROKEN' | 'REVOKED' | 'EXPIRED';\n\nexport interface DashboardEmbedPolicy {\n id: string;\n dashboardId: string;\n status: EmbedPolicyStatus;\n accessMode: string;\n version: number;\n allowedParentOrigins: string[];\n expiresAt?: string | null;\n sessionTtlSeconds: number;\n contentReviewState: string;\n activePublicationId?: string | null;\n locatorPublicId?: string | null;\n fingerprint?: string | null;\n createdAt: string;\n updatedAt: string;\n activatedAt?: string | null;\n revokedAt?: string | null;\n}\n\n/** Returned once on create / rotate — carries the secret embed URL + snippet. */\nexport interface EmbedPolicySecret {\n embedUrl: string;\n iframeSnippet: string;\n fingerprint: string;\n policy: DashboardEmbedPolicy;\n}\n\nexport interface UseDashboardEmbedPolicyResult {\n /** The dashboard's active (or most recent) embed policy, if any. */\n policy: DashboardEmbedPolicy | null;\n /** The one-time secret from the last create/rotate (embed URL + iframe snippet). */\n secret: EmbedPolicySecret | null;\n loading: boolean;\n error: Error | null;\n\n fetchPolicy: (dashboardId: string) => Promise<DashboardEmbedPolicy | null>;\n /** Create (if needed) + publish → returns the live embed secret. */\n publishToWeb: (\n dashboardId: string,\n allowedParentOrigins: string[],\n contentSafetyAttested: boolean\n ) => Promise<EmbedPolicySecret | null>;\n setAllowedOrigins: (policyId: string, allowedParentOrigins: string[]) => Promise<DashboardEmbedPolicy | null>;\n rotateKey: (policyId: string) => Promise<EmbedPolicySecret | null>;\n /** Unpublish: revoke the policy (embed stops serving immediately). */\n unpublish: (policyId: string) => Promise<boolean>;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/** Pick the live-serving policy (ACTIVE) else the most recently updated one. */\nfunction pickCurrent(policies: DashboardEmbedPolicy[]): DashboardEmbedPolicy | null {\n if (policies.length === 0) return null;\n const active = policies.find((p) => p.status === 'ACTIVE');\n if (active) return active;\n return [...policies].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;\n}\n\nexport function useDashboardEmbedPolicy(): UseDashboardEmbedPolicyResult {\n const [policy, setPolicy] = useState<DashboardEmbedPolicy | null>(null);\n const [secret, setSecret] = useState<EmbedPolicySecret | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const apolloClient = useApolloClient();\n\n const fetchPolicy = useCallback(\n async (dashboardId: string): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.query<{\n listDashboardEmbedPolicies?: { nodes?: DashboardEmbedPolicy[] };\n }>({\n query: ListDashboardEmbedPoliciesDocument,\n variables: { dashboardId },\n fetchPolicy: 'network-only',\n });\n const current = pickCurrent(result.data?.listDashboardEmbedPolicies?.nodes ?? []);\n setPolicy(current);\n return current;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load embed policy'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const publishToWeb = useCallback(\n async (\n dashboardId: string,\n allowedParentOrigins: string[],\n contentSafetyAttested: boolean\n ): Promise<EmbedPolicySecret | null> => {\n try {\n setLoading(true);\n setError(null);\n\n // Create a DRAFT policy + anonymous-link locator (returns the secret once).\n const created = await apolloClient.mutate<{ createDashboardEmbedPolicy?: EmbedPolicySecret }>({\n mutation: CreateDashboardEmbedPolicyDocument,\n variables: { input: { dashboardId, allowedParentOrigins } },\n });\n const createdSecret = created.data?.createDashboardEmbedPolicy;\n if (!createdSecret) return null;\n\n // Activate it for live embedding (owner attests content-safety).\n const published = await apolloClient.mutate<{ publishDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: PublishDashboardEmbedPolicyDocument,\n variables: { policyId: createdSecret.policy.id, contentSafetyAttested },\n });\n const activePolicy = published.data?.publishDashboardEmbedPolicy ?? createdSecret.policy;\n\n const result: EmbedPolicySecret = { ...createdSecret, policy: activePolicy };\n setSecret(result);\n setPolicy(activePolicy);\n return result;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to publish to web'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const setAllowedOrigins = useCallback(\n async (policyId: string, allowedParentOrigins: string[]): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ updateDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: UpdateDashboardEmbedPolicyDocument,\n variables: { input: { policyId, allowedParentOrigins } },\n });\n const updated = result.data?.updateDashboardEmbedPolicy ?? null;\n if (updated) setPolicy(updated);\n return updated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to update allowed domains'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const rotateKey = useCallback(\n async (policyId: string): Promise<EmbedPolicySecret | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ rotateDashboardEmbedPolicy?: EmbedPolicySecret }>({\n mutation: RotateDashboardEmbedPolicyDocument,\n variables: { policyId },\n });\n const rotated = result.data?.rotateDashboardEmbedPolicy ?? null;\n if (rotated) {\n setSecret(rotated);\n setPolicy(rotated.policy);\n }\n return rotated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to rotate embed link'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const unpublish = useCallback(\n async (policyId: string): Promise<boolean> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ revokeDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: RevokeDashboardEmbedPolicyDocument,\n variables: { policyId },\n });\n const revoked = result.data?.revokeDashboardEmbedPolicy ?? null;\n if (revoked) {\n setPolicy(revoked);\n setSecret(null);\n return true;\n }\n return false;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to unpublish'));\n return false;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n return { policy, secret, loading, error, fetchPolicy, publishToWeb, setAllowedOrigins, rotateKey, unpublish };\n}\n\nexport default useDashboardEmbedPolicy;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useDashboardEmbedPolicy.js","names":[],"sources":["../../../src/bigconsole/hooks/useDashboardEmbedPolicy.ts"],"sourcesContent":["/**\n * useDashboardEmbedPolicy Hook\n *\n * Owner-facing operations for publishing a dashboard to the web (anonymous-link\n * embed). Mirrors {@link useDashboardSharing}. All mutations are gateway-enforced\n * by `@rbac(manage_embed)` (owner-only). Publishing serves the dashboard's LIVE\n * data anonymously — the caller confirms content-safety before enabling.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport {\n ListDashboardEmbedPoliciesDocument,\n CreateDashboardEmbedPolicyDocument,\n PublishDashboardEmbedPolicyDocument,\n UpdateDashboardEmbedPolicyDocument,\n RotateDashboardEmbedPolicyDocument,\n RevokeDashboardEmbedPolicyDocument,\n} from '../../generated/wspace-operations';\nimport { DashboardEmbedTheme } from '../../generated/wspace-types';\nimport type { EmbedThemeMode } from '../components/embed/types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type EmbedPolicyStatus = 'DRAFT' | 'ACTIVE' | 'NEEDS_REVIEW' | 'BROKEN' | 'REVOKED' | 'EXPIRED';\n\n/**\n * Owner-facing color mode (BOFF-5735); maps to the backend {@link DashboardEmbedTheme}\n * enum. Same shape as the render-model's {@link EmbedThemeMode} — aliased to avoid a\n * duplicate union.\n */\nexport type EmbedThemeChoice = EmbedThemeMode;\n\n/** Publish-time presentation options the owner controls in the Share dialog. */\nexport interface EmbedPresentationChoice {\n theme: EmbedThemeChoice;\n showTitle: boolean;\n showBranding: boolean;\n}\n\nconst THEME_TO_ENUM: Record<EmbedThemeChoice, DashboardEmbedTheme> = {\n light: DashboardEmbedTheme.Light,\n dark: DashboardEmbedTheme.Dark,\n auto: DashboardEmbedTheme.Auto,\n};\n\n/** Map the backend enum back to the lowercase owner-facing choice (defaults to light). */\nexport function themeChoiceFromPolicy(value: string | null | undefined): EmbedThemeChoice {\n switch (value) {\n case DashboardEmbedTheme.Dark:\n return 'dark';\n case DashboardEmbedTheme.Auto:\n return 'auto';\n default:\n return 'light';\n }\n}\n\nexport interface DashboardEmbedPolicy {\n id: string;\n dashboardId: string;\n status: EmbedPolicyStatus;\n accessMode: string;\n version: number;\n allowedParentOrigins: string[];\n expiresAt?: string | null;\n sessionTtlSeconds: number;\n contentReviewState: string;\n presentationTheme: DashboardEmbedTheme;\n showDashboardTitle: boolean;\n showBrandingFooter: boolean;\n activePublicationId?: string | null;\n locatorPublicId?: string | null;\n fingerprint?: string | null;\n createdAt: string;\n updatedAt: string;\n activatedAt?: string | null;\n revokedAt?: string | null;\n}\n\n/** Returned once on create / rotate — carries the secret embed URL + snippet. */\nexport interface EmbedPolicySecret {\n embedUrl: string;\n iframeSnippet: string;\n fingerprint: string;\n policy: DashboardEmbedPolicy;\n}\n\nexport interface UseDashboardEmbedPolicyResult {\n /** The dashboard's active (or most recent) embed policy, if any. */\n policy: DashboardEmbedPolicy | null;\n /** The one-time secret from the last create/rotate (embed URL + iframe snippet). */\n secret: EmbedPolicySecret | null;\n loading: boolean;\n error: Error | null;\n\n fetchPolicy: (dashboardId: string) => Promise<DashboardEmbedPolicy | null>;\n /** Create (if needed) + publish → returns the live embed secret. */\n publishToWeb: (\n dashboardId: string,\n allowedParentOrigins: string[],\n contentSafetyAttested: boolean,\n presentation?: EmbedPresentationChoice\n ) => Promise<EmbedPolicySecret | null>;\n /**\n * Persist the presentation options of an already-published policy. They are\n * read live by the embed renderer, so the change applies on the next render\n * with no re-publish — the embed link/key are untouched.\n */\n applyPresentation: (policyId: string, presentation: EmbedPresentationChoice) => Promise<DashboardEmbedPolicy | null>;\n setAllowedOrigins: (policyId: string, allowedParentOrigins: string[]) => Promise<DashboardEmbedPolicy | null>;\n rotateKey: (policyId: string) => Promise<EmbedPolicySecret | null>;\n /** Unpublish: revoke the policy (embed stops serving immediately). */\n unpublish: (policyId: string) => Promise<boolean>;\n}\n\n// ============================================================================\n// Hook\n// ============================================================================\n\n/** Pick the live-serving policy (ACTIVE) else the most recently updated one. */\nfunction pickCurrent(policies: DashboardEmbedPolicy[]): DashboardEmbedPolicy | null {\n if (policies.length === 0) return null;\n const active = policies.find((p) => p.status === 'ACTIVE');\n if (active) return active;\n return [...policies].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] ?? null;\n}\n\nexport function useDashboardEmbedPolicy(): UseDashboardEmbedPolicyResult {\n const [policy, setPolicy] = useState<DashboardEmbedPolicy | null>(null);\n const [secret, setSecret] = useState<EmbedPolicySecret | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const apolloClient = useApolloClient();\n\n const fetchPolicy = useCallback(\n async (dashboardId: string): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.query<{\n listDashboardEmbedPolicies?: { nodes?: DashboardEmbedPolicy[] };\n }>({\n query: ListDashboardEmbedPoliciesDocument,\n variables: { dashboardId },\n fetchPolicy: 'network-only',\n });\n const current = pickCurrent(result.data?.listDashboardEmbedPolicies?.nodes ?? []);\n setPolicy(current);\n return current;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load embed policy'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const publishToWeb = useCallback(\n async (\n dashboardId: string,\n allowedParentOrigins: string[],\n contentSafetyAttested: boolean,\n presentation?: EmbedPresentationChoice\n ): Promise<EmbedPolicySecret | null> => {\n try {\n setLoading(true);\n setError(null);\n\n // Create a DRAFT policy + anonymous-link locator (returns the secret once).\n // Presentation options (BOFF-5735) are captured here so the very first\n // render reflects them; they're read live thereafter.\n const created = await apolloClient.mutate<{ createDashboardEmbedPolicy?: EmbedPolicySecret }>({\n mutation: CreateDashboardEmbedPolicyDocument,\n variables: {\n input: {\n dashboardId,\n allowedParentOrigins,\n ...(presentation\n ? {\n presentationTheme: THEME_TO_ENUM[presentation.theme],\n showDashboardTitle: presentation.showTitle,\n showBrandingFooter: presentation.showBranding,\n }\n : {}),\n },\n },\n });\n const createdSecret = created.data?.createDashboardEmbedPolicy;\n if (!createdSecret) return null;\n\n // Activate it for live embedding (owner attests content-safety).\n const published = await apolloClient.mutate<{ publishDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: PublishDashboardEmbedPolicyDocument,\n variables: { policyId: createdSecret.policy.id, contentSafetyAttested },\n });\n const activePolicy = published.data?.publishDashboardEmbedPolicy ?? createdSecret.policy;\n\n const result: EmbedPolicySecret = { ...createdSecret, policy: activePolicy };\n setSecret(result);\n setPolicy(activePolicy);\n return result;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to publish to web'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const applyPresentation = useCallback(\n async (policyId: string, presentation: EmbedPresentationChoice): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n // Presentation is read LIVE at view time by the embed renderer, so simply\n // persisting the choices on the policy is enough — no re-publish, and the\n // embed link/key are untouched. Live embeds pick up the change on their\n // next render (bounded by the service's short render-cache window).\n const result = await apolloClient.mutate<{ updateDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: UpdateDashboardEmbedPolicyDocument,\n variables: {\n input: {\n policyId,\n presentationTheme: THEME_TO_ENUM[presentation.theme],\n showDashboardTitle: presentation.showTitle,\n showBrandingFooter: presentation.showBranding,\n },\n },\n });\n const updated = result.data?.updateDashboardEmbedPolicy ?? null;\n if (updated) setPolicy(updated);\n return updated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to update presentation'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const setAllowedOrigins = useCallback(\n async (policyId: string, allowedParentOrigins: string[]): Promise<DashboardEmbedPolicy | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ updateDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: UpdateDashboardEmbedPolicyDocument,\n variables: { input: { policyId, allowedParentOrigins } },\n });\n const updated = result.data?.updateDashboardEmbedPolicy ?? null;\n if (updated) setPolicy(updated);\n return updated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to update allowed domains'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const rotateKey = useCallback(\n async (policyId: string): Promise<EmbedPolicySecret | null> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ rotateDashboardEmbedPolicy?: EmbedPolicySecret }>({\n mutation: RotateDashboardEmbedPolicyDocument,\n variables: { policyId },\n });\n const rotated = result.data?.rotateDashboardEmbedPolicy ?? null;\n if (rotated) {\n setSecret(rotated);\n setPolicy(rotated.policy);\n }\n return rotated;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to rotate embed link'));\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n const unpublish = useCallback(\n async (policyId: string): Promise<boolean> => {\n try {\n setLoading(true);\n setError(null);\n const result = await apolloClient.mutate<{ revokeDashboardEmbedPolicy?: DashboardEmbedPolicy }>({\n mutation: RevokeDashboardEmbedPolicyDocument,\n variables: { policyId },\n });\n const revoked = result.data?.revokeDashboardEmbedPolicy ?? null;\n if (revoked) {\n setPolicy(revoked);\n setSecret(null);\n return true;\n }\n return false;\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to unpublish'));\n return false;\n } finally {\n setLoading(false);\n }\n },\n [apolloClient]\n );\n\n return {\n policy,\n secret,\n loading,\n error,\n fetchPolicy,\n publishToWeb,\n applyPresentation,\n setAllowedOrigins,\n rotateKey,\n unpublish,\n };\n}\n\nexport default useDashboardEmbedPolicy;\n"],"mappings":"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"}
|