@finebi/finebi-cli 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +8 -4
  2. package/package.json +1 -1
  3. package/skills/finebi-cli-agent/references/command-workflows.md +1 -1
  4. package/skills/finebi-cli-agent/references/web-app.md +9 -5
  5. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  6. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +8 -49
  7. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +2 -1
  8. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +8 -5
  9. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +8 -2
  10. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +67 -0
  11. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +2 -2
  12. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +38 -21
  13. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/design.md +43 -2
  14. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +4 -0
  15. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -0
  16. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -0
  17. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package-lock.json +2 -2
  18. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package.json +3 -2
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +2 -1
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +45 -20
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -0
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +24 -15
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +12 -5
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/FilterBar.tsx +16 -10
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -0
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -0
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -0
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -0
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +23 -12
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -0
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -0
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -0
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -0
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -0
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +15 -10
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +1 -0
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +44 -0
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +2 -2
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +13 -0
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -0
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -0
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -0
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -0
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -0
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -0
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -0
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -0
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -0
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -0
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +12 -0
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -0
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +1 -1
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +2 -0
  54. package/src/app.js +8 -49
  55. package/src/help.js +2 -1
  56. package/templates/web-app/AGENTS.md +8 -5
  57. package/templates/web-app/README.md +8 -2
  58. package/templates/web-app/docs/acceptance.md +67 -0
  59. package/templates/web-app/docs/auth-and-deploy.md +2 -2
  60. package/templates/web-app/docs/data-contract.md +38 -21
  61. package/templates/web-app/docs/design.md +43 -2
  62. package/templates/web-app/docs/filters.md +4 -0
  63. package/templates/web-app/examples/cockpit.config.json +22 -0
  64. package/templates/web-app/examples/retail.config.json +185 -0
  65. package/templates/web-app/package-lock.json +2 -2
  66. package/templates/web-app/package.json +3 -2
  67. package/templates/web-app/scripts/dev-proxy.mjs +2 -1
  68. package/templates/web-app/src/App.tsx +45 -20
  69. package/templates/web-app/src/cockpit.css +3 -0
  70. package/templates/web-app/src/components/Chart.tsx +24 -15
  71. package/templates/web-app/src/components/DataTable.tsx +12 -5
  72. package/templates/web-app/src/components/FilterBar.tsx +16 -10
  73. package/templates/web-app/src/components/Insights.tsx +17 -0
  74. package/templates/web-app/src/components/MonthPicker.tsx +32 -0
  75. package/templates/web-app/src/components/OptionFilter.tsx +7 -0
  76. package/templates/web-app/src/components/RangeFilter.tsx +9 -0
  77. package/templates/web-app/src/components/Widget.tsx +23 -12
  78. package/templates/web-app/src/components/calendar-locales.ts +3 -0
  79. package/templates/web-app/src/components/charts/Bar.tsx +6 -0
  80. package/templates/web-app/src/components/charts/Line.tsx +6 -0
  81. package/templates/web-app/src/components/charts/Pie.tsx +6 -0
  82. package/templates/web-app/src/components/month-picker.css +1 -0
  83. package/templates/web-app/src/finebi/client.ts +15 -10
  84. package/templates/web-app/src/finebi/config.d.ts +1 -0
  85. package/templates/web-app/src/finebi/config.js +44 -0
  86. package/templates/web-app/src/finebi/types.ts +2 -2
  87. package/templates/web-app/src/retail.css +13 -0
  88. package/templates/web-app/src/services/presentation.ts +36 -0
  89. package/templates/web-app/src/services/table-data.ts +47 -0
  90. package/templates/web-app/src/services/widget-queries.ts +18 -0
  91. package/templates/web-app/tests/browser/fixtures.mjs +28 -0
  92. package/templates/web-app/tests/browser/index.html +16 -0
  93. package/templates/web-app/tests/browser/main.tsx +149 -0
  94. package/templates/web-app/tests/browser/test-page.css +4 -0
  95. package/templates/web-app/tests/browser/vite.config.mjs +13 -0
  96. package/templates/web-app/tests/bundle.test.mjs +19 -0
  97. package/templates/web-app/tests/cockpit.test.mjs +73 -0
  98. package/templates/web-app/tests/proxy.test.mjs +12 -0
  99. package/templates/web-app/tests/retail.test.mjs +31 -0
  100. package/templates/web-app/tests/runtime.test.mjs +1 -1
  101. package/templates/web-app/vite.config.ts +2 -0
