@bluemarble/bi 0.8.0 → 0.8.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.
package/README.md CHANGED
@@ -1,77 +1,77 @@
1
- # bm-bi (`@bluemarble/bi`)
2
-
3
- Camada semântica para dashboards em React, no lugar do Power BI: o modelo (tabelas,
4
- relacionamentos, dimensões e medidas) é um objeto TS (`defineModel`), o servidor compila as
5
- consultas para SQL (MySQL/MariaDB) e os componentes de filtro e gráfico consultam o modelo pelo
6
- nome. Vem com um devtools (diagrama do modelo, editor de layout, playground, regressão).
7
-
8
- ```ts
9
- import { defineModel, formula, sum } from '@bluemarble/bi/server'
10
-
11
- export const vendas = defineModel({
12
- name: 'vendas',
13
- tables: {
14
- pedidos: { key: 'id', sql: 'SELECT id, loja, criado_em FROM pedidos' },
15
- itens: { sql: 'SELECT pedido_id, valor FROM itens' }
16
- },
17
- relationships: [{ from: 'itens.pedido_id', to: 'pedidos.id' }],
18
- dimensions: { loja: { sql: 'pedidos.loja', type: 'string' } },
19
- measures: { valor: sum('itens.valor', { format: 'currency' }) }
20
- })
21
- ```
22
-
23
- | Import | Onde roda |
24
- | ----------------------- | -------------- |
25
- | `@bluemarble/bi/server` | só no servidor |
26
- | `@bluemarble/bi/client` | browser |
27
- | `@bluemarble/bi/types` | ambos |
28
-
29
- ```bash
30
- npm install @bluemarble/bi # peer: react 19, @mui/material 7, recharts 3
31
- ```
32
-
33
- Publicado no npm já compilado (`dist/`, ESM + `.d.ts`), sem precisar de `transpilePackages`.
34
- Conceitos, API, devtools e configuração do app estão em
35
- [`docs/dashboard-substituicao-powerbi.md`](docs/dashboard-substituicao-powerbi.md).
36
-
37
- ## Desenvolvimento
38
-
39
- Projeto Vite+ com bun.
40
-
41
- ```bash
42
- vp install # dependências
43
- vp test # testes (vitest, sem banco)
44
- vp run typecheck # tsc
45
- vp check # formatação (oxfmt) + lint (oxlint)
46
- vp run playground # dashboard + devtools em http://localhost:5180 (precisa do .env)
47
- ```
48
-
49
- ### Playground
50
-
51
- `playground/` é uma página Vite com o dashboard e o devtools ligados direto ao `src/` (sem passar
52
- pelo app). O endpoint `/api/dashboard/query` roda no próprio servidor do Vite e consulta o MySQL do
53
- `DATABASE_URL` do `.env` na raiz (modelo em `.env.example`; o `.env` não é versionado).
54
-
55
- - `playground/models/` — modelos (`defineModel`), registrados em `models/index.ts`;
56
- - `playground/layouts/` — layouts JSON dos gráficos/filtros;
57
- - `playground/dashboards.ts` — lista do seletor no topo (modelo + layout + filtros iniciais) e formatters.
58
-
59
- O "Salvar no arquivo" do devtools grava nessas pastas (formatando com `vp fmt`) e os snapshots vão
60
- para `playground/snapshots/`. Mudanças em modelos valem na consulta seguinte, sem reiniciar.
61
-
62
- ### Testar no app antes de publicar
63
-
64
- Para usar a versão local num app (ex.: o `custo` do `rocha-pr`), aponte a dependência para esta
65
- pasta (`"@bluemarble/bi": "file:../../../bm-bi"` + `npm install`) e deixe `vp run dev` rodando
66
- (`vp pack --watch`): o app lê o `dist/`. Com o link, o Next precisa de `turbopack.root` e
67
- `turbopack.resolveAlias` — o `next.config.js` do `custo` liga isso sozinho quando detecta o link.
68
-
69
- ## Publicar
70
-
71
- ```bash
72
- vp run verify # o mesmo que o CI roda: check, typecheck, testes e build
73
- vp run release # bumpp: escolhe a versão, commita, cria a tag vX.Y.Z e faz push
74
- ```
75
-
76
- A tag dispara `.github/workflows/publish.yml`, que confere tag x `package.json`, roda o `verify` e
77
- publica com `bun publish` (secret `NPM_TOKEN`). Depois, no app: `npm install @bluemarble/bi@X.Y.Z`.
1
+ # bm-bi (`@bluemarble/bi`)
2
+
3
+ Camada semântica para dashboards em React, no lugar do Power BI: o modelo (tabelas,
4
+ relacionamentos, dimensões e medidas) é um objeto TS (`defineModel`), o servidor compila as
5
+ consultas para SQL (MySQL/MariaDB) e os componentes de filtro e gráfico consultam o modelo pelo
6
+ nome. Vem com um devtools (diagrama do modelo, editor de layout, playground, regressão).
7
+
8
+ ```ts
9
+ import { defineModel, formula, sum } from '@bluemarble/bi/server'
10
+
11
+ export const vendas = defineModel({
12
+ name: 'vendas',
13
+ tables: {
14
+ pedidos: { key: 'id', sql: 'SELECT id, loja, criado_em FROM pedidos' },
15
+ itens: { sql: 'SELECT pedido_id, valor FROM itens' }
16
+ },
17
+ relationships: [{ from: 'itens.pedido_id', to: 'pedidos.id' }],
18
+ dimensions: { loja: { sql: 'pedidos.loja', type: 'string' } },
19
+ measures: { valor: sum('itens.valor', { format: 'currency' }) }
20
+ })
21
+ ```
22
+
23
+ | Import | Onde roda |
24
+ | ----------------------- | -------------- |
25
+ | `@bluemarble/bi/server` | só no servidor |
26
+ | `@bluemarble/bi/client` | browser |
27
+ | `@bluemarble/bi/types` | ambos |
28
+
29
+ ```bash
30
+ npm install @bluemarble/bi # peer: react 19, @mui/material 7, recharts 3
31
+ ```
32
+
33
+ Publicado no npm já compilado (`dist/`, ESM + `.d.ts`), sem precisar de `transpilePackages`.
34
+ Conceitos, API, devtools e configuração do app estão em
35
+ [`docs/dashboard-substituicao-powerbi.md`](docs/dashboard-substituicao-powerbi.md).
36
+
37
+ ## Desenvolvimento
38
+
39
+ Projeto Vite+ com bun.
40
+
41
+ ```bash
42
+ vp install # dependências
43
+ vp test # testes (vitest, sem banco)
44
+ vp run typecheck # tsc
45
+ vp check # formatação (oxfmt) + lint (oxlint)
46
+ vp run playground # dashboard + devtools em http://localhost:5180 (precisa do .env)
47
+ ```
48
+
49
+ ### Playground
50
+
51
+ `playground/` é uma página Vite com o dashboard e o devtools ligados direto ao `src/` (sem passar
52
+ pelo app). O endpoint `/api/dashboard/query` roda no próprio servidor do Vite e consulta o MySQL do
53
+ `DATABASE_URL` do `.env` na raiz (modelo em `.env.example`; o `.env` não é versionado).
54
+
55
+ - `playground/models/` — modelos (`defineModel`), registrados em `models/index.ts`;
56
+ - `playground/layouts/` — layouts JSON dos gráficos/filtros;
57
+ - `playground/dashboards.ts` — lista do seletor no topo (modelo + layout + filtros iniciais) e formatters.
58
+
59
+ O "Salvar no arquivo" do devtools grava nessas pastas (formatando com `vp fmt`) e os snapshots vão
60
+ para `playground/snapshots/`. Mudanças em modelos valem na consulta seguinte, sem reiniciar.
61
+
62
+ ### Testar no app antes de publicar
63
+
64
+ Para usar a versão local num app (ex.: o `custo` do `rocha-pr`), aponte a dependência para esta
65
+ pasta (`"@bluemarble/bi": "file:../../../bm-bi"` + `npm install`) e deixe `vp run dev` rodando
66
+ (`vp pack --watch`): o app lê o `dist/`. Com o link, o Next precisa de `turbopack.root` e
67
+ `turbopack.resolveAlias` — o `next.config.js` do `custo` liga isso sozinho quando detecta o link.
68
+
69
+ ## Publicar
70
+
71
+ ```bash
72
+ vp run verify # o mesmo que o CI roda: check, typecheck, testes e build
73
+ vp run release # bumpp: escolhe a versão, commita, cria a tag vX.Y.Z e faz push
74
+ ```
75
+
76
+ A tag dispara `.github/workflows/publish.yml`, que confere tag x `package.json`, roda o `verify` e
77
+ publica com `bun publish` (secret `NPM_TOKEN`). Depois, no app: `npm install @bluemarble/bi@X.Y.Z`.
@@ -1,7 +1,7 @@
1
1
  import { c as VALUE_FORMATS, f as validateModel, g as parseColumnRef, h as modelEdges, m as measureHome, o as AGGREGATIONS, s as AGGREGATION_LABEL, t as canonicalJson, u as isAggregate, v as referencedTables } from "./codegen-LoQzFAle.js";
