change(ui) - fetch details modal refactor and added prev and next navigation

This commit is contained in:
Shekar Siri 2022-11-15 19:23:32 +01:00
parent 1fd7a848b6
commit 39ddd3d82a
10 changed files with 336 additions and 314 deletions

View file

@ -67,36 +67,36 @@ export function renderStart(r: any) {
);
}
const renderXHRText = () => (
<span className="flex items-center">
{XHR}
<QuestionMarkHint
content={
<>
Use our{' '}
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/plugins/fetch"
>
Fetch plugin
</a>
{' to capture HTTP requests and responses, including status codes and bodies.'} <br />
We also provide{' '}
<a
className="color-teal underline"
target="_blank"
href="https://docs.openreplay.com/plugins/graphql"
>
support for GraphQL
</a>
{' for easy debugging of your queries.'}
</>
}
className="ml-1"
/>
</span>
);
// const renderXHRText = () => (
// <span className="flex items-center">
// {XHR}
// <QuestionMarkHint
// content={
// <>
// Use our{' '}
// <a
// className="color-teal underline"
// target="_blank"
// href="https://docs.openreplay.com/plugins/fetch"
// >
// Fetch plugin
// </a>
// {' to capture HTTP requests and responses, including status codes and bodies.'} <br />
// We also provide{' '}
// <a
// className="color-teal underline"
// target="_blank"
// href="https://docs.openreplay.com/plugins/graphql"
// >
// support for GraphQL
// </a>
// {' for easy debugging of your queries.'}
// </>
// }
// className="ml-1"
// />
// </span>
// );
function renderSize(r: any) {
if (r.responseBodySize) return formatBytes(r.responseBodySize);
@ -180,6 +180,7 @@ function NetworkPanel(props: Props) {
const [sortAscending, setSortAscending] = useState(true);
const [filter, setFilter] = useState('');
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
const [activeRequest, setActiveRequest] = useState(false )
const onTabClick = (activeTab: any) => setActiveTab(activeTab);
const onFilterChange = ({ target: { value } }: any) => setFilter(value);
const additionalHeight = 0;
@ -237,7 +238,7 @@ function NetworkPanel(props: Props) {
}
const onRowClick = (row: any) => {
showModal(<FetchDetailsModal resource={row} fetchPresented={fetchPresented} />, {
showModal(<FetchDetailsModal resource={row} rows={filtered} fetchPresented={fetchPresented} />, {
right: true,
});
};

View file

@ -1,257 +0,0 @@
import React from 'react';
import { JSONTree, NoContent, Button, Tabs, Icon } from 'UI';
import cn from 'classnames';
import stl from './fetchDetails.module.css';
import Headers from './components/Headers';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { TYPES } from 'Types/session/resource';
import { formatBytes } from 'App/utils';
import CopyText from 'Shared/CopyText';
const HEADERS = 'HEADERS';
const REQUEST = 'REQUEST';
const RESPONSE = 'RESPONSE';
const TABS = [HEADERS, REQUEST, RESPONSE].map((tab) => ({ text: tab, key: tab }));
export default class FetchDetailsModal extends React.PureComponent {
state = { activeTab: REQUEST, tabs: [] };
onTabClick = (activeTab) => this.setState({ activeTab });
componentDidMount() {
this.checkTabs();
}
renderActiveTab = (tab) => {
const {
resource: { payload, response = this.props.resource.body },
} = this.props;
let jsonPayload,
jsonResponse,
requestHeaders,
responseHeaders = undefined;
try {
jsonPayload = typeof payload === 'string' ? JSON.parse(payload) : payload;
requestHeaders = jsonPayload.headers;
jsonPayload.body =
typeof jsonPayload.body === 'string' ? JSON.parse(jsonPayload.body) : jsonPayload.body;
delete jsonPayload.headers;
} catch (e) {}
try {
jsonResponse = typeof response === 'string' ? JSON.parse(response) : response;
responseHeaders = jsonResponse.headers;
jsonResponse.body =
typeof jsonResponse.body === 'string' ? JSON.parse(jsonResponse.body) : jsonResponse.body;
delete jsonResponse.headers;
} catch (e) {}
switch (tab) {
case REQUEST:
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
<div className="mt-6 text-2xl">Body is Empty.</div>
</div>
}
size="small"
show={!payload}
// animatedIcon="no-results"
>
<div>
<div className="mt-6">
{jsonPayload === undefined ? (
<div className="ml-3 break-words my-3"> {payload} </div>
) : (
<JSONTree src={jsonPayload} collapsed={false} enableClipboard />
)}
</div>
<div className="divider" />
</div>
</NoContent>
);
case RESPONSE:
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
<div className="mt-6 text-2xl">Body is Empty.</div>
</div>
}
size="small"
show={!response}
// animatedIcon="no-results"
>
<div>
<div className="mt-6">
{jsonResponse === undefined ? (
<div className="ml-3 break-words my-3"> {response} </div>
) : (
<JSONTree src={jsonResponse} collapsed={false} enableClipboard />
)}
</div>
<div className="divider" />
</div>
</NoContent>
);
case HEADERS:
return <Headers requestHeaders={requestHeaders} responseHeaders={responseHeaders} />;
}
};
componentDidUpdate(prevProps) {
if (prevProps.resource.index === this.props.resource.index) return;
this.checkTabs();
}
checkTabs() {
const {
resource: { payload, response, body },
isResult,
} = this.props;
const _tabs = TABS;
// const _tabs = TABS.filter(t => {
// if (t.key == REQUEST && !!payload) {
// return true
// }
// if (t.key == RESPONSE && !!response) {
// return true;
// }
// return false;
// })
this.setState({ tabs: _tabs, activeTab: _tabs.length > 0 ? _tabs[0].key : null });
}
render() {
const {
resource,
fetchPresented,
nextClick,
prevClick,
first = false,
last = false,
} = this.props;
const { method, url, duration } = resource;
const { activeTab, tabs } = this.state;
const _duration = parseInt(duration);
return (
<div className="bg-white p-5 h-screen overflow-y-auto" style={{ width: '500px' }}>
<h5 className="mb-2 text-2xl">Network Request</h5>
<div className="flex items-center py-1">
<div className="font-medium">Name</div>
<div className="rounded-lg bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip cursor-pointer">
<CopyText content={resource.url}>{resource.name}</CopyText>
</div>
</div>
<div className="flex items-center py-1">
<div className="font-medium">Type</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.type}
</div>
</div>
{!!resource.decodedBodySize && (
<div className="flex items-center py-1">
<div className="font-medium">Size</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{formatBytes(resource.decodedBodySize)}
</div>
</div>
)}
{method && (
<div className="flex items-center py-1">
<div className="font-medium">Request Method</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.method}
</div>
</div>
)}
{resource.status && (
<div className="flex items-center py-1">
<div className="font-medium">Status</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip flex items-center">
{resource.status === '200' && (
<div className="w-4 h-4 bg-green rounded-full mr-2"></div>
)}
{resource.status}
</div>
</div>
)}
{!!_duration && (
<div className="flex items-center py-1">
<div className="font-medium">Time</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{_duration} ms
</div>
</div>
)}
{resource.type === TYPES.XHR && !fetchPresented && (
<div className="bg-active-blue rounded p-3 mt-4">
<div className="mb-2 flex items-center">
<Icon name="lightbulb" size="18" />
<span className="ml-2 font-medium">Get more out of network requests</span>
</div>
<ul className="list-disc ml-5">
<li>
Integrate{' '}
<a
href="https://docs.openreplay.com/plugins/fetch"
className="link"
target="_blank"
>
Fetch plugin
</a>{' '}
to capture fetch payloads.
</li>
<li>
Find a detailed{' '}
<a
href="https://www.youtube.com/watch?v=YFCKstPZzZg"
className="link"
target="_blank"
>
video tutorial
</a>{' '}
to understand practical example of how to use fetch plugin.
</li>
</ul>
</div>
)}
<div className="mt-6">
{resource.type === TYPES.XHR && fetchPresented && (
<div>
<Tabs tabs={tabs} active={activeTab} onClick={this.onTabClick} border={true} />
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
{this.renderActiveTab(activeTab)}
</div>
</div>
)}
{/* <div className="flex justify-between absolute bottom-0 left-0 right-0 p-3 border-t bg-white">
<Button variant="outline" onClick={prevClick} disabled={first}>
Prev
</Button>
<Button variant="outline" onClick={nextClick} disabled={last}>
Next
</Button>
</div> */}
</div>
</div>
);
}
}

View file

@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react';
import FetchBasicDetails from './components/FetchBasicDetails';
import { Button } from 'UI';
import FetchPluginMessage from './components/FetchPluginMessage';
import { TYPES } from 'Types/session/resource';
import FetchTabs from './components/FetchTabs/FetchTabs';
interface Props {
resource: any;
rows: any;
fetchPresented?: boolean;
}
function FetchDetailsModal(props: Props) {
const { rows, fetchPresented = false } = props;
const [resource, setResource] = useState(props.resource);
const [first, setFirst] = useState(false);
const [last, setLast] = useState(false);
useEffect(() => {
const index = rows.indexOf(resource);
const length = rows.length - 1;
setFirst(index === 0);
setLast(index === length);
}, [resource]);
const prevClick = () => {
const index = rows.indexOf(resource);
if (index > 0) {
setResource(rows[index - 1]);
}
};
const nextClick = () => {
const index = rows.indexOf(resource);
if (index < rows.length - 1) {
setResource(rows[index + 1]);
}
};
return (
<div className="bg-white p-5 h-screen overflow-y-auto" style={{ width: '500px' }}>
<h5 className="mb-2 text-2xl">Network Request</h5>
<FetchBasicDetails resource={resource} />
{resource.type === TYPES.XHR && !fetchPresented && <FetchPluginMessage />}
{resource.type === TYPES.XHR && fetchPresented && <FetchTabs resource={resource} />}
{rows && rows.length > 0 && (
<div className="flex justify-between absolute bottom-0 left-0 right-0 p-3 border-t bg-white">
<Button variant="outline" onClick={prevClick} disabled={first}>
Prev
</Button>
<Button variant="outline" onClick={nextClick} disabled={last}>
Next
</Button>
</div>
)}
</div>
);
}
export default FetchDetailsModal;

