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]