base 64 encode/decode urls with s3

This commit is contained in:
Edward Jibson 2025-01-02 15:58:35 +00:00
parent ee8208beda
commit 7f25d1ac1a
8 changed files with 224 additions and 104 deletions

View file

@ -23,7 +23,7 @@
navigator.serviceWorker.register('sw.bundle.js'); navigator.serviceWorker.register('sw.bundle.js');
if (!navigator.serviceWorker.controller) if (!navigator.serviceWorker.controller)
await new Promise(f => navigator.serviceWorker.oncontrollerchange = f); await new Promise(f => navigator.serviceWorker.oncontrollerchange = f);
const traceUrl = new URL(location.href).searchParams.get('trace'); let traceUrl = new URL(location.href).searchParams.get('trace');
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('trace', traceUrl); params.set('trace', traceUrl);
await fetch('contexts?' + params.toString()).then(r => r.json()); await fetch('contexts?' + params.toString()).then(r => r.json());

View file

@ -18,7 +18,11 @@ import { splitProgress } from './progress';
import { unwrapPopoutUrl } from './snapshotRenderer'; import { unwrapPopoutUrl } from './snapshotRenderer';
import { SnapshotServer } from './snapshotServer'; import { SnapshotServer } from './snapshotServer';
import { TraceModel } from './traceModel'; import { TraceModel } from './traceModel';
import { FetchTraceModelBackend, TraceViewerServer, ZipTraceModelBackend } from './traceModelBackends'; import {
FetchTraceModelBackend,
TraceViewerServer,
ZipTraceModelBackend,
} from './traceModelBackends';
import { TraceVersionError } from './traceModernizer'; import { TraceVersionError } from './traceModernizer';
// @ts-ignore // @ts-ignore
@ -34,20 +38,47 @@ self.addEventListener('activate', function(event: any) {
const scopePath = new URL(self.registration.scope).pathname; const scopePath = new URL(self.registration.scope).pathname;
const loadedTraces = new Map<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>(); const loadedTraces = new Map<
string,
{ traceModel: TraceModel; snapshotServer: SnapshotServer }
>();
const clientIdToTraceUrls = new Map<string, { limit: number | undefined, traceUrls: Set<string>, traceViewerServer: TraceViewerServer }>(); const clientIdToTraceUrls = new Map<
string,
{
limit: number | undefined;
traceUrls: Set<string>;
traceViewerServer: TraceViewerServer;
}
>();
async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise<TraceModel> { async function loadTrace(
traceUrl: string,
traceFileName: string | null,
client: any | undefined,
limit: number | undefined,
progress: (done: number, total: number) => undefined
): Promise<TraceModel> {
await gc(); await gc();
const clientId = client?.id ?? ''; const clientId = client?.id ?? '';
let data = clientIdToTraceUrls.get(clientId); let data = clientIdToTraceUrls.get(clientId);
if (!data) { if (!data) {
let traceViewerServerBaseUrl = new URL('../', client?.url ?? self.registration.scope); let traceViewerServerBaseUrl = new URL(
if (traceViewerServerBaseUrl.searchParams.has('server')) '../',
traceViewerServerBaseUrl = new URL(traceViewerServerBaseUrl.searchParams.get('server')!, traceViewerServerBaseUrl); client?.url ?? self.registration.scope
);
if (traceViewerServerBaseUrl.searchParams.has('server')) {
traceViewerServerBaseUrl = new URL(
traceViewerServerBaseUrl.searchParams.get('server')!,
traceViewerServerBaseUrl
);
}
data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) }; data = {
limit,
traceUrls: new Set(),
traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl),
};
clientIdToTraceUrls.set(clientId, data); clientIdToTraceUrls.set(clientId, data);
} }
data.traceUrls.add(traceUrl); data.traceUrls.add(traceUrl);
@ -55,21 +86,49 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client:
const traceModel = new TraceModel(); const traceModel = new TraceModel();
try { try {
// Allow 10% to hop from sw to page. // Allow 10% to hop from sw to page.
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]); const [fetchProgress, unzipProgress] = splitProgress(
const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl, data.traceViewerServer) : new ZipTraceModelBackend(traceUrl, data.traceViewerServer, fetchProgress); progress,
[0.5, 0.4, 0.1]
);
const backend = traceUrl.endsWith('json')
? new FetchTraceModelBackend(traceUrl, data.traceViewerServer)
: new ZipTraceModelBackend(
traceUrl,
data.traceViewerServer,
fetchProgress
);
await traceModel.load(backend, unzipProgress); await traceModel.load(backend, unzipProgress);
} catch (error: any) { } catch (error: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);
if (error?.message?.includes('Cannot find .trace file') && await traceModel.hasEntry('index.html')) if (
throw new Error('Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.'); error?.message?.includes('Cannot find .trace file') &&
if (error instanceof TraceVersionError) (await traceModel.hasEntry('index.html'))
throw new Error(`Could not load trace from ${traceFileName || traceUrl}. ${error.message}`); ) {
if (traceFileName) throw new Error(
throw new Error(`Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.`); 'Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.'
throw new Error(`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`); );
}
if (error instanceof TraceVersionError) {
throw new Error(
`Could not load trace from ${traceFileName || traceUrl}. ${
error.message
}`
);
}
if (traceFileName) {
throw new Error(
`Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.`
);
}
throw new Error(
`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`
);
} }
const snapshotServer = new SnapshotServer(traceModel.storage(), sha1 => traceModel.resourceForSha1(sha1)); const snapshotServer = new SnapshotServer(traceModel.storage(), sha1 =>
traceModel.resourceForSha1(sha1)
);
loadedTraces.set(traceUrl, { traceModel, snapshotServer }); loadedTraces.set(traceUrl, { traceModel, snapshotServer });
return traceModel; return traceModel;
} }
@ -98,28 +157,43 @@ async function doFetch(event: FetchEvent): Promise<Response> {
return new Response(null, { status: 200 }); return new Response(null, { status: 200 });
} }
const traceUrl = url.searchParams.get('trace'); let traceUrl = '';
try {
traceUrl = atob(url.searchParams.get('trace') ?? '');
} catch (error) {
traceUrl = url.searchParams.get('trace') ?? '';
}
if (relativePath === '/contexts') { if (relativePath === '/contexts') {
try { try {
const limit = url.searchParams.has('limit') ? +url.searchParams.get('limit')! : undefined; const limit = url.searchParams.has('limit')
const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), client, limit, (done: number, total: number) => { ? +url.searchParams.get('limit')!
client.postMessage({ method: 'progress', params: { done, total } }); : undefined;
}); const traceModel = await loadTrace(
traceUrl!,
url.searchParams.get('traceFileName'),
client,
limit,
(done: number, total: number) => {
client.postMessage({ method: 'progress', params: { done, total } });
}
);
return new Response(JSON.stringify(traceModel!.contextEntries), { return new Response(JSON.stringify(traceModel!.contextEntries), {
status: 200, status: 200,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' },
}); });
} catch (error: any) { } catch (error: any) {
return new Response(JSON.stringify({ error: error?.message }), { return new Response(JSON.stringify({ error: error?.message }), {
status: 500, status: 500,
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' },
}); });
} }
} }
if (relativePath.startsWith('/snapshotInfo/')) { if (relativePath.startsWith('/snapshotInfo/')) {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer) if (!snapshotServer)
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
return snapshotServer.serveSnapshotInfo(relativePath, url.searchParams); return snapshotServer.serveSnapshotInfo(relativePath, url.searchParams);
@ -129,9 +203,17 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer) if (!snapshotServer)
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
const response = snapshotServer.serveSnapshot(relativePath, url.searchParams, url.href); const response = snapshotServer.serveSnapshot(
if (isDeployedAsHttps) relativePath,
response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests'); url.searchParams,
url.href
);
if (isDeployedAsHttps) {
response.headers.set(
'Content-Security-Policy',
'upgrade-insecure-requests'
);
}
return response; return response;
} }
@ -139,7 +221,10 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {}; const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer) if (!snapshotServer)
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
return snapshotServer.serveClosestScreenshot(relativePath, url.searchParams); return snapshotServer.serveClosestScreenshot(
relativePath,
url.searchParams
);
} }
if (relativePath.startsWith('/sha1/')) { if (relativePath.startsWith('/sha1/')) {
@ -147,15 +232,21 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const sha1 = relativePath.slice('/sha1/'.length); const sha1 = relativePath.slice('/sha1/'.length);
for (const trace of loadedTraces.values()) { for (const trace of loadedTraces.values()) {
const blob = await trace.traceModel.resourceForSha1(sha1); const blob = await trace.traceModel.resourceForSha1(sha1);
if (blob) if (blob) {
return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); return new Response(blob, {
status: 200,
headers: downloadHeaders(url.searchParams),
});
}
} }
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
} }
if (relativePath.startsWith('/file/')) { if (relativePath.startsWith('/file/')) {
const path = url.searchParams.get('path')!; const path = url.searchParams.get('path')!;
const traceViewerServer = clientIdToTraceUrls.get(event.clientId ?? '')?.traceViewerServer; const traceViewerServer = clientIdToTraceUrls.get(
event.clientId ?? ''
)?.traceViewerServer;
if (!traceViewerServer) if (!traceViewerServer)
throw new Error('client is not initialized'); throw new Error('client is not initialized');
const response = await traceViewerServer.readFile(path); const response = await traceViewerServer.readFile(path);
@ -186,7 +277,12 @@ function downloadHeaders(searchParams: URLSearchParams): Headers | undefined {
if (!name) if (!name)
return; return;
const headers = new Headers(); const headers = new Headers();
headers.set('Content-Disposition', `attachment; filename="attachment"; filename*=UTF-8''${encodeURIComponent(name)}`); headers.set(
'Content-Disposition',
`attachment; filename="attachment"; filename*=UTF-8''${encodeURIComponent(
name
)}`
);
if (contentType) if (contentType)
headers.set('Content-Type', contentType); headers.set('Content-Type', contentType);
return headers; return headers;
@ -214,6 +310,7 @@ async function gc() {
if (!usedTraces.has(traceUrl)) if (!usedTraces.has(traceUrl))
loadedTraces.delete(traceUrl); loadedTraces.delete(traceUrl);
} }
} }
// @ts-ignore // @ts-ignore

