chore: rearrange settings (#21467)

This commit is contained in:
Pavel Feldman 2023-03-07 12:43:16 -08:00 committed by GitHub
parent c9eac69f2b
commit 9e7abb2a76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 250 additions and 230 deletions

View file

@ -48,7 +48,7 @@ export const ActionList: React.FC<ActionListProps> = ({
itemKey={(action: ActionTraceEvent) => action.callId} itemKey={(action: ActionTraceEvent) => action.callId}
itemType={(action: ActionTraceEvent) => action.error?.message ? 'error' : undefined} itemType={(action: ActionTraceEvent) => action.error?.message ? 'error' : undefined}
itemRender={(action: ActionTraceEvent) => renderAction(action, sdkLanguage, revealConsole)} itemRender={(action: ActionTraceEvent) => renderAction(action, sdkLanguage, revealConsole)}
showNoItemsMessage={true} noItemsMessage='No actions'
></ListView>; ></ListView>;
}; };

View file

@ -26,7 +26,7 @@ export const MetadataView: React.FunctionComponent<{
return <></>; return <></>;
return <div className='vbox'> return <div className='vbox'>
<div className='call-section' style={{ paddingTop: 2 }}>Time</div> <div className='call-section' style={{ paddingTop: 2 }}>Time</div>
{model.wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>} {!!model.wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div> <div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
<div className='call-section'>Browser</div> <div className='call-section'>Browser</div>
<div className='call-line'>engine:<span className='call-value string' title={model.browserName}>{model.browserName}</span></div> <div className='call-line'>engine:<span className='call-value string' title={model.browserName}>{model.browserName}</span></div>

View file

@ -22,7 +22,7 @@
padding: 20px 0 5px; padding: 20px 0 5px;
cursor: text; cursor: text;
user-select: none; user-select: none;
border-bottom: 1px solid var(--vscode-panel-border); margin-left: 10px;
} }
.timeline-divider { .timeline-divider {

View file

@ -153,45 +153,47 @@ export const Timeline: React.FunctionComponent<{
onSelected(entry); onSelected(entry);
}; };
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}> return <div style={{ flex: 'none', borderBottom: '1px solid var(--vscode-panel-border)' }}>
<div className='timeline-grid'>{ <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}>
offsets.map((offset, index) => { <div className='timeline-grid'>{
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}> offsets.map((offset, index) => {
<div className='timeline-time'>{msToString(offset.time - boundaries.minimum)}</div> return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
</div>; <div className='timeline-time'>{msToString(offset.time - boundaries.minimum)}</div>
}) </div>;
}</div> })
<div className='timeline-lane timeline-labels'>{ }</div>
bars.map((bar, index) => { <div className='timeline-lane timeline-labels'>{
return <div key={index} bars.map((bar, index) => {
className={'timeline-label ' + bar.className + (targetBar === bar ? ' selected' : '')} return <div key={index}
style={{ className={'timeline-label ' + bar.className + (targetBar === bar ? ' selected' : '')}
left: bar.leftPosition, style={{
maxWidth: 100, left: bar.leftPosition,
}} maxWidth: 100,
> }}
{bar.label} >
</div>; {bar.label}
}) </div>;
}</div> })
<div className='timeline-lane timeline-bars' ref={barsRef}>{ }</div>
bars.map((bar, index) => { <div className='timeline-lane timeline-bars' ref={barsRef}>{
return <div key={index} bars.map((bar, index) => {
className={'timeline-bar ' + (bar.action ? 'action ' : '') + (bar.event ? 'event ' : '') + bar.className + (targetBar === bar ? ' selected' : '')} return <div key={index}
style={{ className={'timeline-bar ' + (bar.action ? 'action ' : '') + (bar.event ? 'event ' : '') + bar.className + (targetBar === bar ? ' selected' : '')}
left: bar.leftPosition + 'px', style={{
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px', left: bar.leftPosition + 'px',
top: barTop(bar) + 'px', width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
}} top: barTop(bar) + 'px',
title={bar.title} }}
></div>; title={bar.title}
}) ></div>;
}</div> })
<FilmStrip model={model} boundaries={boundaries} previewPoint={previewPoint} /> }</div>
<div className='timeline-marker timeline-marker-hover' style={{ <FilmStrip model={model} boundaries={boundaries} previewPoint={previewPoint} />
display: (previewPoint !== undefined) ? 'block' : 'none', <div className='timeline-marker timeline-marker-hover' style={{
left: (previewPoint?.x || 0) + 'px', display: (previewPoint !== undefined) ? 'block' : 'none',
}}></div> left: (previewPoint?.x || 0) + 'px',
}}></div>
</div>
</div>; </div>;
}; };

