feat(trace viewer): allow hiding route actions (#31726)

Adds a new settings tab above the actions list.

<img width="307" alt="settings tab"
src="https://github.com/user-attachments/assets/792212b7-e2fd-4a5c-8878-654e2e060505">

Toggling the "Show route actions" checkbox hides all route calls:
`continue`, `fulfill`, `fallback`, `abort` and `fetch`.

References #30970.
This commit is contained in:
Dmitry Gozman 2024-07-22 11:34:34 -07:00 committed by GitHub
parent e269092ef9
commit d87cb7a303
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 141 additions and 9 deletions

View 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.
*/
.settings-view {
flex: none;
}
.settings-view .setting label {
display: flex;
flex-direction: row;
align-items: center;
margin: 6px 2px;
}
.settings-view .setting input {
margin-right: 5px;
}

View File

@ -0,0 +1,36 @@
/**
* 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 { Setting } from '@web/uiUtils';
import './settingsView.css';
export const SettingsView: React.FunctionComponent<{
settings: Setting<boolean>[],
}> = ({ settings }) => {
return <div className='vbox settings-view'>
{settings.map(setting => {
return <div key={setting.name} className='setting'>
<label>
<input type='checkbox' checked={setting.value} onClick={() => {
setting.set(!setting.value);
}}/>
{setting.title}
</label>
</div>;
})}
</div>;
};

View File

@ -41,6 +41,7 @@ import type { Entry } from '@trace/har';
import './workbench.css';
import { testStatusIcon, testStatusText } from './testUtils';
import type { UITestStatus } from './testUtils';
import { SettingsView } from './settingsView';
export const Workbench: React.FunctionComponent<{
model?: MultiTraceModel,
@ -66,6 +67,11 @@ export const Workbench: React.FunctionComponent<{
const activeAction = model ? highlightedAction || selectedAction : undefined;
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
const [showRouteActions, , showRouteActionsSetting] = useSetting('show-route-actions', true, 'Show route actions');
const filteredActions = React.useMemo(() => {
return (model?.actions || []).filter(action => showRouteActions || action.class !== 'Route');
}, [model, showRouteActions]);
const setSelectedAction = React.useCallback((action: ActionTraceEventInContext | undefined) => {
setSelectedActionImpl(action);
@ -261,7 +267,7 @@ export const Workbench: React.FunctionComponent<{
</div>}
<ActionList
sdkLanguage={sdkLanguage}
actions={model?.actions || []}
actions={filteredActions}
selectedAction={model ? selectedAction : undefined}
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
@ -277,8 +283,15 @@ export const Workbench: React.FunctionComponent<{
title: 'Metadata',
component: <MetadataView model={model}/>
},
{
id: 'settings',
title: 'Settings',
component: <SettingsView settings={[showRouteActionsSetting]}/>,
}
]}
selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
selectedTab={selectedNavigatorTab}
setSelectedTab={setSelectedNavigatorTab}
/>
</SplitView>
<TabbedPane
tabs={tabs}

View File

@ -24,7 +24,8 @@ export interface ToolbarButtonProps {
disabled?: boolean,
toggled?: boolean,
onClick: (e: React.MouseEvent) => void,
style?: React.CSSProperties
style?: React.CSSProperties,
testId?: string,
}
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
@ -35,6 +36,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
toggled = false,
onClick = () => {},
style,
testId,
}) => {
let className = `toolbar-button ${icon}`;
if (toggled)
@ -47,6 +49,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
title={title}
disabled={!!disabled}
style={style}
data-testId={testId}
>
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
{children}

View File

@ -139,15 +139,29 @@ export function copy(text: string) {
textArea.remove();
}
export function useSetting<S>(name: string | undefined, defaultValue: S): [S, React.Dispatch<React.SetStateAction<S>>] {
const value = name ? settings.getObject(name, defaultValue) : defaultValue;
const [state, setState] = React.useState<S>(value);
const setStateWrapper = (value: React.SetStateAction<S>) => {
export type Setting<T> = {
value: T;
set: (value: T) => void;
name: string;
title: string;
};
export function useSetting<S>(name: string | undefined, defaultValue: S, title?: string): [S, React.Dispatch<React.SetStateAction<S>>, Setting<S>] {
if (name)
defaultValue = settings.getObject(name, defaultValue);
const [value, setValue] = React.useState<S>(defaultValue);
const setValueWrapper = React.useCallback((value: React.SetStateAction<S>) => {
if (name)
settings.setObject(name, value);
setState(value);
setValue(value);
}, [name, setValue]);
const setting = {
value,
set: setValueWrapper,
name: name || '',
title: title || name || '',
};
return [state, setStateWrapper];
return [value, setValueWrapper, setting];
}
export class Settings {

View File

@ -1332,3 +1332,39 @@ test('should show correct request start time', {
expect(parseMillis(duration)).toBeGreaterThan(1000);
expect(parseMillis(start)).toBeLessThan(1000);
});
test('should allow hiding route actions', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30970' },
}, async ({ page, runAndTrace, server }) => {
const traceViewer = await runAndTrace(async () => {
await page.route('**/*', async route => {
await route.fulfill({ contentType: 'text/html', body: 'Yo, page!' });
});
await page.goto(server.EMPTY_PAGE);
});
// Routes are visible by default.
await expect(traceViewer.actionTitles).toHaveText([
/page.route/,
/page.goto.*empty.html/,
/route.fulfill/,
]);
await traceViewer.page.getByText('Settings').click();
await expect(traceViewer.page.getByRole('checkbox', { name: 'Show route actions' })).toBeChecked();
await traceViewer.page.getByRole('checkbox', { name: 'Show route actions' }).uncheck();
await traceViewer.page.getByText('Actions', { exact: true }).click();
await expect(traceViewer.actionTitles).toHaveText([
/page.route/,
/page.goto.*empty.html/,
]);
await traceViewer.page.getByText('Settings').click();
await traceViewer.page.getByRole('checkbox', { name: 'Show route actions' }).check();
await traceViewer.page.getByText('Actions', { exact: true }).click();
await expect(traceViewer.actionTitles).toHaveText([
/page.route/,
/page.goto.*empty.html/,
/route.fulfill/,
]);
});