View file

@ -0,0 +1,70 @@
import React from 'react';
import { Icon } from 'UI';
import { formatBytes } from 'App/utils';
import CopyText from 'Shared/CopyText';
import { TYPES } from 'Types/session/resource';
interface Props {
resource: any;
}
function FetchBasicDetails({ resource }: Props) {
const _duration = parseInt(resource.duration);
return (
<div>
<div className="flex items-center py-1">
<div className="font-medium">Name</div>
<div className="rounded-lg bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip cursor-pointer">
<CopyText content={resource.url}>{resource.name}</CopyText>
</div>
</div>
<div className="flex items-center py-1">
<div className="font-medium">Type</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.type}
</div>
</div>
{!!resource.decodedBodySize && (
<div className="flex items-center py-1">
<div className="font-medium">Size</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{formatBytes(resource.decodedBodySize)}
</div>
</div>
)}
{resource.method && (
<div className="flex items-center py-1">
<div className="font-medium">Request Method</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{resource.method}
</div>
</div>
)}
{resource.status && (
<div className="flex items-center py-1">
<div className="font-medium">Status</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip flex items-center">
{resource.status === '200' && (
<div className="w-4 h-4 bg-green rounded-full mr-2"></div>
)}
{resource.status}
</div>
</div>
)}
{!!_duration && (
<div className="flex items-center py-1">
<div className="font-medium">Time</div>
<div className="rounded bg-active-blue px-2 py-1 ml-2 whitespace-nowrap overflow-hidden text-clip">
{_duration} ms
</div>
</div>
)}
</div>
);
}
export default FetchBasicDetails;

View file

@ -0,0 +1 @@
export { default } from './FetchBasicDetails';

View file

@ -0,0 +1,31 @@
import React from 'react';
import { Icon } from 'UI';
function FetchPluginMessage() {
return (
<div className="bg-active-blue rounded p-3 mt-4">
<div className="mb-2 flex items-center">
<Icon name="lightbulb" size="18" />
<span className="ml-2 font-medium">Get more out of network requests</span>
</div>
<ul className="list-disc ml-5">
<li>
Integrate{' '}
<a href="https://docs.openreplay.com/plugins/fetch" className="link" target="_blank">
Fetch plugin
</a>{' '}
to capture fetch payloads.
</li>
<li>
Find a detailed{' '}
<a href="https://www.youtube.com/watch?v=YFCKstPZzZg" className="link" target="_blank">
video tutorial
</a>{' '}
to understand practical example of how to use fetch plugin.
</li>
</ul>
</div>
);
}
export default FetchPluginMessage;

View file

@ -0,0 +1 @@
export { default } from './FetchPluginMessage';

View file

