ui: check chart/widget components for crashes

This commit is contained in:
nick-delirium 2024-12-12 15:57:11 +01:00
parent 8a7570549c
commit 0743a4fd17
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
7 changed files with 150 additions and 134 deletions

View file

@ -1,10 +1,5 @@
import React, { useState } from 'react';
import { formatTimeOrDate } from 'App/date';
import { Button, Table } from 'antd';
import type { TableProps } from 'antd';
import React from 'react';
import CustomTooltip from "../CustomChartTooltip";
import { Eye, EyeOff } from 'lucide-react';
import { Styles } from '../../common';
import {
ResponsiveContainer,
@ -16,7 +11,7 @@ import {
Line,
Legend,
} from 'recharts';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
interface Props {
data: any;
@ -34,7 +29,6 @@ function CustomMetricLineChart(props: Props) {
const {
data = { chart: [], namesMap: [] },
compData = { chart: [], namesMap: [] },
params,
colors,
onClick = () => null,
yaxis = { ...Styles.yaxis },
@ -117,4 +111,4 @@ function CustomMetricLineChart(props: Props) {
);
}
export default CustomMetricLineChart;
export default observer(CustomMetricLineChart);

View file

@ -1,7 +1,6 @@
import React from 'react';
import { useStore } from 'App/mstore';
import WidgetWrapperNew from 'Components/Dashboard/components/WidgetWrapper/WidgetWrapperNew';
import { Loader } from 'UI';
import { observer } from 'mobx-react-lite';
import AddCardSection from '../AddCardSection/AddCardSection';
import cn from 'classnames';

View file

@ -52,6 +52,7 @@ function WidgetChart(props: Props) {
const { isSaved = false, metric, isTemplate } = props;
const { dashboardStore, metricStore } = useStore();
const _metric: any = metricStore.instance;
const data = _metric.data;
const period = dashboardStore.period;
const drillDownPeriod = dashboardStore.drillDownPeriod;
const drillDownFilter = dashboardStore.drillDownFilter;
@ -61,7 +62,6 @@ function WidgetChart(props: Props) {
const metricParams = _metric.params;
const prevMetricRef = useRef<any>();
const isMounted = useIsMounted();
const [data, setData] = useState<any>(metric.data);
const [compData, setCompData] = useState<any>(null);
const [enabledRows, setEnabledRows] = useState([]);
const isTableWidget =
@ -134,10 +134,7 @@ function WidgetChart(props: Props) {
dashboardStore
.fetchMetricChartData(metric, payload, isSaved, period, isComparison)
.then((res: any) => {
if (isMounted()) {
if (isComparison) setCompData(res);
else setData(res);
}
if (isComparison) setCompData(res);
})
.finally(() => {
setLoading(false);
@ -210,13 +207,12 @@ function WidgetChart(props: Props) {
]);
useEffect(loadPage, [_metric.page]);
const renderChart = () => {
const renderChart = React.useCallback(() => {
const { metricType, metricOf } = metric;
const viewType = metric.viewType;
const metricWithData = { ...metric, data };
if (metricType === FUNNEL) {
console.log(data, compData);
if (viewType === 'table') {
return (
<FunnelTable data={data} compData={compData} />
@ -503,7 +499,7 @@ function WidgetChart(props: Props) {
console.log('Unknown metric type', metricType, viewType);
return <div>Unknown metric type</div>;
};
}, [data, compData, enabledRows, metric]);
return (
<div ref={ref}>

View file

@ -1,98 +1,103 @@
import {useHistory} from "react-router";
import {useStore} from "App/mstore";
import {useObserver} from "mobx-react-lite";
import {Button, Dropdown, MenuProps, message, Modal} from "antd";
import {BellIcon, EllipsisVertical, TrashIcon} from "lucide-react";
import {toast} from "react-toastify";
import React from "react";
import {useModal} from "Components/ModalContext";
import AlertFormModal from "Components/Alerts/AlertFormModal/AlertFormModal";
import { useHistory } from 'react-router';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { Button, Dropdown, MenuProps, message, Modal } from 'antd';
import { BellIcon, EllipsisVertical, TrashIcon } from 'lucide-react';
import { toast } from 'react-toastify';
import React from 'react';
import { useModal } from 'Components/ModalContext';
import AlertFormModal from 'Components/Alerts/AlertFormModal/AlertFormModal';
const CardViewMenu = () => {
const history = useHistory();
const {alertsStore, dashboardStore, metricStore} = useStore();
const widget = useObserver(() => metricStore.instance);
const {openModal, closeModal} = useModal();
const history = useHistory();
const { alertsStore, metricStore } = useStore();
const widget = metricStore.instance;
const { openModal, closeModal } = useModal();
const showAlertModal = () => {
const seriesId = widget.series[0] && widget.series[0].seriesId || '';
alertsStore.init({query: {left: seriesId}})
openModal(<AlertFormModal
onClose={closeModal}
/>, {
// title: 'Set Alerts',
placement: 'right',
width: 620,
const showAlertModal = () => {
const seriesId = (widget.series[0] && widget.series[0].seriesId) || '';
alertsStore.init({ query: { left: seriesId } });
openModal(<AlertFormModal onClose={closeModal} />, {
// title: 'Set Alerts',
placement: 'right',
width: 620,
});
};
const items: MenuProps['items'] = [
{
key: 'alert',
label: 'Set Alerts',
icon: <BellIcon size={16} />,
disabled: !widget.exists() || widget.metricType === 'predefined',
onClick: showAlertModal,
},
{
key: 'remove',
label: 'Delete',
icon: <TrashIcon size={16} />,
onClick: () => {
Modal.confirm({
title: 'Confirm Card Deletion',
icon: null,
content:
'Are you sure you want to remove this card? This action is permanent and cannot be undone.',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
<OkBtn />
</>
),
onOk: () => {
metricStore
.delete(widget)
.then((r) => {
history.goBack();
})
.catch(() => {
toast.error('Failed to remove card');
});
},
});
}
},
},
];
const items: MenuProps['items'] = [
{
key: 'alert',
label: "Set Alerts",
icon: <BellIcon size={16}/>,
disabled: !widget.exists() || widget.metricType === 'predefined',
onClick: showAlertModal,
},
{
key: 'remove',
label: 'Delete',
icon: <TrashIcon size={16}/>,
onClick: () => {
Modal.confirm({
title: 'Confirm Card Deletion',
icon: null,
content:'Are you sure you want to remove this card? This action is permanent and cannot be undone.',
footer: (_, {OkBtn, CancelBtn}) => (
<>
<CancelBtn/>
<OkBtn/>
</>
),
onOk: () => {
metricStore.delete(widget).then(r => {
history.goBack();
}).catch(() => {
toast.error('Failed to remove card');
});
},
})
}
},
];
const onClick: MenuProps['onClick'] = ({key}) => {
if (key === 'alert') {
message.info('Set Alerts');
} else if (key === 'remove') {
Modal.confirm({
title: 'Are you sure you want to remove this card?',
icon: null,
// content: 'Bla bla ...',
footer: (_, {OkBtn, CancelBtn}) => (
<>
<CancelBtn/>
<OkBtn/>
</>
),
onOk: () => {
metricStore.delete(widget).then(r => {
history.goBack();
}).catch(() => {
toast.error('Failed to remove card');
});
},
const onClick: MenuProps['onClick'] = ({ key }) => {
if (key === 'alert') {
message.info('Set Alerts');
} else if (key === 'remove') {
Modal.confirm({
title: 'Are you sure you want to remove this card?',
icon: null,
// content: 'Bla bla ...',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
<OkBtn />
</>
),
onOk: () => {
metricStore
.delete(widget)
.then((r) => {
history.goBack();
})
}
};
.catch(() => {
toast.error('Failed to remove card');
});
},
});
}
};
return (
<div className="flex items-center justify-between">
<Dropdown menu={{items}}>
<Button icon={<EllipsisVertical size={16}/>}/>
</Dropdown>
</div>
);
return (
<div className="flex items-center justify-between">
<Dropdown menu={{ items }}>
<Button icon={<EllipsisVertical size={16} />} />
</Dropdown>
</div>
);
};
export default CardViewMenu;
export default observer(CardViewMenu);

View file

@ -3,7 +3,7 @@ import { useStore } from 'App/mstore';
import { Loader, NoContent } from 'UI';
import WidgetPreview from '../WidgetPreview';
import WidgetSessions from '../WidgetSessions';
import { useObserver } from 'mobx-react-lite';
import { observer } from 'mobx-react-lite';
import { dashboardMetricDetails, metricDetails, withSiteId } from 'App/routes';
import Breadcrumb from 'Shared/Breadcrumb';
import { FilterKey } from 'Types/filter/filterType';
@ -16,7 +16,7 @@ import {
FUNNEL,
INSIGHTS,
USER_PATH,
RETENTION
RETENTION,
} from 'App/constants/card';
import CardUserList from '../CardUserList/CardUserList';
import WidgetViewHeader from 'Components/Dashboard/components/WidgetView/WidgetViewHeader';
@ -34,16 +34,16 @@ interface Props {
function WidgetView(props: Props) {
const {
match: {
params: { siteId, dashboardId, metricId }
}
params: { siteId, dashboardId, metricId },
},
} = props;
const { metricStore, dashboardStore } = useStore();
const widget = useObserver(() => metricStore.instance);
const loading = useObserver(() => metricStore.isLoading);
const widget = metricStore.instance;
const loading = metricStore.isLoading;
const [expanded, setExpanded] = useState(!metricId || metricId === 'create');
const hasChanged = useObserver(() => widget.hasChanged);
const dashboards = useObserver(() => dashboardStore.dashboards);
const dashboard = useObserver(() => dashboards.find((d: any) => d.dashboardId == dashboardId));
const hasChanged = widget.hasChanged;
const dashboards = dashboardStore.dashboards;
const dashboard = dashboards.find((d: any) => d.dashboardId == dashboardId);
const dashboardName = dashboard ? dashboard.name : null;
const [metricNotFound, setMetricNotFound] = useState(false);
const history = useHistory();
@ -83,24 +83,32 @@ function WidgetView(props: Props) {
if (wasCreating) {
if (parseInt(dashboardId, 10) > 0) {
history.replace(
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
withSiteId(
dashboardMetricDetails(dashboardId, savedMetric.metricId),
siteId
)
);
void dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
[savedMetric.metricId]
);
} else {
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId));
history.replace(
withSiteId(metricDetails(savedMetric.metricId), siteId)
);
}
}
};
return useObserver(() => (
return (
<Loader loading={loading}>
<Prompt
when={hasChanged}
message={(location: any) => {
if (location.pathname.includes('/metrics/') || location.pathname.includes('/metric/')) {
if (
location.pathname.includes('/metrics/') ||
location.pathname.includes('/metric/')
) {
return true;
}
return 'You have unsaved changes. Are you sure you want to leave?';
@ -112,9 +120,11 @@ function WidgetView(props: Props) {
items={[
{
label: dashboardName ? dashboardName : 'Cards',
to: dashboardId ? withSiteId('/dashboard/' + dashboardId, siteId) : withSiteId('/metrics', siteId)
to: dashboardId
? withSiteId('/dashboard/' + dashboardId, siteId)
: withSiteId('/metrics', siteId),
},
{ label: widget.name }
{ label: widget.name },
]}
/>
<NoContent
@ -131,21 +141,22 @@ function WidgetView(props: Props) {
<WidgetFormNew />
<WidgetPreview name={widget.name} isEditing={expanded} />
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && (
(widget.metricType === TABLE
|| widget.metricType === TIMESERIES
|| widget.metricType === HEATMAP
|| widget.metricType === INSIGHTS
|| widget.metricType === FUNNEL
|| widget.metricType === USER_PATH) ?
<WidgetSessions /> : null
)}
{widget.metricOf !== FilterKey.SESSIONS &&
widget.metricOf !== FilterKey.ERRORS &&
(widget.metricType === TABLE ||
widget.metricType === TIMESERIES ||
widget.metricType === HEATMAP ||
widget.metricType === INSIGHTS ||
widget.metricType === FUNNEL ||
widget.metricType === USER_PATH ? (
<WidgetSessions />
) : null)}
{widget.metricType === RETENTION && <CardUserList />}
</Space>
</NoContent>
</div>
</Loader>
));
);
}
export default WidgetView;
export default observer(WidgetView);

View file

@ -160,7 +160,17 @@ export default class MetricStore {
}
changeType(value: string) {
const obj: any = { metricType: value };
const defaultData = {
sessionId: '',
sessions: [],
issues: [],
total: 0,
chart: [],
namesMap: {},
avg: 0,
percentiles: []
};
const obj: any = { metricType: value, data: defaultData };
obj.series = this.instance.series;
obj.series = obj.series.slice(0, 1);

View file

@ -261,6 +261,7 @@ export default class Widget {
}
update(data: any) {
console.log(this.data, data.data)
runInAction(() => {
Object.assign(this, data);
});