2
2
  import { n as diffRows } from "./compare-DloZhg1T.js";
3
- import { C as DEFAULT_SCALE_MIN_COLOR, D as UNARY_OPERATORS, E as RULE_OPERATOR_LABEL, O as VALUE_RULE_OPERATORS, S as COLUMN_VERTICAL_ALIGNS, T as RULE_OPERATORS, a as FILTER_FIELDS, b as COLOR_SCALE_TARGETS, c as chartColumns, f as filterReferences, g as percentColumn, h as layoutModelWarnings, i as CARD_FIELDS, l as chartOfType, n as CANVAS_PRESETS, o as cardsGap, r as CARDS_FIELDS, t as BUILTIN_CHART_TYPES, u as chartReferences, v as tableColumns, w as NUMERIC_OPERATORS, x as COLUMN_ALIGNS, y as validateLayout } from "./layout-BS3nO6OT.js";
4
- import { C as useDashboard, D as parseLayoutItemKey, E as layoutItemKey, T as LAYOUT_ITEM_ATTRIBUTE, g as SERIES_COLORS, n as INSPECT_ATTRIBUTE, s as formatMeasure, t as ModelDictionary, w as errorMessage } from "./ModelDictionary-DZiYK1mV.js";
3
+ import { C as COLUMN_VERTICAL_ALIGNS, D as RULE_OPERATOR_LABEL, E as RULE_OPERATORS, O as UNARY_OPERATORS, S as COLUMN_ALIGNS, T as NUMERIC_OPERATORS, _ as percentColumn, a as FILTER_FIELDS, b as validateLayout, d as chartReferences, g as layoutModelWarnings, i as CARD_FIELDS, k as VALUE_RULE_OPERATORS, l as chartColumns, n as CANVAS_PRESETS, o as PARAMETER_FIELDS, p as filterReferences, r as CARDS_FIELDS, s as cardsGap, t as BUILTIN_CHART_TYPES, u as chartOfType, w as DEFAULT_SCALE_MIN_COLOR, x as COLOR_SCALE_TARGETS, y as tableColumns } from "./layout-BuZhfoRI.js";
4
+ import { C as useDashboard, D as parseLayoutItemKey, E as layoutItemKey, T as LAYOUT_ITEM_ATTRIBUTE, g as SERIES_COLORS, n as INSPECT_ATTRIBUTE, s as formatMeasure, t as ModelDictionary, w as errorMessage } from "./ModelDictionary-g5d84D0n.js";
5
5
  import { Alert, Autocomplete, Box, Button, Checkbox, Chip, CircularProgress, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControlLabel, IconButton, List, ListItemButton, ListSubheader, Menu, MenuItem, MenuList, Paper, Popover, Popper, Portal, Switch, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel, Tabs, TextField, ThemeProvider, ToggleButton, ToggleButtonGroup, Tooltip, Typography, createTheme } from "@mui/material";
6
6
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1161,8 +1161,10 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1161
1161
  ] });
1162
1162
  const charts = editing.charts;
1163
1163
  const filters = editing.filters ?? [];
1164
+ const parameters = editing.parameters ?? [];
1164
1165
  const selectedFilter = selection?.kind === "filter" ? filters.find((filter) => filter.dimension === selection.key) ?? null : null;
1165
- const selected = selection?.kind === "filter" || selection?.kind === "canvas" || selection?.kind === "cards" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
1166
+ const selectedParameter = selection?.kind === "parameter" ? parameters.find((parameter) => parameter.name === selection.key) ?? null : null;
1167
+ const selected = selection?.kind === "filter" || selection?.kind === "parameter" || selection?.kind === "canvas" || selection?.kind === "cards" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
1166
1168
  const layoutKey = entry.registered.id;
1167
1169
  const differsFromFile = canonicalJson(editing) !== canonicalJson(entry.registered);
1168
1170
  const dirty = canonicalJson(editing) !== referenceKey;
@@ -1183,6 +1185,8 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1183
1185
  filters: next
1184
1186
  });
1185
1187
  const patchFilter = (dimension, change) => setFilters(filters.map((filter) => filter.dimension === dimension ? change(filter) : filter));
1188
+ const setParameters = (next) => update(withField(editing, "parameters", next.length ? next : void 0));
1189
+ const patchParameter = (name, change) => setParameters(parameters.map((parameter) => parameter.name === name ? change(parameter) : parameter));
1186
1190
  const toggleSidebar = () => {
1187
1191
  const next = !sidebar;
1188
1192
  setSidebar(next);
@@ -1210,8 +1214,11 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1210
1214
  },
