@finebi/finebi-cli 1.5.0 → 1.6.0
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 +10 -0
- package/package.json +4 -3
- package/skills/finebi-cli-agent/SKILL.md +8 -4
- package/skills/finebi-cli-agent/references/command-workflows.md +7 -3
- package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
- package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +8 -1018
- package/skills/finebi-cli-agent/references/dashboard-dsl-core.md +194 -0
- package/skills/finebi-cli-agent/references/dashboard-dsl-examples.md +378 -0
- package/skills/finebi-cli-agent/references/dashboard-dsl-items.md +456 -0
- package/skills/finebi-cli-agent/references/mapping-rules.md +14 -14
- package/skills/finebi-cli-agent/references/scenario-workflows.md +1 -1
- package/skills/finebi-cli-agent/references/web-app.md +13 -0
- package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +3 -4
- package/skills/finebi-cli-agent/references/widget-dsl-api.md +10 -1374
- package/skills/finebi-cli-agent/references/widget-dsl-contexts.md +142 -0
- package/skills/finebi-cli-agent/references/widget-dsl-core.md +374 -0
- package/skills/finebi-cli-agent/references/widget-dsl-encodings.md +457 -0
- package/skills/finebi-cli-agent/references/widget-dsl-styles.md +413 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +189 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +78 -55
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/index.js +5 -0
- 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 +27 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +30 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +21 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/design.md +13 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +89 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package-lock.json +2191 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package.json +15 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +56 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +31 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +31 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +14 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/FilterBar.tsx +42 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +28 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +37 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +77 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +18 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/styles.css +8 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/calendar.test.mjs +25 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +84 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +136 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +13 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
- package/src/app.js +189 -0
- package/src/config.js +27 -27
- package/src/help.js +78 -55
- package/src/index.js +5 -0
- 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 +27 -0
- package/templates/web-app/README.md +30 -0
- package/templates/web-app/docs/auth-and-deploy.md +17 -0
- package/templates/web-app/docs/data-contract.md +21 -0
- package/templates/web-app/docs/design.md +13 -0
- package/templates/web-app/docs/filters.md +89 -0
- package/templates/web-app/index.html +2 -0
- package/templates/web-app/package-lock.json +2191 -0
- package/templates/web-app/package.json +15 -0
- package/templates/web-app/public/app-config.json +11 -0
- package/templates/web-app/scripts/dev-proxy.mjs +56 -0
- package/templates/web-app/src/App.tsx +31 -0
- package/templates/web-app/src/components/Chart.tsx +31 -0
- package/templates/web-app/src/components/DataTable.tsx +14 -0
- package/templates/web-app/src/components/FilterBar.tsx +42 -0
- package/templates/web-app/src/components/Widget.tsx +28 -0
- package/templates/web-app/src/components/calendar.ts +14 -0
- package/templates/web-app/src/finebi/client.ts +37 -0
- package/templates/web-app/src/finebi/date.d.ts +1 -0
- package/templates/web-app/src/finebi/date.js +5 -0
- package/templates/web-app/src/finebi/filters.d.ts +4 -0
- package/templates/web-app/src/finebi/filters.js +106 -0
- package/templates/web-app/src/finebi/normalize.ts +77 -0
- package/templates/web-app/src/finebi/types.ts +18 -0
- package/templates/web-app/src/main.tsx +5 -0
- package/templates/web-app/src/services/view-data.ts +18 -0
- package/templates/web-app/src/styles.css +8 -0
- package/templates/web-app/tests/calendar.test.mjs +25 -0
- package/templates/web-app/tests/proxy.test.mjs +84 -0
- package/templates/web-app/tests/runtime.test.mjs +136 -0
- package/templates/web-app/tsconfig.json +4 -0
- package/templates/web-app/vite.config.ts +13 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# Widget DSL API:图表样式与视觉通道
|
|
2
|
+
|
|
3
|
+
> Widget DSL API 总导航见 [widget-dsl-api.md](widget-dsl-api.md)。
|
|
4
|
+
|
|
5
|
+
## 4.6 图表级视觉通道
|
|
6
|
+
|
|
7
|
+
| DSL API | 业务含义 |
|
|
8
|
+
|-----------------------------|----------|
|
|
9
|
+
| `.color(dimRef)` | 按维度着色。 |
|
|
10
|
+
| `.color(measureRef)` | 按连续指标着色。 |
|
|
11
|
+
| `.size(measureRef)` | 按指标控制大小。 |
|
|
12
|
+
| `.label(dimRef)` | 追加标签维度。 |
|
|
13
|
+
| `.label(measureRef)` | 追加标签指标。 |
|
|
14
|
+
| `.tooltipField(dimRef)` | 追加提示维度。 |
|
|
15
|
+
| `.tooltipField(measureRef)` | 追加提示指标。 |
|
|
16
|
+
| `.granularity(dimRef)` | 追加细粒度维度。 |
|
|
17
|
+
| `.granularity(measureRef)` | 追加细粒度指标。 |
|
|
18
|
+
|
|
19
|
+
## 4.6.1 图形大小
|
|
20
|
+
|
|
21
|
+
| API | 语义与范围 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `SizeOptions.line().width(px)` | 普通折线线宽,有限数 `0.5–35px` |
|
|
24
|
+
| `SizeOptions.interval().widthRatio(ratio)` | 普通柱形柱宽比例,有限数 `0–1` |
|
|
25
|
+
| `.roundRadius(px)` | 柱形圆角,整数 `0–50px` |
|
|
26
|
+
| `.gap(value)` | 柱间距 `small/middle/large`;仅 seriesColumn/stackColumn 图表级且存在绘图指标 |
|
|
27
|
+
| `SizeOptions.box().widthRatio(ratio)` | 箱线宽度比例,有限数 `0–1`;图表级及 BOX Series |
|
|
28
|
+
| `SizeOptions.sankey().nodeWidth(value)` | 桑基节点宽度,有限数 `0–100`,与原生滑块同值;仅图表级 |
|
|
29
|
+
| `SizeOptions.funnel().widthRatio(ratio)` | 漏斗宽度比例,有限数 `0–1`;仅图表级 |
|
|
30
|
+
| `SizeOptions.gauge().radiusRatio(ratio)` | 仪表图尺寸比例,有限数 `0–1`;仅图表级 |
|
|
31
|
+
| `SizeOptions.gauge().radius(px)` | 仪表图像素尺寸,有限数 `1.00001–150`;仅图表级 |
|
|
32
|
+
| `SizeOptions.pie().radiusRatio(ratio)` | 饼图、玫瑰图外半径比例,有限数 `0–1`;仅图表级 |
|
|
33
|
+
| `SizeOptions.multiPie().radiusRatio(ratio)` | 多层饼图外半径比例,有限数 `0–1`;仅图表级 |
|
|
34
|
+
| `.innerRadiusRatio(ratio)` | `SizeOptions.pie/multiPie` 的内径比例,有限数 `0–1`;`0` 为实心 |
|
|
35
|
+
|
|
36
|
+
使用 `.sizeOptions(...)` 挂到图表或绘图 Series,Series 未声明的属性继承图表配置,显式 `0` 可覆盖父级。
|
|
37
|
+
空 `SizeOptions` 工厂表示未声明,不重置;字段映射继续使用 `.size(measure)`。
|
|
38
|
+
线宽适用 `partitionLine/seriesLine`;柱宽与圆角适用 `sliceColumn/stackColumn/seriesColumn/compareColumn`。
|
|
39
|
+
柱宽比例不适用于双轴均含指标的像素布局;支持分类轴转置。柱间距不允许 Series 覆盖。雷达、面积、自定义组合图及地图不接受本入口,也不能与 RadarOptions 混用。
|
|
40
|
+
仪表图编辑器入口为“图形属性 → 样式 → 大小”。仪表图无轴字段或存在纯维度轴时使用 `radiusRatio`,否则使用像素 `radius`(例如仅 `.y(measure)`);单位与布局不符时报错。通过 `.pointer(measure)` 绑定指针而不放轴时使用比例。漏斗仅支持宽度配置,不提供固定高度 API。文本图形字号不在此入口范围内。
|
|
41
|
+
饼图与玫瑰图使用 `.sizeOptions(SizeOptions.pie().radiusRatio(0.8).innerRadiusRatio(0.5))`;
|
|
42
|
+
多层饼图使用 `.sizeOptions(SizeOptions.multiPie().radiusRatio(0.8).innerRadiusRatio(0.2))`。
|
|
43
|
+
两项可独立省略;玫瑰图半径比例控制数据映射的最大半径,不替代 `.size(measure)` 字段映射。
|
|
44
|
+
饼图家族不支持像素半径、逐 Series 大小、共用半径开关或起止/旋转角;扇区角度仍由指标占比决定。
|
|
45
|
+
已有点图 `pointRadius` 与地图 `mapRadius` 保持各自入口。示例见 [折线](widget-dsl-api-examples.md#101-分区折线图)
|
|
46
|
+
与 [柱形](widget-dsl-api-examples.md#102-多系列柱形图)。
|
|
47
|
+
|
|
48
|
+
## 4.7 数值格式
|
|
49
|
+
|
|
50
|
+
| DSL API | 业务含义 |
|
|
51
|
+
|---------------------------------------------------------|--------------|
|
|
52
|
+
| `.numberFormat(measureRef, NumberFormatModel.xxx(...))` | 设置指标展示格式。 |
|
|
53
|
+
| `.numberFormat(dimRef, NumberFormatModel.xxx(...))` | 设置明细数值列展示格式。 |
|
|
54
|
+
|
|
55
|
+
数据条只能作用于分组表和交叉表中已追加的 `measureRef`,不适用于明细表或其他图表。
|
|
56
|
+
当前只支持平台默认形态(数值保留、自动最小/最大值、平台默认颜色)。
|
|
57
|
+
|
|
58
|
+
迷你图仅适用于分组表,一个组件最多一个。类型工厂为 `MiniChart.line/pie/interval`;
|
|
59
|
+
排序为 `MiniChartSort.ASC/DESC/TARGET_ASC/TARGET_DESC`;过滤只使用
|
|
60
|
+
`MiniChartFilter.textBelong/textNotBelong/dateBelong/dateNotBelong`。目标指标必须已追加,
|
|
61
|
+
依赖维度不要求追加;迷你图样式使用平台默认值。
|
|
62
|
+
|
|
63
|
+
## 4.7.1 警戒线与趋势线
|
|
64
|
+
|
|
65
|
+
仅 `partitionLine/seriesLine/sliceColumn/stackColumn/seriesColumn/compareColumn/custom` 支持分析线。
|
|
66
|
+
目标参数必须是绘图轴 Series 中实际绘制的同一个 `MeasureRef` 语义身份;
|
|
67
|
+
`custom` 选择 Series 数量较多的一轴绘图,相等选 y;非绘图轴的 `bar(measure)` 不可绑定分析线。
|
|
68
|
+
`MeasureRef` 包含来源、字段、聚合、别名和快速计算,不是显示名称,也不能用只出现在
|
|
69
|
+
color/size/label/tooltip 等通道中的指标代替。
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const sales = from("销售表").sum("销售额");
|
|
73
|
+
WidgetDsl.create("销售趋势").chart().seriesLine()
|
|
74
|
+
.x(from("销售表").dim("月份"))
|
|
75
|
+
.y(line(sales))
|
|
76
|
+
.warningLine(sales, WarningLine.fixed("目标", 100).solid()
|
|
77
|
+
.color("#E65251").showValue(false))
|
|
78
|
+
.warningLine(sales, WarningLine.average("平均"))
|
|
79
|
+
.trendLine(sales, TrendLine.linear("趋势").dashed()
|
|
80
|
+
.color("#A6A6A6").forecastForward(2).forecastBackward(1))
|
|
81
|
+
.build();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
警戒线工厂:`WarningLine.fixed(name, value)`、`average(name)`、`minimum(name)`、
|
|
85
|
+
`maximum(name)`;修饰器:`solid/dashed/color/showValue`。默认虚线、`#E65251`、显示值。
|
|
86
|
+
趋势线工厂:`TrendLine.linear(name)`、`exponential(name)`、`logarithmic(name)`、
|
|
87
|
+
`polynomial(name)`;修饰器:`solid/dashed/color/forecastForward/forecastBackward`。默认实线、
|
|
88
|
+
`#A6A6A6`、预测 0。预测值不得为负,固定值必须有限,颜色只接受 `#RRGGBB`。
|
|
89
|
+
|
|
90
|
+
不要生成任意警戒线公式、其他拟合类型或多项式阶数;当前 DSL 不支持这些能力。同一目标下
|
|
91
|
+
警戒线名称不能重复,趋势线名称不能重复。
|
|
92
|
+
|
|
93
|
+
## 4.7.2 值轴与双 y 轴
|
|
94
|
+
|
|
95
|
+
入口:`.valueAxis(measureRef, ValueAxis.builder()...build())`。适用于
|
|
96
|
+
`partitionLine/seriesLine/sliceColumn/stackColumn/seriesColumn/compareColumn/custom`,目标必须唯一对应
|
|
97
|
+
实际 x/y 轴上的指标 Series,且来源、聚合、别名、快速计算与 Series 中的引用完全一致。
|
|
98
|
+
|
|
99
|
+
| Builder 方法 | 语义与限制 |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `side(ValueAxisSide.LEFT/RIGHT)` | 不声明时 LEFT;RIGHT 仅用于 POLY 区域。纵轴为左右,横轴实际显示为下/上 |
|
|
102
|
+
| `range(ValueAxisRange.AUTO)` | 自动范围、分区共用范围 |
|
|
103
|
+
| `range(ValueAxisRange.PER_PARTITION)` | 按维度动态范围;仅允许纵轴存在维度分区 |
|
|
104
|
+
| `range(ValueAxisRange.CUSTOM)` | 自定义范围,未设置的边界仍由平台计算 |
|
|
105
|
+
| `min(expr)/max(expr)/interval(expr)` | 设边界即推断 CUSTOM,与其他 range 冲突报错;常量须有限,常量间隔大于 0,常量 min 小于 max |
|
|
106
|
+
| `showLabel(boolean)/labelRotation(int)` | 标签显示及 -90 到 90 度旋转 |
|
|
107
|
+
| `labelFont(font)/titleFont(font)` | `AxisFont.automatic()` 或 `AxisFont.custom(family, size, color, bold, italic)`;正整数字号,颜色为 `#RRGGBB` 或空串(自动颜色) |
|
|
108
|
+
| `reversed(boolean)/logarithmic(boolean)` | 逆序、十进制对数轴;不指定逆序保留对比图默认值 |
|
|
109
|
+
| `showTitle(boolean)/title(text)/autoTitle()` | 标题显示、自定义非空标题或随指标名变化的自动标题 |
|
|
110
|
+
|
|
111
|
+
边界表达式:`AxisValue.fixed(number)`、`AxisValue.min(measureRef)`、`max(measureRef)`、
|
|
112
|
+
`average(measureRef)`,通过 `.add(expr)/subtract(expr)/multiply(expr)/divide(expr)` 组合。
|
|
113
|
+
表达式引用只能来自同一实际轴;不接受任意公式文本。统计值按平台绘制数据计算,
|
|
114
|
+
输入边界使用原始数值;例如展示百万单位,最大值仍写 `AxisValue.fixed(2000000)`。
|
|
115
|
+
|
|
116
|
+
双 y 轴需要**纵轴区域指标聚合 POLY**,`sum/avg` 是字段汇总,不代表区域已聚合。
|
|
117
|
+
`seriesColumn/seriesLine/stackColumn/custom` 使用 POLY;分区柱/分区折线和对比条形图的 PARALLEL 不允许 RIGHT。
|
|
118
|
+
未声明侧别的指标归 LEFT。每侧共用一套轴属性,同属性不同值报错,不按声明顺序覆盖;
|
|
119
|
+
同侧数字格式以平台选中的指标格式显示(左侧首指标、右侧末指标),建议同侧使用一致单位。
|
|
120
|
+
|
|
121
|
+
合同金额/回款金额双轴、两侧百万单位:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
var contract = from("合同表").sum("合同金额");
|
|
125
|
+
var received = from("合同表").sum("回款金额");
|
|
126
|
+
WidgetDsl.create("合同与回款").chart().seriesColumn()
|
|
127
|
+
.x(from("合同表").dim("省份"))
|
|
128
|
+
.y(bar(contract)).y(bar(received))
|
|
129
|
+
.valueAxis(contract, ValueAxis.builder().title("合同金额(百万元)").build())
|
|
130
|
+
.valueAxis(received, ValueAxis.builder().side(ValueAxisSide.RIGHT)
|
|
131
|
+
.title("回款金额(百万元)").build())
|
|
132
|
+
.numberFormat(contract, NumberFormatModel.number(2, NumberFormatModelScale.MILLION, "元", true))
|
|
133
|
+
.numberFormat(received, NumberFormatModel.number(2, NumberFormatModelScale.MILLION, "元", true))
|
|
134
|
+
.build();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
金额与比例双轴(比例字段约定原始值 `0.25` 表示 25%):
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
var amount = from("销售表").sum("销售额");
|
|
141
|
+
var rate = from("销售表").avg("毛利率");
|
|
142
|
+
WidgetDsl.create("金额与毛利率").chart().seriesLine()
|
|
143
|
+
.x(from("销售表").dim("月份"))
|
|
144
|
+
.y(line(amount)).y(line(rate))
|
|
145
|
+
.valueAxis(amount, ValueAxis.builder().title("销售额")
|
|
146
|
+
.max(AxisValue.max(amount).multiply(AxisValue.fixed(1.2))).build())
|
|
147
|
+
.valueAxis(rate, ValueAxis.builder().side(ValueAxisSide.RIGHT).title("毛利率")
|
|
148
|
+
.min(AxisValue.fixed(0)).max(AxisValue.fixed(1)).build())
|
|
149
|
+
.numberFormat(amount, NumberFormatModel.number(2))
|
|
150
|
+
.numberFormat(rate, NumberFormatModel.percentage(1, true))
|
|
151
|
+
.build();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
省略属性沿用默认,显式 false 生效。回读输出有效轴的规范配置,可补出显式默认值;
|
|
155
|
+
自动标题和字体保持自动语义。更新时保留所需 valueAxis 声明,移轴后同步调整同轴公式引用。
|
|
156
|
+
有效公式或样式超出以上范围时导出明确报错,不能据此省略设置后替换保存。
|
|
157
|
+
|
|
158
|
+
## 4.7.3 分类轴
|
|
159
|
+
|
|
160
|
+
笛卡尔图表(含 `custom`)支持 `categoryAxis(DimRef, CategoryAxis.builder()...build())`,目标必须唯一位于 x/y 轴。
|
|
161
|
+
同一目标不可重复声明;同字段不同日期粒度可分别设置。本期不包含钻取。
|
|
162
|
+
|
|
163
|
+
| 方法 | 说明 |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `showLabel(boolean)` | 标签显隐,显式 false 保留 |
|
|
166
|
+
| `labelDisplay(CategoryAxisDisplay.AUTO/ALL/INTERVAL)` | 默认、全部、间隔显示;ALL 的平台值为 ellipsis |
|
|
167
|
+
| `labelAutoWrap(boolean)` | 全部显示时换行;其他策略可保留该配置 |
|
|
168
|
+
| `labelRotation(int)` | -90..90;AUTO 策略由平台决定实际方向 |
|
|
169
|
+
| `autoInterval()` / `interval(int)` | 推导 INTERVAL;自定义间隔为非负整数,0 合法;与显式 AUTO/ALL 冲突时报错 |
|
|
170
|
+
| `labelFont(AxisFont.automatic/custom)` | 自动字体继承主题,自定义字体沿用值轴签名 |
|
|
171
|
+
| `labelAlign(CategoryAxisAlign.AUTO/LEFT/CENTER/RIGHT)` | 非 AUTO 仅支持纵向分类轴,且要求自定义 labelFont |
|
|
172
|
+
| `showTitle(boolean)` / `title(String)` / `autoTitle()` | 显隐与文本独立;允许自定义空文本;自动标题使用当前维度显示名 |
|
|
173
|
+
| `titleFont(AxisFont.automatic/custom)` | 标题字体 |
|
|
174
|
+
|
|
175
|
+
未调用 categoryAxis 或使用空配置时,保留现有创建默认值(标签自动显示、标题隐藏)。
|
|
176
|
+
只修改标题不会生成标签配置。分类轴按 placement 私有保存,允许与 valueAxis、numberFormat 同时使用。
|
|
177
|
+
Widget 创建及 Dashboard 内嵌图表使用同一 DSL;更新通过导出完整 DSL 后修改配置并重建,省略配置不代表远端局部合并。
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
var region = from("合同订单原表").dim("地区");
|
|
181
|
+
var amount = from("合同订单原表").sum("购买金额");
|
|
182
|
+
WidgetDsl.create("地区销售").chart().seriesColumn()
|
|
183
|
+
.x(region).y(bar(amount))
|
|
184
|
+
.categoryAxis(region, CategoryAxis.builder()
|
|
185
|
+
.showLabel(true).labelDisplay(CategoryAxisDisplay.ALL)
|
|
186
|
+
.labelAutoWrap(true).labelRotation(-45)
|
|
187
|
+
.showTitle(true).title("销售地区").build())
|
|
188
|
+
.build();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`interval(n)` 向平台原值传递 n,不做加减转换;具体抽样结果应以目标实例渲染为准。
|
|
192
|
+
标签隐藏时保留旋转、字体及换行;非 INTERVAL 策略下的休眠间隔在导出时归一化。
|
|
193
|
+
反读不静默丢弃未知策略或无法表达的有效字体/标题状态。
|
|
194
|
+
|
|
195
|
+
## 4.7.4 表格指标属性
|
|
196
|
+
|
|
197
|
+
分组表与交叉表支持指标 `.tableColor(target, config)`、`.tableShape(target, config)`;分组表、交叉表和明细表支持维度目标的同名重载。
|
|
198
|
+
分组表/交叉表的指标 `target` 是已绑定且唯一的 MeasureRef,或 `TableTarget.ALL_MEASURES`。明细表的数值目标和判断字段均使用原始数值 DimRef;文本/日期列使用下方的维度配置重载。
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
WidgetDsl.create("前三名标记").chart().table()
|
|
202
|
+
.dimension(from("订单").dim("地区"))
|
|
203
|
+
.measure(from("订单").sum("金额"))
|
|
204
|
+
.tableShape(from("订单").sum("金额"),
|
|
205
|
+
TableShape.by(quickCalculation(from("订单").sum("金额"), QuickCalculationType.RANK_DES))
|
|
206
|
+
.when(NumberRange.atMost(3), TableSymbol.of(TableShapeType.CROWN, "#D4AF37"))
|
|
207
|
+
.affectTotals(false))
|
|
208
|
+
.build();
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
| 配置 | DSL |
|
|
212
|
+
|---|---|
|
|
213
|
+
| 无判断指标的固定颜色 | `TableColor.fixed(CellColor.text("#FF0000").background("#FFFFFF"))` |
|
|
214
|
+
| 绑定判断指标 | `TableColor.by(measure)` / `TableShape.by(measure)`;没有条件时仍保留绑定 |
|
|
215
|
+
| 无判断指标的固定形状 | `TableShape.fixed(TableSymbol.of(TableShapeType.CROWN, "#D4AF37"))` |
|
|
216
|
+
| 数值条件 | `.when(NumberRange.equalTo(1), style)`;也支持 `atLeast/above/atMost/below/between(min, closeMin, max, closeMax)/all` |
|
|
217
|
+
| 颜色基础样式 | `TableColor.by(measure).otherwise(CellColor.text("#333333"))` |
|
|
218
|
+
| 形状基础配置保留 | `TableShape.by(measure).baseStyle(TableSymbol.of(TableShapeType.CROWN, "#D4AF37"))`;条件未命中仍不显示符号,这不是兜底规则 |
|
|
219
|
+
| 合计参与条件 | `.affectTotals(true)`,默认 false;不控制合计行显隐 |
|
|
220
|
+
| 颜色应用整行 | `TableColor...applyToLine(true)`,默认 false;形状没有该开关 |
|
|
221
|
+
|
|
222
|
+
颜色支持 `#RRGGBB`、`transparent` 或空串。形状使用 `TableShapeType` 的单个符号枚举(如 `CROWN/FACE_SMILE/CIRCLE_SOLID/NONE`),不接受组合预设 `GROUP_*`。
|
|
223
|
+
规则按声明顺序保存,最后命中的规则生效;固定模式不能追加数值条件。每个目标每个通道只能声明一次。全部指标配置先应用,再由单指标配置覆盖,与声明先后无关。
|
|
224
|
+
判断字段建立独立隐藏绑定,且不进入轴或自动加入过滤。分组表/交叉表允许独立聚合、已有计算字段和快速计算;明细表只接受原始数值 DimRef。排名不改变行排序;如需排序使用现有 `.sort(...)`。
|
|
225
|
+
不提供属性专属过滤 API;需要过滤时使用组件 `.filter()`,遵循组件结果过滤与共享字段分组语义,不能承诺只改变标记、不改变行数。
|
|
226
|
+
平均值差、不一致标志和极值标志由既有计算字段或数据集提供,表格属性只消费数值结果。
|
|
227
|
+
|
|
228
|
+
明细表的数值字段使用 `TableColor/TableShape`,支持固定样式和数值区间条件;目标无需出现在 `col` 中,未展示时只保留配置,不自动增加展示列。重复展示同一字段时,配置应用到所有匹配列。判断源也可以是未展示的原始数值字段。`TableTarget.ALL_MEASURES` 只作用于可见数值列,隐藏目标配置单独保留。明细表不支持聚合、快速计算或计算字段作为属性判断源。
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
WidgetDsl.create("明细金额标记").chart().detailTable()
|
|
232
|
+
.col(from("订单").dim("金额"))
|
|
233
|
+
.tableColor(from("订单").dim("金额"),
|
|
234
|
+
TableColor.by(from("订单").dim("金额"))
|
|
235
|
+
.when(NumberRange.below(0), CellColor.text("#FF0000")))
|
|
236
|
+
.tableShape(from("订单").dim("金额"),
|
|
237
|
+
TableShape.fixed(TableSymbol.of(TableShapeType.CROWN, "#D4AF37")))
|
|
238
|
+
.build();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### 维度颜色、形状与组件钻取
|
|
242
|
+
|
|
243
|
+
已绑定且唯一的 DimRef 可用 `tableColor(dim, TableDimensionColor.of()...)` 和
|
|
244
|
+
`tableShape(dim, TableDimensionShape.of()...)`。`.otherwise(style)` 设置固定或未命中样式;
|
|
245
|
+
`.when(TableCondition.xxx(...), style)` 按显示文本判断,最后命中规则生效,不过滤结果行。
|
|
246
|
+
维度形状的 `otherwise` 是未命中兜底;颜色支持 `.applyToLine(true)`,默认 false。
|
|
247
|
+
|
|
248
|
+
`TableCondition.in(["华东", null])` / `notIn([...])` 接受非空字符串列表(允许 null);
|
|
249
|
+
`contains/notContains/startsWith/endsWith/notStartsWith/notEndsWith` 接受一个非空字符串。
|
|
250
|
+
日期与分组表/交叉表的数值维度也按格式化后的显示文本匹配;明细表的原始数值列使用上方数值属性。
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
WidgetDsl.create("地区城市标记").chart().table()
|
|
254
|
+
.dimension(from("订单").dim("地区"))
|
|
255
|
+
.measure(from("订单").sum("金额"))
|
|
256
|
+
.tableColor(from("订单").dim("地区"), TableDimensionColor.of()
|
|
257
|
+
.when(TableCondition.in(["华东"]), CellColor.text("#FF0000")))
|
|
258
|
+
.tableDrill(TableDrill.of(from("订单").dim("地区"))
|
|
259
|
+
.initial(from("订单").dim("城市")).order(TableDrillOrder.FREE))
|
|
260
|
+
.tableShape(from("订单").dim("地区"), from("订单").dim("城市"),
|
|
261
|
+
TableDimensionShape.of()
|
|
262
|
+
.when(TableCondition.contains("南京"), TableSymbol.of(TableShapeType.CROWN, "#D4AF37")))
|
|
263
|
+
.build();
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
钻取需要主题中已存在以目标维度为根、至少两层的共享钻取目录。
|
|
267
|
+
`TableDrill.of(root)` 默认初始根层级、`TableDrillOrder.FIXED`;`FREE` 允许自由钻取。
|
|
268
|
+
三参数 `tableColor(root, level, config)` / `tableShape(root, level, config)` 分别配置目录层级;
|
|
269
|
+
不必声明 `tableDrill` 即可设置层级样式。层级须属于该目录,支持 DimRef 分组,非根层级不支持别名。
|
|
270
|
+
每个目标每层每通道只能声明一次;组件钻取设置每个目标只能声明一次。
|
|
271
|
+
配置仅设置初始层级与顺序,不表示交互产生的钻取历史。
|
|
272
|
+
|
|
273
|
+
## 4.8 排序
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
| DSL API | 业务含义 |
|
|
277
|
+
|-------------------------------------------------------------|---------------------|
|
|
278
|
+
| `.sort(dimRef, SortOrder.ASC / SortOrder.DESC)` | 按维度值方向排序。 |
|
|
279
|
+
| `.sort(measureRef, SortOrder.ASC / SortOrder.DESC)` | 按指标值方向排序。 |
|
|
280
|
+
| `.sort(dimRef, ["A", "B", "C"])` | 维度自定义顺序,适合业务枚举排序。 |
|
|
281
|
+
| `.sort(dimRef, measureRef, SortOrder.ASC / SortOrder.DESC)` | 维度按照某个指标结果排序,适合排行榜。 |
|
|
282
|
+
|
|
283
|
+
## 4.9 组件内边距(图表与表格通用)
|
|
284
|
+
|
|
285
|
+
在 chart 链上调用 padding(Padding.of()...build()),适用于全部图表(含指标卡)与三种表格。
|
|
286
|
+
|
|
287
|
+
| Padding Builder 方法 | 含义 |
|
|
288
|
+
|---|---|
|
|
289
|
+
| top(n) / right(n) / bottom(n) / left(n) | 分别设置上、右、下、左 |
|
|
290
|
+
| vertical(n) | 同时设置上下 |
|
|
291
|
+
| horizontal(n) | 同时设置左右 |
|
|
292
|
+
|
|
293
|
+
n 为非负整数像素,0 表示无内边距。未指定的边沿用平台默认;空 Builder 恢复默认。
|
|
294
|
+
重复 padding 调用以最后整组为准,Builder 内按调用顺序覆盖各边。锁按钮仅为编辑联动,DSL 用成对赋值表达。
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
WidgetDsl.create("销售额").chart().kpi()
|
|
298
|
+
.text(from("销售原表").sum("金额"))
|
|
299
|
+
.padding(Padding.of().vertical(0).horizontal(12).build())
|
|
300
|
+
.build();
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
分别设置四边可用 .padding(Padding.of().top(4).right(12).bottom(8).left(16).build())。
|
|
304
|
+
通过 widget add/update --file 保存,widget dsl 回读;dashboard 内联定义同样支持。
|
|
305
|
+
能力来源:[图形组件样式第八节](https://help.fanruan.com/finebi/doc-view-231.html)。
|
|
306
|
+
|
|
307
|
+
## 4.10 组件网格线、轴线与分区线
|
|
308
|
+
|
|
309
|
+
图表支持四个独立入口(表格不支持):
|
|
310
|
+
|
|
311
|
+
| 入口 | 组件样式面板配置 |
|
|
312
|
+
|---|---|
|
|
313
|
+
| `.horizontalGrid(config)` | 横向网格线 |
|
|
314
|
+
| `.verticalGrid(config)` | 纵向网格线 |
|
|
315
|
+
| `.axisLine(config)` | 轴线 |
|
|
316
|
+
| `.sectionLine(config)` | 分区线 |
|
|
317
|
+
|
|
318
|
+
`config` 使用 `ChartLine.builder()`,可选方法为 `enabled(boolean)`、
|
|
319
|
+
`type(ChartLineType.SOLID/DASHED/DOTTED)`、`color(string)` 和 `width(int)`。
|
|
320
|
+
宽度为非负整数;颜色支持空串(自动)、`transparent`、`#RGB`、`#RRGGBB`。
|
|
321
|
+
未设置时启用、宽度 1、自动颜色;网格默认虚线,轴线和分区线默认实线。
|
|
322
|
+
实际可见效果取决于图表类型与分区布局。关闭总网格线时,将横纵两个开关均设为 false。
|
|
323
|
+
`DOTTED` 可保存和回读,但点线渲染依赖平台版本;仅实现 solid/dashed 的渲染器会显示为实线。
|
|
324
|
+
|
|
325
|
+
在已有图表的 `.build()` 前追加:
|
|
326
|
+
|
|
327
|
+
```js
|
|
328
|
+
.horizontalGrid(ChartLine.builder().enabled(true)
|
|
329
|
+
.type(ChartLineType.DOTTED).color("#D9D9D9").width(1).build())
|
|
330
|
+
.verticalGrid(ChartLine.builder().enabled(false).build())
|
|
331
|
+
.axisLine(ChartLine.builder().color("#666666").width(2).build())
|
|
332
|
+
.sectionLine(ChartLine.builder().enabled(false).build())
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
这些是组件样式,不是 `lineOptions` 的数据折线,也不是分析线。
|
|
336
|
+
导出保留非默认配置及关闭状态下的样式;整份 DSL 更新时省略配置会恢复默认值。
|
|
337
|
+
|
|
338
|
+
## 4.11 组件背景(图表与表格通用)
|
|
339
|
+
|
|
340
|
+
所有图表与表格支持 `.titleBackground(WidgetBackground)` 和 `.widgetBackground(WidgetBackground)`。
|
|
341
|
+
使用 `WidgetBackground.of()...build()`:
|
|
342
|
+
|
|
343
|
+
| 方法 | 含义 |
|
|
344
|
+
|---|---|
|
|
345
|
+
| color("") / color("transparent") / color("#RGB" 或 "#RRGGBB") | 自动跟随主题 / 透明 / 自定义颜色 |
|
|
346
|
+
| image(附件ID) | 自定义图片;先用 `dashboard upload-image` 上传,引用返回的 ID |
|
|
347
|
+
| style(WidgetBackground.STYLE1…STYLE6) | 内置样式;标题 1–6,组件 1/2/3/5/6 |
|
|
348
|
+
| borderRadius(0…6) | 组件颜色背景圆角,整数 |
|
|
349
|
+
| borderSize(0…5) / borderColor(颜色) | 组件颜色背景的边框宽度和颜色 |
|
|
350
|
+
|
|
351
|
+
标题 STYLE1–6:标题下横线、标题双横线、标题上横线、科幻标题装饰、商务标题装饰、清新标题装饰。
|
|
352
|
+
组件 STYLE1/2/3/5/6:科幻大屏样式、数据大屏样式、科技感动态样式、指标卡样式、红色组件样式。
|
|
353
|
+
颜色/图片/样式互斥,最后一次模式选择生效。标题、图片和内置样式不支持边框配置。
|
|
354
|
+
省略整个背景或传 `WidgetBackground.of().build()` 恢复自动;显式圆角/宽度 0 会覆盖继承。
|
|
355
|
+
自动继承要求颜色、边框色为空且圆角/宽度均未声明。图片只接受附件 ID;颜色回读为小写。
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
WidgetDsl.create("柱状图背景").chart().sliceColumn()
|
|
359
|
+
.x(from("销售原表").dim("地区"))
|
|
360
|
+
.y(bar(from("销售原表").sum("金额")))
|
|
361
|
+
.titleBackground(WidgetBackground.of().color("").build())
|
|
362
|
+
.widgetBackground(WidgetBackground.of().style(WidgetBackground.STYLE2).build())
|
|
363
|
+
.build();
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
自定义组件颜色背景可替换为
|
|
367
|
+
`.widgetBackground(WidgetBackground.of().color("#102040").borderRadius(4).borderSize(1).borderColor("#4488cc").build())`。
|
|
368
|
+
通过现有 `widget add/update --file` 保存,`widget dsl` 回读;仪表板全局背景使用 `TemplateStyle`。
|
|
369
|
+
能力范围参考:[表格背景](https://help.fanruan.com/finebi/doc-view-988.html)。
|
|
370
|
+
|
|
371
|
+
## 5. SeriesModel
|
|
372
|
+
|
|
373
|
+
`line(measureRef)` 和 `bar(measureRef)` 返回 `SeriesModel`,可以继续定义系列级视觉通道。
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
line(from("销售表").sum("销售额"))
|
|
377
|
+
bar(from("销售表").sum("销售额"))
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
| DSL API | 业务含义 |
|
|
381
|
+
|--------------------------------------|-----------------|
|
|
382
|
+
| `line(measureRef)` | 构造折线系列。 |
|
|
383
|
+
| `bar(measureRef)` | 构造柱形系列。 |
|
|
384
|
+
| `.color(dimRef / measureRef)` | 系列级着色。 |
|
|
385
|
+
| `.size(measureRef)` | 系列级大小控制。 |
|
|
386
|
+
| `.label(dimRef / measureRef)` | 系列标签内容。 |
|
|
387
|
+
| `.tooltipField(dimRef / measureRef)` | 系列悬浮信息补充字段。 |
|
|
388
|
+
| `.granularity(dimRef / measureRef)` | 系列细粒度字段。 |
|
|
389
|
+
| `.lineConnect(dimRef / measureRef)` | 折线系列的连线维度或连线指标。 |
|
|
390
|
+
| `.sizeOptions(SizeOptions...)` | 绘图 Series 大小外观,仅支持普通折线、柱形和 BOX;适用范围与继承规则见 [§4.6.1](#461-图形大小)。 |
|
|
391
|
+
| `.lineOptions(LineOptions...)` | 普通折线的连线样式;也可在图表级设置。 |
|
|
392
|
+
|
|
393
|
+
流向地图使用图表级 `.chart().lineMap().lineConnect(measureRef)`,连线属性仅接受指标。
|
|
394
|
+
`LINE_MAP` 的 basic 模板使用 `.lineConnect(from(tableName).sum(routeField))`,对连线字段求和。
|
|
395
|
+
|
|
396
|
+
### 5.1 普通折线连线样式
|
|
397
|
+
|
|
398
|
+
仅 `partitionLine/seriesLine` 支持图表级和绘图指标级 `lineOptions`:
|
|
399
|
+
|
|
400
|
+
| API | 取值 / 语义 |
|
|
401
|
+
|---|---|
|
|
402
|
+
| `LineOptions.normal()/curve()/step()` | 折线 / 曲线 / 阶梯线 |
|
|
403
|
+
| `LineOptions.defaults()` | 不指定属性,用于只覆盖单项;不重置已有配置 |
|
|
404
|
+
| `.borderStyle(value)` | `solid` / `dashed` |
|
|
405
|
+
| `.marker(value)` | `true` / `false`,显示/隐藏标记点 |
|
|
406
|
+
| `.symbol(value)` | `circle/rect/triangle/circle_hollow/square_hollow/triangle_hollow` |
|
|
407
|
+
| `.nullPattern(value)` | `continuous/disconnect/zero`,空值连续/断开/置零 |
|
|
408
|
+
|
|
409
|
+
单指标只覆盖已声明属性,其他属性继承图表配置;显式 `false` 可以覆盖 `true`。
|
|
410
|
+
未配置时保持默认折线、实线、无标记点、空值连续。雷达使用 `RadarOptions`,不能与 `LineOptions` 混用。
|
|
411
|
+
面积、自定义组合图、流向地图和无对应绘图几何的非绘图轴指标不接受此配置。
|
|
412
|
+
示例见 [普通折线连线样式](widget-dsl-api-examples.md#普通折线连线样式)。
|
|
413
|
+
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {cp, mkdir, readdir, readFile, stat, writeFile} from "node:fs/promises";
|
|
2
|
+
import {dirname, resolve} from "node:path";
|
|
3
|
+
import {fileURLToPath} from "node:url";
|
|
4
|
+
|
|
5
|
+
import {cliError} from "./errors.js";
|
|
6
|
+
import {failure, success} from "./output.js";
|
|
7
|
+
|
|
8
|
+
const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", "web-app");
|
|
9
|
+
const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
10
|
+
const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
|
|
11
|
+
|
|
12
|
+
export async function handleApp(args, context = {}) {
|
|
13
|
+
const action = args[0];
|
|
14
|
+
if (action === "create") return createApp(args.slice(1), context);
|
|
15
|
+
if (action === "inspect") return inspectApp(args.slice(1));
|
|
16
|
+
return invalid("app", "App command is required.", ["finebi-cli app create --name <name> --path <parent>", "finebi-cli app inspect --path <project>"]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function createApp(args, context = {}) {
|
|
20
|
+
const options = parseOptions(args, "app.create");
|
|
21
|
+
if (!options.ok) return options.error;
|
|
22
|
+
const {name, path: parentPath} = options.value;
|
|
23
|
+
if (!name || name.length > 214 || !NAME_PATTERN.test(name) || WINDOWS_DEVICE_NAME.test(name)) {
|
|
24
|
+
return invalid("app.create", "name must be a simple lowercase npm name (letters, digits, and single hyphens).", ["finebi-cli app create --name sales-dashboard --path <parent>"]);
|
|
25
|
+
}
|
|
26
|
+
if (!parentPath) {
|
|
27
|
+
return invalid("app.create", "path must be an existing parent directory.");
|
|
28
|
+
}
|
|
29
|
+
const parent = resolve(parentPath);
|
|
30
|
+
const destination = resolve(parent, name);
|
|
31
|
+
if (dirname(destination) !== parent) {
|
|
32
|
+
return invalid("app.create", "name must stay inside the selected parent directory.");
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (!(await stat(parent)).isDirectory()) return invalid("app.create", "path must point to a directory.");
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return invalid("app.create", error.code === "ENOENT" ? `Parent directory does not exist: ${parent}` : `Cannot inspect parent directory: ${error.message}`);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const templateRoot = context.templateRoot ? resolve(context.templateRoot) : TEMPLATE_ROOT;
|
|
41
|
+
const templatePackage = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
|
|
42
|
+
const templateConfigPath = resolve(templateRoot, "public", "app-config.json");
|
|
43
|
+
let config = {schemaVersion: "1", title: name, basePath: "/webroot/decision", layout: "overview", dashboardId: "", maxRows: 1000, pageSize: 8, widgets: [], filters: []};
|
|
44
|
+
try {
|
|
45
|
+
config = JSON.parse(await readFile(templateConfigPath, "utf8"));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT") throw error;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(destination, {recursive: false});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return invalid("app.create", error.code === "EEXIST" ? `Destination already exists: ${destination}` : `Cannot create destination: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
await copyTemplateContents(templateRoot, destination);
|
|
55
|
+
await mkdir(resolve(destination, "public"), {recursive: true});
|
|
56
|
+
await writeFile(resolve(destination, "package.json"), `${JSON.stringify({...templatePackage, name}, null, 2)}\n`, "utf8");
|
|
57
|
+
await writeFile(resolve(destination, "public", "app-config.json"), `${JSON.stringify({...config, templateVersion: templatePackage.version}, null, 2)}\n`, "utf8");
|
|
58
|
+
const lockPath = resolve(destination, "package-lock.json");
|
|
59
|
+
try {
|
|
60
|
+
const lock = JSON.parse(await readFile(lockPath, "utf8"));
|
|
61
|
+
lock.name = name;
|
|
62
|
+
if (lock.packages?.[""]) lock.packages[""].name = name;
|
|
63
|
+
await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error.code !== "ENOENT") throw error;
|
|
66
|
+
}
|
|
67
|
+
return success("app.create", {name, path: destination, templateVersion: templatePackage.version});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return invalid("app.create", `Could not create project: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function copyTemplateContents(sourceRoot, destinationRoot, relativeDirectory = "") {
|
|
74
|
+
const sourceDirectory = resolve(sourceRoot, relativeDirectory);
|
|
75
|
+
const destinationDirectory = resolve(destinationRoot, relativeDirectory);
|
|
76
|
+
for (const entry of (await readdir(sourceDirectory, {withFileTypes: true})).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
|
|
77
|
+
const relativePath = `${relativeDirectory}/${entry.name}`.replace(/^\//u, "");
|
|
78
|
+
if (!templatePathAllowed(relativePath)) continue;
|
|
79
|
+
const sourcePath = resolve(sourceDirectory, entry.name);
|
|
80
|
+
const destinationPath = resolve(destinationDirectory, entry.name);
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
await mkdir(destinationPath, {recursive: false});
|
|
83
|
+
await copyTemplateContents(sourceRoot, destinationRoot, relativePath);
|
|
84
|
+
} else if (entry.isFile()) {
|
|
85
|
+
await cp(sourcePath, destinationPath, {force: false, errorOnExist: true});
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error(`Unsupported template entry: ${relativePath}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function templatePathAllowed(relative) {
|
|
93
|
+
const relativePath = relative.replaceAll("\\", "/");
|
|
94
|
+
const parts = relativePath.split("/").filter(Boolean);
|
|
95
|
+
const basename = parts.at(-1) || "";
|
|
96
|
+
return !parts.some(part => ["node_modules", "dist", "temp", ".git"].includes(part))
|
|
97
|
+
&& basename !== ".env"
|
|
98
|
+
&& (!basename.startsWith(".env.") || basename === ".env.example");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function inspectApp(args) {
|
|
102
|
+
const options = parseOptions(args, "app.inspect");
|
|
103
|
+
if (!options.ok) return options.error;
|
|
104
|
+
const projectPath = options.value.path;
|
|
105
|
+
if (!projectPath) return invalid("app.inspect", "path must be a project directory.");
|
|
106
|
+
const project = resolve(projectPath);
|
|
107
|
+
try {
|
|
108
|
+
if (!(await stat(project)).isDirectory()) return invalid("app.inspect", "path must point to a project directory.");
|
|
109
|
+
const configPath = resolve(project, "public", "app-config.json");
|
|
110
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
111
|
+
const errors = await validateConfig(config);
|
|
112
|
+
if (errors.length) return failure("app.inspect", cliError("INVALID_ARGUMENT", {path: "public/app-config.json", message: errors.join(" ")}));
|
|
113
|
+
return success("app.inspect", {
|
|
114
|
+
path: project,
|
|
115
|
+
dashboardId: config.dashboardId,
|
|
116
|
+
widgets: config.widgets.length,
|
|
117
|
+
templateVersion: config.templateVersion || null,
|
|
118
|
+
remoteValidated: false,
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return invalid("app.inspect", `Cannot read project configuration: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function validateConfig(config) {
|
|
126
|
+
const errors = [];
|
|
127
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return ["configuration must be a JSON object."];
|
|
128
|
+
const allowedConfigFields = new Set(["schemaVersion", "title", "basePath", "layout", "dashboardId", "maxRows", "pageSize", "widgets", "filters", "templateVersion"]);
|
|
129
|
+
for (const field of Object.keys(config)) if (!allowedConfigFields.has(field)) errors.push(`unsupported config field: ${field}.`);
|
|
130
|
+
if (config.schemaVersion !== "1") errors.push("schemaVersion must be \"1\".");
|
|
131
|
+
if (!nonEmptyString(config.title)) errors.push("title is required.");
|
|
132
|
+
if (!nonEmptyString(config.basePath) || !/^\/(?!\/)[\w/-]+$/.test(config.basePath)) errors.push("basePath must be a same-origin path.");
|
|
133
|
+
if (!["overview", "workbench"].includes(config.layout)) errors.push("layout must be overview or workbench.");
|
|
134
|
+
if (!Number.isInteger(config.maxRows) || config.maxRows < 1 || config.maxRows > 10000) errors.push("maxRows must be an integer from 1 to 10000.");
|
|
135
|
+
if (!Number.isInteger(config.pageSize) || config.pageSize < 1 || config.pageSize > 100) errors.push("pageSize must be an integer from 1 to 100.");
|
|
136
|
+
if (typeof config.dashboardId !== "string" || !config.dashboardId.trim()) {
|
|
137
|
+
if (Array.isArray(config.widgets) && config.widgets.length === 0) return ["project is not bound: dashboardId and widgets must be configured."];
|
|
138
|
+
errors.push("dashboardId is required.");
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(config.widgets) || config.widgets.length === 0) {
|
|
141
|
+
errors.push("widgets must be a non-empty array.");
|
|
142
|
+
return errors;
|
|
143
|
+
}
|
|
144
|
+
for (const [index, widget] of config.widgets.entries()) {
|
|
145
|
+
if (!widget || typeof widget !== "object") {
|
|
146
|
+
errors.push(`widgets[${index}] must be an object.`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const allowedWidgetFields = new Set(["entryId", "name", "type", "datasetId", "categoryFieldId", "valueFieldId"]);
|
|
150
|
+
for (const field of Object.keys(widget)) if (!allowedWidgetFields.has(field)) errors.push(`widgets[${index}].${field} is unsupported.`);
|
|
151
|
+
for (const field of ["entryId", "name", "type"]) {
|
|
152
|
+
if (typeof widget[field] !== "string" || !widget[field].trim()) errors.push(`widgets[${index}].${field} is required.`);
|
|
153
|
+
}
|
|
154
|
+
if (!["table", "bar", "line", "pie", "kpi"].includes(widget.type)) errors.push(`widgets[${index}].type is unsupported.`);
|
|
155
|
+
if (widget.type && widget.type !== "table" && !nonEmptyString(widget.valueFieldId)) errors.push(`widgets[${index}].valueFieldId is required for ${widget.type}.`);
|
|
156
|
+
if (widget.type && !["table", "kpi"].includes(widget.type) && !nonEmptyString(widget.categoryFieldId)) errors.push(`widgets[${index}].categoryFieldId is required for ${widget.type}.`);
|
|
157
|
+
for (const field of ["datasetId", "categoryFieldId", "valueFieldId"]) {
|
|
158
|
+
if (widget[field] !== undefined && !nonEmptyString(widget[field])) errors.push(`widgets[${index}].${field} must be a non-empty string.`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const {validateFilterBindings} = await import("../templates/web-app/src/finebi/filters.js");
|
|
163
|
+
validateFilterBindings(config.filters);
|
|
164
|
+
} catch (error) {errors.push(error.message);}
|
|
165
|
+
return errors;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function nonEmptyString(value) {
|
|
169
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseOptions(args, command) {
|
|
173
|
+
const value = {};
|
|
174
|
+
for (let index = 0; index < args.length; index++) {
|
|
175
|
+
const option = args[index];
|
|
176
|
+
if (option === "--json") continue;
|
|
177
|
+
if (option !== "--name" && option !== "--path") return {ok: false, error: invalid(command, `Unsupported option: ${option}`)};
|
|
178
|
+
const key = option.slice(2);
|
|
179
|
+
const next = args[++index];
|
|
180
|
+
if (!next || next.startsWith("--")) return {ok: false, error: invalid(command, `${option} requires a value.`)};
|
|
181
|
+
if (value[key]) return {ok: false, error: invalid(command, `${option} must only be supplied once.`)};
|
|
182
|
+
value[key] = next;
|
|
183
|
+
}
|
|
184
|
+
return {ok: true, value};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function invalid(command, message, nextActions = []) {
|
|
188
|
+
return failure(command, cliError("INVALID_ARGUMENT", {path: command, message, nextActions}));
|
|
189
|
+
}
|