@@ -0,0 +1,73 @@
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-cockpit-tests',server:{middlewareMode:true,hmr:false}});
7
+ after(()=>server.close());
8
+ const client=await server.ssrLoadModule('/src/finebi/client.ts');
9
+ const data=await server.ssrLoadModule('/src/services/table-data.ts');
10
+ const queries=await server.ssrLoadModule('/src/services/widget-queries.ts');
11
+ const {Widget}=await server.ssrLoadModule('/src/components/Widget.tsx');
12
+ const config={schemaVersion:'1',title:'Test',basePath:'/bi',layout:'cockpit',dashboardId:'r',directoryEntryId:'directory',maxRows:1000,pageSize:8,
13
+ widgets:[{entryId:'a',name:'A',type:'table',filterNames:['month']},{entryId:'b',name:'B',type:'table',filterNames:[]}],filters:[{name:'month',label:'月份',type:'month'}]};
14
+ const native={previewResult:{resultType:'DETAIL',header:[],items:[],size:10,row:0,hasNext:false}};
15
+ test('retry is only exposed after a settled failure, never while an initial request is pending',()=>{
16
+ const props={binding:config.widgets[0],pageSize:8,onRetry:()=>{}};
17
+ assert.doesNotMatch(renderToStaticMarkup(createElement(Widget,{...props,state:{loading:true}})),/重试组件/);
18
+ assert.match(renderToStaticMarkup(createElement(Widget,{...props,state:{loading:false,error:new Error('fixture')}})),/重试组件/);
19
+ });
20
+ test('directory source and verified per-widget scope survive individual queries, never falling back',async()=>{
21
+ const sent=[];
22
+ const fetcher=async(url,init)=>{sent.push({url,...JSON.parse(init.body)});return new Response(JSON.stringify({success:true,data:{a:native,b:native}}));};
23
+ await client.queryWidget(config,config.widgets[0],{month:'2024-02'},undefined,fetcher);
24
+ await client.queryWidget(config,config.widgets[1],{month:'2024-02'},undefined,fetcher);
25
+ assert.deepEqual(sent.map(s=>s.reportEntryId),['directory','directory']);
26
+ assert.deepEqual(sent.map(s=>s.widgetIds),[['a'],['b']]);
27
+ assert.deepEqual(sent.map(s=>s.refer),[{month:['2024-02']},{}]);
28
+ let calls=0;
29
+ await assert.rejects(client.queryWidget(config,config.widgets[0],{},undefined,async()=>{calls++;return new Response('{}',{status:403});}),e=>e.code==='PERMISSION_DENIED');
30
+ assert.equal(calls,1);
31
+ });
32
+ test('component loader limits concurrency, publishes independent failures and respects cancellation',async()=>{
33
+ const updates=[];let active=0,peak=0;
34
+ const many={...config,widgets:[...config.widgets,...Array.from({length:6},(_,i)=>({entryId:'extra-'+i,name:'Extra',type:'table'}))]};
35
+ await queries.loadWidgets(many,{},new AbortController().signal,(id,state)=>updates.push([id,state]),async(_url,init)=>{
36
+ active++;peak=Math.max(peak,active);await new Promise(r=>setTimeout(r,5));active--;
37
+ const id=JSON.parse(init.body).widgetIds[0];return id==='a'?new Response('{}',{status:500}):new Response(JSON.stringify({success:true,data:{[id]:native}}));
38
+ });
39
+ assert.equal(peak,3);assert.ok(updates.find(([id,s])=>id==='0:a'&&s.error));assert.ok(updates.find(([id,s])=>id==='1:b'&&s.result));
40
+ const controller=new AbortController();controller.abort();let calls=0;
41
+ await queries.loadWidgets(config,{},controller.signal,()=>assert.fail('no stale update'),async()=>{calls++;});assert.equal(calls,0);
42
+ });
43
+ test('same native widget can have separate display scopes; aborted old responses publish nothing',async()=>{
44
+ const duplicate={...config,widgets:[config.widgets[0],{...config.widgets[0],filterNames:[]}]};
45
+ const complete=[];
46
+ await queries.loadWidgets(duplicate,{month:'2024-02'},new AbortController().signal,(key,state)=>{if(state.result)complete.push(key);},async()=>new Response(JSON.stringify({success:true,data:{a:native}})));
47
+ assert.deepEqual(complete.sort(),['0:a','1:a']);
48
+ const controller=new AbortController(),updates=[];
49
+ await queries.loadWidgets(config,{},controller.signal,(_key,state)=>updates.push(state),async()=>{controller.abort();return new Response(JSON.stringify({success:true,data:{a:native,b:native}}));});
50
+ assert.ok(updates.every(s=>s.loading));
51
+ });
52
+ const cell=(value,state='AVAILABLE',display=value)=>({value,display,state,reason:null});
53
+ const dataset={id:'main',returnedRowCount:4,columns:[{fieldId:'x',key:'c0',name:'名字',type:'STRING'},{fieldId:'v',key:'c1',name:'金额',type:'DECIMAL'}],rows:[
54
+ {kind:'DATA',cells:[cell('=HYPERLINK("x")'),cell('9007199254740993.01')]},
55
+ {kind:'DATA',cells:[cell('B'),cell('9007199254740993.02','AVAILABLE','9,007,199,254,740,993.02元')]},
56
+ {kind:'UNKNOWN',cells:[cell('C'),cell(null,'RESTRICTED','***')]},
57
+ {kind:'DATA',cells:[cell('D'),cell(null,'NULL',null)]},
58
+ ]};
59
+ test('loaded table sorts decimals exactly, filters and exports matching rows with safe CSV and explicit states',()=>{
60
+ assert.equal(data.compareDecimal('9007199254740993.01','9007199254740993.02'),-1);
61
+ assert.equal(data.compareDecimal('-1.001e3','-1001'),0);
62
+ assert.equal(data.compareDecimal('1e-10','0.00000000009'),1);
63
+ assert.equal(data.compareDecimal('1.20','1.2'),0);
64
+ assert.equal(data.compareDecimal('1200','1.2e3'),0);
65
+ assert.equal(data.compareDecimal('-0.001200','-1.2e-3'),0);
66
+ const rows=data.tableRows(dataset,{search:'',sort:{fieldId:'v',direction:'desc'},filters:{}});
67
+ assert.equal(rows[0].cells[0].value,'B');assert.equal(rows[1].cells[0].value,'=HYPERLINK("x")');
68
+ const filtered=data.tableRows(dataset,{search:'',filters:{x:'B'}});assert.equal(filtered.length,1);
69
+ const csv=data.tableCsv(dataset,rows);
70
+ assert.match(csv,/'=HYPERLINK/);assert.match(csv,/受限/);assert.doesNotMatch(csv,/\*\*\*/);assert.match(csv,/UNKNOWN/);
71
+ assert.match(data.tableCsv(dataset,filtered),/9,007,199/);
72
+ assert.throws(()=>data.tableRows(dataset,{search:'',filters:{missing:'x'}}),/字段/);
73
+ });
@@ -3,6 +3,18 @@ import {test} from 'node:test';
3
3
  import {createServer} from 'node:http';
4
4
  import {createDevMiddleware} from '../scripts/dev-proxy.mjs';
5
5
 
6
+ test('directory proxy pins the published entry and never forwards another source',async()=>{
7
+ const sent=[];
8
+ const middleware=createDevMiddleware({basePath:'/bi',dashboardId:'r',directoryEntryId:'entry',widgets:[{entryId:'w'}],filters:[],maxRows:10},
9
+ {FINEBI_DEV_BASE_URL:'http://bi.example/bi',FINEBI_DEV_TOKEN:'secret'},async(_url,init)=>{sent.push(JSON.parse(init.body));return new Response(JSON.stringify({success:true,data:{}}));});
10
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));await new Promise(r=>server.listen(0,'127.0.0.1',r));
11
+ const send=body=>fetch(`http://127.0.0.1:${server.address().port}/bi/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10,...body})});
12
+ try {
13
+ for(const body of [{},{reportEntryId:'other'},{directoryEntryId:'entry'}])assert.equal((await send(body)).status,400);
14
+ assert.equal(sent.length,0);assert.equal((await send({reportEntryId:'entry'})).status,200);assert.equal(sent[0].reportEntryId,'entry');
15
+ }finally{await new Promise(r=>server.close(r));}
16
+ });
17
+
6
18
  test('development identity stays on server and cross-site or out-of-scope requests are rejected', async () => {
7
19
  const upstream = [];
8
20
  const middleware = createDevMiddleware({basePath:'/webroot/decision',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000}, {FINEBI_DEV_BASE_URL:'http://bi.example/webroot/decision',FINEBI_DEV_USERNAME:'tester',FINEBI_DEV_PASSWORD:'dev-secret'}, async (url,init) => {
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import {after,test} from 'node:test';
3
+ import {createServer} from 'vite';
4
+ const server=await createServer({configFile:false,cacheDir:'temp/vite-retail-tests',server:{middlewareMode:true,hmr:false}});
5
+ after(()=>server.close());
6
+ const {formatMetric,formatCategory}=await server.ssrLoadModule('/src/services/presentation.ts');
7
+ const {configErrors}=await import('../src/finebi/config.js');
8
+ const cell=(value,state='AVAILABLE')=>({value,display:null,state,reason:null});
9
+ test('KPI display preserves decimal precision and formats ratios only by explicit configuration',()=>{
10
+ assert.equal(formatMetric(cell('9007199254740993.01'),'number'),'9,007,199,254,740,993.01');
11
+ assert.equal(formatMetric(cell('1.2345678e7'),'number'),'12,345,678');
12
+ assert.equal(formatMetric(cell('0.123456789'),'percent'),'12.35%');
13
+ assert.equal(formatMetric(cell('-0.00005'),'percent'),'-0.01%');
14
+ assert.equal(formatMetric(cell('42.0'),'number'),'42');
15
+ assert.equal(formatMetric(cell('100','RESTRICTED'),'number'),'受限');
16
+ assert.equal(formatMetric(cell(null,'NULL'),'percent'),'—');
17
+ assert.equal(formatMetric(cell('0.5')),'0.5');
18
+ });
19
+ test('month formatting is explicit, timezone-aware and preserves invalid or restricted values',()=>{
20
+ const binding={categoryFormat:'epoch-month',categoryTimeZone:'Asia/Shanghai'};
21
+ const timestamp=String(Date.UTC(2024,1,29,16));
22
+ assert.equal(formatCategory(cell(timestamp),binding),'2024-03');
23
+ assert.equal(formatCategory(cell(timestamp),{}),timestamp);
24
+ assert.equal(formatCategory(cell('not-a-date'),binding),'not-a-date');
25
+ assert.equal(formatCategory(cell(timestamp,'RESTRICTED'),binding),'受限');
26
+ });
27
+ test('display configuration rejects implicit date timezone and invalid formats',()=>{
28
+ const config={schemaVersion:'1',title:'Test',basePath:'/bi',layout:'retail',dashboardId:'r',maxRows:1000,pageSize:8,filters:[],widgets:[{entryId:'w',name:'Trend',type:'line',categoryFieldId:'x',valueFieldId:'y',categoryFormat:'epoch-month',categoryTimeZone:'Asia/Shanghai'}]};
29
+ assert.deepEqual(configErrors(config),[]);
30
+ for(const patch of [{categoryTimeZone:undefined},{categoryTimeZone:'Bad/Timezone'},{categoryFormat:'guess'},{valueFormat:'guess'}])assert.ok(configErrors({...config,widgets:[{...config.widgets[0],...patch}]}).length);
31
+ });
@@ -52,7 +52,7 @@ test('chart preserves multiple models, rejects formatted numeric strings and mis
52
52
  assert.equal(r.datasets[0].rows[1].cells[1].state,'UNAVAILABLE');
53
53
  assert.equal(r.normalization,'PARTIAL');
54
54
  });
55
- const config = {schemaVersion:'1',basePath:'/webroot/decision',dashboardId:'report',maxRows:1000,widgets:[{entryId:'table',type:'table',name:'明细'}],filters:[{name:'地区',label:'地区',options:[{label:'西部',value:'West'}]}]};
55
+ const config = {schemaVersion:'1',title:'Test',layout:'overview',pageSize:8,basePath:'/webroot/decision',dashboardId:'report',maxRows:1000,widgets:[{entryId:'table',type:'table',name:'明细'}],filters:[{name:'地区',label:'地区',options:[{label:'西部',value:'West'}]}]};
56
56
  test('query sends scoped refer, cookie credentials and bounded pageLimit without browser token storage', async () => {
57
57
  let sent;
58
58
  const r = await queryWidgets(config, {'地区':'West'}, undefined, async (url, init) => {
@@ -1,9 +1,11 @@
1
1
  import {readFileSync} from 'node:fs';
2
+ import {fileURLToPath} from 'node:url';
2
3
  import {defineConfig, loadEnv} from 'vite';
3
4
  // @ts-expect-error The development-only Node module is tested directly with node:test.
4
5
  import {createDevMiddleware} from './scripts/dev-proxy.mjs';
5
6
  export default defineConfig(({mode}) => ({
6
7
  base: './',
8
+ resolve:{alias:[{find:/^date-fns\/locale$/,replacement:fileURLToPath(new URL('./src/components/calendar-locales.ts',import.meta.url))}]},
7
9
  server: {host:'127.0.0.1',port:4173,strictPort:true},
8
10
  plugins:[{name:'finebi-development-session',apply:'serve',configureServer(server) {
9
11
  const config=JSON.parse(readFileSync(new URL('./public/app-config.json',import.meta.url),'utf8'));