@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
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { Theme } from '@mui/material';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Chart chrome, taken from the app's MUI theme.
|
|
5
|
-
*
|
|
6
|
-
* Grid and axis lines are deliberately recessive — a chart's ink belongs to its
|
|
7
|
-
* data, and heavy or dashed rules read as content. Text uses the theme's own
|
|
8
|
-
* ink tokens, never a series colour: a coloured mark beside a label carries the
|
|
9
|
-
* identity, so colouring the text as well adds nothing and costs contrast.
|
|
10
|
-
*/
|
|
11
|
-
export interface ChartChrome {
|
|
12
|
-
dark: boolean;
|
|
13
|
-
grid: string;
|
|
14
|
-
axisText: string;
|
|
15
|
-
tooltipBg: string;
|
|
16
|
-
tooltipText: string;
|
|
17
|
-
surface: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function chartChrome(theme: Theme): ChartChrome {
|
|
21
|
-
const dark = theme.palette.mode === 'dark';
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
dark,
|
|
25
|
-
// ~8% ink: present enough to read a value against, quiet enough to ignore.
|
|
26
|
-
grid: dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
|
|
27
|
-
axisText: theme.palette.text.secondary,
|
|
28
|
-
tooltipBg: dark ? 'rgba(32,32,30,0.96)' : 'rgba(255,255,255,0.96)',
|
|
29
|
-
tooltipText: theme.palette.text.primary,
|
|
30
|
-
surface: theme.palette.background.paper,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Shared scale/plugin options so every widget reads the same. */
|
|
35
|
-
export function baseChartOptions(chrome: ChartChrome, showLegend: boolean) {
|
|
36
|
-
return {
|
|
37
|
-
responsive: true,
|
|
38
|
-
maintainAspectRatio: false,
|
|
39
|
-
interaction: {
|
|
40
|
-
// Whole-bucket hover: the hit target is the column, not the mark, so a
|
|
41
|
-
// thin bar or a small point does not demand pixel-accurate aim.
|
|
42
|
-
mode: 'index' as const,
|
|
43
|
-
intersect: false,
|
|
44
|
-
},
|
|
45
|
-
plugins: {
|
|
46
|
-
legend: {
|
|
47
|
-
display: showLegend,
|
|
48
|
-
position: 'bottom' as const,
|
|
49
|
-
labels: {
|
|
50
|
-
color: chrome.axisText,
|
|
51
|
-
usePointStyle: true,
|
|
52
|
-
pointStyle: 'circle' as const,
|
|
53
|
-
boxWidth: 8,
|
|
54
|
-
boxHeight: 8,
|
|
55
|
-
padding: 16,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
tooltip: {
|
|
59
|
-
backgroundColor: chrome.tooltipBg,
|
|
60
|
-
titleColor: chrome.tooltipText,
|
|
61
|
-
bodyColor: chrome.tooltipText,
|
|
62
|
-
borderColor: chrome.grid,
|
|
63
|
-
borderWidth: 1,
|
|
64
|
-
padding: 10,
|
|
65
|
-
cornerRadius: 6,
|
|
66
|
-
displayColors: true,
|
|
67
|
-
usePointStyle: true,
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
scales: {
|
|
71
|
-
x: {
|
|
72
|
-
grid: { display: false },
|
|
73
|
-
border: { color: chrome.grid },
|
|
74
|
-
ticks: { color: chrome.axisText, maxRotation: 0, autoSkipPadding: 12 },
|
|
75
|
-
},
|
|
76
|
-
y: {
|
|
77
|
-
beginAtZero: true,
|
|
78
|
-
grid: { color: chrome.grid },
|
|
79
|
-
border: { display: false },
|
|
80
|
-
ticks: { color: chrome.axisText, precision: 0 as unknown as number },
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { registerVisualization } from '../../core/visualizationRegistry';
|
|
2
|
-
import { BarWidget } from './BarWidget';
|
|
3
|
-
import { LineWidget } from './LineWidget';
|
|
4
|
-
import { DoughnutWidget } from './DoughnutWidget';
|
|
5
|
-
import { PieWidget } from './PieWidget';
|
|
6
|
-
import { KpiWidget } from './KpiWidget';
|
|
7
|
-
import { FunnelWidget } from './FunnelWidget';
|
|
8
|
-
import { SeriesTableWidget } from './TableWidget';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Register the MUI/Chart.js renderers.
|
|
12
|
-
*
|
|
13
|
-
* Importing this module is what wires the adapter up; `core/` never imports a
|
|
14
|
-
* chart library, so a different adapter can register its own set against the
|
|
15
|
-
* same registry without this one being loaded.
|
|
16
|
-
*/
|
|
17
|
-
registerVisualization('Bar', BarWidget);
|
|
18
|
-
registerVisualization('Line', LineWidget);
|
|
19
|
-
registerVisualization('Doughnut', DoughnutWidget);
|
|
20
|
-
registerVisualization('Pie', PieWidget);
|
|
21
|
-
registerVisualization('Kpi', KpiWidget);
|
|
22
|
-
registerVisualization('Funnel', FunnelWidget);
|
|
23
|
-
registerVisualization('Table', SeriesTableWidget);
|
|
24
|
-
|
|
25
|
-
export { BarWidget, LineWidget, DoughnutWidget, PieWidget, KpiWidget, FunnelWidget, SeriesTableWidget };
|
|
26
|
-
export { DetailTable } from './TableWidget';
|
|
27
|
-
export { PreviewWidget } from './PreviewWidget';
|
|
28
|
-
export * from './palette';
|
|
29
|
-
export { chartChrome, baseChartOptions } from './chartTheme';
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { ReportSpec } from '../../core/types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The categorical series palette.
|
|
5
|
-
*
|
|
6
|
-
* Eight hues, in a fixed order, validated for colour-vision deficiency against
|
|
7
|
-
* both surfaces (worst adjacent CVD ΔE 9.1 light / 8.4 dark on the OKLab×100
|
|
8
|
-
* scale; worst adjacent normal-vision ΔE 19.6 / 19.3). The dark column is the
|
|
9
|
-
* same eight hues re-stepped for a dark surface — not an automatic flip of the
|
|
10
|
-
* light values, which would drop several below the 3:1 contrast floor.
|
|
11
|
-
*
|
|
12
|
-
* Three light-mode hues (aqua, yellow, magenta) sit under 3:1 against the light
|
|
13
|
-
* surface. That is permitted only with "relief": a legend is always rendered
|
|
14
|
-
* for two or more series and every report also offers a Table visualisation,
|
|
15
|
-
* so no value is reachable by colour alone.
|
|
16
|
-
*/
|
|
17
|
-
export const SERIES_LIGHT = [
|
|
18
|
-
'#2a78d6', // blue
|
|
19
|
-
'#eb6834', // orange
|
|
20
|
-
'#1baf7a', // aqua
|
|
21
|
-
'#eda100', // yellow
|
|
22
|
-
'#e87ba4', // magenta
|
|
23
|
-
'#008300', // green
|
|
24
|
-
'#4a3aa7', // violet
|
|
25
|
-
'#e34948', // red
|
|
26
|
-
] as const;
|
|
27
|
-
|
|
28
|
-
export const SERIES_DARK = [
|
|
29
|
-
'#3987e5',
|
|
30
|
-
'#d95926',
|
|
31
|
-
'#199e70',
|
|
32
|
-
'#c98500',
|
|
33
|
-
'#d55181',
|
|
34
|
-
'#008300',
|
|
35
|
-
'#9085e9',
|
|
36
|
-
'#e66767',
|
|
37
|
-
] as const;
|
|
38
|
-
|
|
39
|
-
/** Anything past the eighth slot folds into this rather than inventing a hue. */
|
|
40
|
-
export const OTHER_LIGHT = '#8a8a80';
|
|
41
|
-
export const OTHER_DARK = '#6f6f66';
|
|
42
|
-
|
|
43
|
-
export const MAX_SERIES = SERIES_LIGHT.length;
|
|
44
|
-
|
|
45
|
-
export function seriesPalette(dark: boolean): readonly string[] {
|
|
46
|
-
return dark ? SERIES_DARK : SERIES_LIGHT;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* A stable slot for every series key.
|
|
51
|
-
*
|
|
52
|
-
* Keyed on the ENTITY, never on its position in the current result. Assigning
|
|
53
|
-
* by index would repaint every surviving series the moment a filter removed one
|
|
54
|
-
* above it — the reader sees "delivered" change from aqua to orange and reads it
|
|
55
|
-
* as a different thing. The spec's declared dimension values give a fixed order
|
|
56
|
-
* that survives filtering; metric order does the same when there is no pivot.
|
|
57
|
-
* Keys the spec does not declare (a free-form pivot like product name) fall back
|
|
58
|
-
* to first-seen order, which is at least stable within one result.
|
|
59
|
-
*/
|
|
60
|
-
export function buildSlotMap(spec: ReportSpec | null, pivotKey: string | undefined, seriesKeys: string[]): Map<string, number> {
|
|
61
|
-
const ordered: string[] = [];
|
|
62
|
-
|
|
63
|
-
const pivotDimension = pivotKey ? spec?.dimensions?.find((d) => d.key === pivotKey) : undefined;
|
|
64
|
-
|
|
65
|
-
if (pivotDimension?.values?.length) {
|
|
66
|
-
pivotDimension.values.forEach((v) => ordered.push(String(v.value)));
|
|
67
|
-
} else if (!pivotKey && spec?.metrics?.length) {
|
|
68
|
-
spec.metrics.forEach((m) => ordered.push(m.key));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
seriesKeys.forEach((k) => {
|
|
72
|
-
if (!ordered.includes(k)) ordered.push(k);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const map = new Map<string, number>();
|
|
76
|
-
ordered.forEach((key, index) => map.set(key, index));
|
|
77
|
-
|
|
78
|
-
return map;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function colorForSlot(slot: number, dark: boolean): string {
|
|
82
|
-
if (slot >= MAX_SERIES) return dark ? OTHER_DARK : OTHER_LIGHT;
|
|
83
|
-
|
|
84
|
-
return seriesPalette(dark)[slot];
|
|
85
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
import { useTheme } from '@mui/material';
|
|
3
|
-
import { formatBucket, formatBucketLong } from '../../core/formatBucket';
|
|
4
|
-
import { buildSlotMap, colorForSlot, MAX_SERIES } from './palette';
|
|
5
|
-
import { chartChrome } from './chartTheme';
|
|
6
|
-
import { label as renderLabel } from '../../core/label';
|
|
7
|
-
import type { VisualizationProps } from '../../core/visualizationRegistry';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Everything the cartesian widgets share: stable colours, folded overflow,
|
|
11
|
-
* formatted labels and the resolved chrome.
|
|
12
|
-
*
|
|
13
|
-
* Series past the eighth are folded into a single "Other" series rather than
|
|
14
|
-
* given a generated hue — nine-plus categorical colours stop being
|
|
15
|
-
* distinguishable, and a reader cannot hold that many identities at once. The
|
|
16
|
-
* fold is additive, so the total still reconciles with the table.
|
|
17
|
-
*/
|
|
18
|
-
export function useChartData({ result, spec, translate }: VisualizationProps) {
|
|
19
|
-
const theme = useTheme();
|
|
20
|
-
|
|
21
|
-
return useMemo(() => {
|
|
22
|
-
const chrome = chartChrome(theme);
|
|
23
|
-
const pivotKey = result.meta?.pivot ?? undefined;
|
|
24
|
-
|
|
25
|
-
const slots = buildSlotMap(spec, pivotKey ?? undefined, result.series.map((s) => s.key));
|
|
26
|
-
|
|
27
|
-
const kept = result.series.filter((s) => (slots.get(s.key) ?? 0) < MAX_SERIES);
|
|
28
|
-
const overflow = result.series.filter((s) => (slots.get(s.key) ?? 0) >= MAX_SERIES);
|
|
29
|
-
|
|
30
|
-
const datasets = kept.map((series) => ({
|
|
31
|
-
key: series.key,
|
|
32
|
-
label: renderLabel(translate, series.label, series.labelText),
|
|
33
|
-
data: series.data,
|
|
34
|
-
color: colorForSlot(slots.get(series.key) ?? 0, chrome.dark),
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
if (overflow.length > 0) {
|
|
38
|
-
// Sum the tail position-wise so the folded series is still a real total.
|
|
39
|
-
const summed = result.buckets.map((_, index) =>
|
|
40
|
-
overflow.reduce<number | null>((acc, series) => {
|
|
41
|
-
const value = series.data[index];
|
|
42
|
-
if (value === null || value === undefined) return acc;
|
|
43
|
-
return (acc ?? 0) + value;
|
|
44
|
-
}, null),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
datasets.push({
|
|
48
|
-
key: '__other__',
|
|
49
|
-
label: translate('dash_reports.other', { _: 'Other', count: overflow.length }),
|
|
50
|
-
data: summed,
|
|
51
|
-
color: colorForSlot(MAX_SERIES, chrome.dark),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const labels = result.buckets.map((b) => formatBucket(b, result.granularity, translate));
|
|
56
|
-
const longLabels = result.buckets.map((b) => formatBucketLong(b, result.granularity, translate));
|
|
57
|
-
|
|
58
|
-
return { chrome, datasets, labels, longLabels, foldedCount: overflow.length };
|
|
59
|
-
}, [result, spec, translate, theme]);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export default useChartData;
|
package/src/core/formatBucket.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { Granularity, TranslateFn } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Turn a bucket key into an axis label.
|
|
5
|
-
*
|
|
6
|
-
* Series grains arrive as 'YYYY-MM-DD HH:mm:ss' in the REPORTING timezone —
|
|
7
|
-
* already local, already truncated. They are formatted by string slicing rather
|
|
8
|
-
* than by `new Date(...)`, deliberately: parsing them into a Date would apply
|
|
9
|
-
* the browser's timezone to a value the server already converted, shifting
|
|
10
|
-
* every label by the offset between the viewer and the venue. A restaurant in
|
|
11
|
-
* Santiago read from a laptop in Madrid must still show Santiago's days.
|
|
12
|
-
*/
|
|
13
|
-
export function formatBucket(
|
|
14
|
-
bucket: string | number,
|
|
15
|
-
granularity: Granularity,
|
|
16
|
-
translate: TranslateFn,
|
|
17
|
-
): string {
|
|
18
|
-
// A total has exactly one bucket covering the whole window, so it has no
|
|
19
|
-
// meaningful axis label — the surrounding card already states the period.
|
|
20
|
-
if (granularity === 'total') {
|
|
21
|
-
return translate('dash_reports.granularity.total', { _: 'Total' });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (granularity === 'hourOfDay') {
|
|
25
|
-
return `${String(bucket).padStart(2, '0')}:00`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (granularity === 'dayOfWeek') {
|
|
29
|
-
// ISO day: 1 = Monday .. 7 = Sunday.
|
|
30
|
-
const key = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'][Number(bucket) - 1];
|
|
31
|
-
return translate(`dash_reports.weekdays.${key}`, { _: key ?? String(bucket) });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const raw = String(bucket);
|
|
35
|
-
const date = raw.slice(0, 10); // YYYY-MM-DD
|
|
36
|
-
const [year, month, day] = date.split('-');
|
|
37
|
-
|
|
38
|
-
switch (granularity) {
|
|
39
|
-
case 'yearly':
|
|
40
|
-
return year;
|
|
41
|
-
case 'monthly':
|
|
42
|
-
return `${month}/${year}`;
|
|
43
|
-
case 'weekly':
|
|
44
|
-
return `${day}/${month}`;
|
|
45
|
-
case 'daily':
|
|
46
|
-
default:
|
|
47
|
-
return `${day}/${month}`;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** A longer form for tooltips, where there is room to disambiguate. */
|
|
52
|
-
export function formatBucketLong(
|
|
53
|
-
bucket: string | number,
|
|
54
|
-
granularity: Granularity,
|
|
55
|
-
translate: TranslateFn,
|
|
56
|
-
): string {
|
|
57
|
-
if (granularity === 'total' || granularity === 'hourOfDay' || granularity === 'dayOfWeek') {
|
|
58
|
-
return formatBucket(bucket, granularity, translate);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const raw = String(bucket);
|
|
62
|
-
const [year, month, day] = raw.slice(0, 10).split('-');
|
|
63
|
-
|
|
64
|
-
if (granularity === 'yearly') return year;
|
|
65
|
-
if (granularity === 'monthly') return `${month}/${year}`;
|
|
66
|
-
if (granularity === 'weekly') {
|
|
67
|
-
return translate('dash_reports.week_of', { _: `Week of ${day}/${month}/${year}`, date: `${day}/${month}/${year}` });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return `${day}/${month}/${year}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export default formatBucket;
|
package/src/core/label.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { TranslateFn } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Render a backend label.
|
|
5
|
-
*
|
|
6
|
-
* The API sends a translation KEY plus the text the backend resolved for it.
|
|
7
|
-
* This prefers the app's own translation of the key and falls back to the
|
|
8
|
-
* backend's text — polyglot's `_` option does exactly that, returning the
|
|
9
|
-
* default whenever the key is missing.
|
|
10
|
-
*
|
|
11
|
-
* Why both: sending only the key means every app must redeclare every report's
|
|
12
|
-
* vocabulary, and a newly registered report renders as `reports.x.title` until
|
|
13
|
-
* someone ships a frontend change. Sending only text would take the override
|
|
14
|
-
* away and make responses locale-bound. This way a new report is readable with
|
|
15
|
-
* no frontend change, and an app that wants different wording still wins by
|
|
16
|
-
* defining the key.
|
|
17
|
-
*/
|
|
18
|
-
export function label(
|
|
19
|
-
translate: TranslateFn,
|
|
20
|
-
key: string | undefined,
|
|
21
|
-
text?: string,
|
|
22
|
-
): string {
|
|
23
|
-
if (!key) return text ?? '';
|
|
24
|
-
|
|
25
|
-
return translate(key, { _: text ?? key });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export default label;
|
package/src/core/queryParams.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { ReportFilterState } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Flatten filter state into the query the backend expects.
|
|
5
|
-
*
|
|
6
|
-
* Dimension selections are sent under their own key (`?status[]=CREATED`),
|
|
7
|
-
* which is what ReportQuery::resolveFilters reads. Empty selections are
|
|
8
|
-
* omitted entirely rather than sent as an empty array — an empty `whereIn`
|
|
9
|
-
* matches nothing, so sending one would turn "no filter" into "no rows".
|
|
10
|
-
*/
|
|
11
|
-
export function toQueryParams(filters: ReportFilterState): Record<string, unknown> {
|
|
12
|
-
const params: Record<string, unknown> = {
|
|
13
|
-
start: filters.start,
|
|
14
|
-
end: filters.end,
|
|
15
|
-
granularity: filters.granularity,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
if (filters.pivot) params.pivot = filters.pivot;
|
|
19
|
-
|
|
20
|
-
Object.entries(filters.dimensions ?? {}).forEach(([key, values]) => {
|
|
21
|
-
if (Array.isArray(values) && values.length > 0) params[key] = values;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
return params;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default toQueryParams;
|
package/src/core/types.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The wire contract with app/ReportCore.
|
|
3
|
-
*
|
|
4
|
-
* A ReportSpec is the single description the backend serves for a report, and
|
|
5
|
-
* everything this package renders — filters, chart shells, export controls —
|
|
6
|
-
* is derived from it. Nothing here is KitchnTabs-specific: a domain adds a
|
|
7
|
-
* report by registering a definition server-side, and this package renders it
|
|
8
|
-
* without a frontend change.
|
|
9
|
-
*
|
|
10
|
-
* Deliberately NOT called a "manifest": `ResourceManifest` already means a lazy
|
|
11
|
-
* resource-module map in dash-app-common and dash-admin, and reusing the word
|
|
12
|
-
* for a second, unrelated concept would be a genuine ambiguity in a codebase
|
|
13
|
-
* that already has both.
|
|
14
|
-
*
|
|
15
|
-
* Every `label` is a translation KEY, never display text. The previous
|
|
16
|
-
* generation of this feature returned Spanish strings from PHP, which is what
|
|
17
|
-
* made it unusable for a second domain.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export type Granularity =
|
|
21
|
-
/** One bucket for the whole window — a period total, not a timeline. */
|
|
22
|
-
| 'total'
|
|
23
|
-
| 'daily'
|
|
24
|
-
| 'weekly'
|
|
25
|
-
| 'monthly'
|
|
26
|
-
| 'yearly'
|
|
27
|
-
| 'hourOfDay'
|
|
28
|
-
| 'dayOfWeek';
|
|
29
|
-
|
|
30
|
-
export type VisualizationType =
|
|
31
|
-
| 'Bar'
|
|
32
|
-
| 'Line'
|
|
33
|
-
| 'Pie'
|
|
34
|
-
| 'Doughnut'
|
|
35
|
-
| 'Kpi'
|
|
36
|
-
| 'Funnel'
|
|
37
|
-
| 'Table';
|
|
38
|
-
|
|
39
|
-
export interface DimensionValue {
|
|
40
|
-
value: string | number | boolean;
|
|
41
|
-
/** Translation key. */
|
|
42
|
-
label: string;
|
|
43
|
-
/** Backend-resolved text, used when the app has no translation for `label`. */
|
|
44
|
-
labelText?: string;
|
|
45
|
-
icon?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ReportDimension {
|
|
49
|
-
key: string;
|
|
50
|
-
label: string;
|
|
51
|
-
labelText?: string;
|
|
52
|
-
type: 'column' | 'jsonpath' | 'relation';
|
|
53
|
-
multiple?: boolean;
|
|
54
|
-
source?: string;
|
|
55
|
-
visibleTo?: string[];
|
|
56
|
-
values?: DimensionValue[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface ReportMetric {
|
|
60
|
-
key: string;
|
|
61
|
-
label: string;
|
|
62
|
-
labelText?: string;
|
|
63
|
-
kind: 'count' | 'countNotNull' | 'countDistinct' | 'sum' | 'avg' | 'min' | 'max';
|
|
64
|
-
/** Render as money/decimal rather than an integer. */
|
|
65
|
-
decimal: boolean;
|
|
66
|
-
/** 'minutes' for a duration metric; the UI appends the unit. */
|
|
67
|
-
unit?: string | null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface ReportVisualization {
|
|
71
|
-
type: VisualizationType;
|
|
72
|
-
options?: Record<string, unknown>;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface ReportSpec {
|
|
76
|
-
key: string;
|
|
77
|
-
title: string;
|
|
78
|
-
titleText?: string;
|
|
79
|
-
granularities: Granularity[];
|
|
80
|
-
defaultGranularity: Granularity;
|
|
81
|
-
timezone: string;
|
|
82
|
-
dimensions: ReportDimension[];
|
|
83
|
-
metrics: ReportMetric[];
|
|
84
|
-
visualizations: ReportVisualization[];
|
|
85
|
-
/** How this report is best read; the tab that opens first. */
|
|
86
|
-
defaultVisualization: VisualizationType;
|
|
87
|
-
previewGranularity: Granularity;
|
|
88
|
-
filters: Record<string, { type: string }>;
|
|
89
|
-
exporters: string[];
|
|
90
|
-
defaultRangeDays: number;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface ReportSeries {
|
|
94
|
-
key: string;
|
|
95
|
-
/** Translation key (or literal text for a value like a tenant name). */
|
|
96
|
-
label: string;
|
|
97
|
-
/** Backend-resolved text; used when the app has no translation for `label`. */
|
|
98
|
-
labelText?: string;
|
|
99
|
-
metric: string;
|
|
100
|
-
/** Exactly one point per bucket; null means "no data", 0 means "none". */
|
|
101
|
-
data: Array<number | null>;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface ReportResult {
|
|
105
|
-
key: string;
|
|
106
|
-
granularity: Granularity;
|
|
107
|
-
timezone: string;
|
|
108
|
-
/** Timestamps for series grains, small ints for cyclic ones. */
|
|
109
|
-
buckets: Array<string | number>;
|
|
110
|
-
series: ReportSeries[];
|
|
111
|
-
totals: Record<string, number | null>;
|
|
112
|
-
meta?: {
|
|
113
|
-
start?: string;
|
|
114
|
-
end?: string;
|
|
115
|
-
pivot?: string | null;
|
|
116
|
-
scopedTenantIds?: string[] | null;
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface ReportTablePage {
|
|
121
|
-
data: Array<Record<string, unknown>>;
|
|
122
|
-
total: number;
|
|
123
|
-
page: number;
|
|
124
|
-
perPage: number;
|
|
125
|
-
lastPage: number;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface ReportListEntry {
|
|
129
|
-
key: string;
|
|
130
|
-
title: string;
|
|
131
|
-
titleText?: string;
|
|
132
|
-
metrics?: ReportMetric[];
|
|
133
|
-
visualizations: ReportVisualization[];
|
|
134
|
-
/** How this report is best read — drives the list preview and the first tab. */
|
|
135
|
-
defaultVisualization: VisualizationType;
|
|
136
|
-
/** Coarser grain for the preview: a card is too small for a daily series. */
|
|
137
|
-
previewGranularity: Granularity;
|
|
138
|
-
defaultRangeDays: number;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** The user-driven part of a report request. */
|
|
142
|
-
export interface ReportFilterState {
|
|
143
|
-
start: string;
|
|
144
|
-
end: string;
|
|
145
|
-
granularity: Granularity;
|
|
146
|
-
pivot?: string;
|
|
147
|
-
/** Dimension key -> selected values. */
|
|
148
|
-
dimensions: Record<string, Array<string | number | boolean>>;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Injected rather than imported from react-admin, so this package stays usable
|
|
153
|
-
* in a public/light app that has no react-admin i18n provider at all. The `_`
|
|
154
|
-
* option is polyglot's default-value convention, so a missing key renders
|
|
155
|
-
* sensible text instead of the raw key.
|
|
156
|
-
*/
|
|
157
|
-
export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
|