@ -0,0 +1,112 @@
import React, { useEffect, useState } from 'react';
import Headers from '../Headers';
import { JSONTree, Tabs, NoContent } from 'UI';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
const HEADERS = 'HEADERS';
const REQUEST = 'REQUEST';
const RESPONSE = 'RESPONSE';
const TABS = [HEADERS, REQUEST, RESPONSE].map((tab) => ({ text: tab, key: tab }));
interface Props {
resource: any;
}
function FetchTabs(props: Props) {
const { resource } = props;
const [activeTab, setActiveTab] = useState(HEADERS);
const onTabClick = (tab: string) => setActiveTab(tab);
const [jsonPayload, setJsonPayload] = useState(null);
const [jsonResponse, setJsonResponse] = useState(null);
const [requestHeaders, setRequestHeaders] = useState(null);
const [responseHeaders, setResponseHeaders] = useState(null);
useEffect(() => {
const { payload, response } = resource;
try {
let jsonPayload = typeof payload === 'string' ? JSON.parse(payload) : payload;
let requestHeaders = jsonPayload.headers;
jsonPayload.body =
typeof jsonPayload.body === 'string' ? JSON.parse(jsonPayload.body) : jsonPayload.body;
delete jsonPayload.headers;
setJsonPayload(jsonPayload);
setRequestHeaders(requestHeaders);
} catch (e) {}
try {
let jsonResponse = typeof response === 'string' ? JSON.parse(response) : response;
let responseHeaders = jsonResponse.headers;
jsonResponse.body =
typeof jsonResponse.body === 'string' ? JSON.parse(jsonResponse.body) : jsonResponse.body;
delete jsonResponse.headers;
setJsonResponse(jsonResponse);
setResponseHeaders(responseHeaders);
} catch (e) {}
}, [resource, activeTab]);
const renderActiveTab = () => {
const { payload, response } = resource;
switch (activeTab) {
case REQUEST:
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
<div className="mt-6 text-2xl">Body is Empty.</div>
</div>
}
size="small"
show={!payload}
// animatedIcon="no-results"
>
<div>
<div className="mt-6">
{jsonPayload === undefined ? (
<div className="ml-3 break-words my-3"> {payload} </div>
) : (
<JSONTree src={jsonPayload} collapsed={false} enableClipboard />
)}
</div>
<div className="divider" />
</div>
</NoContent>
);
case RESPONSE:
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
<div className="mt-6 text-2xl">Body is Empty.</div>
</div>
}
size="small"
show={!response}
// animatedIcon="no-results"
>
<div>
<div className="mt-6">
{jsonResponse === undefined ? (
<div className="ml-3 break-words my-3"> {response} </div>
) : (
<JSONTree src={jsonResponse} collapsed={false} enableClipboard />
)}
</div>
<div className="divider" />
</div>
</NoContent>
);
case HEADERS:
return <Headers requestHeaders={requestHeaders} responseHeaders={responseHeaders} />;
}
};
return (
<div>
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={true} />
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>{renderActiveTab()}</div>
</div>
);
}
export default FetchTabs;

View file

@ -1,9 +1,13 @@
import React from 'react'
import { NoContent, TextEllipsis } from 'UI'
import stl from './headers.module.css'
import React from 'react';
import { NoContent, TextEllipsis } from 'UI';
import stl from './headers.module.css';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
function Headers(props) {
interface Props {
requestHeaders: any;
responseHeaders: any;
}
function Headers(props: Props) {
return (
<div>
<NoContent
@ -14,37 +18,33 @@ function Headers(props) {
</div>
}
size="small"
show={ !props.requestHeaders && !props.responseHeaders }
show={!props.requestHeaders && !props.responseHeaders}
// animatedIcon="no-results"
>
{ props.requestHeaders && (
{props.requestHeaders && (
<>
<div className="mb-4 mt-4">
<div className="my-2 font-medium">Request Headers</div>
{
Object.keys(props.requestHeaders).map(h => (
<div className={stl.row}>
<span className="mr-2 font-medium">{h}:</span>
<span>{props.requestHeaders[h]}</span>
</div>
))
}
{Object.keys(props.requestHeaders).map((h) => (
<div className={stl.row}>
<span className="mr-2 font-medium">{h}:</span>
<span>{props.requestHeaders[h]}</span>
</div>
))}
</div>
<hr />
</>
)}
{ props.responseHeaders && (
{props.responseHeaders && (
<div className="mt-4">
<div className="my-2 font-medium">Response Headers</div>
{
Object.keys(props.responseHeaders).map(h => (
<div className={stl.row}>
<span className="mr-2 font-medium">{h}:</span>
<span>{props.responseHeaders[h]}</span>
</div>
))
}
{Object.keys(props.responseHeaders).map((h) => (
<div className={stl.row}>
<span className="mr-2 font-medium">{h}:</span>
<span>{props.responseHeaders[h]}</span>
</div>
))}
</div>
)}
</NoContent>
@ -52,4 +52,4 @@ function Headers(props) {
);
}
export default Headers;
export default Headers;

View file

@ -2,7 +2,7 @@ import React from 'react';
import cn from 'classnames';
import stl from './tabs.module.css';
const Tabs = ({ tabs, active, onClick, border = true, className }) => (
const Tabs = ({ tabs, active, onClick, border = true, className = '' }) => (
<div className={ cn(stl.tabs, className, { [ stl.bordered ]: border }) } role="tablist" >
{ tabs.map(({ key, text, hidden = false, disabled = false }) => (
<div