Merge pull request #712 from openreplay/dev
dev - 1.8.0 fixes and other improvements
This commit is contained in:
commit
c66f0c3ced
14 changed files with 593 additions and 209 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.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>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
|
|||
'error color-red': !!row.isRed && row.isRed(),
|
||||
'cursor-pointer': typeof onRowClick === 'function',
|
||||
[stl.activeRow]: activeIndex === index,
|
||||
[stl.inactiveRow]: !activeIndex || index > activeIndex,
|
||||
// [stl.inactiveRow]: !activeIndex || index > activeIndex,
|
||||
})}
|
||||
onClick={typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined}
|
||||
id="table-row"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Headers'
|
||||
|
|
@ -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;*/
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FetchDetailsModal';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './GraphQLDetailsModal';
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { toast } from 'react-toastify';
|
||||
import { connectPlayer } from 'Player'
|
||||
import { connectPlayer } from 'Player';
|
||||
import withRequest from 'HOCs/withRequest';
|
||||
import { Icon, Button } from 'UI';
|
||||
import styles from './sharePopup.module.css';
|
||||
|
|
@ -11,25 +11,29 @@ import Select from 'Shared/Select';
|
|||
import { Tooltip } from 'react-tippy';
|
||||
import cn from 'classnames';
|
||||
import { fetchList, init } from 'Duck/integrations/slack';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
|
||||
@connectPlayer(state => ({
|
||||
@connectPlayer((state) => ({
|
||||
time: state.time,
|
||||
}))
|
||||
@connect(state => ({
|
||||
channels: state.getIn([ 'slack', 'list' ]),
|
||||
tenantId: state.getIn([ 'user', 'account', 'tenantId' ]),
|
||||
}), { fetchList })
|
||||
@connect(
|
||||
(state) => ({
|
||||
channels: state.getIn(['slack', 'list']),
|
||||
tenantId: state.getIn(['user', 'account', 'tenantId']),
|
||||
}),
|
||||
{ fetchList }
|
||||
)
|
||||
@withRequest({
|
||||
endpoint: ({ id, entity }, integrationId) =>
|
||||
`/integrations/slack/notify/${ integrationId }/${entity}/${ id }`,
|
||||
method: "POST",
|
||||
`/integrations/slack/notify/${integrationId}/${entity}/${id}`,
|
||||
method: 'POST',
|
||||
})
|
||||
export default class SharePopup extends React.PureComponent {
|
||||
state = {
|
||||
comment: '',
|
||||
isOpen: false,
|
||||
channelId: this.props.channels.getIn([ 0, 'webhookId' ]),
|
||||
}
|
||||
channelId: this.props.channels.getIn([0, 'webhookId']),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.channels.size === 0) {
|
||||
|
|
@ -37,104 +41,116 @@ export default class SharePopup extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
editMessage = e => this.setState({ comment: e.target.value })
|
||||
share = () => this.props.request({ comment: this.state.comment }, this.state.channelId)
|
||||
.then(this.handleSuccess)
|
||||
editMessage = (e) => this.setState({ comment: e.target.value });
|
||||
share = () =>
|
||||
this.props
|
||||
.request({ comment: this.state.comment }, this.state.channelId)
|
||||
.then(this.handleSuccess);
|
||||
|
||||
handleOpen = () => {
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
document.getElementById('message').focus();
|
||||
}, 100)
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ comment: '' });
|
||||
}
|
||||
};
|
||||
|
||||
handleSuccess = () => {
|
||||
this.setState({ isOpen: false, comment: '' })
|
||||
toast.success('Sent to Slack.');
|
||||
}
|
||||
};
|
||||
|
||||
changeChannel = ({ value }) => this.setState({ channelId: value })
|
||||
changeChannel = ({ value }) => this.setState({ channelId: value });
|
||||
|
||||
onClickHandler = () => {
|
||||
this.setState({ isOpen: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { trigger, loading, channels, showCopyLink = false, time } = this.props;
|
||||
const { comment, channelId } = this.state;
|
||||
const { comment, channelId, isOpen } = this.state;
|
||||
|
||||
const options = channels.map(({ webhookId, name }) => ({ value: webhookId, label: name })).toJS();
|
||||
const options = channels
|
||||
.map(({ webhookId, name }) => ({ value: webhookId, label: name }))
|
||||
.toJS();
|
||||
return (
|
||||
<Tooltip
|
||||
theme='light'
|
||||
open={isOpen}
|
||||
theme="light"
|
||||
interactive
|
||||
position='bottom'
|
||||
position="bottom"
|
||||
unmountHTMLWhenHide
|
||||
useContext
|
||||
arrow
|
||||
trigger="click"
|
||||
shown={this.handleOpen}
|
||||
beforeHidden={this.handleClose}
|
||||
// beforeHidden={this.handleClose}
|
||||
html={
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.header }>
|
||||
<div className={ cn(styles.title, 'text-lg') }>Share this session link to Slack</div>
|
||||
</div>
|
||||
{ options.length === 0 ?
|
||||
<>
|
||||
<div className={ styles.body }>
|
||||
<IntegrateSlackButton />
|
||||
</div>
|
||||
{ showCopyLink && (
|
||||
<OutsideClickDetectingDiv
|
||||
className={cn('relative flex items-center')}
|
||||
onClickOutside={() => {
|
||||
this.setState({ isOpen: false })
|
||||
}}
|
||||
>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<div className={cn(styles.title, 'text-lg')}>Share this session link to Slack</div>
|
||||
</div>
|
||||
{options.length === 0 ? (
|
||||
<>
|
||||
<div className={styles.body}>
|
||||
<IntegrateSlackButton />
|
||||
</div>
|
||||
{showCopyLink && (
|
||||
<div className={styles.footer}>
|
||||
<SessionCopyLink time={time} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<div className={styles.body}>
|
||||
<textarea
|
||||
name="message"
|
||||
id="message"
|
||||
cols="30"
|
||||
rows="4"
|
||||
resize="none"
|
||||
onChange={this.editMessage}
|
||||
value={comment}
|
||||
placeholder="Add Message (Optional)"
|
||||
className="p-4"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Select
|
||||
options={options}
|
||||
defaultValue={channelId}
|
||||
onChange={this.changeChannel}
|
||||
className="mr-4"
|
||||
/>
|
||||
<div>
|
||||
<Button onClick={this.share} primary>
|
||||
<div className="flex items-center">
|
||||
<Icon name="integrations/slack-bw" size="18" marginRight="10" />
|
||||
{loading ? 'Sending...' : 'Send'}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<SessionCopyLink time={time} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
:
|
||||
<div>
|
||||
<div className={ styles.body }>
|
||||
<textarea
|
||||
name="message"
|
||||
id="message"
|
||||
cols="30"
|
||||
rows="4"
|
||||
resize="none"
|
||||
onChange={ this.editMessage }
|
||||
value={ comment }
|
||||
placeholder="Add Message (Optional)"
|
||||
className="p-4"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Select
|
||||
options={ options }
|
||||
defaultValue={ channelId }
|
||||
onChange={ this.changeChannel }
|
||||
className="mr-4"
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
onClick={ this.share }
|
||||
primary
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<Icon name="integrations/slack-bw" size="18" marginRight="10" />
|
||||
{ loading ? 'Sending...' : 'Send' }
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ styles.footer }>
|
||||
<SessionCopyLink time={time} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</OutsideClickDetectingDiv>
|
||||
}
|
||||
|
||||
>
|
||||
{trigger}
|
||||
<span onClick={this.onClickHandler}>{trigger}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue