@dashadmin/dash-reports 1.3.72 → 1.3.74-srcfree.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ReportFilters.d.ts +9 -0
- package/dist/ReportFiltersDrawer.d.ts +12 -0
- package/dist/ReportPage.d.ts +9 -0
- package/dist/ReportsDashboard.d.ts +7 -0
- package/dist/ReportsIndex.d.ts +7 -0
- package/dist/ReportsModule.d.ts +7 -0
- package/dist/TenantDashboard.d.ts +12 -0
- package/dist/adapters/mui/BarWidget.d.ts +3 -0
- package/dist/adapters/mui/DoughnutWidget.d.ts +3 -0
- package/dist/adapters/mui/FunnelWidget.d.ts +3 -0
- package/dist/adapters/mui/KpiWidget.d.ts +3 -0
- package/dist/adapters/mui/LineWidget.d.ts +3 -0
- package/dist/adapters/mui/PieWidget.d.ts +3 -0
- package/dist/adapters/mui/PreviewWidget.d.ts +10 -0
- package/dist/adapters/mui/TableWidget.d.ts +16 -0
- package/dist/adapters/mui/chartSetup.d.ts +2 -0
- package/dist/adapters/mui/chartTheme.d.ts +71 -0
- package/dist/adapters/mui/index.d.ts +12 -0
- package/dist/adapters/mui/palette.d.ts +9 -0
- package/dist/adapters/mui/useChartData.d.ts +14 -0
- package/dist/core/formatBucket.d.ts +4 -0
- package/dist/core/label.d.ts +3 -0
- package/dist/core/queryParams.d.ts +3 -0
- package/dist/core/types.d.ts +94 -0
- package/dist/core/useDashboard.d.ts +50 -0
- package/dist/core/useReportExport.d.ts +7 -0
- package/dist/core/useReportList.d.ts +7 -0
- package/dist/core/useReportPreview.d.ts +7 -0
- package/dist/core/useReportSeries.d.ts +8 -0
- package/dist/core/useReportSpec.d.ts +9 -0
- package/dist/core/useReportTable.d.ts +10 -0
- package/dist/core/visualizationLayout.d.ts +13 -0
- package/dist/core/visualizationRegistry.d.ts +14 -0
- package/dist/createReportsResource.d.ts +40 -0
- package/dist/i18n/en.d.ts +83 -0
- package/dist/i18n/es.d.ts +83 -0
- package/{src/index.tsx → dist/index.d.ts} +3 -31
- package/package.json +13 -10
- package/src/ReportFilters.tsx +0 -145
- package/src/ReportFiltersDrawer.tsx +0 -73
- package/src/ReportPage.tsx +0 -280
- package/src/ReportsDashboard.tsx +0 -61
- package/src/ReportsIndex.tsx +0 -113
- package/src/ReportsModule.tsx +0 -42
- package/src/TenantDashboard.tsx +0 -290
- package/src/adapters/mui/BarWidget.tsx +0 -66
- package/src/adapters/mui/DoughnutWidget.tsx +0 -112
- package/src/adapters/mui/FunnelWidget.tsx +0 -108
- package/src/adapters/mui/KpiWidget.tsx +0 -123
- package/src/adapters/mui/LineWidget.tsx +0 -70
- package/src/adapters/mui/PieWidget.tsx +0 -16
- package/src/adapters/mui/PreviewWidget.tsx +0 -123
- package/src/adapters/mui/TableWidget.tsx +0 -141
- package/src/adapters/mui/chartSetup.ts +0 -53
- package/src/adapters/mui/chartTheme.ts +0 -84
- package/src/adapters/mui/index.ts +0 -29
- package/src/adapters/mui/palette.ts +0 -85
- package/src/adapters/mui/useChartData.ts +0 -62
- package/src/core/formatBucket.ts +0 -73
- package/src/core/label.ts +0 -28
- package/src/core/queryParams.ts +0 -27
- package/src/core/types.ts +0 -157
- package/src/core/useDashboard.ts +0 -161
- package/src/core/useReportExport.ts +0 -68
- package/src/core/useReportList.ts +0 -51
- package/src/core/useReportPreview.ts +0 -68
- package/src/core/useReportSeries.ts +0 -59
- package/src/core/useReportSpec.ts +0 -79
- package/src/core/useReportTable.ts +0 -71
- package/src/core/visualizationLayout.ts +0 -34
- package/src/core/visualizationRegistry.ts +0 -39
- package/src/createReportsResource.tsx +0 -81
- package/src/i18n/en.ts +0 -95
- package/src/i18n/es.ts +0 -84
- /package/{src/i18n/index.ts → dist/i18n/index.d.ts} +0 -0
package/src/core/useDashboard.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useAxios } from 'dash-axios-hook';
|
|
3
|
-
import type { ReportMetric, ReportResult, VisualizationType } from './types';
|
|
4
|
-
|
|
5
|
-
export interface DashboardPeriod {
|
|
6
|
-
id: string;
|
|
7
|
-
label: string;
|
|
8
|
-
start: string;
|
|
9
|
-
end: string;
|
|
10
|
-
isOpen: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface DashboardWidget {
|
|
14
|
-
key: string;
|
|
15
|
-
reportKey: string;
|
|
16
|
-
title: string;
|
|
17
|
-
titleText?: string;
|
|
18
|
-
pivot?: string;
|
|
19
|
-
metrics?: string[];
|
|
20
|
-
visualization?: VisualizationType;
|
|
21
|
-
size?: 'sm' | 'md' | 'lg' | 'full';
|
|
22
|
-
result?: ReportResult & { metrics?: ReportMetric[] };
|
|
23
|
-
error?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface DashboardTenant {
|
|
27
|
-
tenant: { id: string; name: string };
|
|
28
|
-
period: DashboardPeriod | null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The tenants whose dashboards this user should see, each with its period.
|
|
33
|
-
*
|
|
34
|
-
* One cheap call so the page knows how many tenant panels to mount before any
|
|
35
|
-
* of them fetches data — a TenancyAdmin sees one panel per restaurant, a
|
|
36
|
-
* tenant user sees exactly one.
|
|
37
|
-
*/
|
|
38
|
-
export function useDashboardTenants() {
|
|
39
|
-
const axios = useAxios();
|
|
40
|
-
// See useReportList: useAxios() is unmemoized and must stay out of deps.
|
|
41
|
-
const axiosRef = useRef(axios);
|
|
42
|
-
axiosRef.current = axios;
|
|
43
|
-
|
|
44
|
-
const [tenants, setTenants] = useState<DashboardTenant[]>([]);
|
|
45
|
-
const [widgets, setWidgets] = useState<DashboardWidget[]>([]);
|
|
46
|
-
const [loading, setLoading] = useState(true);
|
|
47
|
-
const [error, setError] = useState<Error | null>(null);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
let alive = true;
|
|
51
|
-
|
|
52
|
-
axiosRef.current
|
|
53
|
-
.get('report/dashboard')
|
|
54
|
-
.then((response: { data: { data: DashboardTenant[]; widgets: DashboardWidget[] } }) => {
|
|
55
|
-
if (!alive) return;
|
|
56
|
-
setTenants(response.data?.data ?? []);
|
|
57
|
-
setWidgets(response.data?.widgets ?? []);
|
|
58
|
-
})
|
|
59
|
-
.catch((e: Error) => {
|
|
60
|
-
if (alive) setError(e);
|
|
61
|
-
})
|
|
62
|
-
.finally(() => {
|
|
63
|
-
if (alive) setLoading(false);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return () => {
|
|
67
|
-
alive = false;
|
|
68
|
-
};
|
|
69
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
|
-
}, []);
|
|
71
|
-
|
|
72
|
-
return { tenants, widgets, loading, error };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface DashboardData {
|
|
76
|
-
tenantId: string;
|
|
77
|
-
period: DashboardPeriod;
|
|
78
|
-
/** 'live' | 'live-no-snapshot' | 'snapshot' — see the backend controller. */
|
|
79
|
-
source: string;
|
|
80
|
-
widgets: DashboardWidget[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* One tenant's widgets for one period.
|
|
85
|
-
*
|
|
86
|
-
* Fetched per tenant rather than in one batch so panels load in parallel and
|
|
87
|
-
* fail independently — a tenant whose data errors shows one broken panel, not
|
|
88
|
-
* an empty dashboard.
|
|
89
|
-
*/
|
|
90
|
-
export function useTenantDashboard(tenantId: string | null, period = 'current') {
|
|
91
|
-
const axios = useAxios();
|
|
92
|
-
const axiosRef = useRef(axios);
|
|
93
|
-
axiosRef.current = axios;
|
|
94
|
-
|
|
95
|
-
const [data, setData] = useState<DashboardData | null>(null);
|
|
96
|
-
const [loading, setLoading] = useState(true);
|
|
97
|
-
const [error, setError] = useState<Error | null>(null);
|
|
98
|
-
|
|
99
|
-
const sequence = useRef(0);
|
|
100
|
-
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (!tenantId) return;
|
|
103
|
-
|
|
104
|
-
const ticket = ++sequence.current;
|
|
105
|
-
setLoading(true);
|
|
106
|
-
setError(null);
|
|
107
|
-
|
|
108
|
-
axiosRef.current
|
|
109
|
-
.get(`report/dashboard/${encodeURIComponent(tenantId)}`, { params: { period } })
|
|
110
|
-
.then((response: { data: DashboardData }) => {
|
|
111
|
-
if (ticket === sequence.current) setData(response.data);
|
|
112
|
-
})
|
|
113
|
-
.catch((e: Error) => {
|
|
114
|
-
if (ticket === sequence.current) setError(e);
|
|
115
|
-
})
|
|
116
|
-
.finally(() => {
|
|
117
|
-
if (ticket === sequence.current) setLoading(false);
|
|
118
|
-
});
|
|
119
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
120
|
-
}, [tenantId, period]);
|
|
121
|
-
|
|
122
|
-
return { data, loading, error };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** The period picker's options for one tenant. */
|
|
126
|
-
export function useTenantPeriods(tenantId: string | null) {
|
|
127
|
-
const axios = useAxios();
|
|
128
|
-
const axiosRef = useRef(axios);
|
|
129
|
-
axiosRef.current = axios;
|
|
130
|
-
|
|
131
|
-
const [periods, setPeriods] = useState<DashboardPeriod[]>([]);
|
|
132
|
-
const [loading, setLoading] = useState(false);
|
|
133
|
-
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (!tenantId) return;
|
|
136
|
-
|
|
137
|
-
let alive = true;
|
|
138
|
-
setLoading(true);
|
|
139
|
-
|
|
140
|
-
axiosRef.current
|
|
141
|
-
.get(`report/dashboard/${encodeURIComponent(tenantId)}/periods`)
|
|
142
|
-
.then((response: { data: { data: DashboardPeriod[] } }) => {
|
|
143
|
-
if (alive) setPeriods(response.data?.data ?? []);
|
|
144
|
-
})
|
|
145
|
-
.catch(() => {
|
|
146
|
-
// A missing period list is not worth surfacing — the dashboard still
|
|
147
|
-
// works on the current period, which is the default.
|
|
148
|
-
if (alive) setPeriods([]);
|
|
149
|
-
})
|
|
150
|
-
.finally(() => {
|
|
151
|
-
if (alive) setLoading(false);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
return () => {
|
|
155
|
-
alive = false;
|
|
156
|
-
};
|
|
157
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
158
|
-
}, [tenantId]);
|
|
159
|
-
|
|
160
|
-
return { periods, loading };
|
|
161
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from 'react';
|
|
2
|
-
import { useAxios } from 'dash-axios-hook';
|
|
3
|
-
import { toQueryParams } from './queryParams';
|
|
4
|
-
import type { ReportFilterState } from './types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Download a report in one of the formats its spec advertises.
|
|
8
|
-
*
|
|
9
|
-
* `responseType: 'blob'` is an AXIOS REQUEST OPTION, not a query parameter.
|
|
10
|
-
* The legacy Stats page put it inside `params`, so it was serialised into the
|
|
11
|
-
* URL as `?responseType=blob` and axios still parsed the response as text —
|
|
12
|
-
* which corrupted the binary and is one reason that export never worked. The
|
|
13
|
-
* distinction is easy to miss because nothing errors; you just get a broken
|
|
14
|
-
* file.
|
|
15
|
-
*/
|
|
16
|
-
export function useReportExport(reportKey: string) {
|
|
17
|
-
const axios = useAxios();
|
|
18
|
-
// See useReportList: useAxios() is unmemoized and must stay out of deps.
|
|
19
|
-
const axiosRef = useRef(axios);
|
|
20
|
-
axiosRef.current = axios;
|
|
21
|
-
|
|
22
|
-
const [exporting, setExporting] = useState(false);
|
|
23
|
-
const [error, setError] = useState<Error | null>(null);
|
|
24
|
-
|
|
25
|
-
const exportReport = useCallback(
|
|
26
|
-
async (filters: ReportFilterState, format = 'xlsx') => {
|
|
27
|
-
setExporting(true);
|
|
28
|
-
setError(null);
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const response = await axiosRef.current.get(`report/${encodeURIComponent(reportKey)}/export`, {
|
|
32
|
-
params: { ...toQueryParams(filters), format },
|
|
33
|
-
responseType: 'blob',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const blob = new Blob([response.data], {
|
|
37
|
-
type: String(response.headers?.['content-type'] ?? 'application/octet-stream'),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Prefer the filename the server chose — it carries the report key and
|
|
41
|
-
// the actual date range, which a client-side guess would drift from.
|
|
42
|
-
const disposition = String(response.headers?.['content-disposition'] ?? '');
|
|
43
|
-
const match = disposition.match(/filename\*?=(?:UTF-8'')?"?([^";]+)"?/i);
|
|
44
|
-
const filename = match ? decodeURIComponent(match[1]) : `${reportKey}.${format}`;
|
|
45
|
-
|
|
46
|
-
const url = URL.createObjectURL(blob);
|
|
47
|
-
const link = document.createElement('a');
|
|
48
|
-
link.href = url;
|
|
49
|
-
link.download = filename;
|
|
50
|
-
document.body.appendChild(link);
|
|
51
|
-
link.click();
|
|
52
|
-
link.remove();
|
|
53
|
-
// Revoked on the next tick: revoking synchronously can cancel the
|
|
54
|
-
// download in some browsers before it has read the blob.
|
|
55
|
-
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
setError(e as Error);
|
|
58
|
-
} finally {
|
|
59
|
-
setExporting(false);
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
[reportKey],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
return { exporting, error, exportReport };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export default useReportExport;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useAxios } from 'dash-axios-hook';
|
|
3
|
-
import type { ReportListEntry } from './types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Every report this caller may see.
|
|
7
|
-
*
|
|
8
|
-
* The backend has already filtered by role, so whatever comes back is safe to
|
|
9
|
-
* render as a menu.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* useAxios() builds a NEW axios instance on every render (dash-axios-hook calls
|
|
13
|
-
* axios.create() unmemoized), so it must never appear in a dependency array:
|
|
14
|
-
* a fresh reference each render re-fires the effect, which sets state, which
|
|
15
|
-
* re-renders — an unbroken request loop against the API. The instance is held
|
|
16
|
-
* in a ref instead, and the effect depends only on values that are stable by
|
|
17
|
-
* value.
|
|
18
|
-
*/
|
|
19
|
-
export function useReportList() {
|
|
20
|
-
const axios = useAxios();
|
|
21
|
-
const axiosRef = useRef(axios);
|
|
22
|
-
axiosRef.current = axios;
|
|
23
|
-
const [reports, setReports] = useState<ReportListEntry[]>([]);
|
|
24
|
-
const [loading, setLoading] = useState(true);
|
|
25
|
-
const [error, setError] = useState<Error | null>(null);
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
let alive = true;
|
|
29
|
-
|
|
30
|
-
axiosRef.current
|
|
31
|
-
.get('report')
|
|
32
|
-
.then((response: { data: { data: ReportListEntry[] } }) => {
|
|
33
|
-
if (alive) setReports(response.data?.data ?? []);
|
|
34
|
-
})
|
|
35
|
-
.catch((e: Error) => {
|
|
36
|
-
if (alive) setError(e);
|
|
37
|
-
})
|
|
38
|
-
.finally(() => {
|
|
39
|
-
if (alive) setLoading(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
return () => {
|
|
43
|
-
alive = false;
|
|
44
|
-
};
|
|
45
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
-
}, []);
|
|
47
|
-
|
|
48
|
-
return { reports, loading, error };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export default useReportList;
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useAxios } from 'dash-axios-hook';
|
|
3
|
-
import type { Granularity, ReportResult } from './types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A small series for one report's list-page card.
|
|
7
|
-
*
|
|
8
|
-
* One request per card, fired in parallel and failing independently — a report
|
|
9
|
-
* whose preview errors shows a card without a chart rather than blanking the
|
|
10
|
-
* list. The engine this replaces fetched every report's full dataset plus one
|
|
11
|
-
* request per chart type before the index rendered at all (2N requests to draw
|
|
12
|
-
* a page nobody had chosen anything on yet); this is N cheap ones, at a coarse
|
|
13
|
-
* grain, and only for what is on screen.
|
|
14
|
-
*/
|
|
15
|
-
export function useReportPreview(
|
|
16
|
-
reportKey: string,
|
|
17
|
-
granularity: Granularity,
|
|
18
|
-
rangeDays: number,
|
|
19
|
-
enabled = true,
|
|
20
|
-
) {
|
|
21
|
-
const axios = useAxios();
|
|
22
|
-
// See useReportList: useAxios() is unmemoized and must stay out of deps.
|
|
23
|
-
const axiosRef = useRef(axios);
|
|
24
|
-
axiosRef.current = axios;
|
|
25
|
-
|
|
26
|
-
const [result, setResult] = useState<ReportResult | null>(null);
|
|
27
|
-
const [loading, setLoading] = useState(enabled);
|
|
28
|
-
const [error, setError] = useState<Error | null>(null);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!enabled || !reportKey) return;
|
|
32
|
-
|
|
33
|
-
let alive = true;
|
|
34
|
-
|
|
35
|
-
const end = new Date();
|
|
36
|
-
const start = new Date();
|
|
37
|
-
start.setDate(start.getDate() - (rangeDays || 30));
|
|
38
|
-
|
|
39
|
-
setLoading(true);
|
|
40
|
-
|
|
41
|
-
axiosRef.current
|
|
42
|
-
.get(`report/${encodeURIComponent(reportKey)}/series`, {
|
|
43
|
-
params: {
|
|
44
|
-
start: start.toISOString().slice(0, 10),
|
|
45
|
-
end: end.toISOString().slice(0, 10),
|
|
46
|
-
granularity,
|
|
47
|
-
},
|
|
48
|
-
})
|
|
49
|
-
.then((response: { data: ReportResult }) => {
|
|
50
|
-
if (alive) setResult(response.data);
|
|
51
|
-
})
|
|
52
|
-
.catch((e: Error) => {
|
|
53
|
-
if (alive) setError(e);
|
|
54
|
-
})
|
|
55
|
-
.finally(() => {
|
|
56
|
-
if (alive) setLoading(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return () => {
|
|
60
|
-
alive = false;
|
|
61
|
-
};
|
|
62
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
-
}, [reportKey, granularity, rangeDays, enabled]);
|
|
64
|
-
|
|
65
|
-
return { result, loading, error };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export default useReportPreview;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useAxios } from 'dash-axios-hook';
|
|
3
|
-
import { toQueryParams } from './queryParams';
|
|
4
|
-
import type { ReportFilterState, ReportResult } from './types';
|
|
5
|
-
|
|
6
|
-
export interface UseReportSeriesResult {
|
|
7
|
-
result: ReportResult | null;
|
|
8
|
-
loading: boolean;
|
|
9
|
-
error: Error | null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Fetch the bucketed series for a report under the current filters.
|
|
14
|
-
*
|
|
15
|
-
* Refetches whenever the filters change, and discards responses that arrive
|
|
16
|
-
* out of order. Without the sequence guard, dragging a date range quickly
|
|
17
|
-
* leaves whichever request happened to finish last on screen, which is not
|
|
18
|
-
* necessarily the one the user is currently looking at.
|
|
19
|
-
*/
|
|
20
|
-
export function useReportSeries(
|
|
21
|
-
reportKey: string,
|
|
22
|
-
filters: ReportFilterState | null,
|
|
23
|
-
): UseReportSeriesResult {
|
|
24
|
-
const axios = useAxios();
|
|
25
|
-
// See useReportList: useAxios() is unmemoized and must stay out of deps.
|
|
26
|
-
const axiosRef = useRef(axios);
|
|
27
|
-
axiosRef.current = axios;
|
|
28
|
-
|
|
29
|
-
const [result, setResult] = useState<ReportResult | null>(null);
|
|
30
|
-
const [loading, setLoading] = useState(false);
|
|
31
|
-
const [error, setError] = useState<Error | null>(null);
|
|
32
|
-
|
|
33
|
-
const sequence = useRef(0);
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (!reportKey || !filters) return;
|
|
37
|
-
|
|
38
|
-
const ticket = ++sequence.current;
|
|
39
|
-
setLoading(true);
|
|
40
|
-
setError(null);
|
|
41
|
-
|
|
42
|
-
axiosRef.current
|
|
43
|
-
.get(`report/${encodeURIComponent(reportKey)}/series`, { params: toQueryParams(filters) })
|
|
44
|
-
.then((response: { data: ReportResult }) => {
|
|
45
|
-
if (ticket === sequence.current) setResult(response.data);
|
|
46
|
-
})
|
|
47
|
-
.catch((e: Error) => {
|
|
48
|
-
if (ticket === sequence.current) setError(e);
|
|
49
|
-
})
|
|
50
|
-
.finally(() => {
|
|
51
|
-
if (ticket === sequence.current) setLoading(false);
|
|
52
|
-
});
|
|
53
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
|
-
}, [reportKey, JSON.stringify(filters)]);
|
|
55
|
-
|
|
56
|
-
return { result, loading, error };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export default useReportSeries;
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useAxios } from 'dash-axios-hook';
|
|
3
|
-
import type { ReportSpec } from './types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Fetch a report's spec.
|
|
7
|
-
*
|
|
8
|
-
* Single-flight per key: React 18 StrictMode double-invokes effects in
|
|
9
|
-
* development, and without the guard every report page would fire its spec
|
|
10
|
-
* request twice on mount. The in-flight promise is shared rather than the
|
|
11
|
-
* second call being dropped, so both callers still resolve.
|
|
12
|
-
*/
|
|
13
|
-
const inFlight = new Map<string, Promise<ReportSpec>>();
|
|
14
|
-
|
|
15
|
-
export interface UseReportSpecResult {
|
|
16
|
-
spec: ReportSpec | null;
|
|
17
|
-
loading: boolean;
|
|
18
|
-
error: Error | null;
|
|
19
|
-
reload: () => void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function useReportSpec(reportKey: string): UseReportSpecResult {
|
|
23
|
-
const axios = useAxios();
|
|
24
|
-
// See useReportList: useAxios() is unmemoized and must stay out of deps.
|
|
25
|
-
const axiosRef = useRef(axios);
|
|
26
|
-
axiosRef.current = axios;
|
|
27
|
-
|
|
28
|
-
const [spec, setSpec] = useState<ReportSpec | null>(null);
|
|
29
|
-
const [loading, setLoading] = useState(true);
|
|
30
|
-
const [error, setError] = useState<Error | null>(null);
|
|
31
|
-
const [nonce, setNonce] = useState(0);
|
|
32
|
-
|
|
33
|
-
// Survives unmount-during-fetch without setting state on a dead component.
|
|
34
|
-
const alive = useRef(true);
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
alive.current = true;
|
|
37
|
-
return () => {
|
|
38
|
-
alive.current = false;
|
|
39
|
-
};
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (!reportKey) return;
|
|
44
|
-
|
|
45
|
-
setLoading(true);
|
|
46
|
-
setError(null);
|
|
47
|
-
|
|
48
|
-
const cacheKey = `${reportKey}:${nonce}`;
|
|
49
|
-
|
|
50
|
-
let promise = inFlight.get(cacheKey);
|
|
51
|
-
if (!promise) {
|
|
52
|
-
promise = axiosRef.current
|
|
53
|
-
.get(`report/${encodeURIComponent(reportKey)}/spec`)
|
|
54
|
-
.then((response: { data: ReportSpec }) => response.data)
|
|
55
|
-
.finally(() => {
|
|
56
|
-
inFlight.delete(cacheKey);
|
|
57
|
-
});
|
|
58
|
-
inFlight.set(cacheKey, promise);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
promise
|
|
62
|
-
.then((data) => {
|
|
63
|
-
if (alive.current) setSpec(data);
|
|
64
|
-
})
|
|
65
|
-
.catch((e: Error) => {
|
|
66
|
-
if (alive.current) setError(e);
|
|
67
|
-
})
|
|
68
|
-
.finally(() => {
|
|
69
|
-
if (alive.current) setLoading(false);
|
|
70
|
-
});
|
|
71
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
-
}, [reportKey, nonce]);
|
|
73
|
-
|
|
74
|
-
const reload = useCallback(() => setNonce((n) => n + 1), []);
|
|
75
|
-
|
|
76
|
-
return { spec, loading, error, reload };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export default useReportSpec;
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useAxios } from 'dash-axios-hook';
|
|
3
|
-
import { toQueryParams } from './queryParams';
|
|
4
|
-
import type { ReportFilterState, ReportTablePage } from './types';
|
|
5
|
-
|
|
6
|
-
export interface UseReportTableResult {
|
|
7
|
-
page: ReportTablePage | null;
|
|
8
|
-
loading: boolean;
|
|
9
|
-
error: Error | null;
|
|
10
|
-
setPage: (page: number) => void;
|
|
11
|
-
currentPage: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The paginated detail rows behind a report.
|
|
16
|
-
*
|
|
17
|
-
* `enabled` exists because the table is usually behind a toggle: fetching
|
|
18
|
-
* detail rows for a report nobody has expanded is a wasted query on every
|
|
19
|
-
* filter change, and these are the widest rows the endpoint serves.
|
|
20
|
-
*/
|
|
21
|
-
export function useReportTable(
|
|
22
|
-
reportKey: string,
|
|
23
|
-
filters: ReportFilterState | null,
|
|
24
|
-
enabled = true,
|
|
25
|
-
perPage = 50,
|
|
26
|
-
): UseReportTableResult {
|
|
27
|
-
const axios = useAxios();
|
|
28
|
-
// See useReportList: useAxios() is unmemoized and must stay out of deps.
|
|
29
|
-
const axiosRef = useRef(axios);
|
|
30
|
-
axiosRef.current = axios;
|
|
31
|
-
|
|
32
|
-
const [page, setPageData] = useState<ReportTablePage | null>(null);
|
|
33
|
-
const [loading, setLoading] = useState(false);
|
|
34
|
-
const [error, setError] = useState<Error | null>(null);
|
|
35
|
-
const [currentPage, setPage] = useState(1);
|
|
36
|
-
|
|
37
|
-
const sequence = useRef(0);
|
|
38
|
-
|
|
39
|
-
// A filter change invalidates the page number: page 7 of the old result set
|
|
40
|
-
// is meaningless against the new one, and often past its end.
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
setPage(1);
|
|
43
|
-
}, [JSON.stringify(filters)]);
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (!enabled || !reportKey || !filters) return;
|
|
47
|
-
|
|
48
|
-
const ticket = ++sequence.current;
|
|
49
|
-
setLoading(true);
|
|
50
|
-
setError(null);
|
|
51
|
-
|
|
52
|
-
axiosRef.current
|
|
53
|
-
.get(`report/${encodeURIComponent(reportKey)}/table`, {
|
|
54
|
-
params: { ...toQueryParams(filters), page: currentPage, perPage },
|
|
55
|
-
})
|
|
56
|
-
.then((response: { data: ReportTablePage }) => {
|
|
57
|
-
if (ticket === sequence.current) setPageData(response.data);
|
|
58
|
-
})
|
|
59
|
-
.catch((e: Error) => {
|
|
60
|
-
if (ticket === sequence.current) setError(e);
|
|
61
|
-
})
|
|
62
|
-
.finally(() => {
|
|
63
|
-
if (ticket === sequence.current) setLoading(false);
|
|
64
|
-
});
|
|
65
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
-
}, [reportKey, JSON.stringify(filters), enabled, currentPage, perPage]);
|
|
67
|
-
|
|
68
|
-
return { page, loading, error, setPage, currentPage };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export default useReportTable;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { VisualizationType } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Which visualizations draw to a <canvas>, and therefore need a parent with a
|
|
5
|
-
* DEFINITE height.
|
|
6
|
-
*
|
|
7
|
-
* Chart.js with `responsive: true` + `maintainAspectRatio: false` sizes its
|
|
8
|
-
* canvas from the parent box. If that parent's height is content-derived —
|
|
9
|
-
* `minHeight`, `auto`, or nothing at all — the canvas grows to fill it, which
|
|
10
|
-
* grows the parent, which fires another resize, which grows the canvas again.
|
|
11
|
-
* The card expands without bound and the page never settles. `minHeight` does
|
|
12
|
-
* NOT prevent this: it sets a floor, not a bound.
|
|
13
|
-
*
|
|
14
|
-
* Kpi, Funnel and Table are ordinary DOM and must size to their content
|
|
15
|
-
* instead — pinning them to a fixed height would clip a long funnel or a wide
|
|
16
|
-
* table.
|
|
17
|
-
*/
|
|
18
|
-
const CANVAS: ReadonlySet<string> = new Set<VisualizationType>(['Bar', 'Line', 'Pie', 'Doughnut']);
|
|
19
|
-
|
|
20
|
-
export function isCanvasVisualization(type: string | null | undefined): boolean {
|
|
21
|
-
return !!type && CANVAS.has(type);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The sx for a visualization's container: a definite height for canvases, and
|
|
26
|
-
* content height for everything else.
|
|
27
|
-
*/
|
|
28
|
-
export function visualizationBoxSx(type: string | null | undefined, height: number) {
|
|
29
|
-
return isCanvasVisualization(type)
|
|
30
|
-
? { height, minHeight: height, maxHeight: height, overflow: 'hidden' }
|
|
31
|
-
: { minHeight: 0 };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export default isCanvasVisualization;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { ComponentType } from 'react';
|
|
2
|
-
import type { ReportResult, ReportSpec, TranslateFn, VisualizationType } from './types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Which component renders which visualization type.
|
|
6
|
-
*
|
|
7
|
-
* A registry rather than a switch so the chart library is a swappable detail:
|
|
8
|
-
* the MUI/Chart.js adapter registers one set, and a different adapter (or a
|
|
9
|
-
* single overridden widget) can replace any of them without this package
|
|
10
|
-
* changing. It is also what keeps `core/` free of any chart import, so the
|
|
11
|
-
* hooks and types stay usable somewhere Chart.js is not wanted.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export interface VisualizationProps {
|
|
15
|
-
result: ReportResult;
|
|
16
|
-
spec: ReportSpec;
|
|
17
|
-
translate: TranslateFn;
|
|
18
|
-
options?: Record<string, unknown>;
|
|
19
|
-
height?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const registry = new Map<VisualizationType, ComponentType<VisualizationProps>>();
|
|
23
|
-
|
|
24
|
-
export function registerVisualization(
|
|
25
|
-
type: VisualizationType,
|
|
26
|
-
component: ComponentType<VisualizationProps>,
|
|
27
|
-
): void {
|
|
28
|
-
registry.set(type, component);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function getVisualization(type: VisualizationType): ComponentType<VisualizationProps> | null {
|
|
32
|
-
return registry.get(type) ?? null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function registeredVisualizations(): VisualizationType[] {
|
|
36
|
-
return Array.from(registry.keys());
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export default registry;
|