/** * 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 type * as modelUtil from './modelUtil'; import './networkTab.css'; import { NetworkResourceDetails } from './networkResourceDetails'; import { bytesToString, msToString } from '@web/uiUtils'; const NetworkListView = ListView; type Filter = 'status' | 'method' | 'file' | 'time' | 'size' | 'content-type'; export const NetworkTab: React.FunctionComponent<{ model: modelUtil.MultiTraceModel | undefined, selectedTime: Boundaries | undefined, }> = ({ model, selectedTime }) => { const [resource, setResource] = React.useState(); const [filter, setFilter] = React.useState(undefined); const [negateFilter, setNegateFilter] = React.useState(false); 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); }); if (filter) sort(filtered, filter, negateFilter); return filtered; }, [filter, model, negateFilter, selectedTime]); const toggleFilter = React.useCallback((f: Filter) => { if (filter === f) { setNegateFilter(!negateFilter); } else { setNegateFilter(false); setFilter(f); } }, [filter, negateFilter]); return <> {!resource &&
} onSelected={setResource} />
} {resource && setResource(undefined)} />} ; }; const NetworkHeader: React.FunctionComponent<{ filter: Filter | undefined, negateFilter: boolean, toggleFilter: (filter: Filter) => void, }> = ({ toggleFilter, filter, negateFilter }) => { return
toggleFilter('status') }>  Status
toggleFilter('method') }> Method
toggleFilter('file') }> Request
toggleFilter('content-type') }> Content Type
toggleFilter('time') }> Time
toggleFilter('size') }> Size
Route
; }; const NetworkResource: React.FunctionComponent<{ resource: Entry, }> = ({ resource }) => { const { routeStatus, resourceName, contentType } = React.useMemo(() => { const routeStatus = formatRouteStatus(resource); const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/')); let contentType = resource.response.content.mimeType; const charset = contentType.match(/^(.*);\s*charset=.*$/); if (charset) contentType = charset[1]; return { routeStatus, resourceName, contentType }; }, [resource]); return
{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[], filter: Filter | undefined, negate: boolean) { const c = comparator(filter); if (c) resources.sort(c); if (negate) resources.reverse(); } function comparator(filter: Filter | undefined) { if (filter === 'time') return (a: Entry, b: Entry) => a.time - b.time; if (filter === 'status') return (a: Entry, b: Entry) => a.response.status - b.response.status; if (filter === 'method') { return (a: Entry, b: Entry) => { const valueA = a.request.method; const valueB = b.request.method; return valueA.localeCompare(valueB); }; } if (filter === '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 (filter === 'content-type') { return (a: Entry, b: Entry) => { const valueA = a.response.content.mimeType; const valueB = b.response.content.mimeType; return valueA.localeCompare(valueB); }; } if (filter === '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); }; } }