2021-01-07 16:15:34 -08:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2022-09-20 18:41:51 -07:00
|
|
|
import type { ActionTraceEvent } from '@trace/trace';
|
2022-03-25 13:12:00 -08:00
|
|
|
import { msToString } from '@web/uiUtils';
|
2021-01-07 16:15:34 -08:00
|
|
|
import * as React from 'react';
|
2022-03-25 13:12:00 -08:00
|
|
|
import './actionList.css';
|
2021-07-01 20:46:56 -07:00
|
|
|
import * as modelUtil from './modelUtil';
|
2022-03-25 13:12:00 -08:00
|
|
|
import './tabbedPane.css';
|
2022-10-18 22:23:40 -04:00
|
|
|
import { asLocator } from '@isomorphic/locatorGenerators';
|
|
|
|
import type { Language } from '@isomorphic/locatorGenerators';
|
2021-01-07 16:15:34 -08:00
|
|
|
|
2021-01-21 08:29:01 -08:00
|
|
|
export interface ActionListProps {
|
2021-05-13 20:41:32 -07:00
|
|
|
actions: ActionTraceEvent[],
|
|
|
|
selectedAction: ActionTraceEvent | undefined,
|
|
|
|
highlightedAction: ActionTraceEvent | undefined,
|
2022-10-18 22:23:40 -04:00
|
|
|
sdkLanguage: Language | undefined;
|
2021-05-13 20:41:32 -07:00
|
|
|
onSelected: (action: ActionTraceEvent) => void,
|
|
|
|
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
2021-07-01 20:46:56 -07:00
|
|
|
setSelectedTab: (tab: string) => void,
|
2021-01-21 08:29:01 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
export const ActionList: React.FC<ActionListProps> = ({
|
|
|
|
actions = [],
|
2022-10-18 22:23:40 -04:00
|
|
|
selectedAction,
|
|
|
|
highlightedAction,
|
|
|
|
sdkLanguage,
|
2021-01-21 08:29:01 -08:00
|
|
|
onSelected = () => {},
|
|
|
|
onHighlighted = () => {},
|
2021-07-01 20:46:56 -07:00
|
|
|
setSelectedTab = () => {},
|
2021-01-21 08:29:01 -08:00
|
|
|
}) => {
|
2021-04-19 19:50:11 -07:00
|
|
|
const actionListRef = React.createRef<HTMLDivElement>();
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
actionListRef.current?.focus();
|
2021-07-28 18:21:55 +02:00
|
|
|
}, [selectedAction, actionListRef]);
|
2021-04-19 19:50:11 -07:00
|
|
|
|
|
|
|
return <div className='action-list vbox'>
|
|
|
|
<div
|
|
|
|
className='action-list-content'
|
|
|
|
tabIndex={0}
|
|
|
|
onKeyDown={event => {
|
|
|
|
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
|
|
|
return;
|
2021-12-17 12:20:01 -08:00
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
2021-04-19 19:50:11 -07:00
|
|
|
const index = selectedAction ? actions.indexOf(selectedAction) : -1;
|
2021-12-17 12:20:01 -08:00
|
|
|
let newIndex = index;
|
2021-04-19 19:50:11 -07:00
|
|
|
if (event.key === 'ArrowDown') {
|
|
|
|
if (index === -1)
|
2021-12-17 12:20:01 -08:00
|
|
|
newIndex = 0;
|
2021-04-19 19:50:11 -07:00
|
|
|
else
|
2021-12-17 12:20:01 -08:00
|
|
|
newIndex = Math.min(index + 1, actions.length - 1);
|
2021-04-19 19:50:11 -07:00
|
|
|
}
|
|
|
|
if (event.key === 'ArrowUp') {
|
|
|
|
if (index === -1)
|
2021-12-17 12:20:01 -08:00
|
|
|
newIndex = actions.length - 1;
|
2021-04-19 19:50:11 -07:00
|
|
|
else
|
2021-12-17 12:20:01 -08:00
|
|
|
newIndex = Math.max(index - 1, 0);
|
2021-04-19 19:50:11 -07:00
|
|
|
}
|
2021-12-17 12:20:01 -08:00
|
|
|
const element = actionListRef.current?.children.item(newIndex);
|
2023-01-10 18:33:20 +01:00
|
|
|
scrollIntoViewIfNeeded(element);
|
2021-12-17 12:20:01 -08:00
|
|
|
onSelected(actions[newIndex]);
|
2021-04-19 19:50:11 -07:00
|
|
|
}}
|
|
|
|
ref={actionListRef}
|
2021-01-21 08:29:01 -08:00
|
|
|
>
|
2021-10-12 20:21:06 -08:00
|
|
|
{actions.length === 0 && <div className='no-actions-entry'>No actions recorded</div>}
|
2023-01-10 18:33:20 +01:00
|
|
|
{actions.map(action => <ActionListItem
|
|
|
|
action={action}
|
|
|
|
highlightedAction={highlightedAction}
|
|
|
|
onSelected={onSelected}
|
|
|
|
onHighlighted={onHighlighted}
|
|
|
|
selectedAction={selectedAction}
|
|
|
|
sdkLanguage={sdkLanguage}
|
|
|
|
setSelectedTab={setSelectedTab}
|
|
|
|
/>)}
|
2021-04-19 19:50:11 -07:00
|
|
|
</div>
|
|
|
|
</div>;
|
2021-01-07 16:15:34 -08:00
|
|
|
};
|
2023-01-10 18:33:20 +01:00
|
|
|
|
|
|
|
const ActionListItem: React.FC<{
|
|
|
|
action: ActionTraceEvent,
|
|
|
|
highlightedAction: ActionTraceEvent | undefined,
|
|
|
|
onSelected: (action: ActionTraceEvent) => void,
|
|
|
|
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
|
|
|
selectedAction: ActionTraceEvent | undefined,
|
|
|
|
sdkLanguage: Language | undefined,
|
|
|
|
setSelectedTab: (tab: string) => void,
|
|
|
|
}> = ({ action, onSelected, onHighlighted, highlightedAction, selectedAction, sdkLanguage, setSelectedTab }) => {
|
|
|
|
const { metadata } = action;
|
|
|
|
const selectedSuffix = action === selectedAction ? ' selected' : '';
|
|
|
|
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
|
|
|
|
const error = metadata.error?.error?.message;
|
|
|
|
const { errors, warnings } = modelUtil.stats(action);
|
|
|
|
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
|
|
|
|
|
|
|
|
const divRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (divRef.current && selectedAction === action)
|
|
|
|
scrollIntoViewIfNeeded(divRef.current);
|
|
|
|
}, [selectedAction, action]);
|
|
|
|
|
|
|
|
return <div
|
|
|
|
className={'action-entry' + selectedSuffix + highlightedSuffix}
|
|
|
|
key={metadata.id}
|
|
|
|
onClick={() => onSelected(action)}
|
|
|
|
onMouseEnter={() => onHighlighted(action)}
|
|
|
|
onMouseLeave={() => (highlightedAction === action) && onHighlighted(undefined)}
|
|
|
|
ref={divRef}
|
|
|
|
>
|
|
|
|
<div className='action-title'>
|
|
|
|
<span>{metadata.apiName}</span>
|
|
|
|
{locator && <div className='action-selector' title={locator}>{locator}</div>}
|
|
|
|
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
|
|
|
</div>
|
|
|
|
<div className='action-duration' style={{ flex: 'none' }}>{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</div>
|
|
|
|
<div className='action-icons' onClick={() => setSelectedTab('console')}>
|
|
|
|
{!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>}
|
|
|
|
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}
|
|
|
|
</div>
|
|
|
|
{error && <div className='codicon codicon-issues' title={error} />}
|
|
|
|
</div>;
|
|
|
|
};
|
|
|
|
|
|
|
|
function scrollIntoViewIfNeeded(element?: Element | null) {
|
|
|
|
if (!element)
|
|
|
|
return;
|
|
|
|
if ((element as any)?.scrollIntoViewIfNeeded)
|
|
|
|
(element as any).scrollIntoViewIfNeeded(false);
|
|
|
|
else
|
|
|
|
element?.scrollIntoView();
|
|
|
|
}
|