@finebi/finebi-cli 1.6.3 → 2.0.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.
Files changed (113) hide show
  1. package/README.md +5 -1
  2. package/agent-installation-guide.md +70 -0
  3. package/package.json +2 -1
  4. package/skills/finebi-cli-agent/SKILL.md +1 -1
  5. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  6. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
  7. package/skills/finebi-cli-agent/references/web-app.md +4 -1
  8. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  9. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +160 -160
  10. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
  11. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +55 -55
  12. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  13. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  14. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +96 -94
  15. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +1 -1
  16. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -80
  17. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
  18. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/gitignore.template +6 -6
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -63
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -40
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -3
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -58
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -235
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -20
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -70
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
  59. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -189
  60. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
  61. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  62. package/src/app.js +160 -160
  63. package/src/config.js +27 -27
  64. package/src/help.js +55 -55
  65. package/src/install-skill.js +89 -89
  66. package/src/profiles.js +158 -158
  67. package/src/skill-files.js +96 -94
  68. package/templates/web-app/AGENTS.md +1 -1
  69. package/templates/web-app/docs/acceptance.md +80 -80
  70. package/templates/web-app/examples/cockpit.config.json +22 -22
  71. package/templates/web-app/examples/retail.config.json +185 -185
  72. package/templates/web-app/gitignore.template +6 -6
  73. package/templates/web-app/index.html +2 -2
  74. package/templates/web-app/public/app-config.json +11 -11
  75. package/templates/web-app/src/App.tsx +63 -63
  76. package/templates/web-app/src/cockpit.css +3 -3
  77. package/templates/web-app/src/components/Chart.tsx +40 -40
  78. package/templates/web-app/src/components/Insights.tsx +17 -17
  79. package/templates/web-app/src/components/MonthPicker.tsx +32 -32
  80. package/templates/web-app/src/components/OptionFilter.tsx +7 -7
  81. package/templates/web-app/src/components/RangeFilter.tsx +9 -9
  82. package/templates/web-app/src/components/Widget.tsx +40 -40
  83. package/templates/web-app/src/components/calendar-locales.ts +3 -3
  84. package/templates/web-app/src/components/calendar.ts +14 -14
  85. package/templates/web-app/src/components/charts/Bar.tsx +6 -6
  86. package/templates/web-app/src/components/charts/Line.tsx +6 -6
  87. package/templates/web-app/src/components/charts/Pie.tsx +6 -6
  88. package/templates/web-app/src/components/month-picker.css +1 -1
  89. package/templates/web-app/src/finebi/config.d.ts +3 -3
  90. package/templates/web-app/src/finebi/config.js +58 -58
  91. package/templates/web-app/src/finebi/date.d.ts +1 -1
  92. package/templates/web-app/src/finebi/date.js +5 -5
  93. package/templates/web-app/src/finebi/filters.d.ts +4 -4
  94. package/templates/web-app/src/finebi/filters.js +106 -106
  95. package/templates/web-app/src/finebi/normalize.ts +235 -235
  96. package/templates/web-app/src/finebi/types.ts +20 -20
  97. package/templates/web-app/src/main.tsx +5 -5
  98. package/templates/web-app/src/retail.css +12 -12
  99. package/templates/web-app/src/services/presentation.ts +36 -36
  100. package/templates/web-app/src/services/table-data.ts +47 -47
  101. package/templates/web-app/src/services/view-data.ts +18 -18
  102. package/templates/web-app/src/services/widget-queries.ts +18 -18
  103. package/templates/web-app/tests/browser/fixtures.mjs +28 -28
  104. package/templates/web-app/tests/browser/index.html +16 -16
  105. package/templates/web-app/tests/browser/main.tsx +149 -149
  106. package/templates/web-app/tests/browser/test-page.css +4 -4
  107. package/templates/web-app/tests/browser/vite.config.mjs +13 -13
  108. package/templates/web-app/tests/bundle.test.mjs +19 -19
  109. package/templates/web-app/tests/cockpit.test.mjs +73 -73
  110. package/templates/web-app/tests/field-selection.test.mjs +70 -70
  111. package/templates/web-app/tests/retail.test.mjs +31 -31
  112. package/templates/web-app/tests/table-normalize.test.mjs +189 -189
  113. package/templates/web-app/tsconfig.json +4 -4
@@ -1,235 +1,235 @@
1
- import type {AxisLabel, Cell, Column, Dataset, QueryResult, RowKind} from './types';
2
-
3
- // Native preview JSON is deliberately confined to this boundary.
4
- type Native = Record<string, any>;
5
- const unavailable = (reason: string, display: string | null = null): Cell => ({value: null, display, state: 'UNAVAILABLE', reason});
6
- const text = (value: unknown): string | null => typeof value === 'string' ? value : null;
7
- const column = (id: string, name: string, index: number, type: Column['type']): Column => ({key: `c${index}`, fieldId: id, name, type, encoding: ({STRING: 'STRING', DECIMAL: 'DECIMAL_STRING', DATE: 'EPOCH_MILLIS_STRING', UNKNOWN: null})[type]});
8
- const warn = (r: QueryResult, message: string) => {if (!r.warnings.includes(message)) r.warnings.push(message);};
9
- const partial = (r: QueryResult, message: string) => {r.normalization = 'PARTIAL'; warn(r, message);};
10
-
11
- function value(raw: unknown, display: string | null, authority: unknown, type: Column['type']): Cell {
12
- if (authority === false) return {value: null, display, state: 'RESTRICTED', reason: 'SOURCE_VALUE_RESTRICTED'};
13
- if (authority !== true) return unavailable('AUTHORITY_UNAVAILABLE', display);
14
- if (raw == null) return display === null ? {value: null, display: null, state: 'NULL', reason: null} : unavailable('DISPLAY_ONLY', display);
15
- if (type === 'STRING' && typeof raw === 'string') return {value: raw, display, state: 'AVAILABLE', reason: null};
16
- if (type === 'DATE') {
17
- const s = String(raw);
18
- if ((typeof raw === 'string' || typeof raw === 'number' && Number.isSafeInteger(raw)) && /^[+-]?\d+$/.test(s)) {
19
- const n = BigInt(s);
20
- if (n >= -9223372036854775808n && n <= 9223372036854775807n) return {value: s, display, state: 'AVAILABLE', reason: null};
21
- }
22
- return unavailable('INVALID_TIMESTAMP_VALUE', display);
23
- }
24
- if (type === 'DECIMAL' && (typeof raw === 'string' || typeof raw === 'number')) {
25
- if (typeof raw === 'number' && (!Number.isFinite(raw) || Number.isInteger(raw) && !Number.isSafeInteger(raw))) return unavailable('UNSAFE_NUMERIC_VALUE', display);
26
- if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(String(raw))) return {value: String(raw), display, state: 'AVAILABLE', reason: null};
27
- }
28
- return unavailable(type === 'UNKNOWN' ? 'TYPE_UNAVAILABLE' : 'DISPLAY_ONLY', display);
29
- }
30
-
31
- export function normalize(source: Native): QueryResult {
32
- const p = source?.previewResult;
33
- const r: QueryResult = {schemaVersion: '1', resultType: 'UNKNOWN', normalization: 'COMPLETE', datasets: [], warnings: [], range: {mode: 'UNKNOWN', page: null, pageSize: null, pageUnit: null, hasNext: null, hasPrevious: null, total: {value: null, unit: null}}};
34
- if (p?.resultType === 'DETAIL' && Array.isArray(p.header) && Array.isArray(p.items)) {
35
- r.resultType = 'DETAIL_TABLE';
36
- r.range = {mode: 'PAGE', page: 1, pageSize: Number.isSafeInteger(p.size) ? p.size : null, pageUnit: 'ROW', hasNext: typeof p.hasNext === 'boolean' ? p.hasNext : null, hasPrevious: typeof p.hasPrev === 'boolean' ? p.hasPrev : null, total: {value: Number.isSafeInteger(p.row) && p.row >= 0 ? p.row : null, unit: 'DETAIL_ROW'}};
37
- const dataset: Dataset = {id: 'main', columns: p.header.map((h: Native, i: number) => column(h.dId, h.text, i, detailType(source.widgetBean?.dimensions?.[h.dId]?.type))), rows: [], returnedRowCount: p.items.length};
38
- for (const row of p.items) {
39
- if (!Array.isArray(row) || row.length !== dataset.columns.length) partial(r, 'ROW_WIDTH_MISMATCH');
40
- dataset.rows.push({kind: 'DATA', cells: dataset.columns.map((c, i) => {
41
- const cell = row?.[i];
42
- if (!cell || typeof cell !== 'object' || cell.dId && cell.dId !== c.fieldId) {partial(r, 'ROW_FIELD_MISMATCH'); return unavailable('MISSING_CELL');}
43
- return value(cell.value, text(cell.formatText) ?? text(cell.text), cell.authority, c.type);
44
- })});
45
- }
46
- r.datasets.push(dataset);
47
- if (p.hasNext) warn(r, 'NEXT_PAGE_UNSUPPORTED');
48
- if (r.range.total.value !== null && dataset.rows.length < r.range.total.value) warn(r, 'RESULT_TRUNCATED');
49
- } else if (p?.resultType === 'BIGROUP') {
50
- r.resultType = 'GROUP_TABLE';
51
- if (!Array.isArray(p.header) || !Array.isArray(p.items)) {
52
- r.normalization = 'UNSUPPORTED'; warn(r, 'INVALID_GROUP_TABLE'); return r;
53
- }
54
- const style = source.widgetBean?.settings?.tableStyle;
55
- if (style?.showRowHeaderIcon === true) r.range.mode = 'INITIAL_VIEW';
56
- else r.range = {mode: 'PAGE', page: 1, pageSize: Number.isSafeInteger(style?.rowCounts) && style.rowCounts > 0 ? style.rowCounts : null, pageUnit: 'GROUP', hasNext: typeof p.hasNext === 'boolean' ? p.hasNext : null, hasPrevious: typeof p.hasPrev === 'boolean' ? p.hasPrev : null, total: {value: null, unit: 'GROUP'}};
57
- r.datasets.push(groupDataset(p, source.widgetBean, r));
58
- if (p.hasNext) warn(r, 'NEXT_PAGE_UNSUPPORTED');
59
- } else if (p?.resultType === 'BICROSS') {
60
- r.resultType = 'CROSS_TABLE'; r.range.mode = 'INITIAL_VIEW';
61
- const flag = (v: unknown) => typeof v === 'boolean' ? v : null;
62
- const either = (a: boolean | null, b: boolean | null) => a === true || b === true ? true : a === false && b === false ? false : null;
63
- const horizontal = {hasNext: flag(p.hasXNext), hasPrevious: flag(p.hasXPrev)};
64
- const vertical = {hasNext: flag(p.hasYNext), hasPrevious: flag(p.hasYPrev)};
65
- r.range.axes = {horizontal, vertical};
66
- r.range.hasNext = either(horizontal.hasNext, vertical.hasNext);
67
- r.range.hasPrevious = either(horizontal.hasPrevious, vertical.hasPrevious);
68
- if (r.range.hasNext) warn(r, 'NEXT_PAGE_UNSUPPORTED');
69
- const dataset = crossDataset(p, r);
70
- if (dataset) r.datasets.push(dataset);
71
- } else if (p?.resultType === 'CHART') {
72
- r.resultType = 'CHART'; r.range.mode = 'UNPAGED';
73
- const models: Native[] = [];
74
- for (const geom of p.geoms || []) {
75
- if (geom?.dataModel) models.push(geom.dataModel); else partial(r, 'GEOMETRY_WITHOUT_INLINE_DATA');
76
- }
77
- if (p.shared?.data) models.push(p.shared.data);
78
- if (p.computedState) partial(r, 'CHART_COMPUTED_STATE_NOT_NORMALIZED');
79
- for (const model of new Set(models)) {
80
- if (!Array.isArray(model.fields) || !Array.isArray(model.colData) || !Number.isSafeInteger(model.rowCount) || model.rowCount < 0) {partial(r, 'INVALID_CHART_MODEL'); continue;}
81
- const dataset: Dataset = {id: `model${r.datasets.length}`, columns: model.fields.map((f: Native, i: number) => column(f.id, f.name, i, f.fieldType === 'MEASURE' ? 'DECIMAL' : f.fieldType === 'DIMENSION' ? 'STRING' : 'UNKNOWN')), rows: [], returnedRowCount: model.rowCount};
82
- if (model.colData.length !== model.fields.length || model.colData.some((c: unknown[]) => !Array.isArray(c) || c.length !== model.rowCount)) partial(r, 'ROW_WIDTH_MISMATCH');
83
- for (let i = 0; i < model.rowCount; i++) dataset.rows.push({kind: 'UNKNOWN', cells: dataset.columns.map((c, j) => {
84
- if (!Array.isArray(model.colData[j]) || i >= model.colData[j].length) return unavailable('MISSING_CELL');
85
- const raw = model.colData[j][i];
86
- if (raw === '***') return {value: null, display: '***', state: 'RESTRICTED', reason: 'SOURCE_VALUE_RESTRICTED'};
87
- if (c.type === 'DECIMAL' && typeof raw === 'string') return unavailable('CHART_VALUE_SEMANTICS_UNKNOWN');
88
- return value(raw, c.type === 'STRING' ? text(raw) : null, true, c.type);
89
- })});
90
- r.datasets.push(dataset);
91
- }
92
- if (!r.datasets.length) {r.normalization = 'UNSUPPORTED'; warn(r, 'NO_INLINE_CHART_DATA');}
93
- } else {r.normalization = 'UNSUPPORTED'; warn(r, 'RESULT_TYPE_UNSUPPORTED');}
94
- return r;
95
- }
96
-
97
- function detailType(type: number): Column['type'] {
98
- return type === 1 ? 'STRING' : type === 2 ? 'DECIMAL' : [3,13,15,20].includes(type) ? 'DATE' : 'UNKNOWN';
99
- }
100
-
101
- function rowKind(values: Native[], axis = 'rowSumInfo'): RowKind {
102
- const kinds = values.map(metric => {
103
- const info = metric?.[axis];
104
- if (typeof info?.totalSum !== 'boolean' || typeof info?.innerSum !== 'boolean' || info.totalSum && info.innerSum) return 'UNKNOWN';
105
- return info.totalSum ? 'GRAND_TOTAL' : info.innerSum ? 'SUBTOTAL' : 'DATA';
106
- });
107
- return kinds.length && kinds.every(kind => kind === kinds[0]) ? kinds[0] : 'UNKNOWN';
108
- }
109
-
110
- // Native top leaves and left value arrays share the converter's slot order.
111
- // Field IDs repeat across cross columns and must never be used to join cells.
112
- function crossDataset(preview: Native, result: QueryResult): Dataset | null {
113
- const reject = (reason: string) => {result.normalization = 'UNSUPPORTED'; warn(result, reason); return null;};
114
- const record = (v: unknown): v is Native => !!v && typeof v === 'object' && !Array.isArray(v);
115
- if (![preview.top, preview.left].every(axis => record(axis) && Array.isArray(axis.header) && Array.isArray(axis.items))) return reject('INVALID_CROSS_TABLE');
116
- type Leaf = {path: AxisLabel[]; values: Native[]; restricted: boolean; unknownAuthority: boolean};
117
- let invalid = false;
118
- const leaves = (items: Native[]): Leaf[] => {
119
- const output: Leaf[] = [];
120
- const visit = (item: Native, parents: AxisLabel[], restricted: boolean, unknownAuthority: boolean) => {
121
- if (!record(item) || item.children != null && !Array.isArray(item.children) || item.values != null && !Array.isArray(item.values)) {invalid = true; return;}
122
- const children: Native[] = item.children ?? [], values: Native[] = item.values ?? [];
123
- if (children.length && values.length || values.some(v => !record(v))) {invalid = true; return;}
124
- restricted ||= item.authority === false;
125
- const label = text(item.formatText) ?? text(item.text);
126
- unknownAuthority ||= label !== null && item.authority !== true && item.authority !== false;
127
- const path = [...parents];
128
- if (label !== null) path.push({fieldId: text(item.dId), label: restricted ? '受限' : unknownAuthority ? '不可用' : label, state: restricted ? 'RESTRICTED' : unknownAuthority ? 'UNAVAILABLE' : 'AVAILABLE'});
129
- if (children.length) for (const child of children) visit(child, path, restricted, unknownAuthority);
130
- else if (values.length || item.dId != null) output.push({path, values, restricted, unknownAuthority});
131
- };
132
- for (const item of items) visit(item, [], false, false);
133
- return output;
134
- };
135
- const top = leaves(preview.top.items), left = leaves(preview.left.items);
136
- if (invalid) return reject('CROSS_TABLE_TOPOLOGY_UNSUPPORTED');
137
- const slots = top.flatMap(leaf => (leaf.values.length ? leaf.values : [{}]).map(metric => {
138
- const path = [...leaf.path], label = text(metric.formatText) ?? text(metric.text);
139
- const restricted = leaf.restricted || metric.authority === false;
140
- const unknownAuthority = leaf.unknownAuthority || label !== null && metric.authority !== true && metric.authority !== false;
141
- if (label !== null && (path.at(-1)?.label !== label || path.at(-1)?.fieldId !== metric.dId)) path.push({fieldId: text(metric.dId), label: restricted ? '受限' : unknownAuthority ? '不可用' : label, state: restricted ? 'RESTRICTED' : unknownAuthority ? 'UNAVAILABLE' : 'AVAILABLE'});
142
- const titleParts = path.map(p => p.label);
143
- if (path.length > leaf.path.length && titleParts.at(-1) === titleParts.at(-2)) titleParts.pop();
144
- return {path, title: titleParts.join(' / '), restricted, unknownAuthority, sourceFieldId: text(metric.dId)};
145
- }));
146
- // With no column dimensions the platform emits an empty top axis;
147
- // each left values array still contains the ordered metric slots.
148
- if (!slots.length && !preview.top.header.length && !preview.top.items.length) {
149
- const width = Math.max(0, ...left.map(leaf => leaf.values.length));
150
- for (let index = 0; index < width; index++) slots.push({path: [], title: '', restricted: false, unknownAuthority: false, sourceFieldId: null});
151
- }
152
- if (left.some(leaf => leaf.values.length > slots.length)) return reject('CROSS_TABLE_WIDTH_MISMATCH');
153
- if (!slots.length && left.some(leaf => leaf.values.length)) return reject('CROSS_TABLE_WITHOUT_COLUMNS');
154
- const dataset: Dataset = {id: 'main', columns: [column('cross:row', '行分组', 0, 'STRING'), ...slots.map((slot, index) => {
155
- const metrics = left.map(leaf => leaf.values[index]).filter(metric => metric?.dId != null);
156
- const kind = rowKind(metrics, 'colSumInfo');
157
- const suffix = kind === 'GRAND_TOTAL' ? '(总计)' : kind === 'SUBTOTAL' ? '(小计)' : '';
158
- return {...column(`cross:${index}`, (slot.title || `列 ${index + 1}`) + suffix, index + 1, 'DECIMAL'), path: slot.path, kind};
159
- })], rows: [], returnedRowCount: left.length};
160
- for (const leaf of left) {
161
- if (leaf.values.length !== slots.length) partial(result, 'CROSS_TABLE_WIDTH_MISMATCH');
162
- const kind = rowKind(leaf.values.filter(metric => metric.dId != null));
163
- const titlePath = leaf.path.filter(p => kind !== 'DATA' || p.fieldId !== '__summary_dim_id__');
164
- const metricIds = leaf.values.map(metric => text(metric.dId)).filter(id => id !== null);
165
- const last = titlePath.at(-1);
166
- if (last?.fieldId && last.label === titlePath.at(-2)?.label
167
- && metricIds.length && metricIds.every(id => id === last.fieldId)) titlePath.pop();
168
- const label = titlePath.map(p => p.label).join(' / ') || '—';
169
- const rowCell: Cell = leaf.restricted ? value(null, '受限', false, 'STRING') : leaf.unknownAuthority ? unavailable('AUTHORITY_UNAVAILABLE') : value(label, label, true, 'STRING');
170
- const cells = slots.map((slot, index): Cell => {
171
- const metric = leaf.values[index];
172
- const meta = {sourceFieldId: text(metric?.dId), columnKind: rowKind(metric ? [metric] : [], 'colSumInfo')};
173
- if (leaf.restricted || slot.restricted || metric?.authority === false) return {...value(null, '受限', false, 'DECIMAL'), ...meta};
174
- if (leaf.unknownAuthority || slot.unknownAuthority) return {...unavailable('AUTHORITY_UNAVAILABLE'), ...meta};
175
- if (!metric) return {...unavailable('MISSING_CELL'), ...meta};
176
- if (typeof metric.dId !== 'string' || !metric.dId.trim()) return {...unavailable('MISSING_FIELD_ID'), ...meta, sourceFieldId: null};
177
- if (slot.sourceFieldId && metric.dId && slot.sourceFieldId !== metric.dId) {
178
- partial(result, 'CROSS_TABLE_METRIC_MISMATCH'); return {...unavailable('AMBIGUOUS_FIELD'), ...meta};
179
- }
180
- const raw = metric.value ?? metric.text, display = text(metric.formatText) ?? text(metric.text);
181
- if (typeof metric.dId === 'string' && metric.dId.trim() && metric.authority === true
182
- && metric.value === null && metric.text === null && metric.formatText === null) {
183
- return {...value(null, null, true, 'DECIMAL'), ...meta};
184
- }
185
- // Empty metric placeholders have no proven business-null semantics.
186
- return {...(raw == null ? unavailable('DISPLAY_ONLY', display) : value(raw, display, metric.authority, 'DECIMAL')), ...meta};
187
- });
188
- dataset.rows.push({kind, path: leaf.path, cells: [rowCell, ...cells]});
189
- }
190
- return dataset;
191
- }
192
-
193
- function groupDataset(preview: Native, widget: Native, result: QueryResult): Dataset {
194
- const list = (items: unknown): Native[] => Array.isArray(items) ? items : [];
195
- const metrics = new Set<string>();
196
- const collect = (items: Native[]) => {
197
- for (const item of items) {
198
- for (const metric of list(item?.values)) if (typeof metric?.dId === 'string') metrics.add(metric.dId);
199
- collect(list(item?.children));
200
- }
201
- };
202
- collect(preview.items);
203
- const dataset: Dataset = {id: 'main', columns: preview.header.map((header: Native, index: number) => {
204
- const type = widget?.dimensions?.[header?.dId]?.type;
205
- return column(header?.dId, header?.text, index, metrics.has(header?.dId) ? 'DECIMAL' : type === 1 ? 'STRING' : type === 2 ? 'DECIMAL' : 'UNKNOWN');
206
- }), rows: [], returnedRowCount: 0};
207
- const ids = new Set(dataset.columns.map(c => c.fieldId));
208
- const visit = (item: Native, parents: Map<string, Native>) => {
209
- if (!item || typeof item !== 'object') {partial(result, 'MISSING_GROUP_NODE'); return;}
210
- const path = new Map(parents);
211
- if (item.dId != null) path.set(item.dId, item);
212
- const values = list(item.values), children = list(item.children);
213
- if (item.values != null && !Array.isArray(item.values) || item.children != null && !Array.isArray(item.children)) partial(result, 'INVALID_GROUP_NODE');
214
- if (item.dId != null && !ids.has(item.dId) || values.some(metric => !ids.has(metric?.dId))) partial(result, 'UNMAPPED_GROUP_FIELD');
215
- if (values.length || !children.length) dataset.rows.push({kind: rowKind(values), cells: dataset.columns.map(c => {
216
- const matching = values.filter(metric => metric?.dId === c.fieldId), dimension = path.get(c.fieldId);
217
- if (matching.length === 1 && !dimension) {
218
- const metric = matching[0], display = text(metric.formatText) ?? text(metric.text), raw = metric.value ?? metric.text;
219
- if (raw == null && metric.authority === true) return unavailable('DISPLAY_ONLY', display);
220
- return value(raw, display, metric.authority, c.type);
221
- }
222
- if (!matching.length && dimension) {
223
- const display = text(dimension.formatText) ?? text(dimension.text);
224
- if (dimension.authority === false) return value(null, display, false, c.type);
225
- return c.type === 'STRING' ? value(dimension.text, display, dimension.authority, c.type) : unavailable('GROUP_KEY_SEMANTICS_UNKNOWN', display);
226
- }
227
- if (matching.length) partial(result, 'AMBIGUOUS_FIELD');
228
- return unavailable(matching.length ? 'AMBIGUOUS_FIELD' : 'MISSING_CELL');
229
- })});
230
- for (const child of children) visit(child, path);
231
- };
232
- for (const item of preview.items) visit(item, new Map());
233
- dataset.returnedRowCount = dataset.rows.length;
234
- return dataset;
235
- }
1
+ import type {AxisLabel, Cell, Column, Dataset, QueryResult, RowKind} from './types';
2
+
3
+ // Native preview JSON is deliberately confined to this boundary.
4
+ type Native = Record<string, any>;
5
+ const unavailable = (reason: string, display: string | null = null): Cell => ({value: null, display, state: 'UNAVAILABLE', reason});
6
+ const text = (value: unknown): string | null => typeof value === 'string' ? value : null;
7
+ const column = (id: string, name: string, index: number, type: Column['type']): Column => ({key: `c${index}`, fieldId: id, name, type, encoding: ({STRING: 'STRING', DECIMAL: 'DECIMAL_STRING', DATE: 'EPOCH_MILLIS_STRING', UNKNOWN: null})[type]});
8
+ const warn = (r: QueryResult, message: string) => {if (!r.warnings.includes(message)) r.warnings.push(message);};
9
+ const partial = (r: QueryResult, message: string) => {r.normalization = 'PARTIAL'; warn(r, message);};
10
+
11
+ function value(raw: unknown, display: string | null, authority: unknown, type: Column['type']): Cell {
12
+ if (authority === false) return {value: null, display, state: 'RESTRICTED', reason: 'SOURCE_VALUE_RESTRICTED'};
13
+ if (authority !== true) return unavailable('AUTHORITY_UNAVAILABLE', display);
14
+ if (raw == null) return display === null ? {value: null, display: null, state: 'NULL', reason: null} : unavailable('DISPLAY_ONLY', display);
15
+ if (type === 'STRING' && typeof raw === 'string') return {value: raw, display, state: 'AVAILABLE', reason: null};
16
+ if (type === 'DATE') {
17
+ const s = String(raw);
18
+ if ((typeof raw === 'string' || typeof raw === 'number' && Number.isSafeInteger(raw)) && /^[+-]?\d+$/.test(s)) {
19
+ const n = BigInt(s);
20
+ if (n >= -9223372036854775808n && n <= 9223372036854775807n) return {value: s, display, state: 'AVAILABLE', reason: null};
21
+ }
22
+ return unavailable('INVALID_TIMESTAMP_VALUE', display);
23
+ }
24
+ if (type === 'DECIMAL' && (typeof raw === 'string' || typeof raw === 'number')) {
25
+ if (typeof raw === 'number' && (!Number.isFinite(raw) || Number.isInteger(raw) && !Number.isSafeInteger(raw))) return unavailable('UNSAFE_NUMERIC_VALUE', display);
26
+ if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(String(raw))) return {value: String(raw), display, state: 'AVAILABLE', reason: null};
27
+ }
28
+ return unavailable(type === 'UNKNOWN' ? 'TYPE_UNAVAILABLE' : 'DISPLAY_ONLY', display);
29
+ }
30
+
31
+ export function normalize(source: Native): QueryResult {
32
+ const p = source?.previewResult;
33
+ const r: QueryResult = {schemaVersion: '1', resultType: 'UNKNOWN', normalization: 'COMPLETE', datasets: [], warnings: [], range: {mode: 'UNKNOWN', page: null, pageSize: null, pageUnit: null, hasNext: null, hasPrevious: null, total: {value: null, unit: null}}};
34
+ if (p?.resultType === 'DETAIL' && Array.isArray(p.header) && Array.isArray(p.items)) {
35
+ r.resultType = 'DETAIL_TABLE';
36
+ r.range = {mode: 'PAGE', page: 1, pageSize: Number.isSafeInteger(p.size) ? p.size : null, pageUnit: 'ROW', hasNext: typeof p.hasNext === 'boolean' ? p.hasNext : null, hasPrevious: typeof p.hasPrev === 'boolean' ? p.hasPrev : null, total: {value: Number.isSafeInteger(p.row) && p.row >= 0 ? p.row : null, unit: 'DETAIL_ROW'}};
37
+ const dataset: Dataset = {id: 'main', columns: p.header.map((h: Native, i: number) => column(h.dId, h.text, i, detailType(source.widgetBean?.dimensions?.[h.dId]?.type))), rows: [], returnedRowCount: p.items.length};
38
+ for (const row of p.items) {
39
+ if (!Array.isArray(row) || row.length !== dataset.columns.length) partial(r, 'ROW_WIDTH_MISMATCH');
40
+ dataset.rows.push({kind: 'DATA', cells: dataset.columns.map((c, i) => {
41
+ const cell = row?.[i];
42
+ if (!cell || typeof cell !== 'object' || cell.dId && cell.dId !== c.fieldId) {partial(r, 'ROW_FIELD_MISMATCH'); return unavailable('MISSING_CELL');}
43
+ return value(cell.value, text(cell.formatText) ?? text(cell.text), cell.authority, c.type);
44
+ })});
45
+ }
46
+ r.datasets.push(dataset);
47
+ if (p.hasNext) warn(r, 'NEXT_PAGE_UNSUPPORTED');
48
+ if (r.range.total.value !== null && dataset.rows.length < r.range.total.value) warn(r, 'RESULT_TRUNCATED');
49
+ } else if (p?.resultType === 'BIGROUP') {
50
+ r.resultType = 'GROUP_TABLE';
51
+ if (!Array.isArray(p.header) || !Array.isArray(p.items)) {
52
+ r.normalization = 'UNSUPPORTED'; warn(r, 'INVALID_GROUP_TABLE'); return r;
53
+ }
54
+ const style = source.widgetBean?.settings?.tableStyle;
55
+ if (style?.showRowHeaderIcon === true) r.range.mode = 'INITIAL_VIEW';
56
+ else r.range = {mode: 'PAGE', page: 1, pageSize: Number.isSafeInteger(style?.rowCounts) && style.rowCounts > 0 ? style.rowCounts : null, pageUnit: 'GROUP', hasNext: typeof p.hasNext === 'boolean' ? p.hasNext : null, hasPrevious: typeof p.hasPrev === 'boolean' ? p.hasPrev : null, total: {value: null, unit: 'GROUP'}};
57
+ r.datasets.push(groupDataset(p, source.widgetBean, r));
58
+ if (p.hasNext) warn(r, 'NEXT_PAGE_UNSUPPORTED');
59
+ } else if (p?.resultType === 'BICROSS') {
60
+ r.resultType = 'CROSS_TABLE'; r.range.mode = 'INITIAL_VIEW';
61
+ const flag = (v: unknown) => typeof v === 'boolean' ? v : null;
62
+ const either = (a: boolean | null, b: boolean | null) => a === true || b === true ? true : a === false && b === false ? false : null;
63
+ const horizontal = {hasNext: flag(p.hasXNext), hasPrevious: flag(p.hasXPrev)};
64
+ const vertical = {hasNext: flag(p.hasYNext), hasPrevious: flag(p.hasYPrev)};
65
+ r.range.axes = {horizontal, vertical};
66
+ r.range.hasNext = either(horizontal.hasNext, vertical.hasNext);
67
+ r.range.hasPrevious = either(horizontal.hasPrevious, vertical.hasPrevious);
68
+ if (r.range.hasNext) warn(r, 'NEXT_PAGE_UNSUPPORTED');
69
+ const dataset = crossDataset(p, r);
70
+ if (dataset) r.datasets.push(dataset);
71
+ } else if (p?.resultType === 'CHART') {
72
+ r.resultType = 'CHART'; r.range.mode = 'UNPAGED';
73
+ const models: Native[] = [];
74
+ for (const geom of p.geoms || []) {
75
+ if (geom?.dataModel) models.push(geom.dataModel); else partial(r, 'GEOMETRY_WITHOUT_INLINE_DATA');
76
+ }
77
+ if (p.shared?.data) models.push(p.shared.data);
78
+ if (p.computedState) partial(r, 'CHART_COMPUTED_STATE_NOT_NORMALIZED');
79
+ for (const model of new Set(models)) {
80
+ if (!Array.isArray(model.fields) || !Array.isArray(model.colData) || !Number.isSafeInteger(model.rowCount) || model.rowCount < 0) {partial(r, 'INVALID_CHART_MODEL'); continue;}
81
+ const dataset: Dataset = {id: `model${r.datasets.length}`, columns: model.fields.map((f: Native, i: number) => column(f.id, f.name, i, f.fieldType === 'MEASURE' ? 'DECIMAL' : f.fieldType === 'DIMENSION' ? 'STRING' : 'UNKNOWN')), rows: [], returnedRowCount: model.rowCount};
82
+ if (model.colData.length !== model.fields.length || model.colData.some((c: unknown[]) => !Array.isArray(c) || c.length !== model.rowCount)) partial(r, 'ROW_WIDTH_MISMATCH');
83
+ for (let i = 0; i < model.rowCount; i++) dataset.rows.push({kind: 'UNKNOWN', cells: dataset.columns.map((c, j) => {
84
+ if (!Array.isArray(model.colData[j]) || i >= model.colData[j].length) return unavailable('MISSING_CELL');
85
+ const raw = model.colData[j][i];
86
+ if (raw === '***') return {value: null, display: '***', state: 'RESTRICTED', reason: 'SOURCE_VALUE_RESTRICTED'};
87
+ if (c.type === 'DECIMAL' && typeof raw === 'string') return unavailable('CHART_VALUE_SEMANTICS_UNKNOWN');
88
+ return value(raw, c.type === 'STRING' ? text(raw) : null, true, c.type);
89
+ })});
90
+ r.datasets.push(dataset);
91
+ }
92
+ if (!r.datasets.length) {r.normalization = 'UNSUPPORTED'; warn(r, 'NO_INLINE_CHART_DATA');}
93
+ } else {r.normalization = 'UNSUPPORTED'; warn(r, 'RESULT_TYPE_UNSUPPORTED');}
94
+ return r;
95
+ }
96
+
97
+ function detailType(type: number): Column['type'] {
98
+ return type === 1 ? 'STRING' : type === 2 ? 'DECIMAL' : [3,13,15,20].includes(type) ? 'DATE' : 'UNKNOWN';
99
+ }
100
+
101
+ function rowKind(values: Native[], axis = 'rowSumInfo'): RowKind {
102
+ const kinds = values.map(metric => {
103
+ const info = metric?.[axis];
104
+ if (typeof info?.totalSum !== 'boolean' || typeof info?.innerSum !== 'boolean' || info.totalSum && info.innerSum) return 'UNKNOWN';
105
+ return info.totalSum ? 'GRAND_TOTAL' : info.innerSum ? 'SUBTOTAL' : 'DATA';
106
+ });
107
+ return kinds.length && kinds.every(kind => kind === kinds[0]) ? kinds[0] : 'UNKNOWN';
108
+ }
109
+
110
+ // Native top leaves and left value arrays share the converter's slot order.
111
+ // Field IDs repeat across cross columns and must never be used to join cells.
112
+ function crossDataset(preview: Native, result: QueryResult): Dataset | null {
113
+ const reject = (reason: string) => {result.normalization = 'UNSUPPORTED'; warn(result, reason); return null;};
114
+ const record = (v: unknown): v is Native => !!v && typeof v === 'object' && !Array.isArray(v);
115
+ if (![preview.top, preview.left].every(axis => record(axis) && Array.isArray(axis.header) && Array.isArray(axis.items))) return reject('INVALID_CROSS_TABLE');
116
+ type Leaf = {path: AxisLabel[]; values: Native[]; restricted: boolean; unknownAuthority: boolean};
117
+ let invalid = false;
118
+ const leaves = (items: Native[]): Leaf[] => {
119
+ const output: Leaf[] = [];
120
+ const visit = (item: Native, parents: AxisLabel[], restricted: boolean, unknownAuthority: boolean) => {
121
+ if (!record(item) || item.children != null && !Array.isArray(item.children) || item.values != null && !Array.isArray(item.values)) {invalid = true; return;}
122
+ const children: Native[] = item.children ?? [], values: Native[] = item.values ?? [];
123
+ if (children.length && values.length || values.some(v => !record(v))) {invalid = true; return;}
124
+ restricted ||= item.authority === false;
125
+ const label = text(item.formatText) ?? text(item.text);
126
+ unknownAuthority ||= label !== null && item.authority !== true && item.authority !== false;
127
+ const path = [...parents];
128
+ if (label !== null) path.push({fieldId: text(item.dId), label: restricted ? '受限' : unknownAuthority ? '不可用' : label, state: restricted ? 'RESTRICTED' : unknownAuthority ? 'UNAVAILABLE' : 'AVAILABLE'});
129
+ if (children.length) for (const child of children) visit(child, path, restricted, unknownAuthority);
130
+ else if (values.length || item.dId != null) output.push({path, values, restricted, unknownAuthority});
131
+ };
132
+ for (const item of items) visit(item, [], false, false);
133
+ return output;
134
+ };
135
+ const top = leaves(preview.top.items), left = leaves(preview.left.items);
136
+ if (invalid) return reject('CROSS_TABLE_TOPOLOGY_UNSUPPORTED');
137
+ const slots = top.flatMap(leaf => (leaf.values.length ? leaf.values : [{}]).map(metric => {
138
+ const path = [...leaf.path], label = text(metric.formatText) ?? text(metric.text);
139
+ const restricted = leaf.restricted || metric.authority === false;
140
+ const unknownAuthority = leaf.unknownAuthority || label !== null && metric.authority !== true && metric.authority !== false;
141
+ if (label !== null && (path.at(-1)?.label !== label || path.at(-1)?.fieldId !== metric.dId)) path.push({fieldId: text(metric.dId), label: restricted ? '受限' : unknownAuthority ? '不可用' : label, state: restricted ? 'RESTRICTED' : unknownAuthority ? 'UNAVAILABLE' : 'AVAILABLE'});
142
+ const titleParts = path.map(p => p.label);
143
+ if (path.length > leaf.path.length && titleParts.at(-1) === titleParts.at(-2)) titleParts.pop();
144
+ return {path, title: titleParts.join(' / '), restricted, unknownAuthority, sourceFieldId: text(metric.dId)};
145
+ }));
146
+ // With no column dimensions the platform emits an empty top axis;
147
+ // each left values array still contains the ordered metric slots.
148
+ if (!slots.length && !preview.top.header.length && !preview.top.items.length) {
149
+ const width = Math.max(0, ...left.map(leaf => leaf.values.length));
150
+ for (let index = 0; index < width; index++) slots.push({path: [], title: '', restricted: false, unknownAuthority: false, sourceFieldId: null});
151
+ }
152
+ if (left.some(leaf => leaf.values.length > slots.length)) return reject('CROSS_TABLE_WIDTH_MISMATCH');
153
+ if (!slots.length && left.some(leaf => leaf.values.length)) return reject('CROSS_TABLE_WITHOUT_COLUMNS');
154
+ const dataset: Dataset = {id: 'main', columns: [column('cross:row', '行分组', 0, 'STRING'), ...slots.map((slot, index) => {
155
+ const metrics = left.map(leaf => leaf.values[index]).filter(metric => metric?.dId != null);
156
+ const kind = rowKind(metrics, 'colSumInfo');
157
+ const suffix = kind === 'GRAND_TOTAL' ? '(总计)' : kind === 'SUBTOTAL' ? '(小计)' : '';
158
+ return {...column(`cross:${index}`, (slot.title || `列 ${index + 1}`) + suffix, index + 1, 'DECIMAL'), path: slot.path, kind};
159
+ })], rows: [], returnedRowCount: left.length};
160
+ for (const leaf of left) {
161
+ if (leaf.values.length !== slots.length) partial(result, 'CROSS_TABLE_WIDTH_MISMATCH');
162
+ const kind = rowKind(leaf.values.filter(metric => metric.dId != null));
163
+ const titlePath = leaf.path.filter(p => kind !== 'DATA' || p.fieldId !== '__summary_dim_id__');
164
+ const metricIds = leaf.values.map(metric => text(metric.dId)).filter(id => id !== null);
165
+ const last = titlePath.at(-1);
166
+ if (last?.fieldId && last.label === titlePath.at(-2)?.label
167
+ && metricIds.length && metricIds.every(id => id === last.fieldId)) titlePath.pop();
168
+ const label = titlePath.map(p => p.label).join(' / ') || '—';
169
+ const rowCell: Cell = leaf.restricted ? value(null, '受限', false, 'STRING') : leaf.unknownAuthority ? unavailable('AUTHORITY_UNAVAILABLE') : value(label, label, true, 'STRING');
170
+ const cells = slots.map((slot, index): Cell => {
171
+ const metric = leaf.values[index];
172
+ const meta = {sourceFieldId: text(metric?.dId), columnKind: rowKind(metric ? [metric] : [], 'colSumInfo')};
173
+ if (leaf.restricted || slot.restricted || metric?.authority === false) return {...value(null, '受限', false, 'DECIMAL'), ...meta};
174
+ if (leaf.unknownAuthority || slot.unknownAuthority) return {...unavailable('AUTHORITY_UNAVAILABLE'), ...meta};
175
+ if (!metric) return {...unavailable('MISSING_CELL'), ...meta};
176
+ if (typeof metric.dId !== 'string' || !metric.dId.trim()) return {...unavailable('MISSING_FIELD_ID'), ...meta, sourceFieldId: null};
177
+ if (slot.sourceFieldId && metric.dId && slot.sourceFieldId !== metric.dId) {
178
+ partial(result, 'CROSS_TABLE_METRIC_MISMATCH'); return {...unavailable('AMBIGUOUS_FIELD'), ...meta};
179
+ }
180
+ const raw = metric.value ?? metric.text, display = text(metric.formatText) ?? text(metric.text);
181
+ if (typeof metric.dId === 'string' && metric.dId.trim() && metric.authority === true
182
+ && metric.value === null && metric.text === null && metric.formatText === null) {
183
+ return {...value(null, null, true, 'DECIMAL'), ...meta};
184
+ }
185
+ // Empty metric placeholders have no proven business-null semantics.
186
+ return {...(raw == null ? unavailable('DISPLAY_ONLY', display) : value(raw, display, metric.authority, 'DECIMAL')), ...meta};
187
+ });
188
+ dataset.rows.push({kind, path: leaf.path, cells: [rowCell, ...cells]});
189
+ }
190
+ return dataset;
191
+ }
192
+
193
+ function groupDataset(preview: Native, widget: Native, result: QueryResult): Dataset {
194
+ const list = (items: unknown): Native[] => Array.isArray(items) ? items : [];
195
+ const metrics = new Set<string>();
196
+ const collect = (items: Native[]) => {
197
+ for (const item of items) {
198
+ for (const metric of list(item?.values)) if (typeof metric?.dId === 'string') metrics.add(metric.dId);
199
+ collect(list(item?.children));
200
+ }
201
+ };
202
+ collect(preview.items);
203
+ const dataset: Dataset = {id: 'main', columns: preview.header.map((header: Native, index: number) => {
204
+ const type = widget?.dimensions?.[header?.dId]?.type;
205
+ return column(header?.dId, header?.text, index, metrics.has(header?.dId) ? 'DECIMAL' : type === 1 ? 'STRING' : type === 2 ? 'DECIMAL' : 'UNKNOWN');
206
+ }), rows: [], returnedRowCount: 0};
207
+ const ids = new Set(dataset.columns.map(c => c.fieldId));
208
+ const visit = (item: Native, parents: Map<string, Native>) => {
209
+ if (!item || typeof item !== 'object') {partial(result, 'MISSING_GROUP_NODE'); return;}
210
+ const path = new Map(parents);
211
+ if (item.dId != null) path.set(item.dId, item);
212
+ const values = list(item.values), children = list(item.children);
213
+ if (item.values != null && !Array.isArray(item.values) || item.children != null && !Array.isArray(item.children)) partial(result, 'INVALID_GROUP_NODE');
214
+ if (item.dId != null && !ids.has(item.dId) || values.some(metric => !ids.has(metric?.dId))) partial(result, 'UNMAPPED_GROUP_FIELD');
215
+ if (values.length || !children.length) dataset.rows.push({kind: rowKind(values), cells: dataset.columns.map(c => {
216
+ const matching = values.filter(metric => metric?.dId === c.fieldId), dimension = path.get(c.fieldId);
217
+ if (matching.length === 1 && !dimension) {
218
+ const metric = matching[0], display = text(metric.formatText) ?? text(metric.text), raw = metric.value ?? metric.text;
219
+ if (raw == null && metric.authority === true) return unavailable('DISPLAY_ONLY', display);
220
+ return value(raw, display, metric.authority, c.type);
221
+ }
222
+ if (!matching.length && dimension) {
223
+ const display = text(dimension.formatText) ?? text(dimension.text);
224
+ if (dimension.authority === false) return value(null, display, false, c.type);
225
+ return c.type === 'STRING' ? value(dimension.text, display, dimension.authority, c.type) : unavailable('GROUP_KEY_SEMANTICS_UNKNOWN', display);
226
+ }
227
+ if (matching.length) partial(result, 'AMBIGUOUS_FIELD');
228
+ return unavailable(matching.length ? 'AMBIGUOUS_FIELD' : 'MISSING_CELL');
229
+ })});
230
+ for (const child of children) visit(child, path);
231
+ };
232
+ for (const item of preview.items) visit(item, new Map());
233
+ dataset.returnedRowCount = dataset.rows.length;
234
+ return dataset;
235
+ }
@@ -1,20 +1,20 @@
1
- export type Cell = {value: string | null; display: string | null; state: 'AVAILABLE' | 'NULL' | 'UNAVAILABLE' | 'RESTRICTED'; reason: string | null; sourceFieldId?: string | null; columnKind?: RowKind};
2
- export type AxisLabel = {fieldId: string | null; label: string; state: Cell['state']};
3
- export type Column = {key: string; fieldId: string; name: string; type: 'STRING' | 'DECIMAL' | 'DATE' | 'UNKNOWN'; encoding: string | null; path?: AxisLabel[]; kind?: RowKind};
4
- export type RowKind = 'DATA' | 'SUBTOTAL' | 'GRAND_TOTAL' | 'UNKNOWN';
5
- export type Dataset = {id: string; columns: Column[]; rows: {kind: RowKind; cells: Cell[]; path?: AxisLabel[]}[]; returnedRowCount: number};
6
- export type QueryResult = {
7
- schemaVersion: '1'; resultType: 'DETAIL_TABLE' | 'GROUP_TABLE' | 'CROSS_TABLE' | 'CHART' | 'UNKNOWN';
8
- normalization: 'COMPLETE' | 'PARTIAL' | 'UNSUPPORTED'; datasets: Dataset[]; warnings: string[];
9
- range: {mode: 'PAGE' | 'INITIAL_VIEW' | 'UNPAGED' | 'UNKNOWN'; page: number | null; pageSize: number | null; pageUnit: string | null; hasNext: boolean | null; hasPrevious: boolean | null; total: {value: number | null; unit: string | null}; axes?: {horizontal: {hasNext: boolean | null; hasPrevious: boolean | null}; vertical: {hasNext: boolean | null; hasPrevious: boolean | null}}};
10
- };
11
- export type WidgetBinding = {entryId: string; dimensionIds?: string[]; name: string; type: 'table' | 'bar' | 'line' | 'pie' | 'kpi'; datasetId?: string; categoryFieldId?: string; valueFieldId?: string; series?: {fieldId:string;label:string}[]; description?:string; filterNames?:string[]; tableFieldIds?:string[]; localFilterFieldIds?:string[]; valueFormat?:'number'|'percent'; categoryFormat?:'epoch-month'; categoryTimeZone?:string};
12
- export type FilterBinding = {name: string; label: string} & (
13
- {type: 'date' | 'date-range' | 'year' | 'month' | 'month-range' | 'quarter'; options?: never; allowNull?: never}
14
- | {type: 'text' | 'number'; options?: never; allowNull?: boolean}
15
- | {type?: 'select' | 'multi-select'; options: {label: string; value: string}[]; allowNull?: boolean}
16
- | {type: 'time-label'; options: {label: string; value: string}[]; allowNull?: never}
17
- );
18
- export type FilterValue = string | string[] | {start:string;end:string} | null | undefined;
19
- export type FilterState = Record<string,FilterValue>;
20
- export type AppConfig = {schemaVersion: '1'; title: string; description?:string; basePath: string; layout: 'overview' | 'workbench' | 'cockpit' | 'retail'; dashboardId: string; directoryEntryId?:string; insights?:boolean; dataNotes?:string[]; maxRows: number; pageSize: number; widgets: WidgetBinding[]; filters: FilterBinding[]};
1
+ export type Cell = {value: string | null; display: string | null; state: 'AVAILABLE' | 'NULL' | 'UNAVAILABLE' | 'RESTRICTED'; reason: string | null; sourceFieldId?: string | null; columnKind?: RowKind};
2
+ export type AxisLabel = {fieldId: string | null; label: string; state: Cell['state']};
3
+ export type Column = {key: string; fieldId: string; name: string; type: 'STRING' | 'DECIMAL' | 'DATE' | 'UNKNOWN'; encoding: string | null; path?: AxisLabel[]; kind?: RowKind};
4
+ export type RowKind = 'DATA' | 'SUBTOTAL' | 'GRAND_TOTAL' | 'UNKNOWN';
5
+ export type Dataset = {id: string; columns: Column[]; rows: {kind: RowKind; cells: Cell[]; path?: AxisLabel[]}[]; returnedRowCount: number};
6
+ export type QueryResult = {
7
+ schemaVersion: '1'; resultType: 'DETAIL_TABLE' | 'GROUP_TABLE' | 'CROSS_TABLE' | 'CHART' | 'UNKNOWN';
8
+ normalization: 'COMPLETE' | 'PARTIAL' | 'UNSUPPORTED'; datasets: Dataset[]; warnings: string[];
9
+ range: {mode: 'PAGE' | 'INITIAL_VIEW' | 'UNPAGED' | 'UNKNOWN'; page: number | null; pageSize: number | null; pageUnit: string | null; hasNext: boolean | null; hasPrevious: boolean | null; total: {value: number | null; unit: string | null}; axes?: {horizontal: {hasNext: boolean | null; hasPrevious: boolean | null}; vertical: {hasNext: boolean | null; hasPrevious: boolean | null}}};
10
+ };
11
+ export type WidgetBinding = {entryId: string; dimensionIds?: string[]; name: string; type: 'table' | 'bar' | 'line' | 'pie' | 'kpi'; datasetId?: string; categoryFieldId?: string; valueFieldId?: string; series?: {fieldId:string;label:string}[]; description?:string; filterNames?:string[]; tableFieldIds?:string[]; localFilterFieldIds?:string[]; valueFormat?:'number'|'percent'; categoryFormat?:'epoch-month'; categoryTimeZone?:string};
12
+ export type FilterBinding = {name: string; label: string} & (
13
+ {type: 'date' | 'date-range' | 'year' | 'month' | 'month-range' | 'quarter'; options?: never; allowNull?: never}
14
+ | {type: 'text' | 'number'; options?: never; allowNull?: boolean}
15
+ | {type?: 'select' | 'multi-select'; options: {label: string; value: string}[]; allowNull?: boolean}
16
+ | {type: 'time-label'; options: {label: string; value: string}[]; allowNull?: never}
17
+ );
18
+ export type FilterValue = string | string[] | {start:string;end:string} | null | undefined;
19
+ export type FilterState = Record<string,FilterValue>;
20
+ export type AppConfig = {schemaVersion: '1'; title: string; description?:string; basePath: string; layout: 'overview' | 'workbench' | 'cockpit' | 'retail'; dashboardId: string; directoryEntryId?:string; insights?:boolean; dataNotes?:string[]; maxRows: number; pageSize: number; widgets: WidgetBinding[]; filters: FilterBinding[]};
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
- import {createRoot} from 'react-dom/client';
3
- import App from './App';
4
- import './styles.css';
5
- createRoot(document.getElementById('root')!).render(<React.StrictMode><App/></React.StrictMode>);
1
+ import React from 'react';
2
+ import {createRoot} from 'react-dom/client';
3
+ import App from './App';
4
+ import './styles.css';
5
+ createRoot(document.getElementById('root')!).render(<React.StrictMode><App/></React.StrictMode>);
@@ -1,13 +1,13 @@
1
- .retail{--chart-1:#3158d5;--chart-2:#e18047;--chart-3:#82a7a0;--chart-4:#9b8fc6;--chart-5:#d2b977;background:#f5f3ee;color:#24272d;min-height:100vh;font-family:"Segoe UI","Microsoft YaHei",sans-serif}
2
- .retail main{max-width:1480px;padding:48px 48px 24px}.retail a{color:#3158d5}.retail button,.retail select,.retail input{border-radius:5px}.retail :focus-visible{outline:3px solid #e18047;outline-offset:3px}
3
- .retail-nav{position:sticky;top:0;z-index:30}.retail :is(#dashboard,#components,#data-notes){scroll-margin-top:100px}
4
- .retail-nav{height:78px;padding:0 48px;border-bottom:1px solid #dcd9d1;display:flex;align-items:center;justify-content:space-between;gap:30px;background:#faf9f6}.retail-nav a{text-decoration:none;color:#40434b}.retail-brand{display:flex;align-items:center;gap:12px;font-weight:700;font-size:17px;white-space:nowrap}.retail-brand b{display:grid;place-items:center;background:#24272d;color:#faf9f6;font-family:Georgia,serif;font-size:30px;width:39px;height:39px;border-radius:4px}.retail-brand small{font-size:12px;font-weight:400;border-left:1px solid #bbb7ad;margin-left:10px;padding-left:12px}.retail-nav>div{display:flex;gap:36px;font-size:13px}.retail-nav>div a[aria-current="location"]{color:#3158d5;font-weight:650}.retail-live{font-size:12px;white-space:nowrap}.retail-live:before{content:"";display:inline-block;width:6px;height:6px;background:#3158d5;border-radius:50%;margin-right:8px}
5
- .retail header{align-items:flex-end;margin-bottom:30px;gap:30px}.retail .eyebrow{font-size:10px;letter-spacing:2.5px;color:#6d706e}.retail h1{font-size:36px;letter-spacing:-1.2px;font-weight:650;margin:12px 0}.retail header .muted{font-size:13px;max-width:750px;line-height:1.9}.retail .muted{color:#75776f}.retail .header-actions{display:flex;align-items:center;gap:20px;white-space:nowrap;font-size:12px}.retail .header-actions button{background:#24272d;color:#fff;border-color:#24272d;padding:11px 18px}.retail .header-actions a{color:#656863;text-decoration:none}
6
- .retail .filter-card{border-radius:8px;border-color:#dedbd3;background:#faf9f6;box-shadow:none;padding:20px 24px;margin-bottom:8px}.retail .filter-heading{margin-bottom:14px}.retail .filter-heading p{display:none}.retail .filter-symbol{height:28px;width:28px;background:#eaeefa;border-radius:5px;color:#3158d5}.retail .filter-heading h2{font-size:13px;color:#424740}.retail .filter-fields{gap:14px}.retail .filter-fields:has(>.filter-field:only-child){max-width:760px}.retail .filter-label-row{margin-bottom:5px}.retail .filter-label-row label{color:#575d65}.retail .filter-bottom{margin-top:9px;padding-top:12px;border-color:#e3e0d8}.retail .filter-scope{font-size:11px;color:#797d7e;line-height:1.8;margin:4px 0}.retail .field-hint{min-height:0;margin-top:6px}.retail .filter-status{background:#e9edf9;color:#3158d5}.retail .filter-status.pending{background:#fff0db;color:#855316}.retail .query-status{font-size:11px;color:#75776f;text-align:right;margin:12px 0 28px}
7
- .retail .section-heading{display:flex;align-items:baseline;gap:16px;margin:35px 0 18px}.retail .section-heading>span{font:10px "Segoe UI",sans-serif;letter-spacing:1.5px;color:#85877d}.retail .section-heading h2{font-size:18px;letter-spacing:-.3px;margin:0}.retail .section-heading p{font-size:11px;color:#85877d;margin:0 0 0 auto}
8
- .retail .kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;border:1px solid #d6d3ca;border-radius:8px;overflow:hidden;background:#fffefb}.retail .kpi-grid .panel{border:0;border-right:1px solid #dedbd3;border-radius:0;background:transparent;padding:23px 25px 18px;display:flex;flex-direction:column;position:relative}.retail .kpi-grid .panel:last-child{border-right:0}.retail .kpi-grid .panel:first-child{background:#eaeefb}.retail .kpi-grid .panel-heading{display:block}.retail .kpi-grid h2{font-size:13px;font-weight:550;margin-bottom:12px}.retail .kpi-grid .component-status{font-size:10px;color:#878b90}.retail .kpi{font-size:clamp(25px,2.45vw,39px);font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-1.5px;color:#252a34;order:1;margin:10px 0 6px}.retail .kpi-grid .panel:first-child .kpi{color:#3158d5}.retail .kpi-grid .muted{order:2;font-size:11px;margin:2px 0;min-height:32px}.retail .kpi-grid .query-details{order:3}.retail .kpi-grid .notice{order:4}
9
- .retail .grid{gap:20px;grid-template-columns:repeat(2,minmax(0,1fr))}.retail .grid .panel{border:1px solid #dedbd3;border-radius:8px;padding:26px;background:#fffefb;box-shadow:none}.retail .panel-heading{display:flex;justify-content:space-between;align-items:baseline;gap:14px}.retail .panel-heading h2{font-size:16px;font-weight:600;letter-spacing:-.3px;margin-bottom:6px}.retail .component-status{color:#898d88;font-size:10px;white-space:nowrap}.retail .component-status.failed{color:#a74632}.retail .grid .muted{font-size:11px;margin:6px 0 10px}.retail .query-details{font-size:10px;color:#91938c;margin:8px 0}.retail .query-details summary{display:inline-block}.retail .notice{font-size:11px;background:#f9f2e4;border-color:#d9ae66;padding:8px 11px;border-radius:2px;color:#857044}.retail .notice details{font-size:10px;margin:2px 0;color:inherit}.retail .series-controls{display:flex;gap:18px;flex-wrap:wrap;font-size:12px;margin:16px 0}.retail .series-controls input{accent-color:#3158d5;margin-right:6px}.retail .chart-sort{font-size:10px;margin:10px 0}.retail .chart-sort select{border-color:#e4e2da;background:#fffefb;font-size:10px;padding:4px 8px}.retail .chart{height:310px}.retail details{font-size:11px}.retail .data-notes{border-top:1px solid #d6d3ca;padding-top:22px;margin-top:30px;line-height:1.9;color:#737870}.retail .data-notes>summary{font-size:13px;color:#404740;font-weight:600}.retail th{background:#f3f2ec;color:#525954;font-size:11px}.retail td{border-color:#ebe9e1;font-size:12px}.retail tbody tr:hover{background:#f6f7fc}.retail .table-tools{font-size:12px;flex-wrap:wrap}.retail .table-tools input{max-width:100%}.retail footer{font-size:10px;color:#8b8d85;text-align:center;letter-spacing:.5px}
10
- @media(max-width:1000px){.retail main{padding:30px 24px}.retail-nav{padding:0 24px}.retail .kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.retail .kpi-grid .panel{border-bottom:1px solid #dedbd3}.retail .kpi-grid .panel:nth-child(2n){border-right:0}.retail .kpi-grid .panel:nth-last-child(-n+2){border-bottom:0}.retail header{align-items:flex-start}.retail h1{font-size:29px}.retail .header-actions{flex-direction:column;gap:10px;align-items:flex-end}}
11
- @media(max-width:700px){.retail main{padding:24px 16px}.retail-nav{height:auto;min-height:68px;padding:16px;flex-wrap:wrap;gap:16px}.retail-nav>div{order:3;width:100%;justify-content:space-between;gap:12px;font-size:12px}.retail .retail-brand{font-size:14px}.retail h1{font-size:26px}.retail header{display:block}.retail .header-actions{flex-direction:row;justify-content:flex-end;margin-top:16px}.retail .grid{grid-template-columns:1fr}.retail .grid .panel{padding:20px 16px}.retail .kpi-grid .panel{padding:18px 15px}.retail .kpi{font-size:26px;letter-spacing:-1px}.retail .section-heading{gap:10px;flex-wrap:wrap;margin-top:28px}.retail .section-heading p{width:100%;margin:0}.retail .section-heading>span{font-size:9px}.retail .panel-heading{display:block}.retail .chart{height:285px}.retail .filter-card{padding:18px 16px}.retail .filter-bottom{align-items:stretch;flex-direction:column}.retail .filter-buttons{justify-content:flex-end}.retail .filter-buttons .ant-btn{font-size:12px}.retail .pager{gap:8px;flex-wrap:wrap;font-size:11px}}
1
+ .retail{--chart-1:#3158d5;--chart-2:#e18047;--chart-3:#82a7a0;--chart-4:#9b8fc6;--chart-5:#d2b977;background:#f5f3ee;color:#24272d;min-height:100vh;font-family:"Segoe UI","Microsoft YaHei",sans-serif}
2
+ .retail main{max-width:1480px;padding:48px 48px 24px}.retail a{color:#3158d5}.retail button,.retail select,.retail input{border-radius:5px}.retail :focus-visible{outline:3px solid #e18047;outline-offset:3px}
3
+ .retail-nav{position:sticky;top:0;z-index:30}.retail :is(#dashboard,#components,#data-notes){scroll-margin-top:100px}
4
+ .retail-nav{height:78px;padding:0 48px;border-bottom:1px solid #dcd9d1;display:flex;align-items:center;justify-content:space-between;gap:30px;background:#faf9f6}.retail-nav a{text-decoration:none;color:#40434b}.retail-brand{display:flex;align-items:center;gap:12px;font-weight:700;font-size:17px;white-space:nowrap}.retail-brand b{display:grid;place-items:center;background:#24272d;color:#faf9f6;font-family:Georgia,serif;font-size:30px;width:39px;height:39px;border-radius:4px}.retail-brand small{font-size:12px;font-weight:400;border-left:1px solid #bbb7ad;margin-left:10px;padding-left:12px}.retail-nav>div{display:flex;gap:36px;font-size:13px}.retail-nav>div a[aria-current="location"]{color:#3158d5;font-weight:650}.retail-live{font-size:12px;white-space:nowrap}.retail-live:before{content:"";display:inline-block;width:6px;height:6px;background:#3158d5;border-radius:50%;margin-right:8px}
5
+ .retail header{align-items:flex-end;margin-bottom:30px;gap:30px}.retail .eyebrow{font-size:10px;letter-spacing:2.5px;color:#6d706e}.retail h1{font-size:36px;letter-spacing:-1.2px;font-weight:650;margin:12px 0}.retail header .muted{font-size:13px;max-width:750px;line-height:1.9}.retail .muted{color:#75776f}.retail .header-actions{display:flex;align-items:center;gap:20px;white-space:nowrap;font-size:12px}.retail .header-actions button{background:#24272d;color:#fff;border-color:#24272d;padding:11px 18px}.retail .header-actions a{color:#656863;text-decoration:none}
6
+ .retail .filter-card{border-radius:8px;border-color:#dedbd3;background:#faf9f6;box-shadow:none;padding:20px 24px;margin-bottom:8px}.retail .filter-heading{margin-bottom:14px}.retail .filter-heading p{display:none}.retail .filter-symbol{height:28px;width:28px;background:#eaeefa;border-radius:5px;color:#3158d5}.retail .filter-heading h2{font-size:13px;color:#424740}.retail .filter-fields{gap:14px}.retail .filter-fields:has(>.filter-field:only-child){max-width:760px}.retail .filter-label-row{margin-bottom:5px}.retail .filter-label-row label{color:#575d65}.retail .filter-bottom{margin-top:9px;padding-top:12px;border-color:#e3e0d8}.retail .filter-scope{font-size:11px;color:#797d7e;line-height:1.8;margin:4px 0}.retail .field-hint{min-height:0;margin-top:6px}.retail .filter-status{background:#e9edf9;color:#3158d5}.retail .filter-status.pending{background:#fff0db;color:#855316}.retail .query-status{font-size:11px;color:#75776f;text-align:right;margin:12px 0 28px}
7
+ .retail .section-heading{display:flex;align-items:baseline;gap:16px;margin:35px 0 18px}.retail .section-heading>span{font:10px "Segoe UI",sans-serif;letter-spacing:1.5px;color:#85877d}.retail .section-heading h2{font-size:18px;letter-spacing:-.3px;margin:0}.retail .section-heading p{font-size:11px;color:#85877d;margin:0 0 0 auto}
8
+ .retail .kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;border:1px solid #d6d3ca;border-radius:8px;overflow:hidden;background:#fffefb}.retail .kpi-grid .panel{border:0;border-right:1px solid #dedbd3;border-radius:0;background:transparent;padding:23px 25px 18px;display:flex;flex-direction:column;position:relative}.retail .kpi-grid .panel:last-child{border-right:0}.retail .kpi-grid .panel:first-child{background:#eaeefb}.retail .kpi-grid .panel-heading{display:block}.retail .kpi-grid h2{font-size:13px;font-weight:550;margin-bottom:12px}.retail .kpi-grid .component-status{font-size:10px;color:#878b90}.retail .kpi{font-size:clamp(25px,2.45vw,39px);font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-1.5px;color:#252a34;order:1;margin:10px 0 6px}.retail .kpi-grid .panel:first-child .kpi{color:#3158d5}.retail .kpi-grid .muted{order:2;font-size:11px;margin:2px 0;min-height:32px}.retail .kpi-grid .query-details{order:3}.retail .kpi-grid .notice{order:4}
9
+ .retail .grid{gap:20px;grid-template-columns:repeat(2,minmax(0,1fr))}.retail .grid .panel{border:1px solid #dedbd3;border-radius:8px;padding:26px;background:#fffefb;box-shadow:none}.retail .panel-heading{display:flex;justify-content:space-between;align-items:baseline;gap:14px}.retail .panel-heading h2{font-size:16px;font-weight:600;letter-spacing:-.3px;margin-bottom:6px}.retail .component-status{color:#898d88;font-size:10px;white-space:nowrap}.retail .component-status.failed{color:#a74632}.retail .grid .muted{font-size:11px;margin:6px 0 10px}.retail .query-details{font-size:10px;color:#91938c;margin:8px 0}.retail .query-details summary{display:inline-block}.retail .notice{font-size:11px;background:#f9f2e4;border-color:#d9ae66;padding:8px 11px;border-radius:2px;color:#857044}.retail .notice details{font-size:10px;margin:2px 0;color:inherit}.retail .series-controls{display:flex;gap:18px;flex-wrap:wrap;font-size:12px;margin:16px 0}.retail .series-controls input{accent-color:#3158d5;margin-right:6px}.retail .chart-sort{font-size:10px;margin:10px 0}.retail .chart-sort select{border-color:#e4e2da;background:#fffefb;font-size:10px;padding:4px 8px}.retail .chart{height:310px}.retail details{font-size:11px}.retail .data-notes{border-top:1px solid #d6d3ca;padding-top:22px;margin-top:30px;line-height:1.9;color:#737870}.retail .data-notes>summary{font-size:13px;color:#404740;font-weight:600}.retail th{background:#f3f2ec;color:#525954;font-size:11px}.retail td{border-color:#ebe9e1;font-size:12px}.retail tbody tr:hover{background:#f6f7fc}.retail .table-tools{font-size:12px;flex-wrap:wrap}.retail .table-tools input{max-width:100%}.retail footer{font-size:10px;color:#8b8d85;text-align:center;letter-spacing:.5px}
10
+ @media(max-width:1000px){.retail main{padding:30px 24px}.retail-nav{padding:0 24px}.retail .kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.retail .kpi-grid .panel{border-bottom:1px solid #dedbd3}.retail .kpi-grid .panel:nth-child(2n){border-right:0}.retail .kpi-grid .panel:nth-last-child(-n+2){border-bottom:0}.retail header{align-items:flex-start}.retail h1{font-size:29px}.retail .header-actions{flex-direction:column;gap:10px;align-items:flex-end}}
11
+ @media(max-width:700px){.retail main{padding:24px 16px}.retail-nav{height:auto;min-height:68px;padding:16px;flex-wrap:wrap;gap:16px}.retail-nav>div{order:3;width:100%;justify-content:space-between;gap:12px;font-size:12px}.retail .retail-brand{font-size:14px}.retail h1{font-size:26px}.retail header{display:block}.retail .header-actions{flex-direction:row;justify-content:flex-end;margin-top:16px}.retail .grid{grid-template-columns:1fr}.retail .grid .panel{padding:20px 16px}.retail .kpi-grid .panel{padding:18px 15px}.retail .kpi{font-size:26px;letter-spacing:-1px}.retail .section-heading{gap:10px;flex-wrap:wrap;margin-top:28px}.retail .section-heading p{width:100%;margin:0}.retail .section-heading>span{font-size:9px}.retail .panel-heading{display:block}.retail .chart{height:285px}.retail .filter-card{padding:18px 16px}.retail .filter-bottom{align-items:stretch;flex-direction:column}.retail .filter-buttons{justify-content:flex-end}.retail .filter-buttons .ant-btn{font-size:12px}.retail .pager{gap:8px;flex-wrap:wrap;font-size:11px}}
12
12
  @media(min-width:900px){.retail .filter-card{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:32px;align-items:start}.retail .filter-heading{grid-column:1/-1;margin-bottom:12px}.retail .filter-fields{width:min(100%,640px)}.retail .filter-bottom{border:0;padding:26px 0 0;margin:0;flex-direction:column;align-items:flex-end;gap:8px}.retail .filter-bottom>p{order:2;font-size:10px}.retail .filter-fields:has(>.filter-field:only-child){max-width:640px}}
13
- @media(max-width:700px){.retail :is(#dashboard,#components,#data-notes){scroll-margin-top:128px}}
13
+ @media(max-width:700px){.retail :is(#dashboard,#components,#data-notes){scroll-margin-top:128px}}