diff --git a/packages/trace-viewer/src/ui/callTab.css b/packages/trace-viewer/src/ui/callTab.css index 3bca9f7141..56928088b5 100644 --- a/packages/trace-viewer/src/ui/callTab.css +++ b/packages/trace-viewer/src/ui/callTab.css @@ -55,16 +55,16 @@ overflow: hidden; line-height: 18px; white-space: nowrap; + max-height: 18px; } -.call-line .copy-icon { +.call-line:not(:hover) .toolbar-button.copy { display: none; - margin-left: 5px; } -.call-line:hover .copy-icon { - display: block; - cursor: pointer; +.call-line .toolbar-button.copy { + margin-left: 5px; + transform: scale(0.8); } .call-value { diff --git a/packages/trace-viewer/src/ui/copyToClipboard.tsx b/packages/trace-viewer/src/ui/copyToClipboard.tsx index 2c5d21aa0d..3e570ede56 100644 --- a/packages/trace-viewer/src/ui/copyToClipboard.tsx +++ b/packages/trace-viewer/src/ui/copyToClipboard.tsx @@ -15,23 +15,24 @@ */ import * as React from 'react'; +import { ToolbarButton } from '@web/components/toolbarButton'; export const CopyToClipboard: React.FunctionComponent<{ value: string, description?: string, }> = ({ value, description }) => { - const [iconClassName, setIconClassName] = React.useState('codicon-clippy'); + const [icon, setIcon] = React.useState('copy'); const handleCopy = React.useCallback(() => { navigator.clipboard.writeText(value).then(() => { - setIconClassName('codicon-check'); + setIcon('check'); setTimeout(() => { - setIconClassName('codicon-clippy'); + setIcon('copy'); }, 3000); }, () => { - setIconClassName('codicon-close'); + setIcon('close'); }); }, [value]); - return ; -}; \ No newline at end of file + return ; +}; diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index 309b210eab..313ec700c7 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -29,7 +29,8 @@ const eventsSymbol = Symbol('events'); export type SourceLocation = { file: string; line: number; - source: SourceModel; + column: number; + source?: SourceModel; }; export type SourceModel = { diff --git a/packages/trace-viewer/src/ui/snapshotTab.css b/packages/trace-viewer/src/ui/snapshotTab.css index 591c545f22..2677cfe53a 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.css +++ b/packages/trace-viewer/src/ui/snapshotTab.css @@ -28,6 +28,10 @@ background-color: var(--vscode-sideBar-background); } +.snapshot-tab .toolbar .pick-locator { + margin: 0 4px; +} + .snapshot-controls { flex: none; background-color: var(--vscode-sideBar-background); @@ -102,29 +106,6 @@ iframe.snapshot-visible[name=snapshot] { padding: 50px; } -.popout-icon { - position: absolute; - top: 0; - right: 0; - color: var(--vscode-sideBarTitle-foreground); - font-size: 14px; - z-index: 100; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; -} - -.popout-icon:not(.popout-disabled):hover { - color: var(--vscode-foreground); -} - -.popout-icon.popout-disabled { - opacity: var(--vscode-disabledForeground); -} - .snapshot-tab .cm-wrapper { line-height: 23px; margin-right: 4px; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 4bc2abc9eb..a5ced023fd 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -181,6 +181,7 @@ export const SnapshotTab: React.FunctionComponent<{ iframe={iframeRef1.current} iteration={loadingRef.current.iteration} /> + setIsInspecting(!isInspecting)} /> {['action', 'before', 'after'].map(tab => { return , rootDir?: string, fallbackLocation?: SourceLocation, -}> = ({ stack, sources, rootDir, fallbackLocation, stackFrameLocation }) => { + onOpenExternally?: (location: SourceLocation) => void, +}> = ({ stack, sources, rootDir, fallbackLocation, stackFrameLocation, onOpenExternally }) => { const [lastStack, setLastStack] = React.useState(); const [selectedFrame, setSelectedFrame] = React.useState(0); @@ -42,7 +45,7 @@ export const SourceTab: React.FunctionComponent<{ } }, [stack, lastStack, setLastStack, setSelectedFrame]); - const { source, highlight, targetLine, fileName } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[] }>(async () => { + const { source, highlight, targetLine, fileName, location } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[], location?: SourceLocation }>(async () => { const actionLocation = stack?.[selectedFrame]; const shouldUseFallback = !actionLocation?.file; if (shouldUseFallback && !fallbackLocation) @@ -56,6 +59,7 @@ export const SourceTab: React.FunctionComponent<{ sources.set(file, source); } + const location = shouldUseFallback ? fallbackLocation! : actionLocation; const targetLine = shouldUseFallback ? fallbackLocation?.line || source.errors[0]?.line || 0 : actionLocation.line; const fileName = rootDir && file.startsWith(rootDir) ? file.substring(rootDir.length + 1) : file; const highlight: SourceHighlight[] = source.errors.map(e => ({ type: 'error', line: e.line, message: e.message })); @@ -76,21 +80,29 @@ export const SourceTab: React.FunctionComponent<{ source.content = ``; } } - return { source, highlight, targetLine, fileName }; + return { source, highlight, targetLine, fileName, location }; }, [stack, selectedFrame, rootDir, fallbackLocation], { source: { errors: [], content: 'Loading\u2026' }, highlight: [] }); + const openExternally = React.useCallback(() => { + if (!location) + return; + if (onOpenExternally) { + onOpenExternally(location); + } else { + // This should open an external protocol handler instead of actually navigating away. + window.location.href = `vscode://file//${location.file}:${location.line}`; + } + }, [onOpenExternally, location]); + const showStackFrames = (stack?.length ?? 0) > 1; return
- {fileName && ( -
- {fileName} - - - -
- )} + { fileName && + {fileName} + + {location && } + }
diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index 70574341d9..239de7d4b4 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -47,8 +47,9 @@ export const TestListView: React.FC<{ isLoading?: boolean, onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void, requestedCollapseAllCount: number, - setFilterText: (text: string) => void; -}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, setFilterText }) => { + setFilterText: (text: string) => void, + onRevealSource: () => void, +}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, setFilterText, onRevealSource }) => { const [treeState, setTreeState] = React.useState({ expandedItems: new Map() }); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState(); const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount); @@ -91,17 +92,7 @@ export const TestListView: React.FC<{ if (!testModel) return { selectedTreeItem: undefined }; const selectedTreeItem = selectedTreeItemId ? testTree.treeItemById(selectedTreeItemId) : undefined; - let testFile: SourceLocation | undefined; - if (selectedTreeItem) { - testFile = { - file: selectedTreeItem.location.file, - line: selectedTreeItem.location.line, - source: { - errors: testModel.loadErrors.filter(e => e.location?.file === selectedTreeItem.location.file).map(e => ({ line: e.location!.line, message: e.message! })), - content: undefined, - } - }; - } + const testFile = itemLocation(selectedTreeItem, testModel); let selectedTest: reporterTypes.TestCase | undefined; if (selectedTreeItem?.kind === 'test') selectedTest = selectedTreeItem.test; @@ -164,7 +155,7 @@ export const TestListView: React.FC<{ {!!treeItem.duration && treeItem.status !== 'skipped' &&
{msToString(treeItem.duration)}
} runTreeItem(treeItem)} disabled={!!runningState}> - testServerConnection?.openNoReply({ location: treeItem.location })} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}> + {!watchAll && { if (watchedTreeIds.value.has(treeItem.id)) watchedTreeIds.value.delete(treeItem.id); @@ -187,3 +178,17 @@ export const TestListView: React.FC<{ autoExpandDepth={filterText ? 5 : 1} noItemsMessage={isLoading ? 'Loading\u2026' : 'No tests'} />; }; + +function itemLocation(item: TreeItem | undefined, model: TestModel | undefined): SourceLocation | undefined { + if (!item || !model) + return; + return { + file: item.location.file, + line: item.location.line, + column: item.location.column, + source: { + errors: model.loadErrors.filter(e => e.location?.file === item.location.file).map(e => ({ line: e.location!.line, message: e.message! })), + content: undefined, + } + }; +} diff --git a/packages/trace-viewer/src/ui/uiModeTraceView.tsx b/packages/trace-viewer/src/ui/uiModeTraceView.tsx index d26f0c3473..a652904683 100644 --- a/packages/trace-viewer/src/ui/uiModeTraceView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTraceView.tsx @@ -31,7 +31,9 @@ export const TraceView: React.FC<{ showRouteActionsSetting: Setting, item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase }, rootDir?: string, -}> = ({ showRouteActionsSetting, item, rootDir }) => { + onOpenExternally?: (location: SourceLocation) => void, + revealSource?: boolean, +}> = ({ showRouteActionsSetting, item, rootDir, onOpenExternally, revealSource }) => { const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>(); const [counter, setCounter] = React.useState(0); const pollTimer = React.useRef(null); @@ -97,7 +99,10 @@ export const TraceView: React.FC<{ onSelectionChanged={onSelectionChanged} fallbackLocation={item.testFile} isLive={model?.isLive} - status={item.treeItem?.status} />; + status={item.treeItem?.status} + onOpenExternally={onOpenExternally} + revealSource={revealSource} + />; }; const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefined => { diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 5bff9543aa..7c1ed69905 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -96,6 +96,8 @@ export const UIModeView: React.FC<{}> = ({ const [testServerConnection, setTestServerConnection] = React.useState(); const [settingsVisible, setSettingsVisible] = React.useState(false); const [testingOptionsVisible, setTestingOptionsVisible] = React.useState(false); + const [revealSource, setRevealSource] = React.useState(false); + const onRevealSource = React.useCallback(() => setRevealSource(true), [setRevealSource]); const [runWorkers, setRunWorkers] = React.useState(queryParams.workers); const singleWorkerSetting = React.useMemo(() => { @@ -435,7 +437,13 @@ export const UIModeView: React.FC<{}> = ({
- + testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })} + />
@@ -487,6 +495,7 @@ export const UIModeView: React.FC<{}> = ({ isLoading={isLoading} requestedCollapseAllCount={collapseAllCount} setFilterText={setFilterText} + onRevealSource={onRevealSource} /> setTestingOptionsVisible(!testingOptionsVisible)}> , openPage?: (url: string, target?: string) => Window | any, -}> = ({ showRouteActionsSetting, model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert, openPage }) => { + onOpenExternally?: (location: modelUtil.SourceLocation) => void, + revealSource?: boolean, +}> = ({ showRouteActionsSetting, model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert, openPage, onOpenExternally, revealSource }) => { const [selectedAction, setSelectedActionImpl] = React.useState(undefined); const [revealedStack, setRevealedStack] = React.useState(undefined); const [highlightedAction, setHighlightedAction] = React.useState(); @@ -63,7 +65,7 @@ export const Workbench: React.FunctionComponent<{ const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState(); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState('actions'); const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting('propertiesTab', showSourcesFirst ? 'source' : 'call'); - const [isInspecting, setIsInspecting] = React.useState(false); + const [isInspecting, setIsInspectingState] = React.useState(false); const [highlightedLocator, setHighlightedLocator] = React.useState(''); const activeAction = model ? highlightedAction || selectedAction : undefined; const [selectedTime, setSelectedTime] = React.useState(); @@ -87,6 +89,7 @@ export const Workbench: React.FunctionComponent<{ React.useEffect(() => { setSelectedTime(undefined); + setRevealedStack(undefined); }, [model]); React.useEffect(() => { @@ -118,14 +121,25 @@ export const Workbench: React.FunctionComponent<{ const selectPropertiesTab = React.useCallback((tab: string) => { setSelectedPropertiesTab(tab); if (tab !== 'inspector') - setIsInspecting(false); + setIsInspectingState(false); }, [setSelectedPropertiesTab]); + const setIsInspecting = React.useCallback((value: boolean) => { + if (!isInspecting && value) + selectPropertiesTab('inspector'); + setIsInspectingState(value); + }, [setIsInspectingState, selectPropertiesTab, isInspecting]); + const locatorPicked = React.useCallback((locator: string) => { setHighlightedLocator(locator); selectPropertiesTab('inspector'); }, [selectPropertiesTab]); + React.useEffect(() => { + if (revealSource) + selectPropertiesTab('source'); + }, [revealSource, selectPropertiesTab]); + const consoleModel = useConsoleTabModel(model, selectedTime); const networkModel = useNetworkTabModel(model, selectedTime); const errorsModel = useErrorsTabModel(model); @@ -174,7 +188,9 @@ export const Workbench: React.FunctionComponent<{ sources={sources} rootDir={rootDir} stackFrameLocation={sidebarLocation === 'bottom' ? 'right' : 'bottom'} - fallbackLocation={fallbackLocation} /> + fallbackLocation={fallbackLocation} + onOpenExternally={onOpenExternally} + /> }; const consoleTab: TabbedPaneTabModel = { id: 'console', @@ -302,13 +318,6 @@ export const Workbench: React.FunctionComponent<{ tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={selectPropertiesTab} - leftToolbar={[ - { - if (!isInspecting) - selectPropertiesTab('inspector'); - setIsInspecting(!isInspecting); - }} /> - ]} rightToolbar={[ sidebarLocation === 'bottom' ? { diff --git a/packages/web/src/components/toolbarButton.tsx b/packages/web/src/components/toolbarButton.tsx index d644106eef..e19afe4c3b 100644 --- a/packages/web/src/components/toolbarButton.tsx +++ b/packages/web/src/components/toolbarButton.tsx @@ -26,6 +26,7 @@ export interface ToolbarButtonProps { onClick: (e: React.MouseEvent) => void, style?: React.CSSProperties, testId?: string, + className?: string, } export const ToolbarButton: React.FC> = ({ @@ -37,8 +38,9 @@ export const ToolbarButton: React.FC onClick = () => {}, style, testId, + className, }) => { - let className = `toolbar-button ${icon}`; + className = (className || '') + ` toolbar-button ${icon}`; if (toggled) className += ' toggled'; return