This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git


The following commit(s) were added to refs/heads/master by this push:
     new f4d0a5f9bce [improvement](website) add a full-screen execution graph 
and an AI wait estimate (#4057)
f4d0a5f9bce is described below

commit f4d0a5f9bcec449fd22bb3f41a5c1d6be8e99968
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Aug 12 23:30:33 2026 +0800

    [improvement](website) add a full-screen execution graph and an AI wait 
estimate (#4057)
    
    ## What
    
    Two usability changes on the [Profile
    Analysis](https://doris.apache.org/profile-analysis) page.
    
    ### 1. Full-screen execution graph
    
    The React Flow canvas under **Visualize Execution** is fixed at 680px,
    which is cramped for large plans. A **Full screen** toggle now sits in
    the canvas's top-right corner, opposite the *Slowest operators* panel.
    
    - Full screen fixes the workspace below the navbar (`z-index: 250`,
    under the sticky navbar's `300`), so the navigation stays visible and
    clickable while the graph takes every remaining pixel.
    - The top offset is measured from `.navbar-next` at runtime rather than
    hard-coded, because the navbar grows on narrow screens; a `resize`
    listener keeps it in sync.
    - `Esc` leaves full screen, the page behind the canvas stops scrolling
    while it is open, and the graph refits itself whenever the canvas
    changes size, so it never ends up off-centre.
    - The operator details sidebar comes along into full screen; the canvas
    and sidebar now derive their height from the workspace grid row instead
    of a hard-coded `680px`, which leaves the normal and mobile layouts
    unchanged.
    
    ### 2. AI analysis wait estimate
    
    An AI run takes a while, and the button only said `Processing…` with no
    sense of how long. It now reads `Processing… · Estimated 1–2 minutes`,
    matching the `·` separator style already used by the status line.
    
    ## Tests
    
    `node --test src/components/profile-analysis/*.test.js` — 79 passed, 0
    failed. Two new cases cover the full-screen behaviour and the wait
    estimate; the existing `Processing…` assertion was updated.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-authored-by: morningman <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 src/components/profile-analysis/AiAnalysisForm.tsx |  2 +-
 src/components/profile-analysis/ProfileDag.scss    | 63 +++++++++++++++--
 src/components/profile-analysis/ProfileDag.tsx     | 82 +++++++++++++++++++++-
 .../profile-analysis.components.test.js            | 43 +++++++++++-
 4 files changed, 181 insertions(+), 9 deletions(-)

diff --git a/src/components/profile-analysis/AiAnalysisForm.tsx 
b/src/components/profile-analysis/AiAnalysisForm.tsx
index 1fe197b2c45..704823efe24 100644
--- a/src/components/profile-analysis/AiAnalysisForm.tsx
+++ b/src/components/profile-analysis/AiAnalysisForm.tsx
@@ -164,7 +164,7 @@ export function AiAnalysisForm({
                     if (hcaptchaToken) onAnalyze(hcaptchaToken, resetCaptcha);
                 }}
             >
-                {disabled ? 'Processing…' : 'Analyze with AI'}
+                {disabled ? 'Processing… · Estimated 1–2 minutes' : 'Analyze 
with AI'}
             </button>
         </div>
     );
diff --git a/src/components/profile-analysis/ProfileDag.scss 
b/src/components/profile-analysis/ProfileDag.scss
index 540d7a16235..3e536bd907b 100644
--- a/src/components/profile-analysis/ProfileDag.scss
+++ b/src/components/profile-analysis/ProfileDag.scss
@@ -77,7 +77,7 @@
         position: relative;
         display: grid;
         grid-template-columns: minmax(0, 1fr);
-        min-height: 680px;
+        grid-template-rows: 680px;
         border: 1px solid var(--brand-border-soft);
         border-radius: 10px;
         overflow: hidden;
@@ -88,9 +88,24 @@
         grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
     }
 
+    // Full screen fills everything below the navbar, which stays reachable 
because
+    // the sticky navbar (z-index 300) keeps a higher stacking order than the 
canvas.
+    &__workspace--fullscreen {
+        position: fixed;
+        top: var(--profile-dag-navbar-offset, 64px);
+        right: 0;
+        bottom: 0;
+        left: 0;
+        z-index: 250;
+        grid-template-rows: minmax(0, 1fr);
+        border-width: 1px 0 0;
+        border-radius: 0;
+    }
+
     &__canvas {
         min-width: 0;
-        height: 680px;
+        min-height: 0;
+        height: 100%;
     }
 
     &__message {
@@ -254,6 +269,42 @@
     }
 }
 
+.profile-dag-fullscreen {
+    &__button {
+        display: inline-flex;
+        gap: 0.35rem;
+        align-items: center;
+        padding: 0.35rem 0.6rem;
+        border: 1px solid var(--brand-border-soft);
+        border-radius: 8px;
+        color: var(--ifm-font-color-base);
+        background: var(--brand-paper);
+        box-shadow: 0 6px 18px rgb(var(--brand-shadow-rgb) / 14%);
+        cursor: pointer;
+        font: inherit;
+        font-size: 0.75rem;
+        font-weight: 700;
+
+        &:hover,
+        &:focus-visible {
+            border-color: var(--brand-primary);
+            color: var(--brand-primary-deep);
+            background: var(--brand-surface-callout);
+        }
+
+        svg {
+            width: 0.85rem;
+            height: 0.85rem;
+            flex: 0 0 auto;
+            fill: none;
+            stroke: currentColor;
+            stroke-linecap: round;
+            stroke-linejoin: round;
+            stroke-width: 1.6;
+        }
+    }
+}
+
 .profile-dag-fragment {
     width: 100%;
     height: 100%;
@@ -372,7 +423,7 @@
 }
 
 .profile-dag-details {
-    height: 680px;
+    height: 100%;
     padding: 1rem;
     overflow-y: auto;
     border-left: 1px solid var(--brand-border-soft);
@@ -448,11 +499,11 @@
         &__workspace,
         &__workspace--details {
             grid-template-columns: minmax(0, 1fr);
-            min-height: 520px;
+            grid-template-rows: 520px;
         }
 
-        &__canvas {
-            height: 520px;
+        &__workspace--fullscreen {
+            grid-template-rows: minmax(0, 1fr);
         }
     }
 
diff --git a/src/components/profile-analysis/ProfileDag.tsx 
b/src/components/profile-analysis/ProfileDag.tsx
index 704713ea3d1..51bf2a57d85 100644
--- a/src/components/profile-analysis/ProfileDag.tsx
+++ b/src/components/profile-analysis/ProfileDag.tsx
@@ -45,6 +45,15 @@ const edgeTypes = {
 /** Zoom applied when a hotspot entry centers its operator on the canvas. */
 const FOCUS_ZOOM = 1;
 
+/** Full screen keeps the site navigation usable, so the canvas starts below 
this element. */
+const NAVBAR_SELECTOR = '.navbar-next';
+
+/** Custom property holding the measured navbar height the full-screen canvas 
starts below. */
+const NAVBAR_OFFSET_PROPERTY = '--profile-dag-navbar-offset';
+
+const EXPAND_ICON_PATH = 'M6 1.5H1.5V6M10 1.5h4.5V6M14.5 10v4.5H10M6 
14.5H1.5V10';
+const COLLAPSE_ICON_PATH = 'M1.5 5.5H6V1.5M14.5 5.5H10V1.5M10 14.5V10h4.5M6 
14.5V10H1.5';
+
 const stateMessages: Partial<Record<DagUiState, string>> = {
     idle: 'Choose a Profile and select Visualize Execution.',
     parsing: 'Parsing the execution graph…',
@@ -205,12 +214,39 @@ function HotspotList({
     );
 }
 
+function FullscreenToggle({
+    active,
+    onToggle,
+}: {
+    active: boolean;
+    onToggle: (next: boolean) => void;
+}): JSX.Element {
+    return (
+        <Panel position="top-right" className="profile-dag-fullscreen">
+            <button
+                type="button"
+                className="profile-dag-fullscreen__button"
+                aria-pressed={active}
+                title={active ? 'Exit full screen (Esc)' : 'Expand the 
execution graph to full screen'}
+                onClick={() => onToggle(!active)}
+            >
+                <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
+                    <path d={active ? COLLAPSE_ICON_PATH : EXPAND_ICON_PATH} />
+                </svg>
+                {active ? 'Exit full screen' : 'Full screen'}
+            </button>
+        </Panel>
+    );
+}
+
 function ProfileDagCanvas({ dag }: { dag: ProfileDagResponse }): JSX.Element {
     const [nodes, setNodes] = useState<ProfileFlowNode[]>([]);
     const [edges, setEdges] = useState<Awaited<ReturnType<typeof 
layoutProfileDag>>['edges']>([]);
     const [selectedNode, setSelectedNode] = useState<ProfileDagNodeData | 
null>(null);
     const [layoutError, setLayoutError] = useState<string | null>(null);
+    const [isFullscreen, setIsFullscreen] = useState(false);
     const canvasRef = useRef<HTMLDivElement>(null);
+    const workspaceRef = useRef<HTMLDivElement>(null);
     const hasFitVisibleCanvasRef = useRef(false);
     const { fitView, getInternalNode, setCenter } = 
useReactFlow<ProfileFlowNode>();
     const hotspots = useMemo(() => selectSlowestOperators(dag), [dag]);
@@ -221,6 +257,7 @@ function ProfileDagCanvas({ dag }: { dag: 
ProfileDagResponse }): JSX.Element {
         setEdges([]);
         setSelectedNode(null);
         setLayoutError(null);
+        setIsFullscreen(false);
         hasFitVisibleCanvasRef.current = false;
         layoutProfileDag(dag)
             .then(layout => {
@@ -254,6 +291,40 @@ function ProfileDagCanvas({ dag }: { dag: 
ProfileDagResponse }): JSX.Element {
         return () => observer.disconnect();
     }, [fitView, nodes.length]);
 
+    const changeFullscreen = useCallback((next: boolean) => {
+        // The canvas changes size, so let the resize observer above fit the 
graph again.
+        hasFitVisibleCanvasRef.current = false;
+        setIsFullscreen(next);
+    }, []);
+
+    // Full screen stops below the navbar, whose height is measured because it 
grows on
+    // narrow screens, and the page behind the canvas is kept from scrolling.
+    useEffect(() => {
+        const workspace = workspaceRef.current;
+        if (!isFullscreen || !workspace) return;
+
+        const navbar = document.querySelector(NAVBAR_SELECTOR);
+        const applyNavbarOffset = () => {
+            const offset = navbar ? Math.max(0, 
navbar.getBoundingClientRect().bottom) : 0;
+            workspace.style.setProperty(NAVBAR_OFFSET_PROPERTY, `${offset}px`);
+        };
+        const exitOnEscape = (event: KeyboardEvent) => {
+            if (event.key === 'Escape') changeFullscreen(false);
+        };
+        const previousBodyOverflow = document.body.style.overflow;
+
+        applyNavbarOffset();
+        document.body.style.overflow = 'hidden';
+        window.addEventListener('resize', applyNavbarOffset);
+        window.addEventListener('keydown', exitOnEscape);
+        return () => {
+            window.removeEventListener('resize', applyNavbarOffset);
+            window.removeEventListener('keydown', exitOnEscape);
+            document.body.style.overflow = previousBodyOverflow;
+            workspace.style.removeProperty(NAVBAR_OFFSET_PROPERTY);
+        };
+    }, [changeFullscreen, isFullscreen]);
+
     const highlightNode = useCallback((nodeId: string) => {
         setNodes(current => current.map(node => ({ ...node, selected: node.id 
=== nodeId })));
     }, []);
@@ -288,8 +359,16 @@ function ProfileDagCanvas({ dag }: { dag: 
ProfileDagResponse }): JSX.Element {
         return <div className="profile-dag__message"><span 
className="profile-analysis__spinner" aria-hidden="true" /> Laying out the 
execution graph…</div>;
     }
 
+    const workspaceClassName = [
+        'profile-dag__workspace',
+        selectedNode ? 'profile-dag__workspace--details' : '',
+        isFullscreen ? 'profile-dag__workspace--fullscreen' : '',
+    ]
+        .filter(Boolean)
+        .join(' ');
+
     return (
-        <div className={`profile-dag__workspace${selectedNode ? ' 
profile-dag__workspace--details' : ''}`}>
+        <div ref={workspaceRef} className={workspaceClassName}>
             <div ref={canvasRef} className="profile-dag__canvas" 
aria-label="Profile execution graph">
                 <ReactFlow
                     nodes={nodes}
@@ -311,6 +390,7 @@ function ProfileDagCanvas({ dag }: { dag: 
ProfileDagResponse }): JSX.Element {
                 >
                     <Background gap={24} size={1} />
                     {hotspots.length > 0 && <HotspotList hotspots={hotspots} 
onFocus={focusNode} />}
+                    <FullscreenToggle active={isFullscreen} 
onToggle={changeFullscreen} />
                     <MiniMap pannable zoomable aria-label="Execution graph 
overview" />
                     <Controls showInteractive={false} />
                 </ReactFlow>
diff --git 
a/src/components/profile-analysis/profile-analysis.components.test.js 
b/src/components/profile-analysis/profile-analysis.components.test.js
index 8586e3a423d..b4b52ef1a77 100644
--- a/src/components/profile-analysis/profile-analysis.components.test.js
+++ b/src/components/profile-analysis/profile-analysis.components.test.js
@@ -113,7 +113,18 @@ test('disables both tab actions without a file and keeps 
visualization available
         disabled: true,
     });
     assert.match(analyzing, /<input[^>]*disabled=""/);
-    assert.match(analyzing, 
/<button[^>]*disabled=""[^>]*>Processing…<\/button>/);
+    assert.match(analyzing, 
/<button[^>]*disabled=""[^>]*>Processing…[^<]*<\/button>/);
+});
+
+test('states how long the AI analysis is expected to take while it runs', () 
=> {
+    const analyzing = renderAiForm({
+        file: new File(['profile'], 'query.txt'),
+        disabled: true,
+    });
+
+    assert.match(analyzing, /Processing… · Estimated 1–2 minutes/);
+    // The estimate belongs to the waiting state only.
+    assert.doesNotMatch(renderAiForm(), /Estimated 1–2 minutes/);
 });
 
 test('keeps local file selection independent from unchecked AI consent and 
displays the required notice', () => {
@@ -276,3 +287,33 @@ test('overlays the slowest operators on the canvas and 
centers the one that is c
     assert.match(styles, /\.react-flow__node\.selected 
\.profile-dag-node\s*{[^}]*outline:\s*3px solid var\(--brand-primary\)/s);
     assert.match(styles, 
/\.profile-dag-hotspots\s*{[^}]*max-width:\s*min\(310px, 48%\)/s);
 });
+
+test('expands the execution graph to every pixel below the navbar in full 
screen', () => {
+    const dagSource = fs.readFileSync(path.join(__dirname, 'ProfileDag.tsx'), 
'utf8');
+
+    assert.match(dagSource, /<Panel position="top-right" 
className="profile-dag-fullscreen"/);
+    assert.match(dagSource, /<FullscreenToggle active=\{isFullscreen\} 
onToggle=\{changeFullscreen\} \/>/);
+    assert.match(dagSource, /\{active \? 'Exit full screen' : 'Full 
screen'\}/);
+    assert.match(dagSource, /aria-pressed=\{active\}/);
+    // The navbar stays visible, so its measured height offsets the canvas.
+    assert.match(dagSource, /const NAVBAR_SELECTOR = '\.navbar-next'/);
+    assert.match(dagSource, /navbar\.getBoundingClientRect\(\)\.bottom/);
+    assert.match(dagSource, 
/workspace\.style\.setProperty\(NAVBAR_OFFSET_PROPERTY/);
+    // Escape leaves full screen, and the page behind the canvas is restored 
on exit.
+    assert.match(dagSource, /event\.key === 'Escape'/);
+    assert.match(dagSource, /document\.body\.style\.overflow = 'hidden'/);
+    assert.match(dagSource, /document\.body\.style\.overflow = 
previousBodyOverflow/);
+    // Resizing the canvas refits the graph instead of leaving it off-centre.
+    assert.match(dagSource, /hasFitVisibleCanvasRef\.current = 
false;\s*\n\s*setIsFullscreen\(next\)/);
+
+    const styles = fs.readFileSync(path.join(__dirname, 'ProfileDag.scss'), 
'utf8');
+    const fullscreenRule = 
styles.match(/&__workspace--fullscreen\s*{[^}]*}/s)[0];
+    assert.match(fullscreenRule, /position:\s*fixed/);
+    assert.match(fullscreenRule, /top:\s*var\(--profile-dag-navbar-offset, 
64px\)/);
+    assert.match(fullscreenRule, /right:\s*0/);
+    assert.match(fullscreenRule, /bottom:\s*0/);
+    assert.match(fullscreenRule, /left:\s*0/);
+    // Below the sticky navbar's z-index (300) so the navigation stays 
clickable.
+    assert.match(fullscreenRule, /z-index:\s*250/);
+    assert.match(styles, /&__canvas\s*{[^}]*height:\s*100%/s);
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to