1211
1215
  onDrop: () => {
1212
1216
  if (drag?.kind !== kind) return;
1213
- if (drag.to !== drag.from && drag.to !== drag.from + 1) if (kind === "chart") setCharts(moveTo(charts, drag.from, drag.to));
1214
- else setFilters(moveTo(filters, drag.from, drag.to));
1217
+ if (drag.to !== drag.from && drag.to !== drag.from + 1) {
1218
+ if (kind === "chart") setCharts(moveTo(charts, drag.from, drag.to));
1219
+ else if (kind === "parameter") setParameters(moveTo(parameters, drag.from, drag.to));
1220
+ else setFilters(moveTo(filters, drag.from, drag.to));
1221
+ }
1215
1222
  setDrag(null);
1216
1223
  },
1217
1224
  onDragEnd: () => setDrag(null)
@@ -1226,6 +1233,21 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1226
1233
  key: dimension
1227
1234
  });
1228
1235
  };
1236
+ const addParameter = () => {
1237
+ const measure = Object.keys(model.measures)[0];
1238
+ if (!measure) return;
1239
+ const used = new Set(parameters.map((parameter) => parameter.name));
1240
+ let name = "medida";
1241
+ for (let i = 2; used.has(name); i++) name = `medida-${i}`;
1242
+ setParameters([...parameters, {
1243
+ name,
1244
+ measures: [measure]
1245
+ }]);
1246
+ setSelection({
1247
+ kind: "parameter",
1248
+ key: name
1249
+ });
1250
+ };
1229
1251
  const focus = (item) => {
1230
1252
  setSelection(item);
1231
1253
  if (stacked) setSidebar(false);
@@ -1236,7 +1258,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1236
1258
  };
1237
1259
  const stacked = rootWidth < STACK_WIDTH;
1238
1260
  const formColumns = editorWidth < 420 ? 1 : editorWidth < 720 ? 2 : 4;
1239
- const selectedTitle = selection?.kind === "canvas" ? "Tela" : selection?.kind === "cards" ? "Cards" : selectedFilter ? selectedFilter.label || model.dimensions[selectedFilter.dimension]?.label || selectedFilter.dimension : selected ? selected.title || selected.id : "";
1261
+ const selectedTitle = selection?.kind === "canvas" ? "Tela" : selection?.kind === "cards" ? "Cards" : selectedFilter ? selectedFilter.label || model.dimensions[selectedFilter.dimension]?.label || selectedFilter.dimension : selectedParameter ? selectedParameter.label || selectedParameter.name : selected ? selected.title || selected.id : "";
1240
1262
  const hover = (item) => onHover(item && itemQueryId(layoutKey, item));
