chore: add log/error tabs and counters (#26843)
This commit is contained in:
parent
ce3e0dcf84
commit
8c494e2519
|
|
@ -19,12 +19,14 @@ import './attachmentsTab.css';
|
||||||
import { ImageDiffView } from '@web/components/imageDiffView';
|
import { ImageDiffView } from '@web/components/imageDiffView';
|
||||||
import type { TestAttachment } from '@web/components/imageDiffView';
|
import type { TestAttachment } from '@web/components/imageDiffView';
|
||||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||||
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
|
|
||||||
export const AttachmentsTab: React.FunctionComponent<{
|
export const AttachmentsTab: React.FunctionComponent<{
|
||||||
model: MultiTraceModel | undefined,
|
model: MultiTraceModel | undefined,
|
||||||
}> = ({ model }) => {
|
}> = ({ model }) => {
|
||||||
if (!model)
|
const attachments = model?.actions.map(a => a.attachments || []).flat() || [];
|
||||||
return null;
|
if (!model || !attachments.length)
|
||||||
|
return <PlaceholderPanel text='No attachments' />;
|
||||||
return <div className='attachments-tab'>
|
return <div className='attachments-tab'>
|
||||||
{ model.actions.map((action, index) => <AttachmentsSection key={index} action={action} />) }
|
{ model.actions.map((action, index) => <AttachmentsSection key={index} action={action} />) }
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,14 @@ import './callTab.css';
|
||||||
import { CopyToClipboard } from './copyToClipboard';
|
import { CopyToClipboard } from './copyToClipboard';
|
||||||
import { asLocator } from '@isomorphic/locatorGenerators';
|
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||||
import type { Language } from '@isomorphic/locatorGenerators';
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
import { ErrorMessage } from '@web/components/errorMessage';
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
|
|
||||||
export const CallTab: React.FunctionComponent<{
|
export const CallTab: React.FunctionComponent<{
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
sdkLanguage: Language | undefined,
|
sdkLanguage: Language | undefined,
|
||||||
}> = ({ action, sdkLanguage }) => {
|
}> = ({ action, sdkLanguage }) => {
|
||||||
if (!action)
|
if (!action)
|
||||||
return null;
|
return <PlaceholderPanel text='No action selected' />;
|
||||||
const logs = action.log;
|
|
||||||
const error = action.error?.message;
|
|
||||||
const params = { ...action.params };
|
const params = { ...action.params };
|
||||||
// Strip down the waitForEventInfo data, we never need it.
|
// Strip down the waitForEventInfo data, we never need it.
|
||||||
delete params.info;
|
delete params.info;
|
||||||
|
|
@ -40,8 +38,6 @@ export const CallTab: React.FunctionComponent<{
|
||||||
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out';
|
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out';
|
||||||
|
|
||||||
return <div className='call-tab'>
|
return <div className='call-tab'>
|
||||||
{!!error && <ErrorMessage error={error} />}
|
|
||||||
{!!error && <div className='call-section'>Call</div>}
|
|
||||||
<div className='call-line'>{action.apiName}</div>
|
<div className='call-line'>{action.apiName}</div>
|
||||||
{<>
|
{<>
|
||||||
<div className='call-section'>Time</div>
|
<div className='call-section'>Time</div>
|
||||||
|
|
@ -58,14 +54,6 @@ export const CallTab: React.FunctionComponent<{
|
||||||
renderProperty(propertyToString(action, name, action.result[name], sdkLanguage), 'result-' + index)
|
renderProperty(propertyToString(action, name, action.result[name], sdkLanguage), 'result-' + index)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<div className='call-section'>Log</div>
|
|
||||||
{
|
|
||||||
logs.map((logLine, index) => {
|
|
||||||
return <div key={index} className='call-line'>
|
|
||||||
{logLine}
|
|
||||||
</div>;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,9 @@ import type { Boundaries } from '../geometry';
|
||||||
import { msToString } from '@web/uiUtils';
|
import { msToString } from '@web/uiUtils';
|
||||||
import { ansi2html } from '@web/ansi2html';
|
import { ansi2html } from '@web/ansi2html';
|
||||||
import type * as trace from '@trace/trace';
|
import type * as trace from '@trace/trace';
|
||||||
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
|
|
||||||
type ConsoleEntry = {
|
export type ConsoleEntry = {
|
||||||
browserMessage?: trace.ConsoleMessageTraceEvent['initializer'],
|
browserMessage?: trace.ConsoleMessageTraceEvent['initializer'],
|
||||||
browserError?: channels.SerializedError;
|
browserError?: channels.SerializedError;
|
||||||
nodeMessage?: {
|
nodeMessage?: {
|
||||||
|
|
@ -36,13 +37,14 @@ type ConsoleEntry = {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ConsoleTabModel = {
|
||||||
|
entries: ConsoleEntry[],
|
||||||
|
};
|
||||||
|
|
||||||
const ConsoleListView = ListView<ConsoleEntry>;
|
const ConsoleListView = ListView<ConsoleEntry>;
|
||||||
|
|
||||||
export const ConsoleTab: React.FunctionComponent<{
|
|
||||||
model: modelUtil.MultiTraceModel | undefined,
|
export function useConsoleTabModel(model: modelUtil.MultiTraceModel | undefined, selectedTime: Boundaries | undefined): ConsoleTabModel {
|
||||||
boundaries: Boundaries,
|
|
||||||
selectedTime: Boundaries | undefined,
|
|
||||||
}> = ({ model, boundaries, selectedTime }) => {
|
|
||||||
const { entries } = React.useMemo(() => {
|
const { entries } = React.useMemo(() => {
|
||||||
if (!model)
|
if (!model)
|
||||||
return { entries: [] };
|
return { entries: [] };
|
||||||
|
|
@ -89,9 +91,20 @@ export const ConsoleTab: React.FunctionComponent<{
|
||||||
return entries.filter(entry => entry.timestamp >= selectedTime.minimum && entry.timestamp <= selectedTime.maximum);
|
return entries.filter(entry => entry.timestamp >= selectedTime.minimum && entry.timestamp <= selectedTime.maximum);
|
||||||
}, [entries, selectedTime]);
|
}, [entries, selectedTime]);
|
||||||
|
|
||||||
|
return { entries: filteredEntries };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConsoleTab: React.FunctionComponent<{
|
||||||
|
boundaries: Boundaries,
|
||||||
|
consoleModel: ConsoleTabModel,
|
||||||
|
selectedTime: Boundaries | undefined,
|
||||||
|
}> = ({ consoleModel, boundaries }) => {
|
||||||
|
if (!consoleModel.entries.length)
|
||||||
|
return <PlaceholderPanel text='No console entries' />;
|
||||||
|
|
||||||
return <div className='console-tab'>
|
return <div className='console-tab'>
|
||||||
<ConsoleListView
|
<ConsoleListView
|
||||||
items={filteredEntries}
|
items={consoleModel.entries}
|
||||||
isError={entry => entry.isError}
|
isError={entry => entry.isError}
|
||||||
isWarning={entry => entry.isWarning}
|
isWarning={entry => entry.isWarning}
|
||||||
render={entry => {
|
render={entry => {
|
||||||
|
|
|
||||||
61
packages/trace-viewer/src/ui/errorsTab.tsx
Normal file
61
packages/trace-viewer/src/ui/errorsTab.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* 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 { ErrorMessage } from '@web/components/errorMessage';
|
||||||
|
import * as React from 'react';
|
||||||
|
import type * as modelUtil from './modelUtil';
|
||||||
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
|
import { renderAction } from './actionList';
|
||||||
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
|
import type { Boundaries } from '../geometry';
|
||||||
|
import { msToString } from '@web/uiUtils';
|
||||||
|
|
||||||
|
type ErrorsTabModel = {
|
||||||
|
errors: Map<string, modelUtil.ActionTraceEventInContext>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined): ErrorsTabModel {
|
||||||
|
return React.useMemo(() => {
|
||||||
|
const errors = new Map<string, modelUtil.ActionTraceEventInContext>();
|
||||||
|
for (const action of model?.actions || []) {
|
||||||
|
// Overwrite errors with the last one.
|
||||||
|
if (action.error?.message)
|
||||||
|
errors.set(action.error.message, action);
|
||||||
|
}
|
||||||
|
return { errors };
|
||||||
|
}, [model]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorsTab: React.FunctionComponent<{
|
||||||
|
errorsModel: ErrorsTabModel,
|
||||||
|
sdkLanguage: Language,
|
||||||
|
boundaries: Boundaries,
|
||||||
|
}> = ({ errorsModel, sdkLanguage, boundaries }) => {
|
||||||
|
if (!errorsModel.errors.size)
|
||||||
|
return <PlaceholderPanel text='No errors' />;
|
||||||
|
|
||||||
|
return <div className='fill' style={{ overflow: 'auto ' }}>
|
||||||
|
{[...errorsModel.errors.entries()].map(([message, action]) => {
|
||||||
|
return <div key={message}>
|
||||||
|
<div className='hbox' style={{ alignItems: 'center', padding: 5 }}>
|
||||||
|
<div style={{ color: 'var(--vscode-editorCodeLens-foreground)', marginRight: 5 }}>{msToString(action.startTime - boundaries.minimum)}</div>
|
||||||
|
{renderAction(action, sdkLanguage)}
|
||||||
|
</div>
|
||||||
|
<ErrorMessage error={message} />
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
34
packages/trace-viewer/src/ui/logTab.tsx
Normal file
34
packages/trace-viewer/src/ui/logTab.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* 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 type { ActionTraceEvent } from '@trace/trace';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ListView } from '@web/components/listView';
|
||||||
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
|
|
||||||
|
const LogList = ListView<string>;
|
||||||
|
|
||||||
|
export const LogTab: React.FunctionComponent<{
|
||||||
|
action: ActionTraceEvent | undefined,
|
||||||
|
}> = ({ action }) => {
|
||||||
|
if (!action?.log.length)
|
||||||
|
return <PlaceholderPanel text='No log entries' />;
|
||||||
|
return <LogList
|
||||||
|
dataTestId='log-list'
|
||||||
|
items={action?.log || []}
|
||||||
|
render={logLine => logLine}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
@ -18,25 +18,21 @@ import type { Entry } from '@trace/har';
|
||||||
import { ListView } from '@web/components/listView';
|
import { ListView } from '@web/components/listView';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { Boundaries } from '../geometry';
|
import type { Boundaries } from '../geometry';
|
||||||
import type * as modelUtil from './modelUtil';
|
|
||||||
import './networkTab.css';
|
import './networkTab.css';
|
||||||
import { NetworkResourceDetails } from './networkResourceDetails';
|
import { NetworkResourceDetails } from './networkResourceDetails';
|
||||||
import { bytesToString, msToString } from '@web/uiUtils';
|
import { bytesToString, msToString } from '@web/uiUtils';
|
||||||
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
|
import type { MultiTraceModel } from './modelUtil';
|
||||||
|
|
||||||
const NetworkListView = ListView<Entry>;
|
const NetworkListView = ListView<Entry>;
|
||||||
|
|
||||||
type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type';
|
type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type';
|
||||||
type Sorting = { by: SortBy, negate: boolean};
|
type Sorting = { by: SortBy, negate: boolean};
|
||||||
|
type NetworkTabModel = {
|
||||||
|
resources: Entry[],
|
||||||
|
};
|
||||||
|
|
||||||
export const NetworkTab: React.FunctionComponent<{
|
export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedTime: Boundaries | undefined): NetworkTabModel {
|
||||||
model: modelUtil.MultiTraceModel | undefined,
|
|
||||||
boundaries: Boundaries,
|
|
||||||
selectedTime: Boundaries | undefined,
|
|
||||||
onEntryHovered: (entry: Entry | undefined) => void,
|
|
||||||
}> = ({ model, boundaries, selectedTime, onEntryHovered }) => {
|
|
||||||
const [resource, setResource] = React.useState<Entry | undefined>();
|
|
||||||
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
|
|
||||||
|
|
||||||
const resources = React.useMemo(() => {
|
const resources = React.useMemo(() => {
|
||||||
const resources = model?.resources || [];
|
const resources = model?.resources || [];
|
||||||
const filtered = resources.filter(resource => {
|
const filtered = resources.filter(resource => {
|
||||||
|
|
@ -44,21 +40,37 @@ export const NetworkTab: React.FunctionComponent<{
|
||||||
return true;
|
return true;
|
||||||
return !!resource._monotonicTime && (resource._monotonicTime >= selectedTime.minimum && resource._monotonicTime <= selectedTime.maximum);
|
return !!resource._monotonicTime && (resource._monotonicTime >= selectedTime.minimum && resource._monotonicTime <= selectedTime.maximum);
|
||||||
});
|
});
|
||||||
if (sorting)
|
|
||||||
sort(filtered, sorting);
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}, [sorting, model, selectedTime]);
|
}, [model, selectedTime]);
|
||||||
|
return { resources };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NetworkTab: React.FunctionComponent<{
|
||||||
|
boundaries: Boundaries,
|
||||||
|
networkModel: NetworkTabModel,
|
||||||
|
onEntryHovered: (entry: Entry | undefined) => void,
|
||||||
|
}> = ({ boundaries, networkModel, onEntryHovered }) => {
|
||||||
|
const [resource, setResource] = React.useState<Entry | undefined>();
|
||||||
|
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
|
||||||
|
|
||||||
|
React.useMemo(() => {
|
||||||
|
if (sorting)
|
||||||
|
sort(networkModel.resources, sorting);
|
||||||
|
}, [networkModel.resources, sorting]);
|
||||||
|
|
||||||
const toggleSorting = React.useCallback((f: SortBy) => {
|
const toggleSorting = React.useCallback((f: SortBy) => {
|
||||||
setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false });
|
setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false });
|
||||||
}, [sorting]);
|
}, [sorting]);
|
||||||
|
|
||||||
|
if (!networkModel.resources.length)
|
||||||
|
return <PlaceholderPanel text='No network calls' />;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{!resource && <div className='vbox'>
|
{!resource && <div className='vbox'>
|
||||||
<NetworkHeader sorting={sorting} toggleSorting={toggleSorting} />
|
<NetworkHeader sorting={sorting} toggleSorting={toggleSorting} />
|
||||||
<NetworkListView
|
<NetworkListView
|
||||||
dataTestId='network-request-list'
|
dataTestId='network-request-list'
|
||||||
items={resources}
|
items={networkModel.resources}
|
||||||
render={entry => <NetworkResource boundaries={boundaries} resource={entry}></NetworkResource>}
|
render={entry => <NetworkResource boundaries={boundaries} resource={entry}></NetworkResource>}
|
||||||
onSelected={setResource}
|
onSelected={setResource}
|
||||||
onHighlighted={onEntryHovered}
|
onHighlighted={onEntryHovered}
|
||||||
|
|
|
||||||
30
packages/trace-viewer/src/ui/placeholderPanel.tsx
Normal file
30
packages/trace-viewer/src/ui/placeholderPanel.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
export const PlaceholderPanel: React.FunctionComponent<{
|
||||||
|
text: string,
|
||||||
|
}> = ({ text }) => {
|
||||||
|
return <div className='fill' style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
opacity: 0.5,
|
||||||
|
}}>{text}</div>;
|
||||||
|
};
|
||||||
|
|
@ -18,10 +18,12 @@ import { SplitView } from '@web/components/splitView';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ActionList } from './actionList';
|
import { ActionList } from './actionList';
|
||||||
import { CallTab } from './callTab';
|
import { CallTab } from './callTab';
|
||||||
import { ConsoleTab } from './consoleTab';
|
import { LogTab } from './logTab';
|
||||||
|
import { ErrorsTab, useErrorsTabModel } from './errorsTab';
|
||||||
|
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
||||||
import type * as modelUtil from './modelUtil';
|
import type * as modelUtil from './modelUtil';
|
||||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||||
import { NetworkTab } from './networkTab';
|
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
||||||
import { SnapshotTab } from './snapshotTab';
|
import { SnapshotTab } from './snapshotTab';
|
||||||
import { SourceTab } from './sourceTab';
|
import { SourceTab } from './sourceTab';
|
||||||
import { TabbedPane } from '@web/components/tabbedPane';
|
import { TabbedPane } from '@web/components/tabbedPane';
|
||||||
|
|
@ -49,7 +51,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||||
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>(showSourcesFirst ? 'source' : 'call');
|
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('propertiesTab', showSourcesFirst ? 'source' : 'call');
|
||||||
const [isInspecting, setIsInspecting] = React.useState(false);
|
const [isInspecting, setIsInspecting] = React.useState(false);
|
||||||
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
||||||
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
||||||
|
|
@ -83,13 +85,20 @@ export const Workbench: React.FunctionComponent<{
|
||||||
setSelectedPropertiesTab(tab);
|
setSelectedPropertiesTab(tab);
|
||||||
if (tab !== 'inspector')
|
if (tab !== 'inspector')
|
||||||
setIsInspecting(false);
|
setIsInspecting(false);
|
||||||
}, []);
|
}, [setSelectedPropertiesTab]);
|
||||||
|
|
||||||
const locatorPicked = React.useCallback((locator: string) => {
|
const locatorPicked = React.useCallback((locator: string) => {
|
||||||
setHighlightedLocator(locator);
|
setHighlightedLocator(locator);
|
||||||
selectPropertiesTab('inspector');
|
selectPropertiesTab('inspector');
|
||||||
}, [selectPropertiesTab]);
|
}, [selectPropertiesTab]);
|
||||||
|
|
||||||
|
const consoleModel = useConsoleTabModel(model, selectedTime);
|
||||||
|
const networkModel = useNetworkTabModel(model, selectedTime);
|
||||||
|
const errorsModel = useErrorsTabModel(model);
|
||||||
|
const attachments = React.useMemo(() => {
|
||||||
|
return model?.actions.map(a => a.attachments || []).flat() || [];
|
||||||
|
}, [model]);
|
||||||
|
|
||||||
const sdkLanguage = model?.sdkLanguage || 'javascript';
|
const sdkLanguage = model?.sdkLanguage || 'javascript';
|
||||||
|
|
||||||
const inspectorTab: TabbedPaneTabModel = {
|
const inspectorTab: TabbedPaneTabModel = {
|
||||||
|
|
@ -106,6 +115,17 @@ export const Workbench: React.FunctionComponent<{
|
||||||
title: 'Call',
|
title: 'Call',
|
||||||
render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} />
|
render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} />
|
||||||
};
|
};
|
||||||
|
const logTab: TabbedPaneTabModel = {
|
||||||
|
id: 'log',
|
||||||
|
title: 'Log',
|
||||||
|
render: () => <LogTab action={activeAction} />
|
||||||
|
};
|
||||||
|
const errorsTab: TabbedPaneTabModel = {
|
||||||
|
id: 'errors',
|
||||||
|
title: 'Errors',
|
||||||
|
errorCount: errorsModel.errors.size,
|
||||||
|
render: () => <ErrorsTab errorsModel={errorsModel} sdkLanguage={sdkLanguage} boundaries={boundaries} />
|
||||||
|
};
|
||||||
const sourceTab: TabbedPaneTabModel = {
|
const sourceTab: TabbedPaneTabModel = {
|
||||||
id: 'source',
|
id: 'source',
|
||||||
title: 'Source',
|
title: 'Source',
|
||||||
|
|
@ -119,34 +139,37 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const consoleTab: TabbedPaneTabModel = {
|
const consoleTab: TabbedPaneTabModel = {
|
||||||
id: 'console',
|
id: 'console',
|
||||||
title: 'Console',
|
title: 'Console',
|
||||||
render: () => <ConsoleTab model={model} boundaries={boundaries} selectedTime={selectedTime} />
|
count: consoleModel.entries.length,
|
||||||
|
render: () => <ConsoleTab consoleModel={consoleModel} boundaries={boundaries} selectedTime={selectedTime} />
|
||||||
};
|
};
|
||||||
const networkTab: TabbedPaneTabModel = {
|
const networkTab: TabbedPaneTabModel = {
|
||||||
id: 'network',
|
id: 'network',
|
||||||
title: 'Network',
|
title: 'Network',
|
||||||
render: () => <NetworkTab model={model} boundaries={boundaries} selectedTime={selectedTime} onEntryHovered={setHighlightedEntry}/>
|
count: networkModel.resources.length,
|
||||||
|
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} onEntryHovered={setHighlightedEntry}/>
|
||||||
};
|
};
|
||||||
const attachmentsTab: TabbedPaneTabModel = {
|
const attachmentsTab: TabbedPaneTabModel = {
|
||||||
id: 'attachments',
|
id: 'attachments',
|
||||||
title: 'Attachments',
|
title: 'Attachments',
|
||||||
|
count: attachments.length,
|
||||||
render: () => <AttachmentsTab model={model} />
|
render: () => <AttachmentsTab model={model} />
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: TabbedPaneTabModel[] = showSourcesFirst ? [
|
const tabs: TabbedPaneTabModel[] = [
|
||||||
inspectorTab,
|
|
||||||
sourceTab,
|
|
||||||
consoleTab,
|
|
||||||
networkTab,
|
|
||||||
callTab,
|
|
||||||
attachmentsTab,
|
|
||||||
] : [
|
|
||||||
inspectorTab,
|
inspectorTab,
|
||||||
callTab,
|
callTab,
|
||||||
|
logTab,
|
||||||
|
errorsTab,
|
||||||
consoleTab,
|
consoleTab,
|
||||||
networkTab,
|
networkTab,
|
||||||
sourceTab,
|
sourceTab,
|
||||||
attachmentsTab,
|
attachmentsTab,
|
||||||
];
|
];
|
||||||
|
if (showSourcesFirst) {
|
||||||
|
const sourceTabIndex = tabs.indexOf(sourceTab);
|
||||||
|
tabs.splice(sourceTabIndex, 1);
|
||||||
|
tabs.splice(1, 0, sourceTab);
|
||||||
|
}
|
||||||
|
|
||||||
const { boundaries } = React.useMemo(() => {
|
const { boundaries } = React.useMemo(() => {
|
||||||
const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 };
|
const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 };
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,14 @@ svg {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.hbox {
|
.hbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,5 @@
|
||||||
font-size: var(--vscode-editor-font-size);
|
font-size: var(--vscode-editor-font-size);
|
||||||
background-color: var(--vscode-inputValidation-errorBackground);
|
background-color: var(--vscode-inputValidation-errorBackground);
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
overflow: auto;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabbed-pane-tab {
|
.tabbed-pane-tab {
|
||||||
padding: 2px 10px 0 10px;
|
padding: 2px 6px 0 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: none;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
@ -54,3 +54,21 @@
|
||||||
.tabbed-pane-tab.selected {
|
.tabbed-pane-tab.selected {
|
||||||
background-color: var(--vscode-tab-activeBackground);
|
background-color: var(--vscode-tab-activeBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabbed-pane-tab-counter {
|
||||||
|
padding: 0 4px;
|
||||||
|
background: var(--vscode-menu-separatorBackground);
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 4px;
|
||||||
|
line-height: 16px;
|
||||||
|
min-width: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabbed-pane-tab-counter.error {
|
||||||
|
background: var(--vscode-list-errorForeground);
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import * as React from 'react';
|
||||||
|
|
||||||
export interface TabbedPaneTabModel {
|
export interface TabbedPaneTabModel {
|
||||||
id: string;
|
id: string;
|
||||||
title: string | JSX.Element;
|
title: string;
|
||||||
|
count?: number;
|
||||||
|
errorCount?: number;
|
||||||
component?: React.ReactElement;
|
component?: React.ReactElement;
|
||||||
render?: () => React.ReactElement;
|
render?: () => React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +46,8 @@ export const TabbedPane: React.FunctionComponent<{
|
||||||
<TabbedPaneTab
|
<TabbedPaneTab
|
||||||
id={tab.id}
|
id={tab.id}
|
||||||
title={tab.title}
|
title={tab.title}
|
||||||
|
count={tab.count}
|
||||||
|
errorCount={tab.errorCount}
|
||||||
selected={selectedTab === tab.id}
|
selected={selectedTab === tab.id}
|
||||||
onSelect={setSelectedTab}
|
onSelect={setSelectedTab}
|
||||||
></TabbedPaneTab>)),
|
></TabbedPaneTab>)),
|
||||||
|
|
@ -67,13 +71,18 @@ export const TabbedPane: React.FunctionComponent<{
|
||||||
|
|
||||||
export const TabbedPaneTab: React.FunctionComponent<{
|
export const TabbedPaneTab: React.FunctionComponent<{
|
||||||
id: string,
|
id: string,
|
||||||
title: string | JSX.Element,
|
title: string,
|
||||||
|
count?: number,
|
||||||
|
errorCount?: number,
|
||||||
selected?: boolean,
|
selected?: boolean,
|
||||||
onSelect: (id: string) => void
|
onSelect: (id: string) => void
|
||||||
}> = ({ id, title, selected, onSelect }) => {
|
}> = ({ id, title, count, errorCount, selected, onSelect }) => {
|
||||||
return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')}
|
return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')}
|
||||||
onClick={() => onSelect(id)}
|
onClick={() => onSelect(id)}
|
||||||
|
title={title}
|
||||||
key={id}>
|
key={id}>
|
||||||
<div className='tabbed-pane-tab-label'>{title}</div>
|
<div className='tabbed-pane-tab-label'>{title}</div>
|
||||||
|
{!!count && <div className='tabbed-pane-tab-counter'>{count}</div>}
|
||||||
|
{!!errorCount && <div className='tabbed-pane-tab-counter error'>{errorCount}</div>}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class TraceViewerPage {
|
||||||
actionTitles: Locator;
|
actionTitles: Locator;
|
||||||
callLines: Locator;
|
callLines: Locator;
|
||||||
consoleLines: Locator;
|
consoleLines: Locator;
|
||||||
|
logLines: Locator;
|
||||||
consoleLineMessages: Locator;
|
consoleLineMessages: Locator;
|
||||||
consoleStacks: Locator;
|
consoleStacks: Locator;
|
||||||
stackFrames: Locator;
|
stackFrames: Locator;
|
||||||
|
|
@ -47,6 +48,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-tab .call-line');
|
this.callLines = page.locator('.call-tab .call-line');
|
||||||
|
this.logLines = page.getByTestId('log-list').locator('.list-view-entry');
|
||||||
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');
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,8 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
test('should contain action info', async ({ showTraceViewer }) => {
|
test('should contain action info', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('locator.click');
|
await traceViewer.selectAction('locator.click');
|
||||||
const logLines = await traceViewer.callLines.allTextContents();
|
await traceViewer.page.getByText('Log', { exact: true }).click();
|
||||||
|
const logLines = await traceViewer.logLines.allTextContents();
|
||||||
expect(logLines.length).toBeGreaterThan(10);
|
expect(logLines.length).toBeGreaterThan(10);
|
||||||
expect(logLines).toContain('attempting click action');
|
expect(logLines).toContain('attempting click action');
|
||||||
expect(logLines).toContain(' click action done');
|
expect(logLines).toContain(' click action done');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue