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 React from 'react';
import { formatTimeOrDate } from 'App/date';
import { Button, Table } from 'antd';
import type { TableProps } from 'antd';
import CustomTooltip from "../CustomChartTooltip"; import CustomTooltip from "../CustomChartTooltip";
import { Eye, EyeOff } from 'lucide-react';
import { Styles } from '../../common'; import { Styles } from '../../common';
import { import {
ResponsiveContainer, ResponsiveContainer,
@ -16,7 +11,7 @@ import {
Line, Line,
Legend, Legend,
} from 'recharts'; } from 'recharts';
import cn from 'classnames'; import { observer } from 'mobx-react-lite';
interface Props { interface Props {
data: any; data: any;
@ -34,7 +29,6 @@ function CustomMetricLineChart(props: Props) {
const { const {
data = { chart: [], namesMap: [] }, data = { chart: [], namesMap: [] },
compData = { chart: [], namesMap: [] }, compData = { chart: [], namesMap: [] },
params,
colors, colors,
onClick = () => null, onClick = () => null,
yaxis = { ...Styles.yaxis }, 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 React from 'react';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import WidgetWrapperNew from 'Components/Dashboard/components/WidgetWrapper/WidgetWrapperNew'; import WidgetWrapperNew from 'Components/Dashboard/components/WidgetWrapper/WidgetWrapperNew';
import { Loader } from 'UI';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import AddCardSection from '../AddCardSection/AddCardSection'; import AddCardSection from '../AddCardSection/AddCardSection';
import cn from 'classnames'; import cn from 'classnames';

View file

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

View file

@ -1,98 +1,103 @@
import {useHistory} from "react-router"; import { useHistory } from 'react-router';
import {useStore} from "App/mstore"; import { useStore } from 'App/mstore';
import {useObserver} from "mobx-react-lite"; import { observer } from 'mobx-react-lite';
import {Button, Dropdown, MenuProps, message, Modal} from "antd"; import { Button, Dropdown, MenuProps, message, Modal } from 'antd';
import {BellIcon, EllipsisVertical, TrashIcon} from "lucide-react"; import { BellIcon, EllipsisVertical, TrashIcon } from 'lucide-react';
import {toast} from "react-toastify"; import { toast } from 'react-toastify';
import React from "react"; import React from 'react';
import {useModal} from "Components/ModalContext"; import { useModal } from 'Components/ModalContext';
import AlertFormModal from "Components/Alerts/AlertFormModal/AlertFormModal"; import AlertFormModal from 'Components/Alerts/AlertFormModal/AlertFormModal';
const CardViewMenu = () => { const CardViewMenu = () => {
const history = useHistory(); const history = useHistory();
const {alertsStore, dashboardStore, metricStore} = useStore(); const { alertsStore, metricStore } = useStore();
const widget = useObserver(() => metricStore.instance); const widget = metricStore.instance;
const {openModal, closeModal} = useModal(); const { openModal, closeModal } = useModal();
const showAlertModal = () => { const showAlertModal = () => {
const seriesId = widget.series[0] && widget.series[0].seriesId || ''; const seriesId = (widget.series[0] && widget.series[0].seriesId) || '';
alertsStore.init({query: {left: seriesId}}) alertsStore.init({ query: { left: seriesId } });
openModal(<AlertFormModal openModal(<AlertFormModal onClose={closeModal} />, {
onClose={closeModal} // title: 'Set Alerts',
/>, { placement: 'right',
// title: 'Set Alerts', width: 620,
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'] = [ const onClick: MenuProps['onClick'] = ({ key }) => {
{ if (key === 'alert') {
key: 'alert', message.info('Set Alerts');
label: "Set Alerts", } else if (key === 'remove') {
icon: <BellIcon size={16}/>, Modal.confirm({
disabled: !widget.exists() || widget.metricType === 'predefined', title: 'Are you sure you want to remove this card?',
onClick: showAlertModal, icon: null,
}, // content: 'Bla bla ...',
{ footer: (_, { OkBtn, CancelBtn }) => (
key: 'remove', <>
label: 'Delete', <CancelBtn />
icon: <TrashIcon size={16}/>, <OkBtn />
onClick: () => { </>
Modal.confirm({ ),
title: 'Confirm Card Deletion', onOk: () => {
icon: null, metricStore
content:'Are you sure you want to remove this card? This action is permanent and cannot be undone.', .delete(widget)
footer: (_, {OkBtn, CancelBtn}) => ( .then((r) => {
<> history.goBack();
<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');
});
},
}) })
} .catch(() => {
}; toast.error('Failed to remove card');
});
},
});
}
};
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Dropdown menu={{items}}> <Dropdown menu={{ items }}>
<Button icon={<EllipsisVertical size={16}/>}/> <Button icon={<EllipsisVertical size={16} />} />
</Dropdown> </Dropdown>
</div> </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 { Loader, NoContent } from 'UI';
import WidgetPreview from '../WidgetPreview'; import WidgetPreview from '../WidgetPreview';
import WidgetSessions from '../WidgetSessions'; import WidgetSessions from '../WidgetSessions';
import { useObserver } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { dashboardMetricDetails, metricDetails, withSiteId } from 'App/routes'; import { dashboardMetricDetails, metricDetails, withSiteId } from 'App/routes';
import Breadcrumb from 'Shared/Breadcrumb'; import Breadcrumb from 'Shared/Breadcrumb';
import { FilterKey } from 'Types/filter/filterType'; import { FilterKey } from 'Types/filter/filterType';
@ -16,7 +16,7 @@ import {
FUNNEL, FUNNEL,
INSIGHTS, INSIGHTS,
USER_PATH, USER_PATH,
RETENTION RETENTION,
} from 'App/constants/card'; } from 'App/constants/card';
import CardUserList from '../CardUserList/CardUserList'; import CardUserList from '../CardUserList/CardUserList';
import WidgetViewHeader from 'Components/Dashboard/components/WidgetView/WidgetViewHeader'; import WidgetViewHeader from 'Components/Dashboard/components/WidgetView/WidgetViewHeader';
@ -34,16 +34,16 @@ interface Props {
function WidgetView(props: Props) { function WidgetView(props: Props) {
const { const {
match: { match: {
params: { siteId, dashboardId, metricId } params: { siteId, dashboardId, metricId },
} },
} = props; } = props;
const { metricStore, dashboardStore } = useStore(); const { metricStore, dashboardStore } = useStore();
const widget = useObserver(() => metricStore.instance); const widget = metricStore.instance;
const loading = useObserver(() => metricStore.isLoading); const loading = metricStore.isLoading;
const [expanded, setExpanded] = useState(!metricId || metricId === 'create'); const [expanded, setExpanded] = useState(!metricId || metricId === 'create');
const hasChanged = useObserver(() => widget.hasChanged); const hasChanged = widget.hasChanged;
const dashboards = useObserver(() => dashboardStore.dashboards); const dashboards = dashboardStore.dashboards;
const dashboard = useObserver(() => dashboards.find((d: any) => d.dashboardId == dashboardId)); const dashboard = dashboards.find((d: any) => d.dashboardId == dashboardId);
const dashboardName = dashboard ? dashboard.name : null; const dashboardName = dashboard ? dashboard.name : null;
const [metricNotFound, setMetricNotFound] = useState(false); const [metricNotFound, setMetricNotFound] = useState(false);
const history = useHistory(); const history = useHistory();
@ -83,24 +83,32 @@ function WidgetView(props: Props) {
if (wasCreating) { if (wasCreating) {
if (parseInt(dashboardId, 10) > 0) { if (parseInt(dashboardId, 10) > 0) {
history.replace( history.replace(
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId) withSiteId(
dashboardMetricDetails(dashboardId, savedMetric.metricId),
siteId
)
); );
void dashboardStore.addWidgetToDashboard( void dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10))!, dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
[savedMetric.metricId] [savedMetric.metricId]
); );
} else { } else {
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId)); history.replace(
withSiteId(metricDetails(savedMetric.metricId), siteId)
);
} }
} }
}; };
return useObserver(() => ( return (
<Loader loading={loading}> <Loader loading={loading}>
<Prompt <Prompt
when={hasChanged} when={hasChanged}
message={(location: any) => { message={(location: any) => {
if (location.pathname.includes('/metrics/') || location.pathname.includes('/metric/')) { if (
location.pathname.includes('/metrics/') ||
location.pathname.includes('/metric/')
) {
return true; return true;
} }
return 'You have unsaved changes. Are you sure you want to leave?'; return 'You have unsaved changes. Are you sure you want to leave?';
@ -112,9 +120,11 @@ function WidgetView(props: Props) {
items={[ items={[
{ {
label: dashboardName ? dashboardName : 'Cards', 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 <NoContent
@ -131,21 +141,22 @@ function WidgetView(props: Props) {
<WidgetFormNew /> <WidgetFormNew />
<WidgetPreview name={widget.name} isEditing={expanded} /> <WidgetPreview name={widget.name} isEditing={expanded} />
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && ( {widget.metricOf !== FilterKey.SESSIONS &&
(widget.metricType === TABLE widget.metricOf !== FilterKey.ERRORS &&
|| widget.metricType === TIMESERIES (widget.metricType === TABLE ||
|| widget.metricType === HEATMAP widget.metricType === TIMESERIES ||
|| widget.metricType === INSIGHTS widget.metricType === HEATMAP ||
|| widget.metricType === FUNNEL widget.metricType === INSIGHTS ||
|| widget.metricType === USER_PATH) ? widget.metricType === FUNNEL ||
<WidgetSessions /> : null widget.metricType === USER_PATH ? (
)} <WidgetSessions />
) : null)}
{widget.metricType === RETENTION && <CardUserList />} {widget.metricType === RETENTION && <CardUserList />}
</Space> </Space>
</NoContent> </NoContent>
</div> </div>
</Loader> </Loader>
)); );
} }
export default WidgetView; export default observer(WidgetView);

View file

@ -160,7 +160,17 @@ export default class MetricStore {
} }
changeType(value: string) { 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 = this.instance.series;
obj.series = obj.series.slice(0, 1); obj.series = obj.series.slice(0, 1);

View file

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