@dashadmin/dash-reports 1.3.73 → 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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export declare const es: {
|
|
2
|
+
dash_reports: {
|
|
3
|
+
title: string;
|
|
4
|
+
units: {
|
|
5
|
+
seconds: string;
|
|
6
|
+
minutes: string;
|
|
7
|
+
hours: string;
|
|
8
|
+
};
|
|
9
|
+
dashboard: {
|
|
10
|
+
title: string;
|
|
11
|
+
};
|
|
12
|
+
period: {
|
|
13
|
+
open: string;
|
|
14
|
+
closed: string;
|
|
15
|
+
current: string;
|
|
16
|
+
};
|
|
17
|
+
back_to_list: string;
|
|
18
|
+
no_reports: string;
|
|
19
|
+
loading: string;
|
|
20
|
+
no_data: string;
|
|
21
|
+
no_rows: string;
|
|
22
|
+
other: string;
|
|
23
|
+
show_detail: string;
|
|
24
|
+
hide_detail: string;
|
|
25
|
+
week_of: string;
|
|
26
|
+
filters: {
|
|
27
|
+
title: string;
|
|
28
|
+
reset: string;
|
|
29
|
+
done: string;
|
|
30
|
+
start: string;
|
|
31
|
+
end: string;
|
|
32
|
+
granularity: string;
|
|
33
|
+
pivot: string;
|
|
34
|
+
no_pivot: string;
|
|
35
|
+
};
|
|
36
|
+
granularity: {
|
|
37
|
+
daily: string;
|
|
38
|
+
weekly: string;
|
|
39
|
+
monthly: string;
|
|
40
|
+
yearly: string;
|
|
41
|
+
total: string;
|
|
42
|
+
hourOfDay: string;
|
|
43
|
+
dayOfWeek: string;
|
|
44
|
+
};
|
|
45
|
+
viz: {
|
|
46
|
+
Bar: string;
|
|
47
|
+
Line: string;
|
|
48
|
+
Pie: string;
|
|
49
|
+
Doughnut: string;
|
|
50
|
+
Kpi: string;
|
|
51
|
+
Funnel: string;
|
|
52
|
+
Table: string;
|
|
53
|
+
};
|
|
54
|
+
funnel: {
|
|
55
|
+
step: string;
|
|
56
|
+
skipped: string;
|
|
57
|
+
};
|
|
58
|
+
table: {
|
|
59
|
+
period: string;
|
|
60
|
+
};
|
|
61
|
+
weekdays: {
|
|
62
|
+
monday: string;
|
|
63
|
+
tuesday: string;
|
|
64
|
+
wednesday: string;
|
|
65
|
+
thursday: string;
|
|
66
|
+
friday: string;
|
|
67
|
+
saturday: string;
|
|
68
|
+
sunday: string;
|
|
69
|
+
};
|
|
70
|
+
export: {
|
|
71
|
+
xlsx: string;
|
|
72
|
+
csv: string;
|
|
73
|
+
};
|
|
74
|
+
errors: {
|
|
75
|
+
spec: string;
|
|
76
|
+
list: string;
|
|
77
|
+
series: string;
|
|
78
|
+
dashboard: string;
|
|
79
|
+
widget: string;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
export default es;
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @dashadmin/dash-reports — the domain-agnostic reporting UI.
|
|
3
|
-
*
|
|
4
|
-
* Renders a report entirely from the spec app/ReportCore serves for it, so a
|
|
5
|
-
* new report is a backend definition plus one resource entry, with no change
|
|
6
|
-
* here.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Core — framework-agnostic, no chart or MUI import.
|
|
10
1
|
export * from './core/types';
|
|
11
2
|
export { useReportSpec } from './core/useReportSpec';
|
|
12
3
|
export { useReportSeries } from './core/useReportSeries';
|
|
@@ -16,37 +7,18 @@ export { useReportPreview } from './core/useReportPreview';
|
|
|
16
7
|
export { useReportExport } from './core/useReportExport';
|
|
17
8
|
export { toQueryParams } from './core/queryParams';
|
|
18
9
|
export { formatBucket, formatBucketLong } from './core/formatBucket';
|
|
19
|
-
export {
|
|
20
|
-
registerVisualization,
|
|
21
|
-
getVisualization,
|
|
22
|
-
registeredVisualizations,
|
|
23
|
-
} from './core/visualizationRegistry';
|
|
10
|
+
export { registerVisualization, getVisualization, registeredVisualizations, } from './core/visualizationRegistry';
|
|
24
11
|
export type { VisualizationProps } from './core/visualizationRegistry';
|
|
25
|
-
|
|
26
|
-
// MUI / Chart.js adapter. Importing it registers the default widget set.
|
|
27
12
|
export * from './adapters/mui';
|
|
28
|
-
|
|
29
|
-
// Composed UI.
|
|
30
13
|
export { ReportPage } from './ReportPage';
|
|
31
14
|
export type { ReportPageProps } from './ReportPage';
|
|
32
15
|
export { ReportFilters } from './ReportFilters';
|
|
33
16
|
export { ReportsModule } from './ReportsModule';
|
|
34
17
|
export { ReportsDashboard } from './ReportsDashboard';
|
|
35
18
|
export { TenantDashboard } from './TenantDashboard';
|
|
36
|
-
export {
|
|
37
|
-
|
|
38
|
-
useTenantDashboard,
|
|
39
|
-
useTenantPeriods,
|
|
40
|
-
} from './core/useDashboard';
|
|
41
|
-
export type {
|
|
42
|
-
DashboardWidget,
|
|
43
|
-
DashboardTenant,
|
|
44
|
-
DashboardPeriod,
|
|
45
|
-
DashboardData,
|
|
46
|
-
} from './core/useDashboard';
|
|
19
|
+
export { useDashboardTenants, useTenantDashboard, useTenantPeriods, } from './core/useDashboard';
|
|
20
|
+
export type { DashboardWidget, DashboardTenant, DashboardPeriod, DashboardData, } from './core/useDashboard';
|
|
47
21
|
export { ReportsIndex } from './ReportsIndex';
|
|
48
22
|
export { createReportsResource } from './createReportsResource';
|
|
49
23
|
export type { CreateReportsResourceOptions } from './createReportsResource';
|
|
50
|
-
|
|
51
|
-
// Strings, merged at app entry — see dash-admin's mergeTranslations.
|
|
52
24
|
export { en as reportsEn, es as reportsEs } from './i18n';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashadmin/dash-reports",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.74-srcfree.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"devDependencies": {
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@mui/material": "^9.3.0",
|
|
14
14
|
"chart.js": "^4.5.0",
|
|
15
|
-
"@dashadmin/dash-axios-hook": "1.3.
|
|
15
|
+
"@dashadmin/dash-axios-hook": "1.3.74-srcfree.1",
|
|
16
16
|
"react-chartjs-2": "^5.3.1"
|
|
17
17
|
},
|
|
18
18
|
"module": "dist/index.js",
|
|
19
|
-
"types": "./
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
20
|
"exports": {
|
|
21
21
|
".": "./dist/index.js",
|
|
22
22
|
"./*": "./dist/*.js",
|
|
@@ -27,22 +27,25 @@
|
|
|
27
27
|
"registry": "https://registry.npmjs.org"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"build": "tsup"
|
|
30
|
+
"build": "tsup && pnpm run build:types",
|
|
31
|
+
"build:types": "tsc -p tsconfig.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src --skipLibCheck || true"
|
|
31
32
|
},
|
|
32
33
|
"type": "module",
|
|
33
34
|
"files": [
|
|
34
|
-
"dist"
|
|
35
|
-
"src"
|
|
35
|
+
"dist"
|
|
36
36
|
],
|
|
37
37
|
"typesVersions": {
|
|
38
38
|
"*": {
|
|
39
|
+
"dist/*": [
|
|
40
|
+
"dist/*"
|
|
41
|
+
],
|
|
39
42
|
"src/*": [
|
|
40
|
-
"
|
|
41
|
-
"
|
|
43
|
+
"dist/*",
|
|
44
|
+
"dist/*/index"
|
|
42
45
|
],
|
|
43
46
|
"*": [
|
|
44
|
-
"
|
|
45
|
-
"
|
|
47
|
+
"dist/*",
|
|
48
|
+
"dist/*/index"
|
|
46
49
|
]
|
|
47
50
|
}
|
|
48
51
|
}
|
package/src/ReportFilters.tsx
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select, Stack, TextField,
|
|
3
|
-
} from '@mui/material';
|
|
4
|
-
import { label } from './core/label';
|
|
5
|
-
import type { ReportFilterState, ReportSpec, TranslateFn } from './core/types';
|
|
6
|
-
|
|
7
|
-
export interface ReportFiltersProps {
|
|
8
|
-
spec: ReportSpec;
|
|
9
|
-
value: ReportFilterState;
|
|
10
|
-
onChange: (next: ReportFilterState) => void;
|
|
11
|
-
translate: TranslateFn;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The filter form.
|
|
16
|
-
*
|
|
17
|
-
* Laid out as a single column because it lives in a right-hand drawer — every
|
|
18
|
-
* control gets full width, so long option labels (a restaurant name, a product)
|
|
19
|
-
* are readable instead of truncated into a narrow inline slot.
|
|
20
|
-
*
|
|
21
|
-
* One filter set for the whole report, not per chart: every visualisation on
|
|
22
|
-
* the page reads the same query, and per-chart filters would imply they can
|
|
23
|
-
* disagree. Because the drawer hides that state while closed, ReportPage keeps
|
|
24
|
-
* a summary of what is applied visible in the header — a filter nobody can see
|
|
25
|
-
* is a filter they forget is on.
|
|
26
|
-
*/
|
|
27
|
-
export function ReportFilters({ spec, value, onChange, translate }: ReportFiltersProps) {
|
|
28
|
-
const set = (patch: Partial<ReportFilterState>) => onChange({ ...value, ...patch });
|
|
29
|
-
|
|
30
|
-
const setDimension = (key: string, selected: Array<string | number | boolean>) =>
|
|
31
|
-
onChange({ ...value, dimensions: { ...value.dimensions, [key]: selected } });
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<Stack spacing={2.5} sx={{ p: 2.5, width: { xs: '86vw', sm: 360 } }}>
|
|
35
|
-
<TextField
|
|
36
|
-
type="date"
|
|
37
|
-
size="small"
|
|
38
|
-
fullWidth
|
|
39
|
-
label={translate('dash_reports.filters.start', { _: 'From' })}
|
|
40
|
-
slotProps={{ inputLabel: { shrink: true } }}
|
|
41
|
-
value={value.start}
|
|
42
|
-
onChange={(e) => set({ start: e.target.value })}
|
|
43
|
-
/>
|
|
44
|
-
|
|
45
|
-
<TextField
|
|
46
|
-
type="date"
|
|
47
|
-
size="small"
|
|
48
|
-
fullWidth
|
|
49
|
-
label={translate('dash_reports.filters.end', { _: 'To' })}
|
|
50
|
-
slotProps={{ inputLabel: { shrink: true } }}
|
|
51
|
-
value={value.end}
|
|
52
|
-
onChange={(e) => set({ end: e.target.value })}
|
|
53
|
-
/>
|
|
54
|
-
|
|
55
|
-
<FormControl size="small" fullWidth>
|
|
56
|
-
<InputLabel id="report-granularity">
|
|
57
|
-
{translate('dash_reports.filters.granularity', { _: 'Group by' })}
|
|
58
|
-
</InputLabel>
|
|
59
|
-
<Select
|
|
60
|
-
labelId="report-granularity"
|
|
61
|
-
label={translate('dash_reports.filters.granularity', { _: 'Group by' })}
|
|
62
|
-
value={value.granularity}
|
|
63
|
-
onChange={(e) => set({ granularity: e.target.value as ReportFilterState['granularity'] })}
|
|
64
|
-
>
|
|
65
|
-
{spec.granularities.map((g) => (
|
|
66
|
-
<MenuItem key={g} value={g}>
|
|
67
|
-
{translate(`dash_reports.granularity.${g}`, { _: g })}
|
|
68
|
-
</MenuItem>
|
|
69
|
-
))}
|
|
70
|
-
</Select>
|
|
71
|
-
</FormControl>
|
|
72
|
-
|
|
73
|
-
{spec.dimensions.length > 0 && (
|
|
74
|
-
<FormControl size="small" fullWidth>
|
|
75
|
-
<InputLabel id="report-pivot">
|
|
76
|
-
{translate('dash_reports.filters.pivot', { _: 'Split by' })}
|
|
77
|
-
</InputLabel>
|
|
78
|
-
<Select
|
|
79
|
-
labelId="report-pivot"
|
|
80
|
-
label={translate('dash_reports.filters.pivot', { _: 'Split by' })}
|
|
81
|
-
value={value.pivot ?? ''}
|
|
82
|
-
onChange={(e) => set({ pivot: e.target.value || undefined })}
|
|
83
|
-
>
|
|
84
|
-
{/* No split is a real choice, not an absence — with none, the chart
|
|
85
|
-
shows one series per metric instead. */}
|
|
86
|
-
<MenuItem value="">
|
|
87
|
-
{translate('dash_reports.filters.no_pivot', { _: 'Nothing' })}
|
|
88
|
-
</MenuItem>
|
|
89
|
-
{spec.dimensions.map((d) => (
|
|
90
|
-
<MenuItem key={d.key} value={d.key}>
|
|
91
|
-
{label(translate, d.label, d.labelText)}
|
|
92
|
-
</MenuItem>
|
|
93
|
-
))}
|
|
94
|
-
</Select>
|
|
95
|
-
</FormControl>
|
|
96
|
-
)}
|
|
97
|
-
|
|
98
|
-
{spec.dimensions
|
|
99
|
-
.filter((d) => (d.values?.length ?? 0) > 0)
|
|
100
|
-
.map((dimension) => {
|
|
101
|
-
const selected = value.dimensions[dimension.key] ?? [];
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<FormControl key={dimension.key} size="small" fullWidth>
|
|
105
|
-
<InputLabel id={`report-dim-${dimension.key}`}>
|
|
106
|
-
{label(translate, dimension.label, dimension.labelText)}
|
|
107
|
-
</InputLabel>
|
|
108
|
-
<Select
|
|
109
|
-
labelId={`report-dim-${dimension.key}`}
|
|
110
|
-
multiple={dimension.multiple !== false}
|
|
111
|
-
value={selected}
|
|
112
|
-
input={<OutlinedInput label={label(translate, dimension.label, dimension.labelText)} />}
|
|
113
|
-
onChange={(e) => {
|
|
114
|
-
const next = e.target.value;
|
|
115
|
-
setDimension(dimension.key, Array.isArray(next) ? next : [next]);
|
|
116
|
-
}}
|
|
117
|
-
renderValue={(picked) => (
|
|
118
|
-
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
|
119
|
-
{(Array.isArray(picked) ? picked : [picked]).map((v) => {
|
|
120
|
-
const option = dimension.values?.find((o) => String(o.value) === String(v));
|
|
121
|
-
return (
|
|
122
|
-
<Chip
|
|
123
|
-
key={String(v)}
|
|
124
|
-
size="small"
|
|
125
|
-
label={option ? label(translate, option.label, option.labelText) : String(v)}
|
|
126
|
-
/>
|
|
127
|
-
);
|
|
128
|
-
})}
|
|
129
|
-
</Box>
|
|
130
|
-
)}
|
|
131
|
-
>
|
|
132
|
-
{dimension.values?.map((option) => (
|
|
133
|
-
<MenuItem key={String(option.value)} value={option.value as string}>
|
|
134
|
-
{label(translate, option.label, option.labelText)}
|
|
135
|
-
</MenuItem>
|
|
136
|
-
))}
|
|
137
|
-
</Select>
|
|
138
|
-
</FormControl>
|
|
139
|
-
);
|
|
140
|
-
})}
|
|
141
|
-
</Stack>
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export default ReportFilters;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { Box, Button, Divider, Drawer, IconButton, Stack, Typography, useTheme } from '@mui/material';
|
|
2
|
-
import { ReportFilters } from './ReportFilters';
|
|
3
|
-
import type { ReportFilterState, ReportSpec, TranslateFn } from './core/types';
|
|
4
|
-
|
|
5
|
-
export interface ReportFiltersDrawerProps {
|
|
6
|
-
open: boolean;
|
|
7
|
-
onClose: () => void;
|
|
8
|
-
spec: ReportSpec;
|
|
9
|
-
value: ReportFilterState;
|
|
10
|
-
onChange: (next: ReportFilterState) => void;
|
|
11
|
-
onReset: () => void;
|
|
12
|
-
translate: TranslateFn;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The filter panel, anchored right.
|
|
17
|
-
*
|
|
18
|
-
* Changes apply live rather than behind an "Apply" button: the charts already
|
|
19
|
-
* refetch on every filter change and keep the previous result visible while
|
|
20
|
-
* loading, so there is nothing for a confirm step to protect against — it would
|
|
21
|
-
* only add a click and a second source of truth for "what is currently applied".
|
|
22
|
-
*
|
|
23
|
-
* `keepMounted` because the drawer holds live form state; unmounting it on
|
|
24
|
-
* close would reset every select the moment the user peeked at the chart.
|
|
25
|
-
*/
|
|
26
|
-
export function ReportFiltersDrawer({
|
|
27
|
-
open, onClose, spec, value, onChange, onReset, translate,
|
|
28
|
-
}: ReportFiltersDrawerProps) {
|
|
29
|
-
const theme = useTheme();
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<Drawer
|
|
33
|
-
anchor="right"
|
|
34
|
-
open={open}
|
|
35
|
-
onClose={onClose}
|
|
36
|
-
keepMounted
|
|
37
|
-
slotProps={{ paper: { sx: { backgroundImage: 'none' } } }}
|
|
38
|
-
>
|
|
39
|
-
<Stack sx={{ height: '100%' }}>
|
|
40
|
-
<Stack
|
|
41
|
-
direction="row"
|
|
42
|
-
sx={{ alignItems: 'center', justifyContent: 'space-between', px: 2.5, py: 2 }}
|
|
43
|
-
>
|
|
44
|
-
<Typography variant="h6" sx={{ color: theme.palette.text.primary }}>
|
|
45
|
-
{translate('dash_reports.filters.title', { _: 'Filters' })}
|
|
46
|
-
</Typography>
|
|
47
|
-
<IconButton onClick={onClose} size="small" aria-label={translate('ra.action.close', { _: 'Close' })}>
|
|
48
|
-
<Box component="span" sx={{ fontSize: 20, lineHeight: 1 }}>×</Box>
|
|
49
|
-
</IconButton>
|
|
50
|
-
</Stack>
|
|
51
|
-
|
|
52
|
-
<Divider />
|
|
53
|
-
|
|
54
|
-
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
|
55
|
-
<ReportFilters spec={spec} value={value} onChange={onChange} translate={translate} />
|
|
56
|
-
</Box>
|
|
57
|
-
|
|
58
|
-
<Divider />
|
|
59
|
-
|
|
60
|
-
<Stack direction="row" spacing={1} sx={{ p: 2 }}>
|
|
61
|
-
<Button fullWidth variant="outlined" onClick={onReset}>
|
|
62
|
-
{translate('dash_reports.filters.reset', { _: 'Reset' })}
|
|
63
|
-
</Button>
|
|
64
|
-
<Button fullWidth variant="contained" onClick={onClose}>
|
|
65
|
-
{translate('dash_reports.filters.done', { _: 'Done' })}
|
|
66
|
-
</Button>
|
|
67
|
-
</Stack>
|
|
68
|
-
</Stack>
|
|
69
|
-
</Drawer>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export default ReportFiltersDrawer;
|
package/src/ReportPage.tsx
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Alert, Badge, Box, Button, Card, CardContent, Chip, Divider, LinearProgress, Stack, Tab, Tabs, Typography, useTheme,
|
|
4
|
-
} from '@mui/material';
|
|
5
|
-
import { useReportSpec } from './core/useReportSpec';
|
|
6
|
-
import { label } from './core/label';
|
|
7
|
-
import { visualizationBoxSx } from './core/visualizationLayout';
|
|
8
|
-
import { useReportSeries } from './core/useReportSeries';
|
|
9
|
-
import { useReportTable } from './core/useReportTable';
|
|
10
|
-
import { getVisualization } from './core/visualizationRegistry';
|
|
11
|
-
import { ReportFiltersDrawer } from './ReportFiltersDrawer';
|
|
12
|
-
import { DetailTable } from './adapters/mui/TableWidget';
|
|
13
|
-
import { useReportExport } from './core/useReportExport';
|
|
14
|
-
import type { ReportFilterState, TranslateFn, VisualizationType } from './core/types';
|
|
15
|
-
import './adapters/mui';
|
|
16
|
-
|
|
17
|
-
export interface ReportPageProps {
|
|
18
|
-
reportKey: string;
|
|
19
|
-
translate: TranslateFn;
|
|
20
|
-
/** Optional heading override; defaults to the spec's translated title. */
|
|
21
|
-
title?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function isoDaysAgo(days: number): string {
|
|
25
|
-
const d = new Date();
|
|
26
|
-
d.setDate(d.getDate() - days);
|
|
27
|
-
return d.toISOString().slice(0, 10);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* A whole report, rendered from its spec.
|
|
32
|
-
*
|
|
33
|
-
* Nothing here knows what the report contains — the filters, the available
|
|
34
|
-
* chart types, the metric labels and the export formats all come from the
|
|
35
|
-
* backend. Adding a report to a domain is a backend change plus one resource
|
|
36
|
-
* entry; this component does not change.
|
|
37
|
-
*/
|
|
38
|
-
export function ReportPage({ reportKey, translate, title }: ReportPageProps) {
|
|
39
|
-
const theme = useTheme();
|
|
40
|
-
const { spec, loading: specLoading, error: specError } = useReportSpec(reportKey);
|
|
41
|
-
|
|
42
|
-
const [filters, setFilters] = useState<ReportFilterState | null>(null);
|
|
43
|
-
const [activeViz, setActiveViz] = useState<VisualizationType | null>(null);
|
|
44
|
-
const [showDetail, setShowDetail] = useState(false);
|
|
45
|
-
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
46
|
-
|
|
47
|
-
// One definition of "the default filters", used both to seed and to reset —
|
|
48
|
-
// two copies would drift and Reset would stop returning to the initial view.
|
|
49
|
-
const defaultFilters = useMemo<ReportFilterState | null>(
|
|
50
|
-
() =>
|
|
51
|
-
spec
|
|
52
|
-
? {
|
|
53
|
-
start: isoDaysAgo(spec.defaultRangeDays ?? 30),
|
|
54
|
-
end: new Date().toISOString().slice(0, 10),
|
|
55
|
-
granularity: spec.defaultGranularity,
|
|
56
|
-
pivot: undefined,
|
|
57
|
-
dimensions: {},
|
|
58
|
-
}
|
|
59
|
-
: null,
|
|
60
|
-
[spec],
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
// Seed once the spec arrives — the default window and granularity are the
|
|
64
|
-
// report's to choose, not this component's.
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
if (!defaultFilters || filters) return;
|
|
67
|
-
|
|
68
|
-
setFilters(defaultFilters);
|
|
69
|
-
// The report's declared default, not merely the first in the array —
|
|
70
|
-
// otherwise reordering visualizations silently changes what opens.
|
|
71
|
-
setActiveViz(
|
|
72
|
-
(spec?.defaultVisualization as VisualizationType | undefined)
|
|
73
|
-
?? spec?.visualizations[0]?.type
|
|
74
|
-
?? null,
|
|
75
|
-
);
|
|
76
|
-
}, [defaultFilters, filters, spec]);
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* What is currently narrowing the data, as readable chips.
|
|
80
|
-
*
|
|
81
|
-
* The drawer hides the filter state while closed, so this keeps it on screen:
|
|
82
|
-
* a filter someone cannot see is one they forget is applied, and then read the
|
|
83
|
-
* numbers as if it were not.
|
|
84
|
-
*/
|
|
85
|
-
const activeChips = useMemo(() => {
|
|
86
|
-
if (!spec || !filters) return [];
|
|
87
|
-
|
|
88
|
-
const chips: string[] = [];
|
|
89
|
-
|
|
90
|
-
Object.entries(filters.dimensions ?? {}).forEach(([key, values]) => {
|
|
91
|
-
if (!Array.isArray(values) || values.length === 0) return;
|
|
92
|
-
|
|
93
|
-
const dimension = spec.dimensions.find((d) => d.key === key);
|
|
94
|
-
if (!dimension) return;
|
|
95
|
-
|
|
96
|
-
const labels = values.map((v) => {
|
|
97
|
-
const option = dimension.values?.find((o) => String(o.value) === String(v));
|
|
98
|
-
return option ? label(translate, option.label, option.labelText) : String(v);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
chips.push(`${label(translate, dimension.label, dimension.labelText)}: ${labels.join(', ')}`);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (filters.pivot) {
|
|
105
|
-
const dimension = spec.dimensions.find((d) => d.key === filters.pivot);
|
|
106
|
-
if (dimension) {
|
|
107
|
-
chips.push(
|
|
108
|
-
`${translate('dash_reports.filters.pivot', { _: 'Split by' })}: ${label(translate, dimension.label, dimension.labelText)}`,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return chips;
|
|
114
|
-
}, [spec, filters, translate]);
|
|
115
|
-
|
|
116
|
-
const { result, loading: seriesLoading, error: seriesError } = useReportSeries(reportKey, filters);
|
|
117
|
-
const table = useReportTable(reportKey, filters, showDetail);
|
|
118
|
-
const { exporting, exportReport } = useReportExport(reportKey);
|
|
119
|
-
|
|
120
|
-
const Visualization = useMemo(
|
|
121
|
-
() => (activeViz ? getVisualization(activeViz) : null),
|
|
122
|
-
[activeViz],
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
if (specLoading) return <LinearProgress />;
|
|
126
|
-
|
|
127
|
-
if (specError || !spec) {
|
|
128
|
-
return (
|
|
129
|
-
<Alert severity="error" sx={{ m: 2 }}>
|
|
130
|
-
{translate('dash_reports.errors.spec', { _: 'This report could not be loaded.' })}
|
|
131
|
-
</Alert>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<Box sx={{ p: { xs: 2, md: 3 } }}>
|
|
137
|
-
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: 'space-between', mb: 2, gap: 2, flexWrap: 'wrap' }}>
|
|
138
|
-
<Typography variant="h5" sx={{ color: theme.palette.text.primary }}>
|
|
139
|
-
{title ?? label(translate, spec.title, spec.titleText)}
|
|
140
|
-
</Typography>
|
|
141
|
-
|
|
142
|
-
<Stack direction="row" spacing={1}>
|
|
143
|
-
{spec.exporters.map((format) => (
|
|
144
|
-
<Button
|
|
145
|
-
key={format}
|
|
146
|
-
size="small"
|
|
147
|
-
variant="outlined"
|
|
148
|
-
disabled={exporting || !filters}
|
|
149
|
-
onClick={() => filters && exportReport(filters, format)}
|
|
150
|
-
>
|
|
151
|
-
{translate(`dash_reports.export.${format}`, { _: format.toUpperCase() })}
|
|
152
|
-
</Button>
|
|
153
|
-
))}
|
|
154
|
-
|
|
155
|
-
<Badge badgeContent={activeChips.length} color="primary">
|
|
156
|
-
<Button size="small" variant="outlined" onClick={() => setFiltersOpen(true)}>
|
|
157
|
-
{translate('dash_reports.filters.title', { _: 'Filters' })}
|
|
158
|
-
</Button>
|
|
159
|
-
</Badge>
|
|
160
|
-
</Stack>
|
|
161
|
-
</Stack>
|
|
162
|
-
|
|
163
|
-
{/* The window is always narrowing the data, so it is always shown —
|
|
164
|
-
unlike the dimension chips, which appear only when something is set. */}
|
|
165
|
-
{filters && (
|
|
166
|
-
<Stack direction="row" sx={{ flexWrap: 'wrap', gap: 1, mb: 2, alignItems: 'center' }}>
|
|
167
|
-
<Chip
|
|
168
|
-
size="small"
|
|
169
|
-
variant="outlined"
|
|
170
|
-
label={`${filters.start} → ${filters.end}`}
|
|
171
|
-
/>
|
|
172
|
-
<Chip
|
|
173
|
-
size="small"
|
|
174
|
-
variant="outlined"
|
|
175
|
-
label={translate(`dash_reports.granularity.${filters.granularity}`, { _: filters.granularity })}
|
|
176
|
-
/>
|
|
177
|
-
{activeChips.map((chip) => (
|
|
178
|
-
<Chip
|
|
179
|
-
key={chip}
|
|
180
|
-
size="small"
|
|
181
|
-
label={chip}
|
|
182
|
-
onDelete={undefined}
|
|
183
|
-
/>
|
|
184
|
-
))}
|
|
185
|
-
</Stack>
|
|
186
|
-
)}
|
|
187
|
-
|
|
188
|
-
{seriesError && (
|
|
189
|
-
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
190
|
-
{translate('dash_reports.errors.series', { _: 'This range could not be loaded.' })}
|
|
191
|
-
</Alert>
|
|
192
|
-
)}
|
|
193
|
-
|
|
194
|
-
<Card variant="outlined">
|
|
195
|
-
{spec.visualizations.length > 1 && (
|
|
196
|
-
<>
|
|
197
|
-
<Tabs
|
|
198
|
-
value={activeViz}
|
|
199
|
-
onChange={(_, next) => setActiveViz(next)}
|
|
200
|
-
variant="scrollable"
|
|
201
|
-
scrollButtons="auto"
|
|
202
|
-
>
|
|
203
|
-
{spec.visualizations.map((v) => (
|
|
204
|
-
<Tab
|
|
205
|
-
key={v.type}
|
|
206
|
-
value={v.type}
|
|
207
|
-
label={translate(`dash_reports.viz.${v.type}`, { _: v.type })}
|
|
208
|
-
/>
|
|
209
|
-
))}
|
|
210
|
-
</Tabs>
|
|
211
|
-
<Divider />
|
|
212
|
-
</>
|
|
213
|
-
)}
|
|
214
|
-
|
|
215
|
-
{/* A refetch dims the existing chart rather than replacing it with a
|
|
216
|
-
skeleton — flashing an empty box on every filter change loses the
|
|
217
|
-
reader's place and reads as slower than it is. */}
|
|
218
|
-
<Box sx={{ position: 'relative', opacity: seriesLoading && result ? 0.55 : 1, transition: 'opacity 120ms' }}>
|
|
219
|
-
{seriesLoading && (
|
|
220
|
-
<LinearProgress sx={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1 }} />
|
|
221
|
-
)}
|
|
222
|
-
|
|
223
|
-
{/* A DEFINITE height for canvas charts. minHeight is a floor, not a
|
|
224
|
-
bound, and Chart.js with maintainAspectRatio:false grows to fill
|
|
225
|
-
its parent — which grows the parent, which resizes the canvas,
|
|
226
|
-
forever. */}
|
|
227
|
-
<CardContent>
|
|
228
|
-
{result && Visualization ? (
|
|
229
|
-
<Box sx={visualizationBoxSx(activeViz, 340)}>
|
|
230
|
-
<Visualization result={result} spec={spec} translate={translate} />
|
|
231
|
-
</Box>
|
|
232
|
-
) : (
|
|
233
|
-
<Box sx={{ height: 340, display: 'grid', placeItems: 'center' }}>
|
|
234
|
-
<Typography variant="body2" sx={{ color: theme.palette.text.secondary }}>
|
|
235
|
-
{seriesLoading
|
|
236
|
-
? translate('dash_reports.loading', { _: 'Loading…' })
|
|
237
|
-
: translate('dash_reports.no_data', { _: 'No data for this range.' })}
|
|
238
|
-
</Typography>
|
|
239
|
-
</Box>
|
|
240
|
-
)}
|
|
241
|
-
</CardContent>
|
|
242
|
-
</Box>
|
|
243
|
-
</Card>
|
|
244
|
-
|
|
245
|
-
<Box sx={{ mt: 2 }}>
|
|
246
|
-
<Button size="small" onClick={() => setShowDetail((v) => !v)}>
|
|
247
|
-
{showDetail
|
|
248
|
-
? translate('dash_reports.hide_detail', { _: 'Hide detail rows' })
|
|
249
|
-
: translate('dash_reports.show_detail', { _: 'Show detail rows' })}
|
|
250
|
-
</Button>
|
|
251
|
-
</Box>
|
|
252
|
-
|
|
253
|
-
{showDetail && (
|
|
254
|
-
<Card variant="outlined" sx={{ mt: 1 }}>
|
|
255
|
-
<DetailTable
|
|
256
|
-
page={table.page}
|
|
257
|
-
loading={table.loading}
|
|
258
|
-
currentPage={table.currentPage}
|
|
259
|
-
onPageChange={table.setPage}
|
|
260
|
-
translate={translate}
|
|
261
|
-
/>
|
|
262
|
-
</Card>
|
|
263
|
-
)}
|
|
264
|
-
|
|
265
|
-
{filters && (
|
|
266
|
-
<ReportFiltersDrawer
|
|
267
|
-
open={filtersOpen}
|
|
268
|
-
onClose={() => setFiltersOpen(false)}
|
|
269
|
-
spec={spec}
|
|
270
|
-
value={filters}
|
|
271
|
-
onChange={setFilters}
|
|
272
|
-
onReset={() => defaultFilters && setFilters(defaultFilters)}
|
|
273
|
-
translate={translate}
|
|
274
|
-
/>
|
|
275
|
-
)}
|
|
276
|
-
</Box>
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export default ReportPage;
|