View file

@ -18,7 +18,7 @@
background-color: var(--vscode-sideBar-background); background-color: var(--vscode-sideBar-background);
} }
.watch-mode-sidebar input { .watch-mode-sidebar input[type=search] {
flex: auto; flex: auto;
} }
@ -44,16 +44,11 @@
margin: 0; margin: 0;
} }
.watch-mode-sidebar .toolbar h3.title { .watch-mode-sidebar .section-title {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 11px; font-size: 11px;
min-width: 3ch;
margin: 0 10px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
text-transform: uppercase; text-transform: uppercase;
font-weight: bold;
margin: 5px;
} }
.watch-mode-sidebar .spacer { .watch-mode-sidebar .spacer {

View file

@ -33,23 +33,97 @@ import type * as trace from '@trace/trace';
let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {}; let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {};
let updateStepsProgress: () => void = () => {}; let updateStepsProgress: () => void = () => {};
let runWatchedTests = () => {}; let runWatchedTests = () => {};
let runVisibleTests = () => {};
export const WatchModeView: React.FC<{}> = ({ export const WatchModeView: React.FC<{}> = ({
}) => { }) => {
const [projectNames, setProjectNames] = React.useState<string[]>([]);
const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined }); const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined });
const [isRunningTest, setIsRunningTest] = React.useState<boolean>(false);
const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0 }); const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0 });
const [selectedTestItem, setSelectedTestItem] = React.useState<TestItem | undefined>(undefined);
const [settingsVisible, setSettingsVisible] = React.useState<boolean>(false);
updateRootSuite = (rootSuite: Suite, { passed, failed }: Progress) => { updateRootSuite = (rootSuite: Suite, { passed, failed }: Progress) => {
setRootSuite({ value: rootSuite }); setRootSuite({ value: rootSuite });
progress.passed = passed; progress.passed = passed;
progress.failed = failed; progress.failed = failed;
setProgress({ ...progress }); setProgress({ ...progress });
}; };
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
const [isRunningTest, setIsRunningTest] = React.useState<boolean>(false);
const [filterText, setFilterText] = React.useState<string>('');
const [projectNames, setProjectNames] = React.useState<string[]>([]);
const [expandedItems, setExpandedItems] = React.useState<Map<string, boolean>>(new Map());
const runTests = (testIds: string[]) => {
setProgress({ total: testIds.length, passed: 0, failed: 0 });
setIsRunningTest(true);
sendMessage('run', { testIds }).then(() => {
setIsRunningTest(false);
});
};
React.useEffect(() => {
if (projectNames.length === 0 && rootSuite.value?.suites.length)
setProjectNames([rootSuite.value?.suites[0].title]);
}, [projectNames, rootSuite]);
return <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
<TraceView testItem={selectedTestItem}></TraceView>
<div className='vbox watch-mode-sidebar'>
<Toolbar>
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
<ToolbarButton icon='play' title='Run' onClick={runVisibleTests} disabled={isRunningTest}></ToolbarButton>
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
<ToolbarButton icon='refresh' title='Reload' onClick={resetCollectingRootSuite} disabled={isRunningTest}></ToolbarButton>
<div className='spacer'></div>
<ToolbarButton icon='gear' title='Toggle color mode' toggled={settingsVisible} onClick={() => { setSettingsVisible(!settingsVisible); }}></ToolbarButton>
</Toolbar>
{ !settingsVisible && <TestList
projectNames={projectNames}
rootSuite={rootSuite}
isRunningTest={isRunningTest}
runTests={runTests}
onTestItemSelected={setSelectedTestItem} />}
{ settingsVisible && <div className='vbox'>
<div className='hbox' style={{ flex: 'none' }}>
<div className='section-title' style={{ marginTop: 10 }}>Projects</div>
<div className='spacer'></div>
<ToolbarButton icon='close' title='Close settings' toggled={false} onClick={() => setSettingsVisible(false)}></ToolbarButton>
</div>
{(rootSuite.value?.suites || []).map(suite => {
return <div style={{ display: 'flex', alignItems: 'center', lineHeight: '24px' }}>
<input id={`project-${suite.title}`} type='checkbox' checked={projectNames.includes(suite.title)} onClick={() => {
const copy = [...projectNames];
if (copy.includes(suite.title))
copy.splice(copy.indexOf(suite.title), 1);
else
copy.push(suite.title);
setProjectNames(copy);
}} style={{ margin: '0 5px 0 10px' }} />
<label htmlFor={`project-${suite.title}`}>
{suite.title}
</label>
</div>;
})}
<div className='section-title'>Appearance</div>
<div style={{ marginLeft: 3 }}>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}>Toggle color mode</ToolbarButton>
</div>
</div>}
{isRunningTest && <div className='status-line'>
Running: {progress.total} tests | {progress.passed} passed | {progress.failed} failed
</div>}
</div>
</SplitView>;
};
export const TestList: React.FC<{
projectNames: string[],
rootSuite: { value: Suite | undefined },
runTests: (testIds: string[]) => void,
isRunningTest: boolean,
onTestItemSelected: (test: TestItem | undefined) => void,
}> = ({ projectNames, rootSuite, runTests, isRunningTest, onTestItemSelected }) => {
const [filterText, setFilterText] = React.useState<string>('');
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
const [expandedItems, setExpandedItems] = React.useState<Map<string, boolean>>(new Map());
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => { React.useEffect(() => {
@ -57,11 +131,6 @@ export const WatchModeView: React.FC<{}> = ({
resetCollectingRootSuite(); resetCollectingRootSuite();
}, []); }, []);
React.useEffect(() => {
if (projectNames.length === 0 && rootSuite.value?.suites.length)
setProjectNames([rootSuite.value?.suites[0].title]);
}, [projectNames, rootSuite]);
const { filteredItems, treeItemMap, visibleTestIds } = React.useMemo(() => { const { filteredItems, treeItemMap, visibleTestIds } = React.useMemo(() => {
const treeItems = createTree(rootSuite.value, projectNames); const treeItems = createTree(rootSuite.value, projectNames);
const filteredItems = filterTree(treeItems, filterText); const filteredItems = filterTree(treeItems, filterText);
@ -78,6 +147,7 @@ export const WatchModeView: React.FC<{}> = ({
return { treeItemMap, visibleTestIds, filteredItems }; return { treeItemMap, visibleTestIds, filteredItems };
}, [filterText, rootSuite, projectNames]); }, [filterText, rootSuite, projectNames]);
runVisibleTests = () => runTests([...visibleTestIds]);
const { listItems } = React.useMemo(() => { const { listItems } = React.useMemo(() => {
const listItems = flattenTree(filteredItems, expandedItems, !!filterText.trim()); const listItems = flattenTree(filteredItems, expandedItems, !!filterText.trim());
@ -95,6 +165,8 @@ export const WatchModeView: React.FC<{}> = ({
return { selectedTreeItem, selectedTestItem }; return { selectedTreeItem, selectedTestItem };
}, [selectedTreeItemId, treeItemMap]); }, [selectedTreeItemId, treeItemMap]);
onTestItemSelected(selectedTestItem);
const runTreeItem = (treeItem: TreeItem) => { const runTreeItem = (treeItem: TreeItem) => {
expandedItems.set(treeItem.id, true); expandedItems.set(treeItem.id, true);
setSelectedTreeItemId(treeItem.id); setSelectedTreeItemId(treeItem.id);
@ -105,128 +177,79 @@ export const WatchModeView: React.FC<{}> = ({
runTests(collectTestIds(selectedTreeItem)); runTests(collectTestIds(selectedTreeItem));
}; };
const runTests = (testIds: string[]) => { return <div className='vbox'>
setProgress({ total: testIds.length, passed: 0, failed: 0 }); <Toolbar>
setIsRunningTest(true); <input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
sendMessage('run', { testIds }).then(() => { onChange={e => {
setIsRunningTest(false); setFilterText(e.target.value);
});
};
return <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
<TraceView testItem={selectedTestItem} isRunningTest={isRunningTest}></TraceView>
<div className='vbox watch-mode-sidebar'>
<Toolbar>
<h3 className='title'>Test explorer</h3>
<ToolbarButton icon='play' title='Run' onClick={() => runTests([...visibleTestIds])} disabled={isRunningTest}></ToolbarButton>
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
<ToolbarButton icon='refresh' title='Reload' onClick={resetCollectingRootSuite} disabled={isRunningTest}></ToolbarButton>
<div className='spacer'></div>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
</Toolbar>
<Toolbar>
<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
onChange={e => {
setFilterText(e.target.value);
}}
onKeyDown={e => {
if (e.key === 'Enter')
runTests([...visibleTestIds]);
}}></input>
</Toolbar>
<ListView
items={listItems}
itemKey={(treeItem: TreeItem) => treeItem.id }
itemRender={(treeItem: TreeItem) => {
return <div className='hbox watch-mode-list-item'>
<div className='watch-mode-list-item-title'>{treeItem.title}</div>
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={isRunningTest}></ToolbarButton>
</div>;
}} }}
itemIcon={(treeItem: TreeItem) => { onKeyDown={e => {
if (treeItem.kind === 'case' && treeItem.children?.length === 1) if (e.key === 'Enter')
treeItem = treeItem.children[0]; runVisibleTests();
if (treeItem.kind === 'test') { }}></input>
const ok = treeItem.test.outcome() === 'expected'; </Toolbar>
const failed = treeItem.test.results.length && treeItem.test.outcome() !== 'expected'; <ListView
const running = treeItem.test.results.some(r => r.duration === -1); items={listItems}
if (running) itemKey={(treeItem: TreeItem) => treeItem.id }
return 'codicon-loading'; itemRender={(treeItem: TreeItem) => {
if (ok) return <div className='hbox watch-mode-list-item'>
return 'codicon-check'; <div className='watch-mode-list-item-title'>{treeItem.title}</div>
if (failed) <ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={isRunningTest}></ToolbarButton>
return 'codicon-error'; </div>;
} else { }}
return treeItem.expanded ? 'codicon-chevron-down' : 'codicon-chevron-right'; itemIcon={(treeItem: TreeItem) => {
} if (treeItem.kind === 'case' && treeItem.children?.length === 1)
}} treeItem = treeItem.children[0];
itemIndent={(treeItem: TreeItem) => treeItem.kind === 'file' ? 0 : treeItem.kind === 'case' ? 1 : 2} if (treeItem.kind === 'test') {
selectedItem={selectedTreeItem} const ok = treeItem.test.outcome() === 'expected';
onAccepted={runTreeItem} const failed = treeItem.test.results.length && treeItem.test.outcome() !== 'expected';
onLeftArrow={(treeItem: TreeItem) => { const running = treeItem.test.results.some(r => r.duration === -1);
if (treeItem.children && treeItem.expanded) { if (running)
expandedItems.set(treeItem.id, false); return 'codicon-loading';
setExpandedItems(new Map(expandedItems)); if (ok)
} else { return 'codicon-check';
setSelectedTreeItemId(treeItem.parent?.id); if (failed)
} return 'codicon-error';
}} } else {
onRightArrow={(treeItem: TreeItem) => { return treeItem.expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
if (treeItem.children) { }
expandedItems.set(treeItem.id, true); }}
setExpandedItems(new Map(expandedItems)); itemIndent={(treeItem: TreeItem) => treeItem.kind === 'file' ? 0 : treeItem.kind === 'case' ? 1 : 2}
} selectedItem={selectedTreeItem}
setRootSuite({ ...rootSuite }); onAccepted={runTreeItem}
}} onLeftArrow={(treeItem: TreeItem) => {
onSelected={(treeItem: TreeItem) => { if (treeItem.children && treeItem.expanded) {
setSelectedTreeItemId(treeItem.id); expandedItems.set(treeItem.id, false);
}}
onIconClicked={(treeItem: TreeItem) => {
if (treeItem.kind === 'test')
return;
if (treeItem.expanded)
expandedItems.set(treeItem.id, false);
else
expandedItems.set(treeItem.id, true);
setExpandedItems(new Map(expandedItems)); setExpandedItems(new Map(expandedItems));
}} } else {
showNoItemsMessage={true}></ListView> setSelectedTreeItemId(treeItem.parent?.id);
{(rootSuite.value?.suites.length || 0) > 1 && <div style={{ flex: 'none', borderTop: '1px solid var(--vscode-panel-border)' }}> }
<Toolbar> }}
<h3 className='title'>Projects</h3> onRightArrow={(treeItem: TreeItem) => {
</Toolbar> if (treeItem.children) {
<ListView expandedItems.set(treeItem.id, true);
items={rootSuite.value!.suites} setExpandedItems(new Map(expandedItems));
onSelected={(suite: Suite) => { }
const copy = [...projectNames]; }}
if (copy.includes(suite.title)) onSelected={(treeItem: TreeItem) => {
copy.splice(copy.indexOf(suite.title), 1); setSelectedTreeItemId(treeItem.id);
else }}
copy.push(suite.title); onIconClicked={(treeItem: TreeItem) => {
setProjectNames(copy); if (treeItem.kind === 'test')
}} return;
itemRender={(suite: Suite) => { if (treeItem.expanded)
return <label style={{ display: 'flex', pointerEvents: 'none' }}> expandedItems.set(treeItem.id, false);
<input type='checkbox' checked={projectNames.includes(suite.title)} /> else
{suite.title} expandedItems.set(treeItem.id, true);
</label>; setExpandedItems(new Map(expandedItems));
}} }}
/> noItemsMessage='No tests' />;
</div>} </div>;
{isRunningTest && <div className='status-line'>
Running: {progress.total} tests | {progress.passed} passed | {progress.failed} failed
</div>}
{!isRunningTest && <div className='status-line'>
Total: {visibleTestIds.size} tests
</div>}
</div>
</SplitView>;
}; };
export const TraceView: React.FC<{ export const TraceView: React.FC<{
testItem: TestItem | undefined, testItem: TestItem | undefined,
isRunningTest: boolean, }> = ({ testItem }) => {
}> = ({ testItem, isRunningTest }) => {
const [model, setModel] = React.useState<MultiTraceModel | undefined>(); const [model, setModel] = React.useState<MultiTraceModel | undefined>();
const [stepsProgress, setStepsProgress] = React.useState(0); const [stepsProgress, setStepsProgress] = React.useState(0);
updateStepsProgress = () => setStepsProgress(stepsProgress + 1); updateStepsProgress = () => setStepsProgress(stepsProgress + 1);
@ -249,20 +272,9 @@ export const TraceView: React.FC<{
setModel(undefined); setModel(undefined);
} }
})(); })();
}, [testItem, isRunningTest, stepsProgress]); }, [testItem, stepsProgress]);
if (!model) { return <Workbench model={model}/>;
return <div className='vbox'>
<div className='drop-target'>
<div>Run test to see the trace</div>
<div style={{ paddingTop: 20 }}>
<div>Double click a test or hit Enter</div>
</div>
</div>
</div>;
}
return <Workbench model={model} />;
}; };
declare global { declare global {

View file

@ -31,25 +31,26 @@ import './workbench.css';
import { MetadataView } from './metadataView'; import { MetadataView } from './metadataView';
export const Workbench: React.FunctionComponent<{ export const Workbench: React.FunctionComponent<{
model: MultiTraceModel, model?: MultiTraceModel,
}> = ({ model }) => { }> = ({ model }) => {
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(); const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>(); const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions'); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs'); const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs');
const activeAction = highlightedAction || selectedAction; const activeAction = model ? highlightedAction || selectedAction : undefined;
const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 }; const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 };
const consoleCount = errors + warnings; const consoleCount = errors + warnings;
const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0; const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0;
const sdkLanguage = model?.sdkLanguage || 'javascript';
const tabs = [ const tabs = [
{ id: 'logs', title: 'Call', count: 0, render: () => <CallTab action={activeAction} sdkLanguage={model.sdkLanguage} /> }, { id: 'logs', title: 'Call', count: 0, render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} /> },
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={activeAction} /> }, { id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={activeAction} /> },
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={activeAction} /> }, { id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={activeAction} /> },
]; ];
if (model.hasSource) if (model?.hasSource)
tabs.push({ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={activeAction} /> }); tabs.push({ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={activeAction} /> });
return <div className='vbox'> return <div className='vbox'>
@ -60,27 +61,33 @@ export const Workbench: React.FunctionComponent<{
/> />
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}> <SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
<SplitView sidebarSize={300} orientation='vertical'> <SplitView sidebarSize={300} orientation='vertical'>
<SnapshotTab action={activeAction} sdkLanguage={model.sdkLanguage || 'javascript'} testIdAttributeName={model.testIdAttributeName || 'data-testid'} /> <SnapshotTab action={activeAction} sdkLanguage={sdkLanguage} testIdAttributeName={model?.testIdAttributeName || 'data-testid'} />
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/> <TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
</SplitView> </SplitView>
<TabbedPane tabs={ <TabbedPane tabs={
[ [
{ id: 'actions', title: 'Actions', count: 0, render: () => <ActionList {
sdkLanguage={model.sdkLanguage} id: 'actions',
actions={model.actions} title: 'Actions',
selectedAction={selectedAction} count: 0,
onSelected={action => { component: <ActionList
setSelectedAction(action); sdkLanguage={sdkLanguage}
}} actions={model?.actions || []}
onHighlighted={action => { selectedAction={model ? selectedAction : undefined}
setHighlightedAction(action); onSelected={action => {
}} setSelectedAction(action);
revealConsole={() => setSelectedPropertiesTab('console')} }}
/> }, onHighlighted={action => {
{ id: 'metadata', setHighlightedAction(action);
}}
revealConsole={() => setSelectedPropertiesTab('console')}
/>
},
{
id: 'metadata',
title: 'Metadata', title: 'Metadata',
count: 0, count: 0,
render: () => <MetadataView model={model} /> component: <MetadataView model={model}/>
}, },
] ]
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/> } selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>

View file

@ -101,7 +101,6 @@ svg {
color: var(--green); color: var(--green);
} }
.codicon-close,
.codicon-error { .codicon-error {
color: var(--red); color: var(--red);
} }

View file

@ -34,25 +34,24 @@
padding-left: 5px; padding-left: 5px;
} }
.list-view-entry.highlighted, .list-view-entry.highlighted:not(.selected) {
.list-view-entry.selected { background-color: var(--vscode-list-inactiveSelectionBackground) !important;
background-color: var(--vscode-list-inactiveSelectionBackground);
} }
.list-view-entry.selected { .list-view-entry.selected {
z-index: 10; z-index: 10;
} }
.list-view-entry.highlighted { .list-view-content:focus .list-view-entry.selected {
background-color: var(--vscode-list-inactiveSelectionBackground);
}
.list-view-content:focus .list-view-entry.selected:not(.error) {
background-color: var(--vscode-list-activeSelectionBackground); background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground); color: var(--vscode-list-activeSelectionForeground);
outline: 1px solid var(--vscode-focusBorder); outline: 1px solid var(--vscode-focusBorder);
} }
.list-view-content:focus .list-view-entry.selected * {
color: var(--vscode-list-activeSelectionForeground) !important;
}
.list-view-content:focus .list-view-entry.error.selected { .list-view-content:focus .list-view-entry.error.selected {
outline: 1px solid var(--vscode-inputValidation-errorBorder); outline: 1px solid var(--vscode-inputValidation-errorBorder);
} }

