src/devtools/views/Network/Network.js (259 lines of code) (raw):

/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow * @format */ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { StoreContext } from '../context'; import ButtonIcon from '../ButtonIcon'; import Button from '../Button'; import InspectedElementTree from '../Components/InspectedElementTree'; import { getEventId } from '../../../utils'; import portaledContent from '../portaledContent'; import styles from './Network.css'; type RequestStatus = 'active' | 'unsubscribed' | 'completed' | 'error'; type RequestEntry = {| +id: number, params: any, variables: { [string]: mixed }, status: RequestStatus, responses: Array<mixed>, infos: Array<mixed>, |}; function Section(props: {| title: string, children: React$Node |}) { return ( <> <div className={styles.SectionTitle}>{props.title}</div> <div className={styles.SectionContent}>{props.children}</div> </> ); } function RequestDetails(props: {| request: ?RequestEntry |}) { const request = props.request; if (request == null) { return <div className={styles.RequestDetails}>No request selected</div>; } const responses = request.responses.map((response, i) => ( <InspectedElementTree key={i} label={ request.responses.length > 1 ? `response (${i + 1} of ${request.responses.length})` : 'response' } data={response} showWhenEmpty /> )); return ( <div className={styles.RequestDetails}> <Section title="Status">{request.status}</Section> <InspectedElementTree label="request" data={request.params} showWhenEmpty /> <InspectedElementTree label="variables" data={request.variables} showWhenEmpty /> <InspectedElementTree label="info" data={request.infos} /> {responses} </div> ); } function appearsInResponse(searchText: string, response: Object) { if (response == null) { return false; } for (const key in response) { if (typeof response[key] == 'object' && response[key] !== null) { return appearsInResponse(searchText, response[key]); } else if ( response[key] !== null && response[key] .toString() .toLowerCase() .includes(searchText) ) { return true; } } return false; } function Network(props: {| +portalContainer: mixed, currentEnvID: ?number |}) { const store = useContext(StoreContext); const [, forceUpdate] = useState({}); const [requestSearch, setRequestSearch] = useState(''); const fetchSearchBarText = useCallback(e => { setRequestSearch(e.target.value); }, []); useEffect(() => { const onMutated = () => { forceUpdate({}); }; store.addListener('mutated', onMutated); return () => { store.removeListener('mutated', onMutated); }; }, [store]); const [selectedRequestID, setSelectedRequestID] = useState(0); if (props.currentEnvID == null) { return null; } const events = store.getEnvironmentEvents(props.currentEnvID) || []; const requests: Map<number, RequestEntry> = new Map(); for (const event of events) { switch (event.name) { case 'network.start': { const eventId = getEventId(event); requests.set(eventId, { id: eventId, params: event.params, variables: event.variables, status: 'active', responses: [], infos: [], }); break; } case 'network.complete': { const eventId = getEventId(event); const request = requests.get(eventId); if (request != null) { request.status = 'completed'; } break; } case 'network.next': { const eventId = getEventId(event); const request = requests.get(eventId); if (request != null) { request.responses.push(event.response); } break; } case 'network.info': { const eventId = getEventId(event); const request = requests.get(eventId); if (request != null) { request.infos.push(event.info); } break; } case 'network.unsbuscribe': { const eventId = getEventId(event); const request = requests.get(eventId); if (request != null) { request.status = 'unsubscribed'; } break; } case 'network.error': { const eventId = getEventId(event); const request = requests.get(eventId); if (request != null) { request.status = 'error'; } break; } case 'store.publish': // ignore break; case 'store.restore': //ignore break; case 'store.gc': //ignore break; case 'store.snapshot': //ignore break; case 'store.notify.start': //ignore break; case 'store.notify.complete': //ignore break; case 'queryresource.fetch': // ignore break; default: { break; // ignore unknown events } } } let selectedRequest = requests.get(selectedRequestID); const requestArray = []; requests.forEach((request, _) => { if ( requestSearch .trim() .split(' ') .some( search => request.params.name.toLowerCase().includes(search.toLowerCase()) || request.responses.some(response => appearsInResponse(search.toLowerCase(), response) ) ) ) { requestArray.push(request); } }); const requestRows = requestArray.map(request => { if (selectedRequest == null) { selectedRequest = request; } let statusClass; switch (request.status) { case 'unsubscribed': statusClass = styles.StatusUnsubscribed; break; case 'error': statusClass = styles.StatusError; break; case 'active': statusClass = styles.StatusActive; break; default: statusClass = ''; break; } return ( <div key={request.id} onClick={() => { setSelectedRequestID(request.id); }} className={`${styles.Request} ${ request.id === selectedRequest?.id ? styles.SelectedRequest : '' } ${statusClass}`} > {request.params.name} ({request.status}) </div> ); }); return ( <div className={styles.Network}> <div className={styles.Toolbar}> <Button onClick={() => props.currentEnvID == null ? {} : store.clearNetworkEvents(props.currentEnvID) } title="Clear Logs" > <ButtonIcon type="clear" /> </Button> <div className={styles.Spacer} /> </div> <div className={styles.Content}> <div className={styles.Requests}> <input className={styles.RequestsSearchBar} type="text" onChange={fetchSearchBarText} placeholder="Search" ></input> {requestRows.length <= 0 && requestSearch !== '' ? ( <p className={styles.RequestNotFound}> Sorry, no requests with the name '{requestSearch}' were found! </p> ) : ( requestRows )} </div> <RequestDetails request={selectedRequest} /> </div> </div> ); } export default portaledContent(Network);