Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/dashboard/src/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@
box-shadow: 0 0 0 2px rgb(var(--recording-red) / 0.35);
}

.mode-toggle.toolbar-button.toggled.mode-debugger,
.mode-toggle.toolbar-button.toggled.mode-debugger > .codicon {
color: var(--color-accent-fg);
}

.mode-toggle.toolbar-button.toggled.mode-debugger {
background: var(--color-accent-subtle);
box-shadow: none;
}

.debugger-paused-label {
flex: none;
font-weight: 600;
color: var(--color-attention-fg);
white-space: nowrap;
}

.mode-toggle.flash {
animation: mode-toggle-flash 0.45s ease-in-out 4;
}
Expand All @@ -75,7 +92,7 @@
height: 40px;
min-height: 40px;
background: var(--color-canvas-overlay);
padding: 0 8px;
padding: 0 12px;
position: relative;
z-index: 100;
border-bottom: 1px solid var(--color-border-muted);
Expand Down
298 changes: 165 additions & 133 deletions packages/dashboard/src/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import { ChevronLeftIcon, ChevronRightIcon, LockIcon, LockOpenIcon, ReloadIcon,
import { clientToViewport, getImageLayout } from './imageLayout';
import { Recording } from './recording';
import { AnnotateSidebar, AnnotateOverlay } from './annotateView';
import { DebuggerPanel } from './debuggerPanel';

import { ToolbarButton } from '@web/components/toolbarButton';
import { SplitView } from '@web/components/splitView';
import { useMeasureForRef } from '@web/uiUtils';

import type { DashboardModel } from './dashboardModel';
Expand Down Expand Up @@ -66,7 +68,7 @@ export const Dashboard: React.FC<DashboardProps> = ({ model }) => {
const [, setRevision] = React.useState(0);
React.useEffect(() => model.subscribe(() => setRevision(r => r + 1)), [model]);

const { tabs, mode, recording, liveFrame, annotateSession, pendingCapture } = model.state;
const { tabs, mode, recording, liveFrame, annotateSession, pendingCapture, debuggerPanelOpen, debuggerPaused, apiCalls } = model.state;
const interactive = mode === 'interactive';
const annotateActive = !!annotateSession;
const selectedFrame = annotateSession?.frames.find(f => f.id === annotateSession.selectedFrameId) ?? null;
Expand Down Expand Up @@ -234,145 +236,175 @@ export const Dashboard: React.FC<DashboardProps> = ({ model }) => {
const modeLabel = annotateActive ? 'Dashboard: annotate' : isRecording ? 'Dashboard: record' : 'Dashboard';

const overlayOpen = !!selectedFrame;

let actionPointStyle: React.CSSProperties | undefined;
if (debuggerPanelOpen && liveFrame?.viewportWidth && liveFrame?.viewportHeight) {
const call = [...apiCalls].reverse().find(c => c.actionPoint && c.status === 'running');
if (call?.actionPoint) {
actionPointStyle = {
left: `${(call.actionPoint.x / liveFrame.viewportWidth) * 100}%`,
top: `${(call.actionPoint.y / liveFrame.viewportHeight) * 100}%`,
};
}
}

return (
<main className={'dashboard-view' + (interactive ? ' interactive' : '') + (annotateActive ? ' has-annotate-sidebar' : '')} aria-label={modeLabel}>
<div className='dashboard-main'>
{/* Toolbar */}
<div ref={toolbarRef} className='toolbar' hidden={overlayOpen}>
<ToolbarButton
ref={interactiveBtnRef}
className='mode-toggle mode-interactive'
title={interactive ? 'Disable interactive mode' : 'Enable interactive mode'}
toggled={interactive}
disabled={!ready}
onClick={() => {
if (interactive)
model.toggleInteractive();
else
model.enterInteractive();
}}
>
{interactive ? <LockOpenIcon /> : <LockIcon />}
</ToolbarButton>
<ToolbarButton
className='mode-annotate'
title={annotateActive ? 'Add screenshot' : 'Take screenshot'}
disabled={!ready || pendingCapture}
onClick={() => {
if (annotateActive)
model.addAnnotateFrame();
else
model.enterAnnotate('user');
}}
>
<ScreenshotRegionIcon />
</ToolbarButton>
<ToolbarButton
className='mode-toggle mode-record'
title={isRecording ? 'Stop recording' : 'Record video'}
icon='record'
toggled={isRecording}
disabled={!ready || showRecording}
onClick={() => {
if (isRecording)
model.stopRecording();
else
model.startRecording();
}}
>
{isRecording && <span className='mode-record-label'>Recording...</span>}
</ToolbarButton>
</div>

{/* Viewport */}
<div className='viewport-wrapper'>
<div ref={viewportMainRef} className='viewport-main'>
<div className='browser-window' style={windowStyle}>
{showBrowserChrome && (
<div ref={browserChromeRef} className='browser-chrome'>
<button className='nav-btn' title='Back' aria-disabled={!interactive || undefined} onClick={() => {
if (!interactive) {
flashInteractiveHint();
return;
}
model.back();
}}>
<ChevronLeftIcon />
</button>
<button className='nav-btn' title='Forward' aria-disabled={!interactive || undefined} onClick={() => {
if (!interactive) {
flashInteractiveHint();
return;
}
model.forward();
}}>
<ChevronRightIcon />
</button>
<button className='nav-btn' title='Reload' aria-disabled={!interactive || undefined} onClick={() => {
if (!interactive) {
flashInteractiveHint();
return;
}
model.reload();
}}>
<ReloadIcon />
</button>
<div className='omnibox-wrap'>
<input
id='omnibox'
className='omnibox'
type='text'
placeholder='Search or enter URL'
spellCheck={false}
autoComplete='off'
value={omniboxValue}
onChange={e => setOmniboxValue(e.target.value)}
onKeyDown={e => {
if (!interactive)
return;
onOmniboxKeyDown(e);
}}
onFocus={e => {
if (!interactive) {
flashInteractiveHint();
e.target.blur();
return;
}
e.target.select();
}}
aria-disabled={!interactive || undefined}
aria-label={interactive ? 'Search or enter URL' : 'URL input - enable interactive mode to use this field'}
readOnly={!interactive}
/>
<SplitView
orientation='vertical'
sidebarSize={260}
minSidebarSize={140}
settingName='dashboardActionsPanel'
sidebarHidden={!debuggerPanelOpen}
sidebar={<DebuggerPanel model={model} />}
main={<div className='dashboard-main'>
{/* Toolbar */}
<div ref={toolbarRef} className='toolbar' hidden={overlayOpen}>
<ToolbarButton
ref={interactiveBtnRef}
className='mode-toggle mode-interactive'
title={interactive ? 'Disable interactive mode' : 'Enable interactive mode'}
toggled={interactive}
disabled={!ready}
onClick={() => {
if (interactive)
model.toggleInteractive();
else
model.enterInteractive();
}}
>
{interactive ? <LockOpenIcon /> : <LockIcon />}
</ToolbarButton>
<ToolbarButton
className='mode-annotate'
title={annotateActive ? 'Add screenshot' : 'Take screenshot'}
disabled={!ready || pendingCapture}
onClick={() => {
if (annotateActive)
model.addAnnotateFrame();
else
model.enterAnnotate('user');
}}
>
<ScreenshotRegionIcon />
</ToolbarButton>
<ToolbarButton
className='mode-toggle mode-record'
title={isRecording ? 'Stop recording' : 'Record video'}
icon='record'
toggled={isRecording}
disabled={!ready || showRecording}
onClick={() => {
if (isRecording)
model.stopRecording();
else
model.startRecording();
}}
>
{isRecording && <span className='mode-record-label'>Recording...</span>}
</ToolbarButton>
<ToolbarButton
className='mode-toggle mode-debugger'
title={debuggerPanelOpen ? 'Hide actions panel' : 'Show actions panel'}
icon='list-unordered'
toggled={debuggerPanelOpen}
onClick={() => model.toggleDebuggerPanel()}
/>
{debuggerPaused && <span className='debugger-paused-label'>Paused</span>}
<div style={{ flex: 'auto' }}></div>
</div>

{/* Viewport */}
<div className='viewport-wrapper'>
<div ref={viewportMainRef} className='viewport-main'>
<div className='browser-window' style={windowStyle}>
{showBrowserChrome && (
<div ref={browserChromeRef} className='browser-chrome'>
<button className='nav-btn' title='Back' aria-disabled={!interactive || undefined} onClick={() => {
if (!interactive) {
flashInteractiveHint();
return;
}
model.back();
}}>
<ChevronLeftIcon />
</button>
<button className='nav-btn' title='Forward' aria-disabled={!interactive || undefined} onClick={() => {
if (!interactive) {
flashInteractiveHint();
return;
}
model.forward();
}}>
<ChevronRightIcon />
</button>
<button className='nav-btn' title='Reload' aria-disabled={!interactive || undefined} onClick={() => {
if (!interactive) {
flashInteractiveHint();
return;
}
model.reload();
}}>
<ReloadIcon />
</button>
<div className='omnibox-wrap'>
<input
id='omnibox'
className='omnibox'
type='text'
placeholder='Search or enter URL'
spellCheck={false}
autoComplete='off'
value={omniboxValue}
onChange={e => setOmniboxValue(e.target.value)}
onKeyDown={e => {
if (!interactive)
return;
onOmniboxKeyDown(e);
}}
onFocus={e => {
if (!interactive) {
flashInteractiveHint();
e.target.blur();
return;
}
e.target.select();
}}
aria-disabled={!interactive || undefined}
aria-label={interactive ? 'Search or enter URL' : 'URL input - enable interactive mode to use this field'}
readOnly={!interactive}
/>
</div>
</div>
)}
<div
ref={screenRef}
className='screen'
tabIndex={0}
style={{ display: liveFrame ? '' : 'none' }}
onMouseDown={onScreenMouseDown}
onMouseUp={onScreenMouseUp}
onMouseMove={onScreenMouseMove}
onWheel={onScreenWheel}
onKeyDown={onScreenKeyDown}
onKeyUp={onScreenKeyUp}
onContextMenu={e => e.preventDefault()}
>
<img
ref={displayRef}
id='display'
className='display'
alt='screencast'
src={liveFrame ? 'data:image/jpeg;base64,' + liveFrame.data : undefined}
/>
{actionPointStyle && <div className='debugger-action-point' style={actionPointStyle}></div>}
</div>
)}
<div
ref={screenRef}
className='screen'
tabIndex={0}
style={{ display: liveFrame ? '' : 'none' }}
onMouseDown={onScreenMouseDown}
onMouseUp={onScreenMouseUp}
onMouseMove={onScreenMouseMove}
onWheel={onScreenWheel}
onKeyDown={onScreenKeyDown}
onKeyUp={onScreenKeyUp}
onContextMenu={e => e.preventDefault()}
>
<img
ref={displayRef}
id='display'
className='display'
alt='screencast'
src={liveFrame ? 'data:image/jpeg;base64,' + liveFrame.data : undefined}
/>
{overlayText && <div className={'screen-overlay' + (liveFrame ? ' has-frame' : '')}><span>{overlayText}</span></div>}
</div>
{overlayText && <div className={'screen-overlay' + (liveFrame ? ' has-frame' : '')}><span>{overlayText}</span></div>}
</div>
</div>
</div>
</div>
</div>}
/>
{selectedFrame && (
<AnnotateOverlay
key={selectedFrame.id + (annotateSession?.focusAnnotationId ?? '')}
Expand Down
24 changes: 24 additions & 0 deletions packages/dashboard/src/dashboardChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ export type DashboardChannelEvents = {
frame: { data: string; viewportWidth: number; viewportHeight: number };
annotate: {};
cancelAnnotate: {};
apiCalls: { apiCalls: ApiCall[] };
debuggerPaused: { paused: boolean };
debuggerSource: { source: DebuggerSource | null };
};

export type ApiCall = {
id: string;
title: string;
location?: { file: string; line?: number; column?: number };
logs: string[];
actionPoint?: { x: number; y: number };
status: 'running' | 'success' | 'error';
error?: string;
};

export type DebuggerSource = {
file: string;
language: 'javascript' | 'python' | 'java' | 'csharp';
text: string;
highlight: { line: number; type: 'running' | 'paused' | 'error' }[];
revealLine?: number;
};

export type MouseButton = 'left' | 'middle' | 'right';
Expand Down Expand Up @@ -76,6 +97,9 @@ export interface DashboardChannel {
screenshot(): Promise<{ data: string; viewportWidth: number; viewportHeight: number; ariaSnapshot: string }>;
submitAnnotation(params: { frames: SubmittedAnnotationFrame[]; feedback: string }): Promise<void>;
cancelAnnotation(): Promise<void>;
debuggerResume(): Promise<void>;
debuggerPause(): Promise<void>;
debuggerStep(): Promise<void>;

on<K extends keyof DashboardChannelEvents>(event: K, listener: (params: DashboardChannelEvents[K]) => void): void;
off<K extends keyof DashboardChannelEvents>(event: K, listener: (params: DashboardChannelEvents[K]) => void): void;
Expand Down
Loading
Loading