1241
1263
  const add = (type, base) => {
1242
1264
  setAddMenu(null);
@@ -1402,6 +1424,38 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1402
1424
  }, filter.dimension);
1403
1425
  })]
1404
1426
  }),
1427
+ /* @__PURE__ */ jsxs(Section, {
1428
+ title: `Parâmetros (${parameters.length})`,
1429
+ collapseKey: "layout:lista:parametros",
1430
+ actions: /* @__PURE__ */ jsx(Button, {
1431
+ size: "small",
1432
+ onClick: addParameter,
1433
+ disabled: !Object.keys(model.measures).length,
1434
+ children: "+ Novo"
1435
+ }),
1436
+ children: [!parameters.length && /* @__PURE__ */ jsx(Typography, {
1437
+ sx: {
1438
+ fontSize: 12,
1439
+ color: "text.disabled",
1440
+ mb: 1
1441
+ },
1442
+ children: "Nenhum parâmetro de medida."
1443
+ }), parameters.map((parameter, index) => {
1444
+ const item = {
1445
+ kind: "parameter",
1446
+ key: parameter.name
1447
+ };
1448
+ return /* @__PURE__ */ jsx(ItemRow, {
1449
+ title: parameter.label || "Medida",
1450
+ subtitle: `${parameter.name} · ${parameter.measures.length} medida(s)`,
1451
+ selected: selectedParameter?.name === parameter.name,
1452
+ onSelect: () => focus(item),
1453
+ onHover: (on) => hover(on ? item : null),
1454
+ ...dragProps("parameter", index, parameters.length),
1455
+ onRemove: () => setParameters(parameters.filter((other) => other.name !== parameter.name))
1456
+ }, parameter.name);
1457
+ })]
1458
+ }),
1405
1459
  /* @__PURE__ */ jsx(Section, {
1406
1460
  title: `Gráficos (${charts.length})`,
1407
1461
  collapseKey: "layout:lista:graficos",
@@ -1501,6 +1555,10 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1501
1555
  filter: selectedFilter,
1502
1556
  model,
1503
1557
  formatters: entry.parts.filters ?? [],
1558
+ charts: editing.charts.map((chart) => ({
1559
+ name: chart.id,
1560
+ label: chart.title || chart.id
1561
+ })),
1504
1562
  columns: formColumns,
1505
1563
  onChange: (change) => {
1506
1564
  patchFilter(selectedFilter.dimension, change);
@@ -1509,12 +1567,27 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1509
1567
  key: change(selectedFilter).dimension
1510
1568
  });
1511
1569
  }
1512
- }, `filter:${selectedFilter.dimension}`) : selected ? /* @__PURE__ */ jsx(ChartForm, {
1570
+ }, `filter:${selectedFilter.dimension}`) : selectedParameter ? /* @__PURE__ */ jsx(ParameterForm, {
1571
+ parameter: selectedParameter,
1572
+ model,
1573
+ columns: formColumns,
1574
+ onChange: (change) => {
1575
+ patchParameter(selectedParameter.name, change);
1576
+ setSelection({
1577
+ kind: "parameter",
1578
+ key: change(selectedParameter).name
1579
+ });
1580
+ }
1581
+ }, `parameter:${selectedParameter.name}`) : selected ? /* @__PURE__ */ jsx(ChartForm, {
1513
1582
  chart: selected,
1514
1583
  cards: editing.cards,
1515
1584
  types: chartTypes,
1516
1585
  model,
1517
1586
  formatters: entry.parts.charts ?? [],
1587
+ measureParameters: parameters.map((parameter) => ({
1588
+ name: parameter.name,
1589
+ label: parameter.label || "Medida"
1590
+ })),
1518
1591
  columns: formColumns,
1519
1592
  onChange: (change) => {
1520
1593
  patchChart(selected.id, change);
@@ -1820,7 +1893,7 @@ function SmallButton({ title, disabled, onClick, children }) {
1820
1893
  }) })
1821
1894
  });
1822
1895
  }
