change(ui) - x-ray include fetch, axios and graphql

This commit is contained in:
Shekar Siri 2022-09-02 20:38:16 +05:30
parent 4ee54e062e
commit 5cb8283bc3
12 changed files with 497 additions and 129 deletions

View file

@ -87,7 +87,7 @@ export default class Fetch extends React.PureComponent {
render() {
const { listNow } = this.props;
const { current, currentIndex, showFetchDetails, filteredList } = this.state;
const hasErrors = filteredList.some((r) => r.status >= 400);
// const hasErrors = filteredList.some((r) => r.status >= 400);
return (
<React.Fragment>
<SlideModal

View file

@ -29,7 +29,8 @@ function OverviewPanel(props: Props) {
const [selectedFeatures, setSelectedFeatures] = React.useState([
'PERFORMANCE',
'ERRORS',
'EVENTS',
// 'EVENTS',
'NETWORK',
]);
const resources: any = React.useMemo(() => {
@ -131,7 +132,10 @@ export default connect(
}
)(
connectPlayer((state: any) => ({
resourceList: state.resourceList.filter((r: any) => r.isRed() || r.isYellow()),
resourceList: state.resourceList
.filter((r: any) => r.isRed() || r.isYellow())
.concat(state.fetchList.filter((i: any) => parseInt(i.status) >= 400))
.concat(state.graphqlList.filter((i: any) => parseInt(i.status) >= 400)),
exceptionsList: state.exceptionsList,
eventsList: state.eventList,
stackEventList: state.stackList,

View file

@ -19,8 +19,9 @@ const EventRow = React.memo((props: Props) => {
!isGraph &&
React.useMemo(() => {
return list.map((item: any, _index: number) => {
const spread = item.toJS ? { ...item.toJS() } : { ...item }
return {
...item.toJS(),
...spread,
left: getTimelinePosition(item.time, scale),
};
});

View file

@ -7,144 +7,166 @@ import { Tooltip } from 'react-tippy';
import { TYPES as EVENT_TYPES } from 'Types/session/event';
import StackEventModal from '../StackEventModal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import FetchDetails from 'Shared/FetchDetailsModal';
import GraphQLDetailsModal from 'Shared/GraphQLDetailsModal';
interface Props {
pointer: any;
type: any;
pointer: any;
type: any;
}
const TimelinePointer = React.memo((props: Props) => {
const { showModal, hideModal } = useModal();
const createEventClickHandler = (pointer: any, type: any) => (e: any) => {
e.stopPropagation();
Controls.jump(pointer.time);
if (!type) {
return;
const { showModal, hideModal } = useModal();
const createEventClickHandler = (pointer: any, type: any) => (e: any) => {
e.stopPropagation();
Controls.jump(pointer.time);
if (!type) {
return;
}
if (type === 'ERRORS') {
showModal(<ErrorDetailsModal errorId={pointer.errorId} />, { right: true });
}
if (type === 'EVENT') {
showModal(<StackEventModal event={pointer} />, { right: true });
}
if (type === NETWORK) {
if (pointer.tp === 'graph_ql') {
showModal(<GraphQLDetailsModal resource={pointer} />, { right: true });
} else {
showModal(<FetchDetails resource={pointer} />, { right: true });
}
}
// props.toggleBottomBlock(type);
};
const renderNetworkElement = (item: any) => {
const name = item.name || '';
return (
<Popup
content={
<div className="">
<b>{item.success ? 'Slow resource: ' : 'Missing resource:'}</b>
<br />
{name.length > 200 ? name.slice(0, 100) + ' ... ' + name.slice(-50) : name}
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer">
<div className="h-3 w-3 rounded-full bg-red" />
</div>
</Popup>
);
};
if (type === 'ERRORS') {
showModal(<ErrorDetailsModal errorId={pointer.errorId} />, { right: true });
const renderClickRageElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Click Rage'}</b>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, null)} className="cursor-pointer">
<Icon className="bg-white" name="funnel/emoji-angry" color="red" size="16" />
</div>
</Popup>
);
};
if (type === 'EVENT') {
showModal(<StackEventModal event={pointer} />, { right: true });
const renderStackEventElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Stack Event'}</b>
</div>
}
// props.toggleBottomBlock(type);
};
delay={0}
position="top"
>
<div
onClick={createEventClickHandler(item, 'EVENT')}
className="cursor-pointer w-1 h-4 bg-red"
>
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Popup>
);
};
const renderNetworkElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{item.success ? 'Slow resource: ' : 'Missing resource:'}</b>
<br />
{item.name.length > 200 ? (item.name.slice(0, 100) + ' ... ' + item.name.slice(-50)) : item.name}
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer">
<div className="h-3 w-3 rounded-full bg-red" />
</div>
</Popup>
);
};
const renderClickRageElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Click Rage'}</b>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, null)} className="cursor-pointer">
<Icon className="bg-white" name="funnel/emoji-angry" color="red" size="16" />
</div>
</Popup>
);
};
const renderStackEventElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Stack Event'}</b>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, 'EVENT')} className="cursor-pointer w-1 h-4 bg-red">
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Popup>
);
};
const renderPerformanceElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{item.type}</b>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, EXCEPTIONS)} className="cursor-pointer w-1 h-4 bg-red">
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Popup>
);
};
const renderExceptionElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Exception'}</b>
<br />
<span>{item.message}</span>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, 'ERRORS')} className="cursor-pointer">
<Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" />
</div>
</Popup>
);
};
const render = () => {
const { pointer, type } = props;
if (type === 'NETWORK') {
return renderNetworkElement(pointer);
}
if (type === 'CLICKRAGE') {
return renderClickRageElement(pointer);
}
if (type === 'ERRORS') {
return renderExceptionElement(pointer);
}
if (type === 'EVENTS') {
return renderStackEventElement(pointer);
const renderPerformanceElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{item.type}</b>
</div>
}
delay={0}
position="top"
>
<div
onClick={createEventClickHandler(item, EXCEPTIONS)}
className="cursor-pointer w-1 h-4 bg-red"
>
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Popup>
);
};
if (type === 'PERFORMANCE') {
return renderPerformanceElement(pointer);
const renderExceptionElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Exception'}</b>
<br />
<span>{item.message}</span>
</div>
}
};
return <div>{render()}</div>;
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, 'ERRORS')} className="cursor-pointer">
<Icon
className="rounded-full bg-white"
name="funnel/exclamation-circle-fill"
color="red"
size="16"
/>
</div>
</Popup>
);
};
const render = () => {
const { pointer, type } = props;
if (type === 'NETWORK') {
return renderNetworkElement(pointer);
}
if (type === 'CLICKRAGE') {
return renderClickRageElement(pointer);
}
if (type === 'ERRORS') {
return renderExceptionElement(pointer);
}
if (type === 'EVENTS') {
return renderStackEventElement(pointer);
}
if (type === 'PERFORMANCE') {
return renderPerformanceElement(pointer);
}
};
return <div>{render()}</div>;
});
export default TimelinePointer;

