import { useEffect, useRef, useState, type CSSProperties } from "react"; import { GRAPH_THEME, withAlpha } from "./graphTheme"; import { GRAPH_LOAD_STAGE_SEQUENCE, createGraphLoadProgress, getGraphLoadStageLabel } from "./graphLoading"; import type { GraphLoadProgress } from "./types"; const LOADING_OVERLAY_CSS = ` .graph-stage-loader { position: absolute; inset: 0; z-index: 9; display: flex; align-items: center; justify-content: center; pointer-events: none; opacity: 1; transition: opacity 220ms ease, transform 220ms ease; } .graph-stage-loader[data-exiting="true"] { opacity: 0; transform: scale(0.985); } .graph-stage-loader-card { width: min(540px, calc(100% - 48px)); border-radius: 24px; padding: 20px 20px 18px; border: 1px solid rgba(127, 208, 255, 0.18); background: radial-gradient(circle at top right, rgba(242, 182, 109, 0.12), transparent 28%), radial-gradient(circle at top left, rgba(127, 208, 255, 0.14), transparent 30%), linear-gradient(145deg, rgba(7, 17, 31, 0.94), rgba(12, 25, 43, 0.82)); box-shadow: 0 26px 90px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255,255,255,0.05); backdrop-filter: blur(18px) saturate(1.08); -webkit-backdrop-filter: blur(18px) saturate(1.08); } .graph-stage-loader-card[data-live="true"] { width: min(500px, calc(100% - 56px)); background: radial-gradient(circle at top right, rgba(242, 182, 109, 0.08), transparent 26%), radial-gradient(circle at top left, rgba(127, 208, 255, 0.12), transparent 28%), linear-gradient(145deg, rgba(7, 17, 31, 0.84), rgba(11, 24, 40, 0.72)); box-shadow: 0 18px 54px rgba(0, 0, 0, 0.26), inset 0 1px 0 rgba(255,255,255,0.04); } .graph-stage-loader-beacon { position: relative; width: 12px; height: 12px; border-radius: 999px; background: linear-gradient(135deg, rgba(127, 208, 255, 0.98), rgba(242, 182, 109, 0.94)); box-shadow: 0 0 18px rgba(127, 208, 255, 0.4); } .graph-stage-loader-beacon::after { content: ""; position: absolute; inset: -7px; border-radius: inherit; border: 1px solid rgba(127, 208, 255, 0.18); animation: graph-loader-beacon 1.9s ease-out infinite; } .graph-stage-loader-track { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; } .graph-stage-loader-step { border-radius: 999px; padding: 7px 0; text-align: center; font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; border: 1px solid rgba(127, 208, 255, 0.08); color: rgba(143, 168, 198, 0.72); background: rgba(255, 255, 255, 0.02); } .graph-stage-loader-step[data-state="done"] { color: rgba(214, 232, 250, 0.92); border-color: rgba(127, 208, 255, 0.18); background: rgba(89, 155, 220, 0.14); } .graph-stage-loader-step[data-state="active"] { color: #eff7ff; border-color: rgba(242, 182, 109, 0.24); background: linear-gradient(135deg, rgba(49, 108, 172, 0.28), rgba(242, 182, 109, 0.16)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); } .graph-stage-loader-bar { position: relative; width: 100%; height: 12px; overflow: hidden; border-radius: 999px; border: 1px solid rgba(127, 208, 255, 0.1); background: rgba(255, 255, 255, 0.05); } .graph-stage-loader-bar-fill { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, rgba(74, 163, 255, 0.9), rgba(127, 208, 255, 0.96), rgba(242, 182, 109, 0.92)); box-shadow: 0 0 30px rgba(74, 163, 255, 0.28); transition: width 220ms ease; } .graph-stage-loader-bar-indeterminate::before { content: ""; position: absolute; top: 1px; bottom: 1px; width: 34%; border-radius: 999px; background: linear-gradient(90deg, rgba(74, 163, 255, 0), rgba(127, 208, 255, 0.94), rgba(242, 182, 109, 0.82), rgba(74, 163, 255, 0)); box-shadow: 0 0 26px rgba(127, 208, 255, 0.18); animation: graph-loader-sweep 1.5s cubic-bezier(0.22, 1, 0.36, 1) infinite; } @keyframes graph-loader-beacon { 0% { transform: scale(0.72); opacity: 0.6; } 100% { transform: scale(1.44); opacity: 0; } } @keyframes graph-loader-sweep { 0% { transform: translateX(-120%); } 100% { transform: translateX(360%); } } `; function formatLayoutSource(source: GraphLoadProgress["layoutSource"]) { switch (source) { case "provided": return "Persisted layout"; case "carried": return "Preserved layout"; case "runtime": return "Runtime layout"; default: return null; } } function formatLayoutState(state: GraphLoadProgress["layoutState"]) { switch (state) { case "bootstrapping": return "Bootstrapping"; case "running": return "Settling"; case "interactive": return "Interactive"; case "stabilized": return "Stable"; case "failed": return "Fallback"; default: return null; } } const loadingMetricStyle = { display: "inline-flex", alignItems: "center", gap: 6, padding: "7px 10px", borderRadius: 999, border: "1px solid rgba(127, 208, 255, 0.12)", background: "rgba(255, 255, 255, 0.03)", color: "#b8cade", fontSize: 11, fontWeight: 600, } satisfies CSSProperties; export function GraphLoadingOverlay({ progress, visible, showGraphBehind, }: { progress: GraphLoadProgress | null; visible: boolean; showGraphBehind: boolean; }) { const [renderVisible, setRenderVisible] = useState(visible); const [exiting, setExiting] = useState(false); const [displayProgress, setDisplayProgress] = useState( progress ?? createGraphLoadProgress({ phase: "bootstrapping", message: "Preparing graph session", progressKind: "indeterminate", }), ); const exitTimerRef = useRef(null); useEffect(() => { if (progress) { setDisplayProgress(progress); } }, [progress]); useEffect(() => { if (visible) { if (exitTimerRef.current !== null) { window.clearTimeout(exitTimerRef.current); exitTimerRef.current = null; } setRenderVisible(true); setExiting(false); return; } if (!renderVisible) { return; } setExiting(true); exitTimerRef.current = window.setTimeout(() => { setRenderVisible(false); setExiting(false); exitTimerRef.current = null; }, 220); return () => { if (exitTimerRef.current !== null) { window.clearTimeout(exitTimerRef.current); exitTimerRef.current = null; } }; }, [renderVisible, visible]); if (!renderVisible) { return null; } const activeProgress = progress ?? displayProgress; const isLiveStage = activeProgress.phase === "stabilizing_layout" || activeProgress.showGraphBehind || showGraphBehind; const overlayBackground = isLiveStage ? "linear-gradient(180deg, rgba(1,4,9,0.04), rgba(1,4,9,0.18))" : "linear-gradient(180deg, rgba(1,4,9,0.22), rgba(1,4,9,0.5))"; const determinateRatio = activeProgress.progressKind === "determinate" && activeProgress.total ? Math.max(0.05, Math.min(activeProgress.loaded ?? 0, activeProgress.total) / Math.max(activeProgress.total, 1)) : null; const layoutSource = formatLayoutSource(activeProgress.layoutSource); const layoutState = formatLayoutState(activeProgress.layoutState); return (
{activeProgress.title}
{activeProgress.message}
{GRAPH_LOAD_STAGE_SEQUENCE.map((phase, index) => { const current = activeProgress.stageIndex ?? 1; const state = index + 1 < current ? "done" : index + 1 === current ? "active" : "upcoming"; return (
{getGraphLoadStageLabel(phase)}
); })}
{activeProgress.progressKind === "determinate" && activeProgress.total ? `${(activeProgress.loaded ?? 0).toLocaleString()} / ${activeProgress.total.toLocaleString()} in current stage` : "Working through this stage"}
{activeProgress.progressKind === "determinate" && determinateRatio !== null ? `${Math.round(determinateRatio * 100)}%` : "Live"}
{activeProgress.progressKind === "determinate" && determinateRatio !== null ? ( ) : null}
{activeProgress.nodesLoaded.toLocaleString()} {activeProgress.nodesTotal ? ` / ${activeProgress.nodesTotal.toLocaleString()}` : ""} nodes {activeProgress.edgesLoaded.toLocaleString()} {activeProgress.edgesTotal ? ` / ${activeProgress.edgesTotal.toLocaleString()}` : ""} relationships {layoutSource ? ( {layoutSource} {layoutState ? ` ยท ${layoutState}` : ""} ) : null}
); }