1823
- function ChartForm({ chart, cards, types, model, formatters, columns, onChange }) {
1896
+ function ChartForm({ chart, cards, types, model, formatters, measureParameters, columns, onChange }) {
1824
1897
  const set = (key, value) => onChange((current) => withField(current, key, value));
1825
1898
  const setMany = (changes) => onChange((current) => Object.entries(changes).reduce((chart, [key, value]) => withField(chart, key, value), current));
1826
1899
  const setCard = (key, value) => onChange((current) => withField(current, "card", nonEmpty(withField(current.card ?? {}, key, value))));
@@ -1831,6 +1904,7 @@ function ChartForm({ chart, cards, types, model, formatters, columns, onChange }
1831
1904
  onSetMany: setMany,
1832
1905
  model,
1833
1906
  formatters,
1907
+ measureParameters,
1834
1908
  columns: chartColumns(chart, types),
1835
1909
  formColumns: columns,
1836
1910
  chosenDimensions: chartReferences(chart, types).dimensions,
@@ -1866,23 +1940,42 @@ function ChartForm({ chart, cards, types, model, formatters, columns, onChange }
1866
1940
  formatters: [],
1867
1941
  columns: [],
1868
1942
  formColumns: columns,
1869
- chosenDimensions: []
1943
+ chosenDimensions: [],
1944
+ sectionActions: { Card: /* @__PURE__ */ jsx(Button, {
1945
+ size: "small",
1946
+ disabled: !chart.card || !Object.keys(chart.card).length,
1947
+ onClick: () => onChange((current) => withField(current, "card", void 0)),
1948
+ children: "Usar valor padrão"
1949
+ }) }
1870
1950
  })] });
1871
1951
  }
