IO-3373 Dashboard Errors on Large Datasets
Signed-off-by: Allan Carr <allan@imexsystems.ca>
This commit is contained in:
@@ -2,11 +2,13 @@ import { gql } from "@apollo/client";
|
|||||||
import dayjs from "../../utils/day.js";
|
import dayjs from "../../utils/day.js";
|
||||||
import componentList from "./componentList.js";
|
import componentList from "./componentList.js";
|
||||||
|
|
||||||
const createDashboardQuery = (state) => {
|
const createDashboardQuery = (items) => {
|
||||||
const componentBasedAdditions =
|
const componentBasedAdditions =
|
||||||
state &&
|
Array.isArray(items) &&
|
||||||
Array.isArray(state.layout) &&
|
items
|
||||||
state.layout.map((item) => componentList[item.i].gqlFragment || "").join("");
|
.map((item) => (componentList[item.i] && componentList[item.i].gqlFragment) || "")
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("");
|
||||||
return gql`
|
return gql`
|
||||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
||||||
monthly_sales: jobs(where: {_and: [
|
monthly_sales: jobs(where: {_and: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||||
import { cloneDeep, isEmpty } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Dropdown, Space } from "antd";
|
import { Button, Dropdown, Space } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
@@ -34,14 +34,25 @@ const mapDispatchToProps = () => ({
|
|||||||
|
|
||||||
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState(() => {
|
||||||
...(bodyshop.associations[0].user.dashboardlayout
|
const persisted = bodyshop.associations[0].user.dashboardlayout;
|
||||||
? bodyshop.associations[0].user.dashboardlayout
|
// Normalize persisted structure to avoid malformed shapes that can cause recursive layout recalculations
|
||||||
: { items: [], layout: {}, layouts: [] })
|
if (persisted) {
|
||||||
|
return {
|
||||||
|
items: Array.isArray(persisted.items) ? persisted.items : [],
|
||||||
|
layout: Array.isArray(persisted.layout) ? persisted.layout : [],
|
||||||
|
layouts: typeof persisted.layouts === "object" && !Array.isArray(persisted.layouts) ? persisted.layouts : {},
|
||||||
|
cols: persisted.cols
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { items: [], layout: [], layouts: {}, cols: 12 };
|
||||||
});
|
});
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(createDashboardQuery(state), {
|
// Memoize the query document so Apollo doesn't treat each render as a brand-new query causing continuous re-fetches
|
||||||
|
const dashboardQueryDoc = useMemo(() => createDashboardQuery(state.items), [state.items]);
|
||||||
|
|
||||||
|
const { loading, error, data, refetch } = useQuery(dashboardQueryDoc, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
@@ -49,21 +60,32 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||||
|
|
||||||
const handleLayoutChange = async (layout, layouts) => {
|
const handleLayoutChange = async (layout, layouts) => {
|
||||||
logImEXEvent("dashboard_change_layout");
|
try {
|
||||||
|
logImEXEvent("dashboard_change_layout");
|
||||||
|
|
||||||
setState({ ...state, layout, layouts });
|
setState((prev) => ({ ...prev, layout, layouts }));
|
||||||
|
|
||||||
const result = await updateLayout({
|
const result = await updateLayout({
|
||||||
variables: {
|
variables: {
|
||||||
email: currentUser.email,
|
email: currentUser.email,
|
||||||
layout: { ...state, layout, layouts }
|
layout: { ...state, layout, layouts }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.errors && result.errors.length) {
|
||||||
|
const errorMessages = result.errors.map((e) => e?.message || String(e));
|
||||||
|
notification.error({
|
||||||
|
message: t("dashboard.errors.updatinglayout", {
|
||||||
|
message: errorMessages.join("; ")
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
} catch (err) {
|
||||||
|
// Catch any unexpected errors (including potential cyclic JSON issues) so the promise never rejects unhandled
|
||||||
if (!isEmpty(result?.errors)) {
|
console.error("Dashboard layout update failed", err);
|
||||||
notification.error({
|
notification.error({
|
||||||
message: t("dashboard.errors.updatinglayout", {
|
message: t("dashboard.errors.updatinglayout", {
|
||||||
message: JSON.stringify(result.errors)
|
message: err?.message || String(err)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -80,19 +102,26 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddComponent = (e) => {
|
const handleAddComponent = (e) => {
|
||||||
logImEXEvent("dashboard_add_component", { name: e });
|
// Avoid passing the full AntD menu click event (contains circular refs) to analytics
|
||||||
setState({
|
logImEXEvent("dashboard_add_component", { key: e.key });
|
||||||
...state,
|
const compSpec = componentList[e.key] || {};
|
||||||
items: [
|
const minW = compSpec.minW || 1;
|
||||||
...state.items,
|
const minH = compSpec.minH || 1;
|
||||||
|
const baseW = compSpec.w || 2;
|
||||||
|
const baseH = compSpec.h || 2;
|
||||||
|
setState((prev) => {
|
||||||
|
const nextItems = [
|
||||||
|
...prev.items,
|
||||||
{
|
{
|
||||||
i: e.key,
|
i: e.key,
|
||||||
x: (state.items.length * 2) % (state.cols || 12),
|
// Position near bottom: use a large y so RGL places it last without triggering cascading relayout loops
|
||||||
y: 99, // puts it at the bottom
|
x: (prev.items.length * 2) % (prev.cols || 12),
|
||||||
w: componentList[e.key].w || 2,
|
y: 1000,
|
||||||
h: componentList[e.key].h || 2
|
w: Math.max(baseW, minW),
|
||||||
|
h: Math.max(baseH, minH)
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
|
return { ...prev, items: nextItems };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,25 +159,33 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
className="layout"
|
className="layout"
|
||||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||||
width="100%"
|
|
||||||
layouts={state.layouts}
|
layouts={state.layouts}
|
||||||
onLayoutChange={handleLayoutChange}
|
onLayoutChange={handleLayoutChange}
|
||||||
>
|
>
|
||||||
{state.items.map((item) => {
|
{state.items.map((item) => {
|
||||||
const TheComponent = componentList[item.i].component;
|
const spec = componentList[item.i] || {};
|
||||||
|
const TheComponent = spec.component;
|
||||||
|
const minW = spec.minW || 1;
|
||||||
|
const minH = spec.minH || 1;
|
||||||
|
// Ensure current width/height respect minimums to avoid react-grid-layout prop warnings
|
||||||
|
const safeItem = {
|
||||||
|
...item,
|
||||||
|
w: Math.max(item.w || spec.w || minW, minW),
|
||||||
|
h: Math.max(item.h || spec.h || minH, minH)
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.i}
|
key={safeItem.i}
|
||||||
data-grid={{
|
data-grid={{
|
||||||
...item,
|
...safeItem,
|
||||||
minH: componentList[item.i].minH || 1,
|
minH,
|
||||||
minW: componentList[item.i].minW || 1
|
minW
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LoadingSkeleton loading={loading}>
|
<LoadingSkeleton loading={loading}>
|
||||||
<Icon
|
<Icon
|
||||||
component={MdClose}
|
component={MdClose}
|
||||||
key={item.i}
|
key={safeItem.i}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
zIndex: "2",
|
zIndex: "2",
|
||||||
@@ -156,9 +193,9 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
top: ".25rem",
|
top: ".25rem",
|
||||||
cursor: "pointer"
|
cursor: "pointer"
|
||||||
}}
|
}}
|
||||||
onClick={() => handleRemoveComponent(item.i)}
|
onClick={() => handleRemoveComponent(safeItem.i)}
|
||||||
/>
|
/>
|
||||||
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboardData} />
|
{TheComponent && <TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboardData} />}
|
||||||
</LoadingSkeleton>
|
</LoadingSkeleton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user