/** * 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 type { Entry } from '@trace/har'; import { ListView } from '@web/components/listView'; import * as React from 'react'; import type { Boundaries } from '../geometry'; import './networkTab.css'; import { NetworkResourceDetails } from './networkResourceDetails'; import { bytesToString, msToString } from '@web/uiUtils'; import { PlaceholderPanel } from './placeholderPanel'; import type { MultiTraceModel } from './modelUtil'; const NetworkListView = ListView; type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type'; type Sorting = { by: SortBy, negate: boolean}; type NetworkTabModel = { resources: Entry[], }; export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedTime: Boundaries | undefined): NetworkTabModel { const resources = React.useMemo(() => { const resources = model?.resources || []; const filtered = resources.filter(resource => { if (!selectedTime) return true; return !!resource._monotonicTime && (resource._monotonicTime >= selectedTime.minimum && resource._monotonicTime <= selectedTime.maximum); }); return filtered; }, [model, selectedTime]); return { resources }; } export const NetworkTab: React.FunctionComponent<{ boundaries: Boundaries, networkModel: NetworkTabModel, onEntryHovered: (entry: Entry | undefined) => void, }> = ({ boundaries, networkModel, onEntryHovered }) => { const [resource, setResource] = React.useState(); const [sorting, setSorting] = React.useState(undefined); React.useMemo(() => { if (sorting) sort(networkModel.resources, sorting); }, [networkModel.resources, sorting]); const toggleSorting = React.useCallback((f: SortBy) => { setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false }); }, [sorting]); if (!networkModel.resources.length) return ; return <> {!resource &&
} onSelected={setResource} onHighlighted={onEntryHovered} />
} {resource && setResource(undefined)} />} ; }; const NetworkHeader: React.FunctionComponent<{ sorting: Sorting | undefined, toggleSorting: (sortBy: SortBy) => void, }> = ({ toggleSorting: toggleSortBy, sorting }) => { return
toggleSortBy('start') }>
toggleSortBy('status') }>  Status
toggleSortBy('method') }> Method
toggleSortBy('file') }> Request
toggleSortBy('content-type') }> Content Type
toggleSortBy('duration') }> Duration
toggleSortBy('size') }> Size
Route
; }; const NetworkResource: React.FunctionComponent<{ resource: Entry, boundaries: Boundaries, }> = ({ resource, boundaries }) => { const { routeStatus, resourceName, contentType } = React.useMemo(() => { const routeStatus = formatRouteStatus(resource); let resourceName: string; try { const url = new URL(resource.request.url); resourceName = url.pathname; } catch { resourceName = resource.request.url; } let contentType = resource.response.content.mimeType; const charset = contentType.match(/^(.*);\s*charset=.*$/); if (charset) contentType = charset[1]; return { routeStatus, resourceName, contentType }; }, [resource]); return
{msToString(resource._monotonicTime! - boundaries.minimum)}
{resource.response.status}
{resource.request.method}
{resourceName}
{contentType}
{msToString(resource.time)}
{bytesToString(resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize)}
{routeStatus &&
{routeStatus}
}
; }; function formatStatus(status: number): string { if (status >= 200 && status < 400) return 'status-success'; if (status >= 400) return 'status-failure'; return ''; } function formatRouteStatus(request: Entry): string { if (request._wasAborted) return 'aborted'; if (request._wasContinued) return 'continued'; if (request._wasFulfilled) return 'fulfilled'; if (request._apiRequest) return 'api'; return ''; } function sort(resources: Entry[], sorting: Sorting) { const c = comparator(sorting?.by); if (c) resources.sort(c); if (sorting.negate) resources.reverse(); } function comparator(sortBy: SortBy) { if (sortBy === 'start') return (a: Entry, b: Entry) => a._monotonicTime! - b._monotonicTime!; if (sortBy === 'duration') return (a: Entry, b: Entry) => a.time - b.time; if (sortBy === 'status') return (a: Entry, b: Entry) => a.response.status - b.response.status; if (sortBy === 'method') { return (a: Entry, b: Entry) => { const valueA = a.request.method; const valueB = b.request.method; return valueA.localeCompare(valueB); }; } if (sortBy === 'size') { return (a: Entry, b: Entry) => { const sizeA = a.response._transferSize! > 0 ? a.response._transferSize! : a.response.bodySize; const sizeB = b.response._transferSize! > 0 ? b.response._transferSize! : b.response.bodySize; return sizeA - sizeB; }; } if (sortBy === 'content-type') { return (a: Entry, b: Entry) => { const valueA = a.response.content.mimeType; const valueB = b.response.content.mimeType; return valueA.localeCompare(valueB); }; } if (sortBy === 'file') { return (a: Entry, b: Entry) => { const nameA = a.request.url.substring(a.request.url.lastIndexOf('/')); const nameB = b.request.url.substring(b.request.url.lastIndexOf('/')); return nameA.localeCompare(nameB); }; } }