1872
- function FilterForm({ filter, model, formatters, columns, onChange }) {
1952
+ function FilterForm({ filter, model, formatters, charts, columns, onChange }) {
1873
1953
  return /* @__PURE__ */ jsx(FieldsForm, {
1874
1954
  fields: FILTER_FIELDS,
1875
1955
  values: filter,
1876
1956
  onSet: (key, value) => onChange((current) => withField(current, key, value)),
1877
1957
  model,
1878
1958
  formatters,
1959
+ charts,
1879
1960
  columns: [],
1880
1961
  formColumns: columns,
1881
1962
  chosenDimensions: [filter.dimension]
1882
1963
  });
1883
1964
  }
1965
+ function ParameterForm({ parameter, model, columns, onChange }) {
1966
+ return /* @__PURE__ */ jsx(FieldsForm, {
1967
+ fields: PARAMETER_FIELDS,
1968
+ values: parameter,
1969
+ onSet: (key, value) => onChange((current) => withField(current, key, value)),
1970
+ model,
1971
+ formatters: [],
1972
+ columns: parameter.measures,
1973
+ formColumns: columns,
1974
+ chosenDimensions: []
1975
+ });
1976
+ }
1884
1977
  /** Formulário gerado dos `FieldSpec` (campos do tipo de gráfico ou do filtro). */
1885
- function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters, columns, formColumns, chosenDimensions, chosenMeasures = [], header }) {
1978
+ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters, columns, formColumns, chosenDimensions, chosenMeasures = [], charts = [], measureParameters = [], sectionActions, header }) {
1886
1979
  const dimensions = Object.entries(model.dimensions).map(([name, dim]) => ({
1887
1980
  name,
1888
1981
  label: dim.label
@@ -2077,6 +2170,9 @@ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters,
2077
2170
  case "dimensions": return dimensions;
2078
2171
  case "measure":
2079
2172
  case "measures": return measures;
2173
+ case "filters": return dimensions;
2174
+ case "charts": return charts;
2175
+ case "parameter": return measureParameters;
2080
2176
  case "formatter": return formatters.map((name) => ({ name }));
2081
2177
  case "column": return spec.outside ? fieldOptions : columnOptionsOf(columns);
2082
2178
  case "enum": return spec.values.map((name) => ({
@@ -2165,6 +2261,28 @@ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters,
2165
2261
  }
2166
2262
  });
2167
2263
  }
2264
+ case "filters": return /* @__PURE__ */ jsx(ColumnList, {
2265
+ items: (value ?? []).map((name) => ({
2266
+ kind: "dimension",
2267
+ name
2268
+ })),
2269
+ options: { dimension: options(spec) },
2270
+ onChange: (next) => {
2271
+ const list = next.map((item) => item.name);
2272
+ set(key, list.length ? list : void 0);
2273
+ }
2274
+ });
2275
+ case "charts": return /* @__PURE__ */ jsx(ColumnList, {
2276
+ items: (value ?? []).map((name) => ({
2277
+ kind: "chart",
2278
+ name
2279
+ })),
2280
+ options: { chart: options(spec) },
2281
+ onChange: (next) => {
2282
+ const list = next.map((item) => item.name);
2283
+ set(key, list.length ? list : void 0);
2284
+ }
2285
+ });
2168
2286
  case "valueRule": return /* @__PURE__ */ jsx(ValueRuleInput, {
2169
2287
  rule: value,
2170
2288
  label: spec.label,
@@ -2259,13 +2377,14 @@ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters,
2259
2377
  ...pairedFormatters
2260
2378
  ].filter(Boolean));
2261
2379
  const sections = [...new Set(entries.filter(([key]) => !hidden.has(key)).map(([, spec]) => spec.section))];
2262
- const fullRow = (spec) => spec.kind === "dimensions" || spec.kind === "measures" || spec.kind === "formatters" || spec.kind === "formatRules" || spec.kind === "colorScales" || spec.kind === "colors" || spec.kind === "valueRule";
2380
+ const fullRow = (spec) => spec.kind === "dimensions" || spec.kind === "measures" || spec.kind === "filters" || spec.kind === "charts" || spec.kind === "formatters" || spec.kind === "formatRules" || spec.kind === "colorScales" || spec.kind === "colors" || spec.kind === "valueRule";
2263
2381
  const gridColumn = (spec, key) => fullRow(spec) ? "1 / -1" : key === "title" && formColumns > 1 ? "span 2" : void 0;
2264
2382
  return /* @__PURE__ */ jsx(Fragment, { children: sections.map((section) => {
2265
2383
  const inSection = entries.filter(([, spec]) => spec.section === section);
2266
2384
  return /* @__PURE__ */ jsx(Section, {
2267
2385
  title: section,
2268
2386
  collapseKey: `layout:form:${section}`,
2387
+ actions: sectionActions?.[section],
2269
2388
  children: /* @__PURE__ */ jsxs(Box, {
2270
2389
  sx: formGrid(formColumns),
2271
2390
  children: [section === sections[0] && header, inSection.filter(([key]) => !hidden.has(key)).map(([key, spec]) => /* @__PURE__ */ jsx(Box, {
@@ -3436,6 +3555,12 @@ const KIND_STYLE = {
3436
3555
  color: "#9775fa",
3437
3556
  noun: "% do total",
3438
3557
  group: "% do total (da medida)"
3558
+ },
3559
+ chart: {
3560
+ icon: "▦",
3561
+ color: "#51cf66",
3562
+ noun: "gráfico",
3563
+ group: "Gráficos"
3439
3564
  }
3440
3565
  };
3441
3566
  /**
@@ -4060,7 +4185,7 @@ function Builder({ tables, sql, onClose, onApply }) {
4060
4185
  ] });
4061
4186
  }
4062
4187
  //#endregion
4063
- //#region src/devtools/queryTemplates.ts
4188
+ //#region src/devtools/queryTemplateDefs.ts
4064
4189
  /**
4065
4190
  * `WITH` da origem: a tabela do banco entra pelo nome; o SQL de uma tabela do modelo vira o CTE
4066
4191
  * `origem`, antes dos CTEs do template.
@@ -9329,4 +9454,4 @@ function ToggleButton$1({ draft, loading, problems, inspecting, onOpen }) {
9329
9454
  //#endregion
9330
9455
  export { DashboardDevtools as default };
9331
9456
 
9332
- //# sourceMappingURL=DashboardDevtools-wk4vMfhy.js.map
9457
+ //# sourceMappingURL=DashboardDevtools-TFwjFdqn.js.map