2022-03-29 17:13:08 -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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import * as React from 'react';
|
|
|
|
import './colors.css';
|
|
|
|
import './common.css';
|
|
|
|
import './theme.css';
|
2025-01-29 16:22:50 +00:00
|
|
|
import './metadataView.css';
|
|
|
|
import type { Metadata } from '@playwright/test';
|
|
|
|
import type { GitCommitInfo } from '@testIsomorphic/types';
|
|
|
|
import { CopyToClipboardContainer } from './copyToClipboard';
|
|
|
|
import { linkifyText } from '@web/renderUtils';
|
2022-03-29 17:13:08 -08:00
|
|
|
|
2025-01-29 16:22:50 +00:00
|
|
|
type MetadataEntries = [string, unknown][];
|
|
|
|
|
2025-02-10 15:02:19 +01:00
|
|
|
export const MetadataContext = React.createContext<MetadataEntries>([]);
|
|
|
|
|
|
|
|
export function MetadataProvider({ metadata, children }: React.PropsWithChildren<{ metadata: Metadata }>) {
|
|
|
|
const entries = React.useMemo(() => {
|
|
|
|
// TODO: do not plumb actualWorkers through metadata.
|
|
|
|
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
|
|
|
|
}, [metadata]);
|
|
|
|
|
|
|
|
return <MetadataContext.Provider value={entries}>{children}</MetadataContext.Provider>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useMetadata() {
|
|
|
|
return React.useContext(MetadataContext);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useGitCommitInfo() {
|
|
|
|
const metadataEntries = useMetadata();
|
|
|
|
return metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
|
2025-01-29 16:22:50 +00:00
|
|
|
}
|
2022-05-02 16:28:14 -07:00
|
|
|
|
2022-06-06 21:05:47 -07:00
|
|
|
class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
|
2023-08-31 18:08:38 +02:00
|
|
|
override state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
|
2022-05-02 16:28:14 -07:00
|
|
|
error: null,
|
|
|
|
errorInfo: null,
|
|
|
|
};
|
|
|
|
|
2023-08-31 18:08:38 +02:00
|
|
|
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
2022-05-02 16:28:14 -07:00
|
|
|
this.setState({ error, errorInfo });
|
|
|
|
}
|
|
|
|
|
2023-08-31 18:08:38 +02:00
|
|
|
override render() {
|
2022-05-02 16:28:14 -07:00
|
|
|
if (this.state.error || this.state.errorInfo) {
|
|
|
|
return (
|
2025-01-29 16:22:50 +00:00
|
|
|
<div className='metadata-view p-3'>
|
|
|
|
<p>An error was encountered when trying to render metadata.</p>
|
2022-05-02 16:28:14 -07:00
|
|
|
<p>
|
|
|
|
<pre style={{ overflow: 'scroll' }}>{this.state.error?.message}<br/>{this.state.error?.stack}<br/>{this.state.errorInfo?.componentStack}</pre>
|
|
|
|
</p>
|
2025-01-29 16:22:50 +00:00
|
|
|
</div>
|
2022-05-02 16:28:14 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.props.children;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-10 15:02:19 +01:00
|
|
|
export const MetadataView = () => {
|
|
|
|
return <ErrorBoundary><InnerMetadataView/></ErrorBoundary>;
|
2025-01-29 16:22:50 +00:00
|
|
|
};
|
2022-05-02 16:28:14 -07:00
|
|
|
|
2025-02-10 15:02:19 +01:00
|
|
|
const InnerMetadataView = () => {
|
|
|
|
const metadataEntries = useMetadata();
|
|
|
|
const gitCommitInfo = useGitCommitInfo();
|
2025-01-29 16:22:50 +00:00
|
|
|
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
|
|
|
|
if (!gitCommitInfo && !entries.length)
|
2022-05-02 16:28:14 -07:00
|
|
|
return null;
|
2025-01-29 16:22:50 +00:00
|
|
|
return <div className='metadata-view'>
|
|
|
|
{gitCommitInfo && <>
|
|
|
|
<GitCommitInfoView info={gitCommitInfo}/>
|
|
|
|
{entries.length > 0 && <div className='metadata-separator' />}
|
|
|
|
</>}
|
2025-02-25 09:21:17 -08:00
|
|
|
<div className='metadata-section metadata-properties' role='list'>
|
2025-02-11 05:16:46 -08:00
|
|
|
{entries.map(([propertyName, value]) => {
|
|
|
|
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
|
|
|
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
|
|
|
return (
|
2025-02-25 09:21:17 -08:00
|
|
|
<div key={propertyName} className='copyable-property' role='listitem'>
|
2025-02-11 05:16:46 -08:00
|
|
|
<CopyToClipboardContainer value={valueString}>
|
|
|
|
<span style={{ fontWeight: 'bold' }} title={propertyName}>{propertyName}</span>
|
|
|
|
: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span>
|
|
|
|
</CopyToClipboardContainer>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
2025-01-29 16:22:50 +00:00
|
|
|
</div>;
|
2022-03-29 17:13:08 -08:00
|
|
|
};
|
|
|
|
|
2025-01-29 16:22:50 +00:00
|
|
|
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
2025-02-25 09:21:17 -08:00
|
|
|
const email = info.revision?.email ? ` <${info.revision?.email}>` : '';
|
|
|
|
const author = `${info.revision?.author || ''}${email}`;
|
2025-02-14 09:32:06 +00:00
|
|
|
|
2025-02-25 09:21:17 -08:00
|
|
|
let subject = info.revision?.subject || '';
|
|
|
|
let link = info.revision?.link;
|
2025-02-14 09:32:06 +00:00
|
|
|
|
2025-02-25 09:21:17 -08:00
|
|
|
if (info.pull_request?.link && info.pull_request?.title) {
|
|
|
|
subject = info.pull_request?.title;
|
|
|
|
link = info.pull_request?.link;
|
2025-02-14 09:32:06 +00:00
|
|
|
}
|
|
|
|
|
2025-02-25 09:21:17 -08:00
|
|
|
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info.revision?.timestamp);
|
|
|
|
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info.revision?.timestamp);
|
|
|
|
return <div className='metadata-section' role='list'>
|
|
|
|
<div role='listitem'>
|
|
|
|
{link ? (
|
|
|
|
<a href={link} target='_blank' rel='noopener noreferrer' title={subject}>
|
2025-02-11 05:16:46 -08:00
|
|
|
{subject}
|
2025-02-25 09:21:17 -08:00
|
|
|
</a>
|
|
|
|
) : <span title={subject}>
|
|
|
|
{subject}
|
|
|
|
</span>}
|
|
|
|
</div>
|
|
|
|
<div role='listitem' className='hbox'>
|
|
|
|
<span className='mr-1'>{author}</span>
|
|
|
|
<span title={longTimestamp}> on {shortTimestamp}</span>
|
|
|
|
{info.ci?.link && (
|
|
|
|
<>
|
|
|
|
<span className='mx-2'>·</span>
|
|
|
|
<a href={info.ci?.link} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>Logs</a>
|
|
|
|
</>
|
|
|
|
)}
|
2022-03-29 17:13:08 -08:00
|
|
|
</div>
|
2025-01-29 16:22:50 +00:00
|
|
|
</div>;
|
2022-03-29 17:13:08 -08:00
|
|
|
};
|