diff --git a/frontend/app/components/Session_/Fetch/Fetch.js b/frontend/app/components/Session_/Fetch/Fetch.js index df0c44864..fc7d7e76b 100644 --- a/frontend/app/components/Session_/Fetch/Fetch.js +++ b/frontend/app/components/Session_/Fetch/Fetch.js @@ -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 ( { @@ -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, diff --git a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx index cf8aece3c..8b5ac8571 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/EventRow/EventRow.tsx @@ -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), }; }); diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx index ad179dfea..7786376de 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -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(, { right: true }); + } + + if (type === 'EVENT') { + showModal(, { right: true }); + } + + if (type === NETWORK) { + if (pointer.tp === 'graph_ql') { + showModal(, { right: true }); + } else { + showModal(, { right: true }); + } + } + // props.toggleBottomBlock(type); + }; + + const renderNetworkElement = (item: any) => { + const name = item.name || ''; + return ( + + {item.success ? 'Slow resource: ' : 'Missing resource:'} +
+ {name.length > 200 ? name.slice(0, 100) + ' ... ' + name.slice(-50) : name} + } + delay={0} + position="top" + > +
+
+
+ + ); + }; - if (type === 'ERRORS') { - showModal(, { right: true }); + const renderClickRageElement = (item: any) => { + return ( + + {'Click Rage'} +
} + delay={0} + position="top" + > +
+ +
+
+ ); + }; - if (type === 'EVENT') { - showModal(, { right: true }); + const renderStackEventElement = (item: any) => { + return ( + + {'Stack Event'} + } - // props.toggleBottomBlock(type); - }; + delay={0} + position="top" + > +
+ {/* */} +
+
+ ); + }; - const renderNetworkElement = (item: any) => { - return ( - - {item.success ? 'Slow resource: ' : 'Missing resource:'} -
- {item.name.length > 200 ? (item.name.slice(0, 100) + ' ... ' + item.name.slice(-50)) : item.name} - - } - delay={0} - position="top" - > -
-
-
- - ); - }; - - const renderClickRageElement = (item: any) => { - return ( - - {'Click Rage'} -
- } - delay={0} - position="top" - > -
- -
-
- ); - }; - - const renderStackEventElement = (item: any) => { - return ( - - {'Stack Event'} - - } - delay={0} - position="top" - > -
- {/* */} -
-
- ); - }; - - const renderPerformanceElement = (item: any) => { - return ( - - {item.type} - - } - delay={0} - position="top" - > -
- {/* */} -
-
- ); - }; - - const renderExceptionElement = (item: any) => { - return ( - - {'Exception'} -
- {item.message} - - } - delay={0} - position="top" - > -
- -
-
- ); - }; - - 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 ( + + {item.type} + } + delay={0} + position="top" + > +
+ {/* */} +
+
+ ); + }; - if (type === 'PERFORMANCE') { - return renderPerformanceElement(pointer); + const renderExceptionElement = (item: any) => { + return ( + + {'Exception'} +
+ {item.message} + } - }; - return
{render()}
; + delay={0} + position="top" + > +
+ +
+
+ ); + }; + + 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
{render()}
; }); export default TimelinePointer; diff --git a/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.js b/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.js new file mode 100644 index 000000000..47d7d487d --- /dev/null +++ b/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.js @@ -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 ( + + +
Body is Empty.
+ + } + size="small" + show={!payload} + // animatedIcon="no-results" + > +
+
+ {jsonPayload === undefined ? ( +
{payload}
+ ) : ( + + )} +
+
+
+ + ); + case RESPONSE: + return ( + + +
Body is Empty.
+
+ } + size="small" + show={!response} + // animatedIcon="no-results" + > +
+
+ {jsonResponse === undefined ? ( +
{response}
+ ) : ( + + )} +
+
+
+ + ); + case HEADERS: + return ; + } + }; + + 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 ( +
+
{'URL'}
+
{url}
+
+
+
Method
+
{method}
+
+
+
Duration
+
{parseInt(duration)} ms
+
+
+ +
+
+ +
+ {this.renderActiveTab(activeTab)} +
+
+ + {/*
+ + +
*/} +
+
+ ); + } +} diff --git a/frontend/app/components/shared/FetchDetailsModal/components/Headers/Headers.tsx b/frontend/app/components/shared/FetchDetailsModal/components/Headers/Headers.tsx new file mode 100644 index 000000000..c2ec31a07 --- /dev/null +++ b/frontend/app/components/shared/FetchDetailsModal/components/Headers/Headers.tsx @@ -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 ( +
+ + +
No data available.
+
+ } + size="small" + show={ !props.requestHeaders && !props.responseHeaders } + // animatedIcon="no-results" + > + { props.requestHeaders && ( + <> +
+
Request Headers
+ { + Object.keys(props.requestHeaders).map(h => ( +
+ {h}: + {props.requestHeaders[h]} +
+ )) + } +
+
+ + )} + + { props.responseHeaders && ( +
+
Response Headers
+ { + Object.keys(props.responseHeaders).map(h => ( +
+ {h}: + {props.responseHeaders[h]} +
+ )) + } +
+ )} + +
+ ); +} + +export default Headers; \ No newline at end of file diff --git a/frontend/app/components/shared/FetchDetailsModal/components/Headers/headers.module.css b/frontend/app/components/shared/FetchDetailsModal/components/Headers/headers.module.css new file mode 100644 index 000000000..3368eb84c --- /dev/null +++ b/frontend/app/components/shared/FetchDetailsModal/components/Headers/headers.module.css @@ -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%; + } +} \ No newline at end of file diff --git a/frontend/app/components/shared/FetchDetailsModal/components/Headers/index.ts b/frontend/app/components/shared/FetchDetailsModal/components/Headers/index.ts new file mode 100644 index 000000000..69b6be26c --- /dev/null +++ b/frontend/app/components/shared/FetchDetailsModal/components/Headers/index.ts @@ -0,0 +1 @@ +export { default } from './Headers' \ No newline at end of file diff --git a/frontend/app/components/shared/FetchDetailsModal/fetchDetails.module.css b/frontend/app/components/shared/FetchDetailsModal/fetchDetails.module.css new file mode 100644 index 000000000..75cae8d8f --- /dev/null +++ b/frontend/app/components/shared/FetchDetailsModal/fetchDetails.module.css @@ -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;*/ +} \ No newline at end of file diff --git a/frontend/app/components/shared/FetchDetailsModal/index.js b/frontend/app/components/shared/FetchDetailsModal/index.js new file mode 100644 index 000000000..bc85e2574 --- /dev/null +++ b/frontend/app/components/shared/FetchDetailsModal/index.js @@ -0,0 +1 @@ +export { default } from './FetchDetailsModal'; \ No newline at end of file diff --git a/frontend/app/components/shared/GraphQLDetailsModal/GraphQLDetailsModal.tsx b/frontend/app/components/shared/GraphQLDetailsModal/GraphQLDetailsModal.tsx new file mode 100644 index 000000000..a742d4ddf --- /dev/null +++ b/frontend/app/components/shared/GraphQLDetailsModal/GraphQLDetailsModal.tsx @@ -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 ( +
+
{'Operation Name'}
+
{operationName}
+ +
+
+
Operation Kind
+
{operationKind}
+
+
+
Duration
+
{duration ? parseInt(duration) : '???'} ms
+
+
+ +
+
+
+
{'Variables'}
+
+
+ {jsonVars === undefined ? variables : } +
+
+
+ +
+
+
{'Response'}
+
+
+ {jsonResponse === undefined ? response : } +
+
+
+ + {/*
+ + +
*/} +
+ ); +} + +export default GraphQLDetailsModal; diff --git a/frontend/app/components/shared/GraphQLDetailsModal/index.ts b/frontend/app/components/shared/GraphQLDetailsModal/index.ts new file mode 100644 index 000000000..ecfc192c9 --- /dev/null +++ b/frontend/app/components/shared/GraphQLDetailsModal/index.ts @@ -0,0 +1 @@ +export { default } from './GraphQLDetailsModal';