View file

@ -0,0 +1,175 @@
import React from 'react';
import { JSONTree, NoContent, Button, Tabs } from 'UI';
import cn from 'classnames';
import stl from './fetchDetails.module.css';
import Headers from './components/Headers';
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 }));
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: { method, url, duration },
nextClick,
prevClick,
first = false,
last = false,
} = this.props;
const { activeTab, tabs } = this.state;
return (
<div className="bg-white p-5 h-screen overflow-y-auto" style={{ width: '500px' }}>
<h5 className="mb-2">{'URL'}</h5>
<div className={cn(stl.url, 'color-gray-darkest')}>{url}</div>
<div className="flex items-center mt-4">
<div className="w-4/12">
<div className="font-medium mb-2">Method</div>
<div>{method}</div>
</div>
<div className="w-4/12">
<div className="font-medium mb-2">Duration</div>
<div>{parseInt(duration)} ms</div>
</div>
</div>
<div className="mt-6">
<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,55 @@
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) {
return (
<div>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_RESULTS} size="170" />
<div className="mt-6 text-2xl">No data available.</div>
</div>
}
size="small"
show={ !props.requestHeaders && !props.responseHeaders }
// animatedIcon="no-results"
>
{ 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>
))
}
</div>
<hr />
</>
)}
{ 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>
))
}
</div>
)}
</NoContent>
</div>
);
}
export default Headers;

View file

@ -0,0 +1,14 @@
.row {
/* display: flex; */
padding: 5px 0px;
font-size: 13px;
word-break: break-all;
/* padding-left: 20px; */
&:hover {
background-color: $active-blue;
}
& div:last-child {
max-width: 80%;
}
}

View file

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

View file

@ -0,0 +1,18 @@
.url {
padding: 10px;
border-radius: 3px;
background-color: $gray-lightest;
/* border: solid thin $gray-light; */
/* max-width: 90%; */
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
.status {
padding: 3px 8px;
border-radius: 12px;
/*border: 1px solid $gray-light;*/
}

View file

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

View file

@ -0,0 +1,76 @@
import React from 'react';
import { JSONTree, Button } from 'UI';
import cn from 'classnames';
interface Props {
resource: any;
}
function GraphQLDetailsModal(props: Props) {
const {
resource: { variables, response, duration, operationKind, operationName },
// nextClick,
// prevClick,
// first = false,
// last = false,
} = props;
let jsonVars = undefined;
let jsonResponse = undefined;
try {
jsonVars = JSON.parse(variables);
} catch (e) {}
try {
jsonResponse = JSON.parse(response);
} catch (e) {}
const dataClass = cn('p-2 bg-gray-lightest rounded color-gray-darkest');
return (
<div className="p-5 bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
<h5 className="mb-2">{'Operation Name'}</h5>
<div className={dataClass}>{operationName}</div>
<div className="flex items-center gap-4 mt-4">
<div className="w-6/12">
<div className="mb-2">Operation Kind</div>
<div className={dataClass}>{operationKind}</div>
</div>
<div className="w-6/12">
<div className="mb-2">Duration</div>
<div className={dataClass}>{duration ? parseInt(duration) : '???'} ms</div>
</div>
</div>
<div style={{ height: 'calc(100vh - 314px)', overflowY: 'auto' }}>
<div>
<div className="flex justify-between items-start mt-6 mb-2">
<h5 className="mt-1 mr-1">{'Variables'}</h5>
</div>
<div className={dataClass}>
{jsonVars === undefined ? variables : <JSONTree src={jsonVars} />}
</div>
<div className="divider" />
</div>
<div>
<div className="flex justify-between items-start mt-6 mb-2">
<h5 className="mt-1 mr-1">{'Response'}</h5>
</div>
<div className={dataClass}>
{jsonResponse === undefined ? response : <JSONTree src={jsonResponse} />}
</div>
</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>
);
}
export default GraphQLDetailsModal;

View file

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