@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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {after, test} from 'node:test';
|
|
3
|
+
import {createServer} from 'vite';
|
|
4
|
+
import {createElement} from 'react';
|
|
5
|
+
import {renderToStaticMarkup} from 'react-dom/server';
|
|
6
|
+
const server = await createServer({configFile: false, cacheDir: '../../../temp/vite-table-tests', server: {middlewareMode: true, hmr: false}});
|
|
7
|
+
after(() => server.close());
|
|
8
|
+
const {normalize} = await server.ssrLoadModule('/src/finebi/normalize.ts');
|
|
9
|
+
const {DataTable} = await server.ssrLoadModule('/src/components/DataTable.tsx');
|
|
10
|
+
const {tableCsv} = await server.ssrLoadModule('/src/services/table-data.ts');
|
|
11
|
+
const metric = (text, rowSumInfo = {totalSum: false, innerSum: false}, extra = {}) => ({dId: 'amount', text, authority: true, rowSumInfo, ...extra});
|
|
12
|
+
const group = items => ({previewResult: {resultType: 'BIGROUP', header: [{dId: 'region', text: '地区'}, {dId: 'city', text: '城市'}, {dId: 'amount', text: '金额'}], items, hasNext: true, hasPrev: false}, widgetBean: {dimensions: {region: {type: 1}, city: {type: 1}}, settings: {tableStyle: {rowCounts: 10}}}});
|
|
13
|
+
const detailInfo = {totalSum: false, innerSum: false};
|
|
14
|
+
const totalInfo = {totalSum: true, innerSum: false};
|
|
15
|
+
const node = (dId, text, children = null, values = null) => ({dId, text, formatText: text, authority: true, children, values});
|
|
16
|
+
const crossMetric = (text, colSumInfo = detailInfo, rowSumInfo = detailInfo, dId = 'amount') => ({dId, text, authority: true, colSumInfo, rowSumInfo});
|
|
17
|
+
const cross = (top, left) => ({previewResult: {resultType: 'BICROSS', top: {header: [{dId: 'year', text: '年'}], items: [node(null, null, top)]}, left: {header: [{dId: 'region', text: '地区'}], items: [node(null, null, left)]}, hasXNext: true, hasXPrev: false, hasYNext: false, hasYPrev: false}});
|
|
18
|
+
|
|
19
|
+
test('cross display removes only repeated metric captions and DATA summary carriers', () => {
|
|
20
|
+
const source = cross([node('region', '金额', [node('target-name', '金额', null, [node('amount', '金额')])])], [
|
|
21
|
+
node('region', '合计', [node('__summary_dim_id__', '合计', null, [crossMetric('1')])]),
|
|
22
|
+
node('__summary_dim_id__', '合计', null, [crossMetric('2', detailInfo, totalInfo)]),
|
|
23
|
+
node('__summary_dim_id__', '合计', null, [crossMetric('3', detailInfo, {totalSum: false, innerSum: true})]),
|
|
24
|
+
node('__summary_dim_id__', '合计', null, [{...crossMetric('4'), rowSumInfo: null}]),
|
|
25
|
+
]);
|
|
26
|
+
const data = normalize(source).datasets[0];
|
|
27
|
+
assert.equal(data.columns[1].name, '金额 / 金额', 'real repeated grouping labels must remain');
|
|
28
|
+
assert.equal(data.columns[1].path.length, 3, 'source path metadata must remain intact');
|
|
29
|
+
assert.equal(data.rows[0].cells[0].display, '合计', 'business dimension named 合计 must remain');
|
|
30
|
+
assert.equal(data.rows[0].path.length, 2);
|
|
31
|
+
assert.deepEqual(data.rows.slice(1).map(row => [row.kind, row.cells[0].display]), [['GRAND_TOTAL', '合计'], ['SUBTOTAL', '合计'], ['UNKNOWN', '合计']]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('cross metric rows deduplicate only the verified trailing metric caption', () => {
|
|
35
|
+
const rows = [
|
|
36
|
+
node('region', '金额', [node('target-name', '金额', [node('amount', '金额', null, [crossMetric('7')])])]),
|
|
37
|
+
node('region', '金额', [node('other-dimension', '金额', null, [crossMetric('8')])]),
|
|
38
|
+
node('target-name', '金额', [node('amount', '别名', null, [crossMetric('9')])]),
|
|
39
|
+
node('target-name', '金额', [node('amount', '金额', null, [crossMetric('10', detailInfo, detailInfo, 'count')])]),
|
|
40
|
+
];
|
|
41
|
+
const data = normalize(cross([node('year', '2024', null, [{}])], rows)).datasets[0];
|
|
42
|
+
assert.deepEqual(data.rows.map(r => r.cells[0].display), ['金额 / 金额', '金额 / 金额', '金额 / 别名', '金额 / 金额']);
|
|
43
|
+
assert.equal(data.rows[0].path.length, 3);
|
|
44
|
+
assert.equal(data.rows[0].cells[1].sourceFieldId, 'amount');
|
|
45
|
+
const html = renderToStaticMarkup(createElement(DataTable, {dataset: data, pageSize: 10, binding: {type: 'table'}}));
|
|
46
|
+
assert.doesNotMatch(html, /金额 \/ 金额 \/ 金额/);
|
|
47
|
+
assert.doesNotMatch(tableCsv(data, data.rows), /金额 \/ 金额 \/ 金额/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('cross real null renders as dash while placeholders, missing payload and permissions stay distinct', () => {
|
|
51
|
+
const empty = {...crossMetric(null), value: null, formatText: null};
|
|
52
|
+
const cells = [empty, {...empty, dId: null}, {...empty, authority: false}, {...empty, authority: undefined}, {dId: 'amount', authority: true}, {...empty, formatText: '仅展示'}];
|
|
53
|
+
const data = normalize(cross([node('year', '2024', null, cells.map(() => ({})))], [node('region', '东部', null, cells)])).datasets[0];
|
|
54
|
+
assert.deepEqual(data.rows[0].cells.slice(1).map(c => c.state), ['NULL', 'UNAVAILABLE', 'RESTRICTED', 'UNAVAILABLE', 'UNAVAILABLE', 'UNAVAILABLE']);
|
|
55
|
+
assert.equal(data.rows[0].cells[1].reason, null);
|
|
56
|
+
const html = renderToStaticMarkup(createElement(DataTable, {dataset: data, pageSize: 10, binding: {type: 'table'}}));
|
|
57
|
+
assert.match(html, /<td>—<\/td><td>不可用<\/td><td>受限<\/td>/);
|
|
58
|
+
assert.match(tableCsv(data, data.rows), /"—","不可用","受限"/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('cross placeholders without valid metric ids never become available numeric values', () => {
|
|
62
|
+
const cells = [null, undefined, '', ' ', 12].map(dId => ({...crossMetric('12'), dId, value: 12}));
|
|
63
|
+
const data = normalize(cross([node('year', '2024', null, cells.map(() => ({})))], [node('region', '东部', null, cells)])).datasets[0];
|
|
64
|
+
for (const cell of data.rows[0].cells.slice(1)) {
|
|
65
|
+
assert.equal(cell.state, 'UNAVAILABLE');
|
|
66
|
+
assert.equal(cell.value, null);
|
|
67
|
+
assert.equal(cell.sourceFieldId, null);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('cross table aligns repeated metric ids by column slots and preserves both axes and totals', () => {
|
|
72
|
+
for (const totalFirst of [false, true]) {
|
|
73
|
+
const columns = [node('year', '2024', null, [node('amount', '金额')]), node('year', '2025', null, [node('amount', '金额')])];
|
|
74
|
+
const summary = node('__summary_dim_id__', '合计', [node('amount', '金额', null, [{}])]);
|
|
75
|
+
columns.splice(totalFirst ? 0 : columns.length, 0, summary);
|
|
76
|
+
const values = [crossMetric('9007199254740993.12'), crossMetric('2.50')];
|
|
77
|
+
values.splice(totalFirst ? 0 : values.length, 0, crossMetric('9007199254740995.62', totalInfo));
|
|
78
|
+
const source = cross(columns, [node('region', '东部', [node('__summary_dim_id__', null, null, values)]), node('__summary_dim_id__', '合计', [node('amount', '金额', null, values.map(v => ({...v, rowSumInfo: totalInfo})))])]);
|
|
79
|
+
const result = normalize(source), data = result.datasets[0];
|
|
80
|
+
assert.equal(result.resultType, 'CROSS_TABLE'); assert.equal(result.normalization, 'COMPLETE');
|
|
81
|
+
assert.deepEqual(data.rows[0].cells.slice(1).map(c => c.value), values.map(v => v.text));
|
|
82
|
+
assert.equal(new Set(data.columns.map(c => c.fieldId)).size, 4);
|
|
83
|
+
assert.equal(data.rows[0].cells[1].sourceFieldId, 'amount');
|
|
84
|
+
assert.deepEqual(data.rows.map(r => r.kind), ['DATA', 'GRAND_TOTAL']);
|
|
85
|
+
assert.equal(data.columns[totalFirst ? 1 : 3].kind, 'GRAND_TOTAL');
|
|
86
|
+
assert.equal(result.range.mode, 'INITIAL_VIEW'); assert.equal(result.range.page, null);
|
|
87
|
+
assert.deepEqual(result.range.axes, {horizontal: {hasNext: true, hasPrevious: false}, vertical: {hasNext: false, hasPrevious: false}});
|
|
88
|
+
assert.equal(result.range.hasNext, true); assert.ok(result.warnings.includes('NEXT_PAGE_UNSUPPORTED'));
|
|
89
|
+
const html = renderToStaticMarkup(createElement(DataTable, {dataset: data, pageSize: 10, binding: {type: 'table'}}));
|
|
90
|
+
assert.match(html, /2024/); assert.match(html, /2025/); assert.match(html, /总计/);
|
|
91
|
+
assert.match(tableCsv(data, data.rows), /9007199254740993.12/);
|
|
92
|
+
assert.equal(source.previewResult.top.items[0].children.length, 3);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('cross table supports metrics on rows and multi-level columns without id joins', () => {
|
|
97
|
+
const source = cross([node('year', '2024', [node('quarter', 'Q1', null, []), node('quarter', 'Q2', null, [])])], [
|
|
98
|
+
node('region', '东部', [node('target-name', '金额', [node('amount', '金额', null, [crossMetric('12'), crossMetric('18')])]), node('target-name', '数量', [node('count', '数量', null, [crossMetric('3', detailInfo, detailInfo, 'count'), crossMetric('4', detailInfo, detailInfo, 'count')])])]),
|
|
99
|
+
]);
|
|
100
|
+
const data = normalize(source).datasets[0];
|
|
101
|
+
assert.equal(data.columns.length, 3); assert.equal(data.rows.length, 2);
|
|
102
|
+
assert.match(data.columns[1].name, /2024.*Q1/);
|
|
103
|
+
assert.match(data.rows[1].cells[0].display, /东部.*数量/);
|
|
104
|
+
assert.deepEqual(data.rows[1].cells.slice(1).map(c => c.sourceFieldId), ['count', 'count']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('cross table never exposes restricted axis labels and retains missing/ambiguous value states', () => {
|
|
108
|
+
const secret = node('year', 'secret column', null, [{}]); secret.authority = false;
|
|
109
|
+
const row = node('region', 'secret row', null, [crossMetric('123')]); row.authority = false;
|
|
110
|
+
const result = normalize(cross([secret], [row]));
|
|
111
|
+
assert.doesNotMatch(JSON.stringify(result), /secret column|secret row/);
|
|
112
|
+
assert.equal(result.datasets[0].rows[0].cells[0].state, 'RESTRICTED');
|
|
113
|
+
assert.equal(result.datasets[0].rows[0].cells[1].state, 'RESTRICTED');
|
|
114
|
+
const source = cross([node('year', 'A', null, [{}, {}])], [node('region', 'X', null, [{...crossMetric('1万'), formatText: '1万'}])]);
|
|
115
|
+
const incomplete = normalize(source);
|
|
116
|
+
assert.equal(incomplete.normalization, 'PARTIAL');
|
|
117
|
+
assert.equal(incomplete.datasets[0].rows[0].cells[1].state, 'UNAVAILABLE');
|
|
118
|
+
assert.equal(incomplete.datasets[0].rows[0].cells[2].reason, 'MISSING_CELL');
|
|
119
|
+
source.previewResult.left.items[0].children[0].values.push(crossMetric('2'), crossMetric('3'));
|
|
120
|
+
assert.equal(normalize(source).normalization, 'UNSUPPORTED', 'extra values must not be silently dropped');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('cross table distinguishes empty views, missing axes and unsupported mixed node topology', () => {
|
|
124
|
+
assert.equal(normalize(cross([], [])).normalization, 'COMPLETE');
|
|
125
|
+
assert.equal(normalize(cross([], [])).datasets[0].returnedRowCount, 0);
|
|
126
|
+
assert.equal(normalize({previewResult: {resultType: 'BICROSS', left: {header: [], items: []}}}).normalization, 'UNSUPPORTED');
|
|
127
|
+
assert.equal(normalize(cross([node('x', 'A', [node('x', 'B')], [{}])], [])).normalization, 'UNSUPPORTED');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('cross table handles no column dimensions and preserves metric-name intersection placeholders', () => {
|
|
131
|
+
const source = cross([], [node('target', '金额', null, [crossMetric('7'), {dId: null, text: null, value: null, authority: true}])]);
|
|
132
|
+
source.previewResult.top = {header: [], items: []};
|
|
133
|
+
const result = normalize(source);
|
|
134
|
+
assert.equal(result.normalization, 'COMPLETE');
|
|
135
|
+
assert.deepEqual(result.datasets[0].rows[0].cells.slice(1).map(c => c.state), ['AVAILABLE', 'UNAVAILABLE']);
|
|
136
|
+
assert.equal(result.datasets[0].rows[0].cells[2].value, null);
|
|
137
|
+
const swapped = cross([node('year', '2024', null, [node('amount', '金额')])], [node('region', '东部', null, [crossMetric('1', detailInfo, detailInfo, 'count')])]);
|
|
138
|
+
assert.equal(normalize(swapped).normalization, 'PARTIAL');
|
|
139
|
+
assert.equal(normalize(swapped).datasets[0].rows[0].cells[1].reason, 'AMBIGUOUS_FIELD');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('group preserves parent keys, totals, metric precision and the first group page', () => {
|
|
143
|
+
const source = group([{dId: 'region', text: '东部', authority: true, values: [metric('9007199254740993.12', {totalSum: false, innerSum: true})], children: [
|
|
144
|
+
{dId: 'city', text: '甲城', authority: true, values: [metric('12.50', undefined, {formatText: '12.50元'})]},
|
|
145
|
+
]}, {values: [metric('9007199254741005.62', {totalSum: true, innerSum: false})]}]);
|
|
146
|
+
const result = normalize(source), data = result.datasets[0];
|
|
147
|
+
assert.equal(result.resultType, 'GROUP_TABLE');
|
|
148
|
+
assert.equal(result.normalization, 'COMPLETE');
|
|
149
|
+
assert.deepEqual(data.rows.map(r => r.kind), ['SUBTOTAL', 'DATA', 'GRAND_TOTAL']);
|
|
150
|
+
assert.deepEqual(data.rows[1].cells.map(c => c.value), ['东部', '甲城', '12.50']);
|
|
151
|
+
assert.equal(data.rows[0].cells[2].value, '9007199254740993.12');
|
|
152
|
+
assert.equal(data.returnedRowCount, 3);
|
|
153
|
+
assert.deepEqual(result.range, {mode: 'PAGE', page: 1, pageSize: 10, pageUnit: 'GROUP', hasNext: true, hasPrevious: false, total: {value: null, unit: 'GROUP'}});
|
|
154
|
+
assert.ok(result.warnings.includes('NEXT_PAGE_UNSUPPORTED'));
|
|
155
|
+
const html = renderToStaticMarkup(createElement(DataTable, {dataset: data, pageSize: 10, binding: {type: 'table'}}));
|
|
156
|
+
assert.match(html, /小计/); assert.match(html, /总计/);
|
|
157
|
+
assert.match(tableCsv(data, data.rows), /GRAND_TOTAL/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('group keeps restricted and ambiguous cells unavailable and does not infer numeric group keys', () => {
|
|
161
|
+
const source = group([{dId: 'region', text: '42', authority: true, values: [metric('12万', undefined, {formatText: '12万'})], children: [
|
|
162
|
+
{dId: 'city', text: '秘密', authority: false, values: [metric('999', undefined, {authority: false})]},
|
|
163
|
+
{dId: 'city', text: '乙城', authority: true, values: [metric('1'), metric('2')]},
|
|
164
|
+
{dId: 'city', text: '丙城', authority: true, values: [metric(null, null)]},
|
|
165
|
+
]}]);
|
|
166
|
+
source.widgetBean.dimensions.region.type = 2;
|
|
167
|
+
const result = normalize(source), rows = result.datasets[0].rows;
|
|
168
|
+
assert.equal(rows[0].cells[0].reason, 'GROUP_KEY_SEMANTICS_UNKNOWN');
|
|
169
|
+
assert.equal(rows[0].cells[2].state, 'UNAVAILABLE');
|
|
170
|
+
assert.equal(rows[1].cells[1].state, 'RESTRICTED');
|
|
171
|
+
assert.equal(rows[1].cells[2].state, 'RESTRICTED');
|
|
172
|
+
assert.equal(rows[2].cells[2].reason, 'AMBIGUOUS_FIELD');
|
|
173
|
+
assert.equal(rows[3].cells[2].state, 'UNAVAILABLE');
|
|
174
|
+
assert.equal(rows[3].kind, 'UNKNOWN');
|
|
175
|
+
assert.equal(result.normalization, 'PARTIAL');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('group header expansion reports an initial view and malformed groups are not empty successes', () => {
|
|
179
|
+
const source = group([]);
|
|
180
|
+
source.widgetBean.settings.tableStyle.showRowHeaderIcon = true;
|
|
181
|
+
const result = normalize(source);
|
|
182
|
+
assert.equal(result.range.mode, 'INITIAL_VIEW');
|
|
183
|
+
assert.equal(result.range.page, null);
|
|
184
|
+
assert.equal(result.range.hasNext, null);
|
|
185
|
+
assert.equal(result.datasets[0].returnedRowCount, 0);
|
|
186
|
+
assert.equal(normalize({previewResult: {resultType: 'BIGROUP'}}).normalization, 'UNSUPPORTED');
|
|
187
|
+
source.previewResult.items = [null, {dId: 'unknown', values: [metric('1')]}];
|
|
188
|
+
assert.equal(normalize(source).normalization, 'PARTIAL');
|
|
189
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {test} from 'node:test';
|
|
3
|
+
import {createServer as httpServer} from 'node:http';
|
|
4
|
+
import {mkdir,mkdtemp,cp,writeFile,readFile} from 'node:fs/promises';
|
|
5
|
+
import {resolve} from 'node:path';
|
|
6
|
+
import {createServer} from 'vite';
|
|
7
|
+
|
|
8
|
+
test('real Vite uses project env, persists sessions, blocks credential files and survives token updates',async()=>{
|
|
9
|
+
await mkdir('temp',{recursive:true});
|
|
10
|
+
const root=await mkdtemp(resolve('temp','vite-auth-'));
|
|
11
|
+
for(const name of ['vite.config.ts','scripts','src','public','index.html','package.json'])await cp(resolve(name),resolve(root,name),{recursive:true});
|
|
12
|
+
await mkdir(resolve(root,'temp'));
|
|
13
|
+
await writeFile(resolve(root,'temp','staged-session'),'staged-secret');
|
|
14
|
+
let logins=0,queries=0;
|
|
15
|
+
const upstream=httpServer(async(req,res)=>{
|
|
16
|
+
for await(const _ of req){};
|
|
17
|
+
res.setHeader('content-type','application/json');
|
|
18
|
+
if(req.url==='/decision/login'){logins++;res.end(JSON.stringify({data:{accessToken:'persisted-session'}}));return;}
|
|
19
|
+
assert.equal(req.headers.cookie,'fine_auth_token=persisted-session');queries++;
|
|
20
|
+
res.end(JSON.stringify({success:true,data:{}}));
|
|
21
|
+
});
|
|
22
|
+
await new Promise(r=>upstream.listen(0,'127.0.0.1',r));
|
|
23
|
+
const baseUrl=`http://127.0.0.1:${upstream.address().port}/decision`;
|
|
24
|
+
await writeFile(resolve(root,'.env.local'),`FINEBI_DEV_BASE_URL=${baseUrl}\nFINEBI_DEV_USERNAME=alice\nFINEBI_DEV_PASSWORD=password\n`);
|
|
25
|
+
await writeFile(resolve(root,'public','app-config.json'),JSON.stringify({basePath:'/decision',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:10,filters:[]}));
|
|
26
|
+
const server=await createServer({root,configFile:resolve(root,'vite.config.ts'),cacheDir:resolve(root,'temp','cache'),server:{port:0},logLevel:'silent'});
|
|
27
|
+
try {
|
|
28
|
+
await server.listen();
|
|
29
|
+
const url=`http://127.0.0.1:${server.httpServer.address().port}`;
|
|
30
|
+
assert.equal((await fetch(url+'/')).status,200,'project under a temp parent must still serve its index');
|
|
31
|
+
assert.equal((await fetch(url+'/src/main.tsx')).status,200,'project source must remain loadable');
|
|
32
|
+
const send=()=>fetch(url+'/decision/v5/api/dashboard/report/r/widgets/query',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10})});
|
|
33
|
+
const result=await send();assert.equal(result.status,200);assert.doesNotMatch(await result.text(),/persisted-session|password/);
|
|
34
|
+
assert.match(await readFile(resolve(root,'.env.local'),'utf8'),/FINEBI_DEV_TOKEN="persisted-session"/);
|
|
35
|
+
assert.equal((await fetch(url+'/.env.local')).status,403);
|
|
36
|
+
assert.equal((await fetch(url+'/temp/staged-session')).status,403);
|
|
37
|
+
// Allow the file watcher to process the save; it must not restart the session manager.
|
|
38
|
+
await new Promise(r=>setTimeout(r,400));
|
|
39
|
+
assert.equal((await send()).status,200);assert.equal(logins,1);assert.equal(queries,2);
|
|
40
|
+
} finally {await server.close();await new Promise(r=>upstream.close(r));}
|
|
41
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {"target":"ES2020","lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true},
|
|
3
|
-
"include":["src","vite.config.ts"]
|
|
4
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {"target":"ES2020","lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true},
|
|
3
|
+
"include":["src","vite.config.ts"]
|
|
4
|
+
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import {readFileSync} from 'node:fs';
|
|
2
|
+
import {resolve} from 'node:path';
|
|
2
3
|
import {fileURLToPath} from 'node:url';
|
|
3
|
-
import {defineConfig, loadEnv} from 'vite';
|
|
4
|
+
import {defineConfig, loadEnv, normalizePath} from 'vite';
|
|
4
5
|
// @ts-expect-error The development-only Node module is tested directly with node:test.
|
|
5
6
|
import {createDevMiddleware} from './scripts/dev-proxy.mjs';
|
|
7
|
+
const projectTemp=normalizePath(fileURLToPath(new URL('./temp',import.meta.url)))+'/**';
|
|
6
8
|
export default defineConfig(({mode}) => ({
|
|
7
9
|
base: './',
|
|
8
10
|
resolve:{alias:[{find:/^date-fns\/locale$/,replacement:fileURLToPath(new URL('./src/components/calendar-locales.ts',import.meta.url))}]},
|
|
9
|
-
server: {host:'127.0.0.1',port:4173,strictPort:true},
|
|
11
|
+
server: {host:'127.0.0.1',port:4173,strictPort:true,watch:{ignored:['**/.env.local',projectTemp]},
|
|
12
|
+
fs:{deny:['.env','.env.*','*.{crt,pem}','**/.git/**',projectTemp]}},
|
|
10
13
|
plugins:[{name:'finebi-development-session',apply:'serve',configureServer(server) {
|
|
11
14
|
const config=JSON.parse(readFileSync(new URL('./public/app-config.json',import.meta.url),'utf8'));
|
|
12
|
-
const env={...loadEnv(mode,
|
|
13
|
-
|
|
15
|
+
const env={...loadEnv(mode,server.config.root,'FINEBI_DEV_'),...process.env};
|
|
16
|
+
const middleware=createDevMiddleware(config,env,fetch,{file:resolve(server.config.root,'.env.local')});
|
|
17
|
+
server.middlewares.use(middleware);
|
|
18
|
+
server.httpServer?.once('close',()=>middleware.dispose());
|
|
14
19
|
}}],
|
|
15
20
|
}));
|