View file

@ -31,7 +31,7 @@ export type ListViewProps = {
onRightArrow?: (item: any) => void, onRightArrow?: (item: any) => void,
onHighlighted?: (item: any | undefined) => void, onHighlighted?: (item: any | undefined) => void,
onIconClicked?: (item: any) => void, onIconClicked?: (item: any) => void,
showNoItemsMessage?: boolean, noItemsMessage?: string,
dataTestId?: string, dataTestId?: string,
}; };
@ -49,7 +49,7 @@ export const ListView: React.FC<ListViewProps> = ({
onRightArrow, onRightArrow,
onHighlighted, onHighlighted,
onIconClicked, onIconClicked,
showNoItemsMessage, noItemsMessage,
dataTestId, dataTestId,
}) => { }) => {
const itemListRef = React.createRef<HTMLDivElement>(); const itemListRef = React.createRef<HTMLDivElement>();
@ -102,7 +102,7 @@ export const ListView: React.FC<ListViewProps> = ({
}} }}
ref={itemListRef} ref={itemListRef}
> >
{showNoItemsMessage && items.length === 0 && <div className='list-view-empty'>No items</div>} {noItemsMessage && items.length === 0 && <div className='list-view-empty'>{noItemsMessage}</div>}
{items.map((item, index) => <ListItemView {items.map((item, index) => <ListItemView
key={itemKey ? itemKey(item) : String(index)} key={itemKey ? itemKey(item) : String(index)}
hasIcons={!!itemIcon} hasIcons={!!itemIcon}

View file

@ -22,31 +22,37 @@ export interface TabbedPaneTabModel {
id: string; id: string;
title: string | JSX.Element; title: string | JSX.Element;
count?: number; count?: number;
render: () => React.ReactElement; component?: React.ReactElement;
render?: () => React.ReactElement;
} }
export const TabbedPane: React.FunctionComponent<{ export const TabbedPane: React.FunctionComponent<{
tabs: TabbedPaneTabModel[], tabs: TabbedPaneTabModel[],
leftToolbar?: React.ReactElement[],
rightToolbar?: React.ReactElement[],
selectedTab: string, selectedTab: string,
setSelectedTab: (tab: string) => void setSelectedTab: (tab: string) => void
}> = ({ tabs, selectedTab, setSelectedTab }) => { }> = ({ tabs, selectedTab, setSelectedTab, leftToolbar, rightToolbar }) => {
return <div className='tabbed-pane'> return <div className='tabbed-pane'>
<div className='vbox'> <div className='vbox'>
<Toolbar>{ <Toolbar>{[
tabs.map(tab => ( ...leftToolbar || [],
...tabs.map(tab => (
<TabbedPaneTab <TabbedPaneTab
id={tab.id} id={tab.id}
title={tab.title} title={tab.title}
count={tab.count} count={tab.count}
selected={selectedTab === tab.id} selected={selectedTab === tab.id}
onSelect={setSelectedTab} onSelect={setSelectedTab}
></TabbedPaneTab> ></TabbedPaneTab>)),
)) ...rightToolbar || [],
}</Toolbar> ]}</Toolbar>
{ {
tabs.map(tab => { tabs.map(tab => {
if (tab.component)
return <div key={tab.id} className='tab-content' style={{ display: selectedTab === tab.id ? 'inherit' : 'none' }}>{tab.component}</div>;
if (selectedTab === tab.id) if (selectedTab === tab.id)
return <div key={tab.id} className='tab-content'>{tab.render()}</div>; return <div key={tab.id} className='tab-content'>{tab.component || tab.render!()}</div>;
}) })
} }
</div> </div>

View file

@ -45,7 +45,7 @@ class TraceViewerPage {
constructor(public page: Page) { constructor(public page: Page) {
this.actionTitles = page.locator('.action-title'); this.actionTitles = page.locator('.action-title');
this.callLines = page.locator('.call-line'); this.callLines = page.locator('.call-tab .call-line');
this.consoleLines = page.locator('.console-line'); this.consoleLines = page.locator('.console-line');
this.consoleLineMessages = page.locator('.console-line-message'); this.consoleLineMessages = page.locator('.console-line-message');
this.consoleStacks = page.locator('.console-stack'); this.consoleStacks = page.locator('.console-stack');