View file

@ -14,6 +14,7 @@
limitations under the License. limitations under the License.
*/ */
import type { Language } from '@isomorphic/locatorGenerators';
import type * as actionTypes from '@recorder/actions'; import type * as actionTypes from '@recorder/actions';
import { SourceChooser } from '@web/components/sourceChooser'; import { SourceChooser } from '@web/components/sourceChooser';
import { SplitView } from '@web/components/splitView'; import { SplitView } from '@web/components/splitView';
@ -31,12 +32,10 @@ import type * as modelUtil from '../modelUtil';
import type { SourceLocation } from '../modelUtil'; import type { SourceLocation } from '../modelUtil';
import { NetworkTab, useNetworkTabModel } from '../networkTab'; import { NetworkTab, useNetworkTabModel } from '../networkTab';
import { collectSnapshots, extendSnapshot, SnapshotView } from '../snapshotTab'; import { collectSnapshots, extendSnapshot, SnapshotView } from '../snapshotTab';
import { SourceTab } from '../sourceTab';
import { ModelContext, ModelProvider } from './modelContext';
import './recorderView.css';
import { ActionListView } from './actionListView'; import { ActionListView } from './actionListView';
import { BackendContext, BackendProvider } from './backendContext'; import { BackendContext, BackendProvider } from './backendContext';
import type { Language } from '@isomorphic/locatorGenerators'; import { ModelContext, ModelProvider } from './modelContext';
import './recorderView.css';
export const RecorderView: React.FunctionComponent = () => { export const RecorderView: React.FunctionComponent = () => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
@ -219,15 +218,15 @@ const PropertiesView: React.FunctionComponent<{
setHighlightedLocator={setHighlightedLocator} />, setHighlightedLocator={setHighlightedLocator} />,
}; };
const sourceTab: TabbedPaneTabModel = { // const sourceTab: TabbedPaneTabModel = {
id: 'source', // id: 'source',
title: 'Source', // title: 'Source',
render: () => <SourceTab // render: () => <SourceTab
sources={sourceModel.current} // sources={sourceModel.current}
stackFrameLocation={'right'} // stackFrameLocation={'right'}
fallbackLocation={sourceLocation} // fallbackLocation={sourceLocation}
/> // />
}; // };
const consoleTab: TabbedPaneTabModel = { const consoleTab: TabbedPaneTabModel = {
id: 'console', id: 'console',
title: 'Console', title: 'Console',
@ -242,7 +241,6 @@ const PropertiesView: React.FunctionComponent<{
}; };
const tabs: TabbedPaneTabModel[] = [ const tabs: TabbedPaneTabModel[] = [
sourceTab,
inspectorTab, inspectorTab,
consoleTab, consoleTab,
networkTab, networkTab,
@ -283,6 +281,7 @@ const TraceView: React.FunctionComponent<{
return snapshot ? extendSnapshot(snapshot) : undefined; return snapshot ? extendSnapshot(snapshot) : undefined;
}, [snapshot]); }, [snapshot]);
return <SnapshotView return <SnapshotView
sdkLanguage={sdkLanguage} sdkLanguage={sdkLanguage}
testIdAttributeName='data-testid' testIdAttributeName='data-testid'

