chore: start building tv-recorder toolbar
This commit is contained in:
parent
17ed944a84
commit
c9215adc03
|
|
@ -43,6 +43,7 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp
|
||||||
constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
|
constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
|
||||||
super();
|
super();
|
||||||
this._transport = transport;
|
this._transport = transport;
|
||||||
|
this._transport.eventSink.resolve(this);
|
||||||
this._tracePage = tracePage;
|
this._tracePage = tracePage;
|
||||||
this._traceServer = traceServer;
|
this._traceServer = traceServer;
|
||||||
this.wsEndpointForTest = wsEndpointForTest;
|
this.wsEndpointForTest = wsEndpointForTest;
|
||||||
|
|
@ -94,6 +95,7 @@ async function openApp(trace: string, options?: TraceViewerServerOptions & { hea
|
||||||
|
|
||||||
class RecorderTransport implements Transport {
|
class RecorderTransport implements Transport {
|
||||||
private _connected = new ManualPromise<void>();
|
private _connected = new ManualPromise<void>();
|
||||||
|
readonly eventSink = new ManualPromise<EventEmitter>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +105,8 @@ class RecorderTransport implements Transport {
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispatch(method: string, params: any): Promise<any> {
|
async dispatch(method: string, params: any): Promise<any> {
|
||||||
|
const eventSink = await this.eventSink;
|
||||||
|
eventSink.emit('event', { event: method, params });
|
||||||
}
|
}
|
||||||
|
|
||||||
onclose() {
|
onclose() {
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,6 @@
|
||||||
flex: auto;
|
flex: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recorder-chooser {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
outline: none;
|
|
||||||
color: var(--vscode-sideBarTitle-foreground);
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder .codicon {
|
.recorder .codicon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import type { CallLog, Mode, Source } from './recorderTypes';
|
import type { CallLog, Mode, Source } from './recorderTypes';
|
||||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
import { SplitView } from '@web/components/splitView';
|
import { SplitView } from '@web/components/splitView';
|
||||||
|
import { emptySource, SourceChooser } from '@web/components/sourceChooser';
|
||||||
import { TabbedPane } from '@web/components/tabbedPane';
|
import { TabbedPane } from '@web/components/tabbedPane';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
|
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
|
||||||
|
|
@ -54,15 +55,7 @@ export const Recorder: React.FC<RecorderProps> = ({
|
||||||
if (source)
|
if (source)
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
const source: Source = {
|
return emptySource();
|
||||||
id: 'default',
|
|
||||||
isRecorded: false,
|
|
||||||
text: '',
|
|
||||||
language: 'javascript',
|
|
||||||
label: '',
|
|
||||||
highlight: []
|
|
||||||
};
|
|
||||||
return source;
|
|
||||||
}, [sources, fileId]);
|
}, [sources, fileId]);
|
||||||
|
|
||||||
const [locator, setLocator] = React.useState('');
|
const [locator, setLocator] = React.useState('');
|
||||||
|
|
@ -152,10 +145,10 @@ export const Recorder: React.FC<RecorderProps> = ({
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<div style={{ flex: 'auto' }}></div>
|
<div style={{ flex: 'auto' }}></div>
|
||||||
<div>Target:</div>
|
<div>Target:</div>
|
||||||
<select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => {
|
<SourceChooser fileId={fileId} sources={sources} setFileId={fileId => {
|
||||||
setFileId(event.target.selectedOptions[0].value);
|
setFileId(fileId);
|
||||||
window.dispatch({ event: 'fileChanged', params: { file: event.target.selectedOptions[0].value } });
|
window.dispatch({ event: 'fileChanged', params: { file: fileId } });
|
||||||
}}>{renderSourceOptions(sources)}</select>
|
}} />
|
||||||
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
|
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
|
||||||
window.dispatch({ event: 'clear' });
|
window.dispatch({ event: 'clear' });
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
|
|
@ -184,22 +177,3 @@ export const Recorder: React.FC<RecorderProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderSourceOptions(sources: Source[]): React.ReactNode {
|
|
||||||
const transformTitle = (title: string): string => title.replace(/.*[/\\]([^/\\]+)/, '$1');
|
|
||||||
const renderOption = (source: Source): React.ReactNode => (
|
|
||||||
<option key={source.id} value={source.id}>{transformTitle(source.label)}</option>
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasGroup = sources.some(s => s.group);
|
|
||||||
if (hasGroup) {
|
|
||||||
const groups = new Set(sources.map(s => s.group));
|
|
||||||
return [...groups].filter(Boolean).map(group => (
|
|
||||||
<optgroup label={group} key={group}>
|
|
||||||
{sources.filter(s => s.group === group).map(source => renderOption(source))}
|
|
||||||
</optgroup>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return sources.map(source => renderOption(source));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ import type { SourceLocation } from './modelUtil';
|
||||||
import { Workbench } from './workbench';
|
import { Workbench } from './workbench';
|
||||||
import type { Mode, Source } from '@recorder/recorderTypes';
|
import type { Mode, Source } from '@recorder/recorderTypes';
|
||||||
import type { ContextEntry } from '../entries';
|
import type { ContextEntry } from '../entries';
|
||||||
|
import { emptySource, SourceChooser } from '@web/components/sourceChooser';
|
||||||
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
|
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
|
||||||
|
import { toggleTheme } from '@web/theme';
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const guid = searchParams.get('ws');
|
const guid = searchParams.get('ws');
|
||||||
|
|
@ -29,33 +33,81 @@ const trace = searchParams.get('trace') + '.json';
|
||||||
export const RecorderView: React.FunctionComponent = () => {
|
export const RecorderView: React.FunctionComponent = () => {
|
||||||
const [connection, setConnection] = React.useState<Connection | null>(null);
|
const [connection, setConnection] = React.useState<Connection | null>(null);
|
||||||
const [sources, setSources] = React.useState<Source[]>([]);
|
const [sources, setSources] = React.useState<Source[]>([]);
|
||||||
|
const [mode, setMode] = React.useState<Mode>('none');
|
||||||
|
const [fileId, setFileId] = React.useState<string | undefined>();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!fileId && sources.length > 0)
|
||||||
|
setFileId(sources[0].id);
|
||||||
|
}, [fileId, sources]);
|
||||||
|
|
||||||
|
const source = React.useMemo(() => {
|
||||||
|
if (fileId) {
|
||||||
|
const source = sources.find(s => s.id === fileId);
|
||||||
|
if (source)
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
return emptySource();
|
||||||
|
}, [sources, fileId]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const wsURL = new URL(`../${guid}`, window.location.toString());
|
const wsURL = new URL(`../${guid}`, window.location.toString());
|
||||||
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
||||||
const webSocket = new WebSocket(wsURL.toString());
|
const webSocket = new WebSocket(wsURL.toString());
|
||||||
setConnection(new Connection(webSocket, { setSources }));
|
setConnection(new Connection(webSocket, { setSources, setMode }));
|
||||||
return () => {
|
return () => {
|
||||||
webSocket.close();
|
webSocket.close();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!connection)
|
|
||||||
return;
|
|
||||||
connection.setMode('recording');
|
|
||||||
}, [connection]);
|
|
||||||
|
|
||||||
return <div className='vbox workbench-loader'>
|
return <div className='vbox workbench-loader'>
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarButton icon='circle-large-filled' title='Record' toggled={mode === 'recording' || mode === 'recording-inspecting' || mode === 'assertingText' || mode === 'assertingVisibility'} onClick={() => {
|
||||||
|
connection?.setMode(mode === 'none' || mode === 'standby' || mode === 'inspecting' ? 'recording' : 'standby');
|
||||||
|
}}>Record</ToolbarButton>
|
||||||
|
<ToolbarSeparator />
|
||||||
|
<ToolbarButton icon='inspect' title='Pick locator' toggled={mode === 'inspecting' || mode === 'recording-inspecting'} onClick={() => {
|
||||||
|
const newMode = ({
|
||||||
|
'inspecting': 'standby',
|
||||||
|
'none': 'inspecting',
|
||||||
|
'standby': 'inspecting',
|
||||||
|
'recording': 'recording-inspecting',
|
||||||
|
'recording-inspecting': 'recording',
|
||||||
|
'assertingText': 'recording-inspecting',
|
||||||
|
'assertingVisibility': 'recording-inspecting',
|
||||||
|
'assertingValue': 'recording-inspecting',
|
||||||
|
} as Record<string, Mode>)[mode];
|
||||||
|
connection?.setMode(newMode);
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<ToolbarButton icon='eye' title='Assert visibility' toggled={mode === 'assertingVisibility'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
|
||||||
|
connection?.setMode(mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility');
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<ToolbarButton icon='whole-word' title='Assert text' toggled={mode === 'assertingText'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
|
||||||
|
connection?.setMode(mode === 'assertingText' ? 'recording' : 'assertingText');
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<ToolbarButton icon='symbol-constant' title='Assert value' toggled={mode === 'assertingValue'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
|
||||||
|
connection?.setMode(mode === 'assertingValue' ? 'recording' : 'assertingValue');
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<ToolbarSeparator />
|
||||||
|
<div style={{ flex: 'auto' }}></div>
|
||||||
|
<div>Target:</div>
|
||||||
|
<SourceChooser fileId={fileId} sources={sources} setFileId={fileId => {
|
||||||
|
setFileId(fileId);
|
||||||
|
}} />
|
||||||
|
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
||||||
|
</Toolbar>
|
||||||
<TraceView
|
<TraceView
|
||||||
traceLocation={trace}
|
traceLocation={trace}
|
||||||
sources={sources} />
|
source={source} />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TraceView: React.FC<{
|
export const TraceView: React.FC<{
|
||||||
traceLocation: string,
|
traceLocation: string,
|
||||||
sources: Source[],
|
source: Source | undefined,
|
||||||
}> = ({ traceLocation, sources }) => {
|
}> = ({ traceLocation, source }) => {
|
||||||
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
||||||
const [counter, setCounter] = React.useState(0);
|
const [counter, setCounter] = React.useState(0);
|
||||||
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
|
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
@ -82,7 +134,7 @@ export const TraceView: React.FC<{
|
||||||
}, [counter, traceLocation]);
|
}, [counter, traceLocation]);
|
||||||
|
|
||||||
const fallbackLocation = React.useMemo(() => {
|
const fallbackLocation = React.useMemo(() => {
|
||||||
if (!sources.length)
|
if (!source)
|
||||||
return undefined;
|
return undefined;
|
||||||
const fallbackLocation: SourceLocation = {
|
const fallbackLocation: SourceLocation = {
|
||||||
file: '',
|
file: '',
|
||||||
|
|
@ -90,11 +142,11 @@ export const TraceView: React.FC<{
|
||||||
column: 0,
|
column: 0,
|
||||||
source: {
|
source: {
|
||||||
errors: [],
|
errors: [],
|
||||||
content: sources[0].text
|
content: source.text
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return fallbackLocation;
|
return fallbackLocation;
|
||||||
}, [sources]);
|
}, [source]);
|
||||||
|
|
||||||
return <Workbench
|
return <Workbench
|
||||||
key='workbench'
|
key='workbench'
|
||||||
|
|
@ -103,6 +155,7 @@ export const TraceView: React.FC<{
|
||||||
fallbackLocation={fallbackLocation}
|
fallbackLocation={fallbackLocation}
|
||||||
isLive={true}
|
isLive={true}
|
||||||
hideTimeline={true}
|
hideTimeline={true}
|
||||||
|
hideMetatada={true}
|
||||||
/>;
|
/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -114,13 +167,19 @@ async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
|
||||||
return new MultiTraceModel(contextEntries);
|
return new MultiTraceModel(contextEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ConnectionOptions = {
|
||||||
|
setSources: (sources: Source[]) => void;
|
||||||
|
setMode: (mode: Mode) => void;
|
||||||
|
};
|
||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
private _lastId = 0;
|
private _lastId = 0;
|
||||||
private _webSocket: WebSocket;
|
private _webSocket: WebSocket;
|
||||||
private _callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
|
private _callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
|
||||||
private _options: { setSources: (sources: Source[]) => void; };
|
private _options: ConnectionOptions;
|
||||||
|
|
||||||
constructor(webSocket: WebSocket, options: { setSources: (sources: Source[]) => void }) {
|
constructor(webSocket: WebSocket, options: ConnectionOptions) {
|
||||||
this._webSocket = webSocket;
|
this._webSocket = webSocket;
|
||||||
this._callbacks = new Map();
|
this._callbacks = new Map();
|
||||||
this._options = options;
|
this._options = options;
|
||||||
|
|
@ -157,7 +216,7 @@ class Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _sendMessageNoReply(method: string, params?: any) {
|
private _sendMessageNoReply(method: string, params?: any) {
|
||||||
this._sendMessage(method, params).catch(() => { });
|
this._sendMessage(method, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dispatchEvent(method: string, params?: any) {
|
private _dispatchEvent(method: string, params?: any) {
|
||||||
|
|
@ -166,5 +225,10 @@ class Connection {
|
||||||
this._options.setSources(sources);
|
this._options.setSources(sources);
|
||||||
window.playwrightSourcesEchoForTest = sources;
|
window.playwrightSourcesEchoForTest = sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === 'setMode') {
|
||||||
|
const { mode } = params as { mode: Mode };
|
||||||
|
this._options.setMode(mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,6 @@
|
||||||
background-color: var(--vscode-sideBar-background);
|
background-color: var(--vscode-sideBar-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-tab .toolbar .pick-locator {
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-controls {
|
.snapshot-controls {
|
||||||
flex: none;
|
flex: none;
|
||||||
background-color: var(--vscode-sideBar-background);
|
background-color: var(--vscode-sideBar-background);
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,8 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
iframe={iframeRef1.current}
|
iframe={iframeRef1.current}
|
||||||
iteration={loadingRef.current.iteration} />
|
iteration={loadingRef.current.iteration} />
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarButton className='pick-locator' title={showScreenshotInsteadOfSnapshot ? 'Disable "screenshots instead of snapshots" to pick a locator' : 'Pick locator'} icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} />
|
<ToolbarButton title={showScreenshotInsteadOfSnapshot ? 'Disable "screenshots instead of snapshots" to pick a locator' : 'Pick locator'} icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} />
|
||||||
|
<div style={{ width: 4 }}></div>
|
||||||
{['action', 'before', 'after'].map(tab => {
|
{['action', 'before', 'after'].map(tab => {
|
||||||
return <TabbedPaneTab
|
return <TabbedPaneTab
|
||||||
key={tab}
|
key={tab}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { SplitView } from '@web/components/splitView';
|
import { SplitView } from '@web/components/splitView';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useAsyncMemo } from '@web/uiUtils';
|
import { copy, useAsyncMemo } from '@web/uiUtils';
|
||||||
import './sourceTab.css';
|
import './sourceTab.css';
|
||||||
import { StackTraceView } from './stackTrace';
|
import { StackTraceView } from './stackTrace';
|
||||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
|
|
@ -104,13 +104,18 @@ export const SourceTab: React.FunctionComponent<{
|
||||||
orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'}
|
orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'}
|
||||||
sidebarHidden={!showStackFrames}
|
sidebarHidden={!showStackFrames}
|
||||||
main={<div className='vbox' data-testid='source-code'>
|
main={<div className='vbox' data-testid='source-code'>
|
||||||
{ fileName && <Toolbar>
|
{fileName && <Toolbar>
|
||||||
<div className='source-tab-file-name' title={fileName}>
|
<div className='source-tab-file-name' title={fileName}>
|
||||||
<div>{shortFileName}</div>
|
<div>{shortFileName}</div>
|
||||||
</div>
|
</div>
|
||||||
<CopyToClipboard description='Copy filename' value={shortFileName}/>
|
<CopyToClipboard description='Copy filename' value={shortFileName}/>
|
||||||
{location && <ToolbarButton icon='link-external' title='Open in VS Code' onClick={openExternally}></ToolbarButton>}
|
{location && <ToolbarButton icon='link-external' title='Open in VS Code' onClick={openExternally}></ToolbarButton>}
|
||||||
</Toolbar> }
|
</Toolbar>}
|
||||||
|
{!fileName && <div style={{ position: 'absolute', right: 5, top: 5 }}>
|
||||||
|
<ToolbarButton icon='files' title='Copy' onClick={() => {
|
||||||
|
copy(source.content || '');
|
||||||
|
}} />
|
||||||
|
</div>}
|
||||||
<CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} />
|
<CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} />
|
||||||
</div>}
|
</div>}
|
||||||
sidebar={<StackTraceView stack={stack} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} />}
|
sidebar={<StackTraceView stack={stack} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} />}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
fallbackLocation?: modelUtil.SourceLocation,
|
fallbackLocation?: modelUtil.SourceLocation,
|
||||||
isLive?: boolean,
|
isLive?: boolean,
|
||||||
hideTimeline?: boolean,
|
|
||||||
status?: UITestStatus,
|
status?: UITestStatus,
|
||||||
annotations?: { type: string; description?: string; }[];
|
annotations?: { type: string; description?: string; }[];
|
||||||
inert?: boolean,
|
inert?: boolean,
|
||||||
|
|
@ -58,7 +57,9 @@ export const Workbench: React.FunctionComponent<{
|
||||||
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
||||||
revealSource?: boolean,
|
revealSource?: boolean,
|
||||||
showSettings?: boolean,
|
showSettings?: boolean,
|
||||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => {
|
hideTimeline?: boolean,
|
||||||
|
hideMetatada?: boolean,
|
||||||
|
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, hideMetatada, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => {
|
||||||
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
||||||
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
||||||
|
|
||||||
|
|
@ -280,42 +281,46 @@ export const Workbench: React.FunctionComponent<{
|
||||||
else if (model && model.wallTime)
|
else if (model && model.wallTime)
|
||||||
time = Date.now() - model.wallTime;
|
time = Date.now() - model.wallTime;
|
||||||
|
|
||||||
|
const actionsView = <div className='vbox'>
|
||||||
|
{status && <div className='workbench-run-status'>
|
||||||
|
<span className={clsx('codicon', testStatusIcon(status))}></span>
|
||||||
|
<div>{testStatusText(status)}</div>
|
||||||
|
<div className='spacer'></div>
|
||||||
|
<div className='workbench-run-duration'>{time ? msToString(time) : ''}</div>
|
||||||
|
</div>}
|
||||||
|
<ActionList
|
||||||
|
sdkLanguage={sdkLanguage}
|
||||||
|
actions={filteredActions}
|
||||||
|
selectedAction={model ? selectedAction : undefined}
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
setSelectedTime={setSelectedTime}
|
||||||
|
onSelected={onActionSelected}
|
||||||
|
onHighlighted={setHighlightedAction}
|
||||||
|
revealConsole={() => selectPropertiesTab('console')}
|
||||||
|
isLive={isLive}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
const metadataView = hideMetatada ? null : <MetadataView model={model}/>;
|
||||||
|
const settingsView = showSettings ? <SettingsView settings={[
|
||||||
|
{ value: showRouteActions, set: setShowRouteActions, title: 'Show route actions' },
|
||||||
|
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' }
|
||||||
|
]}/> : null;
|
||||||
|
|
||||||
const actionsTab: TabbedPaneTabModel = {
|
const actionsTab: TabbedPaneTabModel = {
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
component: <div className='vbox'>
|
component: actionsView,
|
||||||
{status && <div className='workbench-run-status'>
|
|
||||||
<span className={clsx('codicon', testStatusIcon(status))}></span>
|
|
||||||
<div>{testStatusText(status)}</div>
|
|
||||||
<div className='spacer'></div>
|
|
||||||
<div className='workbench-run-duration'>{time ? msToString(time) : ''}</div>
|
|
||||||
</div>}
|
|
||||||
<ActionList
|
|
||||||
sdkLanguage={sdkLanguage}
|
|
||||||
actions={filteredActions}
|
|
||||||
selectedAction={model ? selectedAction : undefined}
|
|
||||||
selectedTime={selectedTime}
|
|
||||||
setSelectedTime={setSelectedTime}
|
|
||||||
onSelected={onActionSelected}
|
|
||||||
onHighlighted={setHighlightedAction}
|
|
||||||
revealConsole={() => selectPropertiesTab('console')}
|
|
||||||
isLive={isLive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
};
|
};
|
||||||
const metadataTab: TabbedPaneTabModel = {
|
const metadataTab: TabbedPaneTabModel | null = metadataView ? {
|
||||||
id: 'metadata',
|
id: 'metadata',
|
||||||
title: 'Metadata',
|
title: 'Metadata',
|
||||||
component: <MetadataView model={model}/>
|
component: metadataView,
|
||||||
};
|
} : null;
|
||||||
const settingsTab: TabbedPaneTabModel = {
|
const settingsTab: TabbedPaneTabModel | null = settingsView ? {
|
||||||
id: 'settings',
|
id: 'settings',
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
component: <SettingsView settings={[
|
component: settingsView,
|
||||||
{ value: showRouteActions, set: setShowRouteActions, title: 'Show route actions' },
|
} : null;
|
||||||
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' }
|
|
||||||
]}/>,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
|
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
|
||||||
{!hideTimeline && <Timeline
|
{!hideTimeline && <Timeline
|
||||||
|
|
@ -348,13 +353,11 @@ export const Workbench: React.FunctionComponent<{
|
||||||
highlightedLocator={highlightedLocator}
|
highlightedLocator={highlightedLocator}
|
||||||
setHighlightedLocator={locatorPicked}
|
setHighlightedLocator={locatorPicked}
|
||||||
openPage={openPage} />}
|
openPage={openPage} />}
|
||||||
sidebar={
|
sidebar={<TabbedPane
|
||||||
<TabbedPane
|
tabs={[actionsTab, metadataTab, settingsTab].filter(Boolean) as TabbedPaneTabModel[]}
|
||||||
tabs={showSettings ? [actionsTab, metadataTab, settingsTab] : [actionsTab, metadataTab]}
|
selectedTab={selectedNavigatorTab}
|
||||||
selectedTab={selectedNavigatorTab}
|
setSelectedTab={setSelectedNavigatorTab}
|
||||||
setSelectedTab={setSelectedNavigatorTab}
|
/>}
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>}
|
/>}
|
||||||
sidebar={<TabbedPane
|
sidebar={<TabbedPane
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
|
|
|
||||||
23
packages/web/src/components/sourceChooser.css
Normal file
23
packages/web/src/components/sourceChooser.css
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.source-chooser {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--vscode-sideBarTitle-foreground);
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
58
packages/web/src/components/sourceChooser.tsx
Normal file
58
packages/web/src/components/sourceChooser.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import type { Source } from '@recorder/recorderTypes';
|
||||||
|
|
||||||
|
export const SourceChooser: React.FC<{
|
||||||
|
sources: Source[],
|
||||||
|
fileId: string | undefined,
|
||||||
|
setFileId: (fileId: string) => void,
|
||||||
|
}> = ({ sources, fileId, setFileId }) => {
|
||||||
|
return <select className='source-chooser' hidden={!sources.length} value={fileId} onChange={event => {
|
||||||
|
setFileId(event.target.selectedOptions[0].value);
|
||||||
|
}}>{renderSourceOptions(sources)}</select>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderSourceOptions(sources: Source[]): React.ReactNode {
|
||||||
|
const transformTitle = (title: string): string => title.replace(/.*[/\\]([^/\\]+)/, '$1');
|
||||||
|
const renderOption = (source: Source): React.ReactNode => (
|
||||||
|
<option key={source.id} value={source.id}>{transformTitle(source.label)}</option>
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasGroup = sources.some(s => s.group);
|
||||||
|
if (hasGroup) {
|
||||||
|
const groups = new Set(sources.map(s => s.group));
|
||||||
|
return [...groups].filter(Boolean).map(group => (
|
||||||
|
<optgroup label={group} key={group}>
|
||||||
|
{sources.filter(s => s.group === group).map(source => renderOption(source))}
|
||||||
|
</optgroup>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources.map(source => renderOption(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emptySource(): Source {
|
||||||
|
return {
|
||||||
|
id: 'default',
|
||||||
|
isRecorded: false,
|
||||||
|
text: '',
|
||||||
|
language: 'javascript',
|
||||||
|
label: '',
|
||||||
|
highlight: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: none;
|
flex: none;
|
||||||
padding-right: 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar:after {
|
.toolbar:after {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue