@finebi/finebi-cli 1.6.1 → 1.6.2
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/README.md +8 -8
- package/package.json +1 -1
- package/skills/finebi-cli-agent/SKILL.md +43 -123
- package/skills/finebi-cli-agent/agents/openai.yaml +1 -1
- package/skills/finebi-cli-agent/references/artifact-ordering.md +3 -3
- package/skills/finebi-cli-agent/references/command-workflows.md +30 -1
- package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
- package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
- package/skills/finebi-cli-agent/references/dsl-handoff-contract.md +1 -1
- package/skills/finebi-cli-agent/references/handoff-contract.md +6 -4
- package/skills/finebi-cli-agent/references/mutation-confirmation-contract.md +2 -0
- package/skills/finebi-cli-agent/references/scenario-workflows.md +10 -8
- package/skills/finebi-cli-agent/references/web-app.md +4 -2
- package/skills/finebi-cli-agent/references/widget-dsl-core.md +6 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +157 -148
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/browser-auth.js +3 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +28 -28
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/error-catalog.js +2 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +63 -60
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +18 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +12 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -67
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +66 -38
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +2 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/browser-session.mjs +59 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +25 -24
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-session.mjs +57 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/session-file.mjs +45 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -56
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -39
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +20 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -44
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -77
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -18
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +20 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +7 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/session.test.mjs +91 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/vite-auth.test.mjs +41 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +9 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
- package/src/app.js +157 -148
- package/src/browser-auth.js +3 -4
- package/src/config.js +28 -28
- package/src/error-catalog.js +2 -2
- package/src/help.js +63 -60
- package/src/install-skill.js +89 -89
- package/src/profiles.js +158 -158
- package/src/skill-files.js +94 -94
- package/templates/web-app/AGENTS.md +18 -5
- package/templates/web-app/README.md +12 -5
- package/templates/web-app/docs/acceptance.md +80 -67
- package/templates/web-app/docs/auth-and-deploy.md +17 -2
- package/templates/web-app/docs/data-contract.md +66 -38
- package/templates/web-app/docs/filters.md +2 -2
- package/templates/web-app/examples/cockpit.config.json +22 -22
- package/templates/web-app/examples/retail.config.json +185 -185
- package/templates/web-app/index.html +2 -2
- package/templates/web-app/public/app-config.json +11 -11
- package/templates/web-app/scripts/browser-session.mjs +59 -0
- package/templates/web-app/scripts/dev-proxy.mjs +25 -24
- package/templates/web-app/scripts/dev-session.mjs +57 -0
- package/templates/web-app/scripts/session-file.mjs +45 -0
- package/templates/web-app/src/App.tsx +63 -56
- package/templates/web-app/src/cockpit.css +3 -3
- package/templates/web-app/src/components/Chart.tsx +40 -40
- package/templates/web-app/src/components/DataTable.tsx +1 -1
- package/templates/web-app/src/components/Insights.tsx +17 -17
- package/templates/web-app/src/components/MonthPicker.tsx +32 -32
- package/templates/web-app/src/components/OptionFilter.tsx +7 -7
- package/templates/web-app/src/components/RangeFilter.tsx +9 -9
- package/templates/web-app/src/components/Widget.tsx +40 -39
- package/templates/web-app/src/components/calendar-locales.ts +3 -3
- package/templates/web-app/src/components/calendar.ts +14 -14
- package/templates/web-app/src/components/charts/Bar.tsx +6 -6
- package/templates/web-app/src/components/charts/Line.tsx +6 -6
- package/templates/web-app/src/components/charts/Pie.tsx +6 -6
- package/templates/web-app/src/components/month-picker.css +1 -1
- package/templates/web-app/src/finebi/client.ts +20 -4
- package/templates/web-app/src/finebi/config.d.ts +3 -1
- package/templates/web-app/src/finebi/config.js +58 -44
- package/templates/web-app/src/finebi/date.d.ts +1 -1
- package/templates/web-app/src/finebi/date.js +5 -5
- package/templates/web-app/src/finebi/filters.d.ts +4 -4
- package/templates/web-app/src/finebi/filters.js +106 -106
- package/templates/web-app/src/finebi/normalize.ts +235 -77
- package/templates/web-app/src/finebi/types.ts +20 -18
- package/templates/web-app/src/main.tsx +5 -5
- package/templates/web-app/src/retail.css +12 -12
- package/templates/web-app/src/services/presentation.ts +36 -36
- package/templates/web-app/src/services/table-data.ts +47 -47
- package/templates/web-app/src/services/view-data.ts +18 -18
- package/templates/web-app/src/services/widget-queries.ts +18 -18
- package/templates/web-app/tests/browser/fixtures.mjs +28 -28
- package/templates/web-app/tests/browser/index.html +16 -16
- package/templates/web-app/tests/browser/main.tsx +149 -149
- package/templates/web-app/tests/browser/test-page.css +4 -4
- package/templates/web-app/tests/browser/vite.config.mjs +13 -13
- package/templates/web-app/tests/bundle.test.mjs +19 -19
- package/templates/web-app/tests/cockpit.test.mjs +73 -73
- package/templates/web-app/tests/field-selection.test.mjs +70 -0
- package/templates/web-app/tests/proxy.test.mjs +20 -5
- package/templates/web-app/tests/retail.test.mjs +31 -31
- package/templates/web-app/tests/runtime.test.mjs +7 -0
- package/templates/web-app/tests/session.test.mjs +91 -0
- package/templates/web-app/tests/table-normalize.test.mjs +189 -0
- package/templates/web-app/tests/vite-auth.test.mjs +41 -0
- package/templates/web-app/tsconfig.json +4 -4
- package/templates/web-app/vite.config.ts +9 -4
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts
CHANGED
|
@@ -1,77 +1,235 @@
|
|
|
1
|
-
import type {Cell, Column, Dataset, QueryResult} 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 === '
|
|
50
|
-
r.resultType = '
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
} else
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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,18 +1,20 @@
|
|
|
1
|
-
export type Cell = {value: string | null; display: string | null; state: 'AVAILABLE' | 'NULL' | 'UNAVAILABLE' | 'RESTRICTED'; reason: string | null};
|
|
2
|
-
export type
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
| {type: '
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export type
|
|
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
|
|
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}}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import type {Cell,WidgetBinding} from '../finebi/types';
|
|
2
|
-
import {displayCell} from './view-data';
|
|
3
|
-
|
|
4
|
-
// Decimal strings stay strings/integers: formatting a large amount must not lose cents.
|
|
5
|
-
export function formatMetric(cell:Cell,format?:WidgetBinding['valueFormat']):string {
|
|
6
|
-
if(!format||cell.state!=='AVAILABLE'||cell.value===null)return displayCell(cell);
|
|
7
|
-
const match=/^(-?)(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(cell.value);
|
|
8
|
-
if(!match)return displayCell(cell);
|
|
9
|
-
const exponent=Number(match[4]??0),fraction=match[3]??'';
|
|
10
|
-
if(Math.abs(exponent)>100)return displayCell(cell);
|
|
11
|
-
let digits=(match[2]+fraction).replace(/^0+(?=\d)/,'');
|
|
12
|
-
let scale=fraction.length-exponent;
|
|
13
|
-
if(format==='percent') {
|
|
14
|
-
scale-=2;
|
|
15
|
-
const shift=2-scale;
|
|
16
|
-
let amount=BigInt(digits);
|
|
17
|
-
if(shift>=0)amount*=10n**BigInt(shift);
|
|
18
|
-
else {const divisor=10n**BigInt(-shift);amount=(amount+divisor/2n)/divisor;}
|
|
19
|
-
digits=amount.toString();scale=2;
|
|
20
|
-
}
|
|
21
|
-
if(scale<0){digits+='0'.repeat(-scale);scale=0;}
|
|
22
|
-
digits=digits.padStart(scale+1,'0');
|
|
23
|
-
const whole=(scale?digits.slice(0,-scale):digits).replace(/\B(?=(\d{3})+(?!\d))/g,',');
|
|
24
|
-
const tail=scale?digits.slice(-scale):'';
|
|
25
|
-
const decimal=format==='percent'?tail:tail.replace(/0+$/,'');
|
|
26
|
-
const sign=match[1]&&/[1-9]/.test(digits)?'-':'';
|
|
27
|
-
return `${sign}${whole}${decimal?'.'+decimal:''}${format==='percent'?'%':''}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function formatCategory(cell:Cell,binding:Pick<WidgetBinding,'categoryFormat'|'categoryTimeZone'>):string {
|
|
31
|
-
if(binding.categoryFormat!=='epoch-month'||!binding.categoryTimeZone||cell.state!=='AVAILABLE'||!cell.value||! /^-?\d+$/.test(cell.value))return displayCell(cell);
|
|
32
|
-
const timestamp=Number(cell.value);
|
|
33
|
-
if(!Number.isSafeInteger(timestamp)||!Number.isFinite(new Date(timestamp).getTime()))return displayCell(cell);
|
|
34
|
-
const parts=new Intl.DateTimeFormat('en',{year:'numeric',month:'2-digit',timeZone:binding.categoryTimeZone}).formatToParts(new Date(timestamp));
|
|
35
|
-
return `${parts.find(p=>p.type==='year')!.value}-${parts.find(p=>p.type==='month')!.value}`;
|
|
36
|
-
}
|
|
1
|
+
import type {Cell,WidgetBinding} from '../finebi/types';
|
|
2
|
+
import {displayCell} from './view-data';
|
|
3
|
+
|
|
4
|
+
// Decimal strings stay strings/integers: formatting a large amount must not lose cents.
|
|
5
|
+
export function formatMetric(cell:Cell,format?:WidgetBinding['valueFormat']):string {
|
|
6
|
+
if(!format||cell.state!=='AVAILABLE'||cell.value===null)return displayCell(cell);
|
|
7
|
+
const match=/^(-?)(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(cell.value);
|
|
8
|
+
if(!match)return displayCell(cell);
|
|
9
|
+
const exponent=Number(match[4]??0),fraction=match[3]??'';
|
|
10
|
+
if(Math.abs(exponent)>100)return displayCell(cell);
|
|
11
|
+
let digits=(match[2]+fraction).replace(/^0+(?=\d)/,'');
|
|
12
|
+
let scale=fraction.length-exponent;
|
|
13
|
+
if(format==='percent') {
|
|
14
|
+
scale-=2;
|
|
15
|
+
const shift=2-scale;
|
|
16
|
+
let amount=BigInt(digits);
|
|
17
|
+
if(shift>=0)amount*=10n**BigInt(shift);
|
|
18
|
+
else {const divisor=10n**BigInt(-shift);amount=(amount+divisor/2n)/divisor;}
|
|
19
|
+
digits=amount.toString();scale=2;
|
|
20
|
+
}
|
|
21
|
+
if(scale<0){digits+='0'.repeat(-scale);scale=0;}
|
|
22
|
+
digits=digits.padStart(scale+1,'0');
|
|
23
|
+
const whole=(scale?digits.slice(0,-scale):digits).replace(/\B(?=(\d{3})+(?!\d))/g,',');
|
|
24
|
+
const tail=scale?digits.slice(-scale):'';
|
|
25
|
+
const decimal=format==='percent'?tail:tail.replace(/0+$/,'');
|
|
26
|
+
const sign=match[1]&&/[1-9]/.test(digits)?'-':'';
|
|
27
|
+
return `${sign}${whole}${decimal?'.'+decimal:''}${format==='percent'?'%':''}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatCategory(cell:Cell,binding:Pick<WidgetBinding,'categoryFormat'|'categoryTimeZone'>):string {
|
|
31
|
+
if(binding.categoryFormat!=='epoch-month'||!binding.categoryTimeZone||cell.state!=='AVAILABLE'||!cell.value||! /^-?\d+$/.test(cell.value))return displayCell(cell);
|
|
32
|
+
const timestamp=Number(cell.value);
|
|
33
|
+
if(!Number.isSafeInteger(timestamp)||!Number.isFinite(new Date(timestamp).getTime()))return displayCell(cell);
|
|
34
|
+
const parts=new Intl.DateTimeFormat('en',{year:'numeric',month:'2-digit',timeZone:binding.categoryTimeZone}).formatToParts(new Date(timestamp));
|
|
35
|
+
return `${parts.find(p=>p.type==='year')!.value}-${parts.find(p=>p.type==='month')!.value}`;
|
|
36
|
+
}
|