View file

@ -14,22 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */
import './snapshotTab.css'; import ConsoleAPI from '@injected/consoleApi';
import * as React from 'react'; import { InjectedScript } from '@injected/injectedScript';
import { Recorder } from '@injected/recorder/recorder';
import type { Language } from '@isomorphic/locatorGenerators';
import { asLocator } from '@isomorphic/locatorGenerators';
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
import type { ElementInfo } from '@recorder/recorderTypes';
import type { ActionTraceEvent } from '@trace/trace'; import type { ActionTraceEvent } from '@trace/trace';
import { context, type MultiTraceModel, prevInList } from './modelUtil'; import { TabbedPaneTab } from '@web/components/tabbedPane';
import { Toolbar } from '@web/components/toolbar'; import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
import { clsx, useMeasure } from '@web/uiUtils'; import { clsx, useMeasure } from '@web/uiUtils';
import { InjectedScript } from '@injected/injectedScript'; import * as React from 'react';
import { Recorder } from '@injected/recorder/recorder';
import ConsoleAPI from '@injected/consoleApi';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
import { TabbedPaneTab } from '@web/components/tabbedPane';
import { BrowserFrame } from './browserFrame'; import { BrowserFrame } from './browserFrame';
import type { ElementInfo } from '@recorder/recorderTypes'; import { context, type MultiTraceModel, prevInList } from './modelUtil';
import './snapshotTab.css';
export const SnapshotTabsView: React.FunctionComponent<{ export const SnapshotTabsView: React.FunctionComponent<{
action: ActionTraceEvent | undefined, action: ActionTraceEvent | undefined,
@ -329,7 +329,7 @@ const serverParam = new URLSearchParams(window.location.search).get('server');
export function extendSnapshot(snapshot: Snapshot): SnapshotUrls { export function extendSnapshot(snapshot: Snapshot): SnapshotUrls {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('trace', context(snapshot.action).traceUrl); params.set('trace', btoa(context(snapshot.action).traceUrl));
params.set('name', snapshot.snapshotName); params.set('name', snapshot.snapshotName);
if (isUnderTest) if (isUnderTest)
params.set('isUnderTest', 'true'); params.set('isUnderTest', 'true');
@ -360,6 +360,7 @@ export async function fetchSnapshotInfo(snapshotInfoUrl: string | undefined) {
const result = { url: '', viewport: kDefaultViewport, timestamp: undefined, wallTime: undefined }; const result = { url: '', viewport: kDefaultViewport, timestamp: undefined, wallTime: undefined };
if (snapshotInfoUrl) { if (snapshotInfoUrl) {
const response = await fetch(snapshotInfoUrl); const response = await fetch(snapshotInfoUrl);
const info = await response.json(); const info = await response.json();
if (!info.error) { if (!info.error) {
result.url = info.url; result.url = info.url;

View file

@ -14,18 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import type { StackFrame } from '@protocol/channels';
import type { SourceHighlight } from '@web/components/codeMirrorWrapper';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import { SplitView } from '@web/components/splitView'; import { SplitView } from '@web/components/splitView';
import * as React from 'react'; import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton } from '@web/components/toolbarButton';
import { useAsyncMemo } from '@web/uiUtils'; import { useAsyncMemo } from '@web/uiUtils';
import * as React from 'react';
import { CopyToClipboard } from './copyToClipboard';
import type { SourceLocation, SourceModel } from './modelUtil';
import './sourceTab.css'; import './sourceTab.css';
import { StackTraceView } from './stackTrace'; import { StackTraceView } from './stackTrace';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import type { SourceHighlight } from '@web/components/codeMirrorWrapper';
import type { SourceLocation, SourceModel } from './modelUtil';
import type { StackFrame } from '@protocol/channels';
import { CopyToClipboard } from './copyToClipboard';
import { ToolbarButton } from '@web/components/toolbarButton';
import { Toolbar } from '@web/components/toolbar';
export const SourceTab: React.FunctionComponent<{ export const SourceTab: React.FunctionComponent<{
stack?: StackFrame[], stack?: StackFrame[],
@ -98,7 +98,7 @@ export const SourceTab: React.FunctionComponent<{
const showStackFrames = (stack?.length ?? 0) > 1; const showStackFrames = (stack?.length ?? 0) > 1;
const shortFileName = getFileName(fileName); const shortFileName = getFileName(fileName);
return null;
return <SplitView return <SplitView
sidebarSize={200} sidebarSize={200}
orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'} orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'}

View file

@ -14,34 +14,33 @@
limitations under the License. limitations under the License.
*/ */
import type { Entry } from '@trace/har';
import type { AfterActionTraceEventAttachment } from '@trace/trace';
import { SplitView } from '@web/components/splitView'; import { SplitView } from '@web/components/splitView';
import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
import { TabbedPane } from '@web/components/tabbedPane';
import { ToolbarButton } from '@web/components/toolbarButton';
import { clsx, msToString, useSetting } from '@web/uiUtils';
import * as React from 'react'; import * as React from 'react';
import { ActionList } from './actionList'; import { ActionList } from './actionList';
import { AnnotationsTab } from './annotationsTab';
import { AttachmentsTab } from './attachmentsTab';
import { CallTab } from './callTab'; import { CallTab } from './callTab';
import { LogTab } from './logTab';
import { ErrorsTab, useErrorsTabModel } from './errorsTab';
import type { ErrorDescription } from './errorsTab';
import type { ConsoleEntry } from './consoleTab'; import type { ConsoleEntry } from './consoleTab';
import { ConsoleTab, useConsoleTabModel } from './consoleTab'; import { ConsoleTab, useConsoleTabModel } from './consoleTab';
import type { ErrorDescription } from './errorsTab';
import { ErrorsTab, useErrorsTabModel } from './errorsTab';
import type { Boundaries } from './geometry';
import { InspectorTab } from './inspectorTab';
import { LogTab } from './logTab';
import { MetadataView } from './metadataView';
import type * as modelUtil from './modelUtil'; import type * as modelUtil from './modelUtil';
import { NetworkTab, useNetworkTabModel } from './networkTab'; import { NetworkTab, useNetworkTabModel } from './networkTab';
import { SnapshotTabsView } from './snapshotTab'; import { SnapshotTabsView } from './snapshotTab';
import { SourceTab } from './sourceTab';
import { TabbedPane } from '@web/components/tabbedPane';
import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
import { Timeline } from './timeline';
import { MetadataView } from './metadataView';
import { AttachmentsTab } from './attachmentsTab';
import { AnnotationsTab } from './annotationsTab';
import type { Boundaries } from './geometry';
import { InspectorTab } from './inspectorTab';
import { ToolbarButton } from '@web/components/toolbarButton';
import { useSetting, msToString, clsx } from '@web/uiUtils';
import type { Entry } from '@trace/har';
import './workbench.css';
import { testStatusIcon, testStatusText } from './testUtils';
import type { UITestStatus } from './testUtils'; import type { UITestStatus } from './testUtils';
import type { AfterActionTraceEventAttachment } from '@trace/trace'; import { testStatusIcon, testStatusText } from './testUtils';
import { Timeline } from './timeline';
import './workbench.css';
export const Workbench: React.FunctionComponent<{ export const Workbench: React.FunctionComponent<{
model?: modelUtil.MultiTraceModel, model?: modelUtil.MultiTraceModel,
@ -202,19 +201,19 @@ export const Workbench: React.FunctionComponent<{
if (!selectedAction && fallbackLocation) if (!selectedAction && fallbackLocation)
fallbackSourceErrorCount = fallbackLocation.source?.errors.length; fallbackSourceErrorCount = fallbackLocation.source?.errors.length;
const sourceTab: TabbedPaneTabModel = { // const sourceTab: TabbedPaneTabModel = {
id: 'source', // id: 'source',
title: 'Source', // title: 'Source',
errorCount: fallbackSourceErrorCount, // errorCount: fallbackSourceErrorCount,
render: () => <SourceTab // render: () => <SourceTab
stack={revealedStack} // stack={revealedStack}
sources={sources} // sources={sources}
rootDir={rootDir} // rootDir={rootDir}
stackFrameLocation={sidebarLocation === 'bottom' ? 'right' : 'bottom'} // stackFrameLocation={sidebarLocation === 'bottom' ? 'right' : 'bottom'}
fallbackLocation={fallbackLocation} // fallbackLocation={fallbackLocation}
onOpenExternally={onOpenExternally} // onOpenExternally={onOpenExternally}
/> // />
}; // };
const consoleTab: TabbedPaneTabModel = { const consoleTab: TabbedPaneTabModel = {
id: 'console', id: 'console',
title: 'Console', title: 'Console',
@ -247,7 +246,6 @@ export const Workbench: React.FunctionComponent<{
errorsTab, errorsTab,
consoleTab, consoleTab,
networkTab, networkTab,
sourceTab,
attachmentsTab, attachmentsTab,
]; ];

View file

@ -73,6 +73,28 @@ body.dark-mode .drop-target {
z-index: 10; z-index: 10;
} }
.loading-inset {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.inner-progress { .inner-progress {
background-color: var(--vscode-progressBar-background); background-color: var(--vscode-progressBar-background);
height: 100%; height: 100%;

View file

@ -14,14 +14,14 @@
limitations under the License. limitations under the License.
*/ */
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
import { toggleTheme } from '@web/theme';
import * as React from 'react'; import * as React from 'react';
import type { ContextEntry } from '../types/entries'; import type { ContextEntry } from '../types/entries';
import { MultiTraceModel } from './modelUtil'; import { MultiTraceModel } from './modelUtil';
import './workbenchLoader.css';
import { toggleTheme } from '@web/theme';
import { Workbench } from './workbench'; import { Workbench } from './workbench';
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection'; import './workbenchLoader.css';
export const WorkbenchLoader: React.FunctionComponent<{ export const WorkbenchLoader: React.FunctionComponent<{
}> = () => { }> = () => {
@ -166,6 +166,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
<div className='progress'> <div className='progress'>
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div> <div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
</div> </div>
{progress.done < progress.total && <div className='loading-inset'>
<div className='spinner'></div>
</div>}
<Workbench model={model} inert={showFileUploadDropArea} /> <Workbench model={model} inert={showFileUploadDropArea} />
{fileForLocalModeError && <div className='drop-target'> {fileForLocalModeError && <div className='drop-target'>
<div>Trace Viewer uses Service Workers to show traces. To view trace:</div> <div>Trace Viewer uses Service Workers to show traces. To view trace:</div>