split up highlight and reveal

This commit is contained in:
Simon Knott 2024-11-04 10:48:22 +01:00
parent c397634fc8
commit 26e7b1e77a
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
4 changed files with 32 additions and 21 deletions

View file

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
import type { ActionTraceEvent } from '@trace/trace'; import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace';
import { msToString } from '@web/uiUtils'; import { msToString } from '@web/uiUtils';
import * as React from 'react'; import * as React from 'react';
import './actionList.css'; import './actionList.css';
@ -36,7 +36,7 @@ export interface ActionListProps {
onSelected?: (action: ActionTraceEventInContext) => void, onSelected?: (action: ActionTraceEventInContext) => void,
onHighlighted?: (action: ActionTraceEventInContext | undefined) => void, onHighlighted?: (action: ActionTraceEventInContext | undefined) => void,
revealConsole?: () => void, revealConsole?: () => void,
revealAttachments(): void, revealAttachment(attachment: AfterActionTraceEventAttachment): void,
isLive?: boolean, isLive?: boolean,
} }
@ -51,7 +51,7 @@ export const ActionList: React.FC<ActionListProps> = ({
onSelected, onSelected,
onHighlighted, onHighlighted,
revealConsole, revealConsole,
revealAttachments, revealAttachment,
isLive, isLive,
}) => { }) => {
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() }); const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
@ -71,8 +71,8 @@ export const ActionList: React.FC<ActionListProps> = ({
}, [setSelectedTime]); }, [setSelectedTime]);
const render = React.useCallback((item: ActionTreeItem) => { const render = React.useCallback((item: ActionTreeItem) => {
return renderAction(item.action!, { sdkLanguage, revealConsole, revealAttachments, isLive, showDuration: true, showBadges: true }); return renderAction(item.action!, { sdkLanguage, revealConsole, revealAttachment, isLive, showDuration: true, showBadges: true });
}, [isLive, revealConsole, revealAttachments, sdkLanguage]); }, [isLive, revealConsole, revealAttachment, sdkLanguage]);
const isVisible = React.useCallback((item: ActionTreeItem) => { const isVisible = React.useCallback((item: ActionTreeItem) => {
return !selectedTime || !item.action || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum); return !selectedTime || !item.action || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum);
@ -109,15 +109,15 @@ export const renderAction = (
options: { options: {
sdkLanguage?: Language, sdkLanguage?: Language,
revealConsole?: () => void, revealConsole?: () => void,
revealAttachments?(): void, revealAttachment?(attachment: AfterActionTraceEventAttachment): void,
isLive?: boolean, isLive?: boolean,
showDuration?: boolean, showDuration?: boolean,
showBadges?: boolean, showBadges?: boolean,
}) => { }) => {
const { sdkLanguage, revealConsole, revealAttachments, isLive, showDuration, showBadges } = options; const { sdkLanguage, revealConsole, revealAttachment, isLive, showDuration, showBadges } = options;
const { errors, warnings } = modelUtil.stats(action); const { errors, warnings } = modelUtil.stats(action);
const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined; const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined;
const showAttachments = !!action.attachments?.length && !!revealAttachments; const showAttachments = !!action.attachments?.length && !!revealAttachment;
let time: string = ''; let time: string = '';
if (action.endTime) if (action.endTime)
@ -134,7 +134,7 @@ export const renderAction = (
{action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>} {action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>}
</div> </div>
{(showDuration || showBadges || showAttachments) && <div className='spacer'></div>} {(showDuration || showBadges || showAttachments) && <div className='spacer'></div>}
{showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={revealAttachments} />} {showAttachments && <ToolbarButton icon='attach' title='Open Attachment' onClick={() => revealAttachment(action.attachments![0])} />}
{showDuration && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>} {showDuration && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>}
{showBadges && <div className='action-icons' onClick={() => revealConsole?.()}> {showBadges && <div className='action-icons' onClick={() => revealConsole?.()}>
{!!errors && <div className='action-icon'><span className='codicon codicon-error'></span><span className='action-icon-value'>{errors}</span></div>} {!!errors && <div className='action-icon'><span className='codicon codicon-error'></span><span className='action-icon-value'>{errors}</span></div>}

View file

@ -30,10 +30,11 @@ type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
type ExpandableAttachmentProps = { type ExpandableAttachmentProps = {
attachment: Attachment; attachment: Attachment;
highlight?: boolean; reveal: boolean;
highlight: boolean;
}; };
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, highlight }) => { const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal, highlight }) => {
const [expanded, setExpanded] = React.useState(false); const [expanded, setExpanded] = React.useState(false);
const [attachmentText, setAttachmentText] = React.useState<string | null>(null); const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
const [placeholder, setPlaceholder] = React.useState<string | null>(null); const [placeholder, setPlaceholder] = React.useState<string | null>(null);
@ -43,9 +44,9 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
const hasContent = !!attachment.sha1 || !!attachment.path; const hasContent = !!attachment.sha1 || !!attachment.path;
React.useEffect(() => { React.useEffect(() => {
if (highlight) if (reveal)
ref.current?.scrollIntoView({ behavior: 'smooth' }); ref.current?.scrollIntoView({ behavior: 'smooth' });
}, [highlight]); }, [reveal]);
React.useEffect(() => { React.useEffect(() => {
if (expanded && attachmentText === null && placeholder === null) { if (expanded && attachmentText === null && placeholder === null) {
@ -92,7 +93,8 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
export const AttachmentsTab: React.FunctionComponent<{ export const AttachmentsTab: React.FunctionComponent<{
model: MultiTraceModel | undefined, model: MultiTraceModel | undefined,
selectedAction: ActionTraceEventInContext | undefined, selectedAction: ActionTraceEventInContext | undefined,
}> = ({ model, selectedAction }) => { revealedAttachment?: AfterActionTraceEventAttachment,
}> = ({ model, selectedAction, revealedAttachment }) => {
const { diffMap, screenshots, attachments } = React.useMemo(() => { const { diffMap, screenshots, attachments } = React.useMemo(() => {
const attachments = new Set<Attachment>(); const attachments = new Set<Attachment>();
const screenshots = new Set<Attachment>(); const screenshots = new Set<Attachment>();
@ -149,14 +151,18 @@ export const AttachmentsTab: React.FunctionComponent<{
{attachments.size ? <div className='attachments-section'>Attachments</div> : undefined} {attachments.size ? <div className='attachments-section'>Attachments</div> : undefined}
{[...attachments.values()].map((a, i) => { {[...attachments.values()].map((a, i) => {
return <div className='attachment-item' key={attachmentKey(a, i)}> return <div className='attachment-item' key={attachmentKey(a, i)}>
<ExpandableAttachment attachment={a} highlight={isActiveAttachment(a, selectedAction)} /> <ExpandableAttachment
attachment={a}
highlight={selectedAction?.attachments?.some(selected => isEqualAttachment(a, selected)) ?? false}
reveal={!!revealedAttachment && isEqualAttachment(a, revealedAttachment)}
/>
</div>; </div>;
})} })}
</div>; </div>;
}; };
function isActiveAttachment(attachment: Attachment, activeAction: ActionTraceEventInContext | undefined): boolean { function isEqualAttachment(a: Attachment, b: AfterActionTraceEventAttachment): boolean {
return activeAction?.attachments?.some(a => a.name === attachment.name && a.path === attachment.path && a.sha1 === attachment.sha1) ?? false; return a.name === b.name && a.path === b.path && a.sha1 === b.sha1;
} }
function attachmentURL(attachment: Attachment, queryParams: Record<string, string> = {}) { function attachmentURL(attachment: Attachment, queryParams: Record<string, string> = {}) {

View file

@ -41,6 +41,7 @@ import type { Entry } from '@trace/har';
import './workbench.css'; import './workbench.css';
import { testStatusIcon, testStatusText } from './testUtils'; import { testStatusIcon, testStatusText } from './testUtils';
import type { UITestStatus } from './testUtils'; import type { UITestStatus } from './testUtils';
import type { AfterActionTraceEventAttachment } from '@trace/trace';
export const Workbench: React.FunctionComponent<{ export const Workbench: React.FunctionComponent<{
model?: modelUtil.MultiTraceModel, model?: modelUtil.MultiTraceModel,
@ -58,6 +59,7 @@ export const Workbench: React.FunctionComponent<{
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, openPage, onOpenExternally, revealSource }) => { }> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, openPage, onOpenExternally, revealSource }) => {
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);
const [revealedAttachment, setRevealedAttachment] = React.useState<AfterActionTraceEventAttachment | undefined>(undefined);
const [highlightedCallId, setHighlightedCallId] = React.useState<string | undefined>(); const [highlightedCallId, setHighlightedCallId] = React.useState<string | undefined>();
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>(); const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>(); const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
@ -231,7 +233,7 @@ export const Workbench: React.FunctionComponent<{
id: 'attachments', id: 'attachments',
title: 'Attachments', title: 'Attachments',
count: attachments.length, count: attachments.length,
render: () => <AttachmentsTab model={model} selectedAction={selectedAction} /> render: () => <AttachmentsTab model={model} selectedAction={selectedAction} revealedAttachment={revealedAttachment} />
}; };
const tabs: TabbedPaneTabModel[] = [ const tabs: TabbedPaneTabModel[] = [
@ -296,7 +298,10 @@ export const Workbench: React.FunctionComponent<{
setSelectedTime={setSelectedTime} setSelectedTime={setSelectedTime}
onSelected={onActionSelected} onSelected={onActionSelected}
onHighlighted={setHighlightedAction} onHighlighted={setHighlightedAction}
revealAttachments={() => selectPropertiesTab('attachments')} revealAttachment={attachment => {
selectPropertiesTab('attachments');
setRevealedAttachment(attachment);
}}
revealConsole={() => selectPropertiesTab('console')} revealConsole={() => selectPropertiesTab('console')}
isLive={isLive} isLive={isLive}
/> />

View file

@ -167,9 +167,9 @@ test('should link from attachment step to attachments view', async ({ runUITest
const panel = page.getByRole('tabpanel', { name: 'Attachments' }); const panel = page.getByRole('tabpanel', { name: 'Attachments' });
const attachment = panel.getByLabel('my-attachment'); const attachment = panel.getByLabel('my-attachment');
await page.getByText('attach "spacer-1"').click(); await page.getByRole('treeitem', { name: 'attach "spacer-1"' }).getByLabel('Open Attachment').click();
await expect(attachment).not.toBeInViewport(); await expect(attachment).not.toBeInViewport();
await page.getByText('attach "my-attachment"').click(); await page.getByRole('treeitem', { name: 'attach "my-attachment"' }).getByLabel('Open Attachment').click();
await expect(attachment).toBeInViewport(); await expect(attachment).toBeInViewport();
}); });