@bluemarble/bi 0.6.1 → 0.6.3

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.
@@ -1,11 +1,46 @@
1
1
  import { a as toModelCode, 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-DkfI6UJr.js";
2
2
  import { n as diffRows } from "./compare-DloZhg1T.js";
3
3
  import { C as RULE_OPERATOR_LABEL, S as RULE_OPERATORS, _ as tableColumns, a as FILTER_FIELDS, b as COLUMN_VERTICAL_ALIGNS, c as chartOfType, d as filterReferences, h as percentColumn, i as CARD_FIELDS, l as chartReferences, m as layoutModelWarnings, n as CANVAS_PRESETS, p as layoutFileContent, r as CARDS_FIELDS, s as chartColumns, t as BUILTIN_CHART_TYPES, v as validateLayout, w as UNARY_OPERATORS, x as NUMERIC_OPERATORS, y as COLUMN_ALIGNS } from "./layout-DAFeHIuP.js";
4
- import { d as useDashboard, f as errorMessage, h as parseLayoutItemKey, m as layoutItemKey, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as LAYOUT_ITEM_ATTRIBUTE, t as ModelDictionary } from "./ModelDictionary-B26oLtPH.js";
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, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel, Tabs, TextField, ThemeProvider, ToggleButton, ToggleButtonGroup, Tooltip, Typography, createTheme } from "@mui/material";
4
+ import { d as useDashboard, f as errorMessage, h as parseLayoutItemKey, m as layoutItemKey, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as LAYOUT_ITEM_ATTRIBUTE, t as ModelDictionary } from "./ModelDictionary-Dc3_LKqr.js";
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";
8
8
  import { createPortal } from "react-dom";
9
+ //#region src/devtools/autoApply.ts
10
+ /**
11
+ * Preferência "Aplicar automaticamente" dos editores (Layout e Modelo): ligada, cada edição válida
12
+ * vai para a página sozinha, como se clicasse em "Aplicar na página". Uma só para os dois editores,
13
+ * guardada no navegador.
14
+ */
15
+ const KEY = "bm-dashboard-devtools:auto-apply";
16
+ const listeners = /* @__PURE__ */ new Set();
17
+ function read() {
18
+ try {
19
+ return localStorage.getItem(KEY) === "1";
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ let enabled = read();
25
+ function setAutoApply(next) {
26
+ enabled = next;
27
+ try {
28
+ localStorage.setItem(KEY, next ? "1" : "0");
29
+ } catch {}
30
+ for (const listener of listeners) listener();
31
+ }
32
+ const subscribe = (listener) => {
33
+ listeners.add(listener);
34
+ return () => listeners.delete(listener);
35
+ };
36
+ const getSnapshot = () => enabled;
37
+ const useAutoApply = () => useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
38
+ /** Espera entre a última edição e o aplicar automático (digitação não aplica a cada tecla). */
39
+ const AUTO_APPLY_DELAY = {
40
+ layout: 500,
41
+ model: 1200
42
+ };
43
+ //#endregion
9
44
  //#region src/devtools/sqlHighlight.tsx
10
45
  const KEYWORDS$1 = new Set(`SELECT FROM WHERE AND OR NOT IN IS NULL JOIN LEFT RIGHT INNER OUTER CROSS ON AS GROUP BY ORDER
11
46
  HAVING LIMIT OFFSET UNION ALL DISTINCT CASE WHEN THEN ELSE END WITH ASC DESC BETWEEN LIKE EXISTS
@@ -484,6 +519,67 @@ function RowDiffTable({ diffs, expectedLabel = "esperado", actualLabel = "atual"
484
519
  })
485
520
  });
486
521
  }
522
+ /**
523
+ * Botão que pede confirmação antes de agir (ações que perdem trabalho, como desfazer edições).
524
+ * Diálogo do MUI, e não `confirm()`: no modo janela separada, ele abre no popup.
525
+ */
526
+ function ConfirmButton({ confirmTitle, confirmText, confirmLabel = "Confirmar", onConfirm, ...props }) {
527
+ const [open, setOpen] = useState(false);
528
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
529
+ ...props,
530
+ onClick: () => setOpen(true)
531
+ }), /* @__PURE__ */ jsxs(Dialog, {
532
+ open,
533
+ onClose: () => setOpen(false),
534
+ maxWidth: "xs",
535
+ fullWidth: true,
536
+ children: [
537
+ /* @__PURE__ */ jsx(DialogTitle, {
538
+ sx: { fontSize: 15 },
539
+ children: confirmTitle
540
+ }),
541
+ /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsx(Typography, {
542
+ sx: { fontSize: 13 },
543
+ children: confirmText
544
+ }) }),
545
+ /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
546
+ onClick: () => setOpen(false),
547
+ children: "Cancelar"
548
+ }), /* @__PURE__ */ jsx(Button, {
549
+ variant: "contained",
550
+ color: "warning",
551
+ autoFocus: true,
552
+ onClick: () => {
553
+ setOpen(false);
554
+ onConfirm();
555
+ },
556
+ children: confirmLabel
557
+ })] })
558
+ ]
559
+ })] });
560
+ }
561
+ /** Liga/desliga o "Aplicar automaticamente" (compartilhado entre os editores). */
562
+ function AutoApplySwitch({ applying }) {
563
+ const enabled = useAutoApply();
564
+ return /* @__PURE__ */ jsx(Tooltip, {
565
+ title: "Cada edição válida vai para a página sozinha (como “Aplicar na página”). Desligado, aparece o botão Aplicar.",
566
+ children: /* @__PURE__ */ jsx(FormControlLabel, {
567
+ sx: {
568
+ m: 0,
569
+ mr: .5
570
+ },
571
+ control: /* @__PURE__ */ jsx(Switch, {
572
+ size: "small",
573
+ checked: enabled,
574
+ onChange: (e) => setAutoApply(e.target.checked)
575
+ }),
576
+ label: /* @__PURE__ */ jsxs(Typography, {
577
+ sx: { fontSize: 12 },
578
+ children: ["Aplicar automaticamente", enabled && applying ? " · aplicando…" : ""]
579
+ })
580
+ })
581
+ });
582
+ }
487
583
  //#endregion
488
584
  //#region src/devtools/FiltersPanel.tsx
489
585
  function parseFilters(text) {
@@ -1003,6 +1099,25 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1003
1099
  setSelection(parsed.item);
1004
1100
  }, [picked]);
1005
1101
  const errors = useMemo(() => editing ? validateLayout(editing, chartTypes) : [], [editing, chartTypes]);
1102
+ /** Manda o editor para a página (vira rascunho e a página re-renderiza com ele). */
1103
+ const applyEditing = () => {
1104
+ if (!entry || !editing || validateLayout(editing, chartTypes).length) return;
1105
+ devtools.store.expectRefetch();
1106
+ layouts.setPending(entry.registered.id, null);
1107
+ layouts.setDraft(entry.registered.id, editing);
1108
+ };
1109
+ const autoApply = useAutoApply();
1110
+ const editingKey = canonicalJson(editing);
1111
+ useEffect(() => {
1112
+ if (!autoApply || editingKey === referenceKey || errors.length) return;
1113
+ const timer = setTimeout(applyEditing, AUTO_APPLY_DELAY.layout);
1114
+ return () => clearTimeout(timer);
1115
+ }, [
1116
+ autoApply,
1117
+ editingKey,
1118
+ referenceKey,
1119
+ errors.length
1120
+ ]);
1006
1121
  const warnings = useMemo(() => {
1007
1122
  if (!editing || errors.length) return [];
1008
1123
  const list = layoutModelWarnings(editing, model, chartTypes);
@@ -1044,12 +1159,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1044
1159
  setEditing(next);
1045
1160
  setSave(null);
1046
1161
  };
1047
- const apply = () => {
1048
- if (validateLayout(editing, chartTypes).length) return;
1049
- devtools.store.expectRefetch();
1050
- layouts.setPending(entry.registered.id, null);
1051
- layouts.setDraft(entry.registered.id, editing);
1052
- };
1162
+ const apply = applyEditing;
1053
1163
  const setCharts = (next) => update({
1054
1164
  ...editing,
1055
1165
  charts: next
@@ -1420,23 +1530,33 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1420
1530
  borderColor: "divider"
1421
1531
  },
1422
1532
  children: [
1423
- dirty && /* @__PURE__ */ jsx(Typography, {
1533
+ /* @__PURE__ */ jsxs(Box, {
1424
1534
  sx: {
1425
- fontSize: 12,
1426
- color: "warning.main",
1427
- mr: "auto"
1535
+ mr: "auto",
1536
+ display: "flex",
1537
+ alignItems: "center",
1538
+ gap: 1
1428
1539
  },
1429
- children: "Edições não aplicadas"
1540
+ children: [/* @__PURE__ */ jsx(AutoApplySwitch, { applying: dirty && !errors.length }), dirty && (!autoApply || !!errors.length) && /* @__PURE__ */ jsx(Typography, {
1541
+ sx: {
1542
+ fontSize: 12,
1543
+ color: "warning.main"
1544
+ },
1545
+ children: errors.length ? "Edições com erro, não aplicadas" : "Edições não aplicadas"
1546
+ })]
1430
1547
  }),
1431
1548
  /* @__PURE__ */ jsx(CopyButton, {
1432
1549
  text: layoutFileContent(editing),
1433
1550
  label: "Copiar JSON"
1434
1551
  }),
1435
- /* @__PURE__ */ jsx(Button, {
1552
+ /* @__PURE__ */ jsx(ConfirmButton, {
1436
1553
  size: "small",
1437
1554
  disabled: !dirty,
1438
- onClick: () => setEditing(reference),
1555
+ onConfirm: () => setEditing(reference),
1439
1556
  title: "Desfaz as edições ainda não aplicadas",
1557
+ confirmTitle: "Desfazer as edições?",
1558
+ confirmText: "As edições ainda não aplicadas na página serão perdidas; o editor volta ao que a página mostra.",
1559
+ confirmLabel: "Desfazer",
1440
1560
  children: "Desfazer"
1441
1561
  }),
1442
1562
  /* @__PURE__ */ jsx(Button, {
@@ -1460,7 +1580,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1460
1580
  title: `Reescreve ${entry.registered.id}.json com o que está no editor`,
1461
1581
  children: busy ? "Gerando…" : "Salvar no arquivo"
1462
1582
  }),
1463
- /* @__PURE__ */ jsx(Button, {
1583
+ !autoApply && /* @__PURE__ */ jsx(Button, {
1464
1584
  size: "small",
1465
1585
  variant: "contained",
1466
1586
  disabled: !dirty || !!errors.length,
@@ -4153,7 +4273,6 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4153
4273
  });
4154
4274
  const [selection, setSelection] = useState(null);
4155
4275
  const [panel, setPanel] = useState(loadPanel);
4156
- const [picking, setPicking] = useState(null);
4157
4276
  useEffect(() => {
4158
4277
  try {
4159
4278
  localStorage.setItem(PANEL_KEY, JSON.stringify(panel));
@@ -4567,376 +4686,305 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4567
4686
  height: "100%",
4568
4687
  minHeight: 0
4569
4688
  },
4570
- children: [
4571
- /* @__PURE__ */ jsxs(Box, {
4572
- ref: canvas,
4573
- tabIndex: -1,
4574
- onMouseDown: startPan,
4575
- onMouseDownCapture: () => canvas.current?.focus({ preventScroll: true }),
4576
- onKeyDown: (event) => {
4577
- if ((event.key === "Delete" || event.key === "Backspace") && event.target === canvas.current) removeSelection();
4578
- },
4579
- onWheel,
4580
- sx: {
4581
- outline: "none",
4582
- flex: 1,
4583
- position: "relative",
4584
- overflow: "hidden",
4585
- cursor: "grab",
4586
- bgcolor: "#15171b",
4587
- backgroundImage: "radial-gradient(rgba(255,255,255,.07) 1px, transparent 1px)",
4588
- backgroundSize: `${20 * view.zoom}px ${20 * view.zoom}px`,
4589
- backgroundPosition: `${view.x}px ${view.y}px`
4590
- },
4591
- children: [
4592
- /* @__PURE__ */ jsxs(Box, {
4593
- sx: {
4594
- position: "absolute",
4595
- left: 0,
4596
- top: 0,
4597
- transformOrigin: "0 0",
4598
- transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`
4599
- },
4600
- children: [/* @__PURE__ */ jsxs("svg", {
4601
- width: 1,
4602
- height: 1,
4603
- style: {
4604
- position: "absolute",
4605
- overflow: "visible",
4606
- pointerEvents: "none"
4607
- },
4608
- "aria-hidden": true,
4609
- children: [lines.map((line) => /* @__PURE__ */ jsx(RelationshipLine, {
4610
- line,
4611
- hovered: hoveredEdge === line.id,
4612
- onHover: (hovered) => setHoveredEdge(hovered ? line.id : null)
4613
- }, line.id)), linkPreview && /* @__PURE__ */ jsx("path", {
4614
- d: linkPreview,
4615
- fill: "none",
4616
- stroke: "#F58220",
4617
- strokeWidth: 2,
4618
- strokeDasharray: "5 4"
4619
- })]
4620
- }), nodes.map((node) => /* @__PURE__ */ jsx(TableCard, {
4621
- node,
4622
- position: placed(node),
4623
- size: sizeOf(node),
4624
- collapsed: !!collapsed[node.id],
4625
- selected: !!isSelected(node),
4626
- linking: !!linking && linking.node.id !== node.id,
4627
- selectedField: selection?.kind === "dimension" || selection?.kind === "measure" ? selection.uid : null,
4628
- onToggle: () => setCollapsed((current) => ({
4629
- ...current,
4630
- [node.id]: !current[node.id]
4631
- })),
4632
- onScroll: (top) => setScrolls((current) => ({
4633
- ...current,
4634
- [node.id]: top
4635
- })),
4636
- onHeaderDown: (event) => startMove(event, node),
4637
- onResizeDown: (event, edges) => startResize(event, node, edges),
4638
- onResizeReset: () => resetSize(node),
4639
- onColumnDown: (event, column) => startLink(event, node, column),
4640
- onFieldClick: (item) => setSelection({
4641
- kind: item.kind,
4642
- uid: item.uid
4643
- }),
4644
- onAddField: (kind, column) => addField(kind, node.name, column)
4645
- }, node.id))]
4646
- }),
4647
- /* @__PURE__ */ jsxs(Box, {
4648
- onMouseDown: (event) => event.stopPropagation(),
4649
- sx: {
4650
- position: "absolute",
4651
- top: 8,
4652
- left: 8,
4653
- right: 8,
4654
- display: "flex",
4655
- gap: .75,
4656
- flexWrap: "wrap",
4657
- alignItems: "center",
4658
- pointerEvents: "none",
4659
- "& > *": { pointerEvents: "auto" }
4660
- },
4661
- children: [
4662
- /* @__PURE__ */ jsx(Button, {
4663
- size: "small",
4664
- variant: "outlined",
4665
- onClick: addTable,
4666
- children: "+ Tabela"
4667
- }),
4668
- /* @__PURE__ */ jsx(Button, {
4669
- size: "small",
4670
- variant: "outlined",
4671
- disabled: !editor.tables.length,
4672
- onClick: () => setPicking(editor.tables[0]?.name ?? null),
4673
- title: "Escolher a tabela e marcar as colunas que viram dimensão",
4674
- children: "+ Dimensões"
4675
- }),
4676
- /* @__PURE__ */ jsx(Button, {
4677
- size: "small",
4678
- onClick: onLoadColumns,
4679
- disabled: loadingColumns,
4680
- title: "Lê uma linha de cada tabela para listar as colunas",
4681
- sx: { bgcolor: "#1b1e23" },
4682
- children: loadingColumns ? "Lendo colunas…" : "Recarregar colunas"
4683
- }),
4684
- /* @__PURE__ */ jsxs(Box, {
4685
- sx: {
4686
- ml: "auto",
4687
- display: "flex",
4688
- gap: .5,
4689
- alignItems: "center"
4690
- },
4691
- children: [
4692
- /* @__PURE__ */ jsx(Button, {
4693
- size: "small",
4694
- onClick: () => {
4695
- const layout = autoLayout(nodes, sizes);
4696
- setPositions((current) => Object.fromEntries(Object.entries(layout).map(([id, point]) => [id, {
4697
- ...current[id],
4698
- ...point
4699
- }])));
4700
- fitToScreen(layout);
4701
- },
4702
- title: "Dimensões em cima, fatos embaixo (troca as posições salvas)",
4703
- sx: { bgcolor: "#1b1e23" },
4704
- children: "Organizar"
4705
- }),
4706
- /* @__PURE__ */ jsx(Button, {
4707
- size: "small",
4708
- onClick: () => fitToScreen(),
4709
- sx: { bgcolor: "#1b1e23" },
4710
- children: "Ajustar à tela"
4711
- }),
4712
- /* @__PURE__ */ jsx(IconButton, {
4713
- size: "small",
4714
- "aria-label": "Diminuir zoom",
4715
- onClick: () => setView((current) => ({
4716
- ...current,
4717
- zoom: Math.max(.3, current.zoom - .1)
4718
- })),
4719
- children: "−"
4720
- }),
4721
- /* @__PURE__ */ jsxs(Typography, {
4722
- sx: {
4723
- fontSize: 11,
4724
- fontFamily: MONO,
4725
- width: 36,
4726
- textAlign: "center"
4727
- },
4728
- children: [Math.round(view.zoom * 100), "%"]
4729
- }),
4730
- /* @__PURE__ */ jsx(IconButton, {
4731
- size: "small",
4732
- "aria-label": "Aumentar zoom",
4733
- onClick: () => setView((current) => ({
4734
- ...current,
4735
- zoom: Math.min(2, current.zoom + .1)
4736
- })),
4737
- children: "+"
4738
- })
4739
- ]
4740
- })
4741
- ]
4742
- }),
4743
- !nodes.length && /* @__PURE__ */ jsx(Box, {
4744
- sx: {
4745
- position: "absolute",
4746
- inset: 0,
4747
- display: "flex",
4748
- alignItems: "center",
4749
- justifyContent: "center",
4750
- pointerEvents: "none",
4751
- color: "text.secondary",
4752
- fontSize: 13,
4753
- textAlign: "center",
4754
- px: 4
4755
- },
4756
- children: "Modelo vazio. Adicione as tabelas dimensão (cadastros: navio, calendário…) e fato (lançamentos: pesagens, custos…) e arraste a coluna da fato até a chave da dimensão para relacioná-las."
4757
- }),
4758
- !!nodes.length && /* @__PURE__ */ jsx(Typography, {
4759
- sx: {
4689
+ children: [/* @__PURE__ */ jsxs(Box, {
4690
+ ref: canvas,
4691
+ tabIndex: -1,
4692
+ onMouseDown: startPan,
4693
+ onMouseDownCapture: () => canvas.current?.focus({ preventScroll: true }),
4694
+ onKeyDown: (event) => {
4695
+ if ((event.key === "Delete" || event.key === "Backspace") && event.target === canvas.current) removeSelection();
4696
+ },
4697
+ onWheel,
4698
+ sx: {
4699
+ outline: "none",
4700
+ flex: 1,
4701
+ position: "relative",
4702
+ overflow: "hidden",
4703
+ cursor: "grab",
4704
+ bgcolor: "#15171b",
4705
+ backgroundImage: "radial-gradient(rgba(255,255,255,.07) 1px, transparent 1px)",
4706
+ backgroundSize: `${20 * view.zoom}px ${20 * view.zoom}px`,
4707
+ backgroundPosition: `${view.x}px ${view.y}px`
4708
+ },
4709
+ children: [
4710
+ /* @__PURE__ */ jsxs(Box, {
4711
+ sx: {
4712
+ position: "absolute",
4713
+ left: 0,
4714
+ top: 0,
4715
+ transformOrigin: "0 0",
4716
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`
4717
+ },
4718
+ children: [/* @__PURE__ */ jsxs("svg", {
4719
+ width: 1,
4720
+ height: 1,
4721
+ style: {
4760
4722
  position: "absolute",
4761
- left: 10,
4762
- bottom: 6,
4763
- fontSize: 11,
4764
- color: "text.disabled",
4723
+ overflow: "visible",
4765
4724
  pointerEvents: "none"
4766
4725
  },
4767
- children: "Arraste uma coluna até a coluna de outra tabela para relacionar · +◇/+Σ na coluna cria dimensão/medida · Delete remove · Ctrl+roda: zoom"
4768
- })
4769
- ]
4770
- }),
4771
- panel.open ? /* @__PURE__ */ jsxs(Box, {
4772
- sx: {
4773
- width: panel.width,
4774
- flexShrink: 0,
4775
- borderLeft: 1,
4776
- borderColor: "divider",
4777
- position: "relative",
4778
- display: "flex",
4779
- flexDirection: "column",
4780
- minHeight: 0
4781
- },
4782
- children: [
4783
- /* @__PURE__ */ jsx(Box, {
4784
- onMouseDown: startPanelResize,
4785
- title: "Arraste para redimensionar",
4786
- sx: {
4787
- position: "absolute",
4788
- left: -3,
4789
- top: 0,
4790
- bottom: 0,
4791
- width: 6,
4792
- cursor: "ew-resize",
4793
- zIndex: 2,
4794
- "&:hover": {
4795
- bgcolor: "primary.main",
4796
- opacity: .4
4797
- }
4798
- }
4799
- }),
4800
- /* @__PURE__ */ jsxs(Box, {
4801
- sx: {
4802
- display: "flex",
4803
- alignItems: "center",
4804
- px: 1.5,
4805
- py: .5,
4806
- borderBottom: 1,
4807
- borderColor: "divider"
4808
- },
4809
- children: [/* @__PURE__ */ jsx(Typography, {
4810
- sx: {
4811
- fontSize: 12,
4812
- fontWeight: 700,
4813
- color: "text.secondary",
4814
- flex: 1
4815
- },
4816
- children: "Propriedades"
4817
- }), /* @__PURE__ */ jsx(IconButton, {
4818
- size: "small",
4819
- "aria-label": "Recolher propriedades",
4820
- title: "Recolher",
4821
- onClick: () => setPanel((current) => ({
4822
- ...current,
4823
- open: false
4824
- })),
4825
- children: "⟩"
4726
+ "aria-hidden": true,
4727
+ children: [lines.map((line) => /* @__PURE__ */ jsx(RelationshipLine, {
4728
+ line,
4729
+ hovered: hoveredEdge === line.id,
4730
+ onHover: (hovered) => setHoveredEdge(hovered ? line.id : null)
4731
+ }, line.id)), linkPreview && /* @__PURE__ */ jsx("path", {
4732
+ d: linkPreview,
4733
+ fill: "none",
4734
+ stroke: "#F58220",
4735
+ strokeWidth: 2,
4736
+ strokeDasharray: "5 4"
4826
4737
  })]
4827
- }),
4828
- /* @__PURE__ */ jsx(Box, {
4829
- sx: {
4830
- overflow: "auto",
4831
- flex: 1,
4832
- p: 1.5
4833
- },
4834
- children: /* @__PURE__ */ jsx(Properties, {
4835
- editor,
4836
- selection,
4837
- columns,
4838
- errors,
4839
- warnings,
4840
- usage,
4841
- onSelect: setSelection,
4842
- onTry,
4843
- setEditor,
4844
- patchTable,
4845
- patchRelationship,
4846
- patchDimension,
4847
- patchMeasure,
4848
- removeTable,
4849
- commitRename,
4850
- addField,
4851
- unplaced
4852
- })
4853
- })
4854
- ]
4855
- }) : /* @__PURE__ */ jsx(Box, {
4856
- component: "button",
4857
- type: "button",
4858
- title: "Mostrar as propriedades",
4859
- onClick: () => setPanel((current) => ({
4860
- ...current,
4861
- open: true
4862
- })),
4863
- sx: {
4864
- width: 28,
4865
- flexShrink: 0,
4866
- border: 0,
4867
- borderLeft: 1,
4868
- borderColor: "divider",
4869
- bgcolor: "background.paper",
4870
- color: "text.secondary",
4871
- cursor: "pointer",
4872
- writingMode: "vertical-rl",
4873
- fontSize: 12,
4874
- py: 1,
4875
- "&:hover": { color: "primary.main" }
4876
- },
4877
- children: "⟨ Propriedades"
4878
- }),
4879
- /* @__PURE__ */ jsxs(Dialog, {
4880
- open: picking !== null,
4881
- onClose: () => setPicking(null),
4882
- maxWidth: "xs",
4883
- fullWidth: true,
4884
- children: [
4885
- /* @__PURE__ */ jsx(DialogTitle, {
4886
- sx: { fontSize: 14 },
4887
- children: "Dimensões"
4888
- }),
4889
- /* @__PURE__ */ jsxs(DialogContent, { children: [
4890
- /* @__PURE__ */ jsxs(Typography, {
4891
- sx: {
4892
- fontSize: 12,
4893
- color: "text.secondary",
4894
- mb: 1
4895
- },
4896
- children: [
4897
- "Marque as colunas que viram dimensão (filtro e eixo dos gráficos). Para expressões como",
4898
- " ",
4899
- /* @__PURE__ */ jsx("code", { children: "YEAR(tabela.data)" }),
4900
- ", use o modo SQL da dimensão."
4901
- ]
4738
+ }), nodes.map((node) => /* @__PURE__ */ jsx(TableCard, {
4739
+ node,
4740
+ position: placed(node),
4741
+ size: sizeOf(node),
4742
+ collapsed: !!collapsed[node.id],
4743
+ selected: !!isSelected(node),
4744
+ linking: !!linking && linking.node.id !== node.id,
4745
+ selectedField: selection?.kind === "dimension" || selection?.kind === "measure" ? selection.uid : null,
4746
+ onToggle: () => setCollapsed((current) => ({
4747
+ ...current,
4748
+ [node.id]: !current[node.id]
4749
+ })),
4750
+ onScroll: (top) => setScrolls((current) => ({
4751
+ ...current,
4752
+ [node.id]: top
4753
+ })),
4754
+ onHeaderDown: (event) => startMove(event, node),
4755
+ onResizeDown: (event, edges) => startResize(event, node, edges),
4756
+ onResizeReset: () => resetSize(node),
4757
+ onColumnDown: (event, column) => startLink(event, node, column),
4758
+ onFieldClick: (item) => setSelection({
4759
+ kind: item.kind,
4760
+ uid: item.uid
4902
4761
  }),
4903
- /* @__PURE__ */ jsx(TextField, {
4904
- select: true,
4762
+ onAddField: (kind, column) => addField(kind, node.name, column)
4763
+ }, node.id))]
4764
+ }),
4765
+ /* @__PURE__ */ jsxs(Box, {
4766
+ onMouseDown: (event) => event.stopPropagation(),
4767
+ sx: {
4768
+ position: "absolute",
4769
+ top: 8,
4770
+ left: 8,
4771
+ right: 8,
4772
+ display: "flex",
4773
+ gap: .75,
4774
+ flexWrap: "wrap",
4775
+ alignItems: "center",
4776
+ pointerEvents: "none",
4777
+ "& > *": { pointerEvents: "auto" }
4778
+ },
4779
+ children: [
4780
+ /* @__PURE__ */ jsx(Button, {
4905
4781
  size: "small",
4906
- fullWidth: true,
4907
- label: "tabela",
4908
- value: picking ?? "",
4909
- onChange: (e) => setPicking(e.target.value),
4782
+ variant: "outlined",
4783
+ onClick: addTable,
4784
+ children: "+ Tabela"
4785
+ }),
4786
+ /* @__PURE__ */ jsx(Button, {
4787
+ size: "small",
4788
+ onClick: onLoadColumns,
4789
+ disabled: loadingColumns,
4790
+ title: "Lê uma linha de cada tabela para listar as colunas",
4791
+ sx: { bgcolor: "#1b1e23" },
4792
+ children: loadingColumns ? "Lendo colunas…" : "Recarregar colunas"
4793
+ }),
4794
+ /* @__PURE__ */ jsxs(Box, {
4910
4795
  sx: {
4911
- ...inputSx,
4912
- mb: 1
4796
+ ml: "auto",
4797
+ display: "flex",
4798
+ gap: .5,
4799
+ alignItems: "center"
4913
4800
  },
4914
- children: editor.tables.map((table) => /* @__PURE__ */ jsx(MenuItem, {
4915
- value: table.name,
4916
- children: table.name
4917
- }, table.uid))
4918
- }),
4919
- picking && /* @__PURE__ */ jsx(DimensionPicker, {
4920
- editor,
4921
- setEditor,
4922
- columns,
4923
- table: picking,
4924
- onOpen: (uid) => {
4925
- setPicking(null);
4926
- setSelection({
4927
- kind: "dimension",
4928
- uid
4929
- });
4930
- }
4801
+ children: [
4802
+ /* @__PURE__ */ jsx(Button, {
4803
+ size: "small",
4804
+ onClick: () => {
4805
+ const layout = autoLayout(nodes, sizes);
4806
+ setPositions((current) => Object.fromEntries(Object.entries(layout).map(([id, point]) => [id, {
4807
+ ...current[id],
4808
+ ...point
4809
+ }])));
4810
+ fitToScreen(layout);
4811
+ },
4812
+ title: "Dimensões em cima, fatos embaixo (troca as posições salvas)",
4813
+ sx: { bgcolor: "#1b1e23" },
4814
+ children: "Organizar"
4815
+ }),
4816
+ /* @__PURE__ */ jsx(Button, {
4817
+ size: "small",
4818
+ onClick: () => fitToScreen(),
4819
+ sx: { bgcolor: "#1b1e23" },
4820
+ children: "Ajustar à tela"
4821
+ }),
4822
+ /* @__PURE__ */ jsx(IconButton, {
4823
+ size: "small",
4824
+ "aria-label": "Diminuir zoom",
4825
+ onClick: () => setView((current) => ({
4826
+ ...current,
4827
+ zoom: Math.max(.3, current.zoom - .1)
4828
+ })),
4829
+ children: "−"
4830
+ }),
4831
+ /* @__PURE__ */ jsxs(Typography, {
4832
+ sx: {
4833
+ fontSize: 11,
4834
+ fontFamily: MONO,
4835
+ width: 36,
4836
+ textAlign: "center"
4837
+ },
4838
+ children: [Math.round(view.zoom * 100), "%"]
4839
+ }),
4840
+ /* @__PURE__ */ jsx(IconButton, {
4841
+ size: "small",
4842
+ "aria-label": "Aumentar zoom",
4843
+ onClick: () => setView((current) => ({
4844
+ ...current,
4845
+ zoom: Math.min(2, current.zoom + .1)
4846
+ })),
4847
+ children: "+"
4848
+ })
4849
+ ]
4931
4850
  })
4932
- ] }),
4933
- /* @__PURE__ */ jsx(DialogActions, { children: /* @__PURE__ */ jsx(Button, {
4934
- onClick: () => setPicking(null),
4935
- children: "Fechar"
4936
- }) })
4937
- ]
4938
- })
4939
- ]
4851
+ ]
4852
+ }),
4853
+ !nodes.length && /* @__PURE__ */ jsx(Box, {
4854
+ sx: {
4855
+ position: "absolute",
4856
+ inset: 0,
4857
+ display: "flex",
4858
+ alignItems: "center",
4859
+ justifyContent: "center",
4860
+ pointerEvents: "none",
4861
+ color: "text.secondary",
4862
+ fontSize: 13,
4863
+ textAlign: "center",
4864
+ px: 4
4865
+ },
4866
+ children: "Modelo vazio. Adicione as tabelas dimensão (cadastros: navio, calendário…) e fato (lançamentos: pesagens, custos…) e arraste a coluna da fato até a chave da dimensão para relacioná-las."
4867
+ }),
4868
+ !!nodes.length && /* @__PURE__ */ jsx(Typography, {
4869
+ sx: {
4870
+ position: "absolute",
4871
+ left: 10,
4872
+ bottom: 6,
4873
+ fontSize: 11,
4874
+ color: "text.disabled",
4875
+ pointerEvents: "none"
4876
+ },
4877
+ children: "Arraste uma coluna até a coluna de outra tabela para relacionar · +◇/+Σ na coluna cria dimensão/medida · Delete remove · Ctrl+roda: zoom"
4878
+ })
4879
+ ]
4880
+ }), panel.open ? /* @__PURE__ */ jsxs(Box, {
4881
+ sx: {
4882
+ width: panel.width,
4883
+ flexShrink: 0,
4884
+ borderLeft: 1,
4885
+ borderColor: "divider",
4886
+ position: "relative",
4887
+ display: "flex",
4888
+ flexDirection: "column",
4889
+ minHeight: 0
4890
+ },
4891
+ children: [
4892
+ /* @__PURE__ */ jsx(Box, {
4893
+ onMouseDown: startPanelResize,
4894
+ title: "Arraste para redimensionar",
4895
+ sx: {
4896
+ position: "absolute",
4897
+ left: -3,
4898
+ top: 0,
4899
+ bottom: 0,
4900
+ width: 6,
4901
+ cursor: "ew-resize",
4902
+ zIndex: 2,
4903
+ "&:hover": {
4904
+ bgcolor: "primary.main",
4905
+ opacity: .4
4906
+ }
4907
+ }
4908
+ }),
4909
+ /* @__PURE__ */ jsxs(Box, {
4910
+ sx: {
4911
+ display: "flex",
4912
+ alignItems: "center",
4913
+ px: 1.5,
4914
+ py: .5,
4915
+ borderBottom: 1,
4916
+ borderColor: "divider"
4917
+ },
4918
+ children: [/* @__PURE__ */ jsx(Typography, {
4919
+ sx: {
4920
+ fontSize: 12,
4921
+ fontWeight: 700,
4922
+ color: "text.secondary",
4923
+ flex: 1
4924
+ },
4925
+ children: "Propriedades"
4926
+ }), /* @__PURE__ */ jsx(IconButton, {
4927
+ size: "small",
4928
+ "aria-label": "Recolher propriedades",
4929
+ title: "Recolher",
4930
+ onClick: () => setPanel((current) => ({
4931
+ ...current,
4932
+ open: false
4933
+ })),
4934
+ children: "⟩"
4935
+ })]
4936
+ }),
4937
+ /* @__PURE__ */ jsx(Box, {
4938
+ sx: {
4939
+ overflow: "auto",
4940
+ flex: 1,
4941
+ p: 1.5
4942
+ },
4943
+ children: /* @__PURE__ */ jsx(Properties, {
4944
+ editor,
4945
+ selection,
4946
+ columns,
4947
+ errors,
4948
+ warnings,
4949
+ usage,
4950
+ onSelect: setSelection,
4951
+ onTry,
4952
+ setEditor,
4953
+ patchTable,
4954
+ patchRelationship,
4955
+ patchDimension,
4956
+ patchMeasure,
4957
+ removeTable,
4958
+ commitRename,
4959
+ addField,
4960
+ unplaced
4961
+ })
4962
+ })
4963
+ ]
4964
+ }) : /* @__PURE__ */ jsx(Box, {
4965
+ component: "button",
4966
+ type: "button",
4967
+ title: "Mostrar as propriedades",
4968
+ onClick: () => setPanel((current) => ({
4969
+ ...current,
4970
+ open: true
4971
+ })),
4972
+ sx: {
4973
+ width: 28,
4974
+ flexShrink: 0,
4975
+ border: 0,
4976
+ borderLeft: 1,
4977
+ borderColor: "divider",
4978
+ bgcolor: "background.paper",
4979
+ color: "text.secondary",
4980
+ cursor: "pointer",
4981
+ writingMode: "vertical-rl",
4982
+ fontSize: 12,
4983
+ py: 1,
4984
+ "&:hover": { color: "primary.main" }
4985
+ },
4986
+ children: "⟨ Propriedades"
4987
+ })]
4940
4988
  });
4941
4989
  }
4942
4990
  /** Curva com tangentes horizontais (saída e chegada pelas laterais dos cards). */
@@ -5844,7 +5892,12 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
5844
5892
  const [busy, setBusy] = useState(null);
5845
5893
  const [actionError, setActionError] = useState(null);
5846
5894
  const [save, setSave] = useState(null);
5847
- useEffect(() => setEditor(toEditor(draft ?? original)), [draft, original]);
5895
+ const applied = useRef(null);
5896
+ useEffect(() => {
5897
+ const next = draft ?? original;
5898
+ if (draft && canonicalJson(draft) === applied.current) return;
5899
+ setEditor((current) => same(fromEditor(current), next) ? current : toEditor(next));
5900
+ }, [draft, original]);
5848
5901
  const errors = useMemo(() => validateEditor(editor), [editor]);
5849
5902
  const edited = useMemo(() => fromEditor(editor), [editor]);
5850
5903
  const structure = useMemo(() => canonicalJson(withoutDiagram(edited)), [edited]);
@@ -5916,8 +5969,26 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
5916
5969
  const apply = () => act("apply", async () => {
5917
5970
  const result = canonicalJson(withoutDiagram(reference)) === structure ? null : await validateSql();
5918
5971
  if (result && Object.keys(result.errors).length) return;
5972
+ applied.current = differsFromOriginal ? canonicalJson(edited) : null;
5919
5973
  setDraft(differsFromOriginal ? edited : null);
5920
5974
  });
5975
+ const autoApply = useAutoApply();
5976
+ const editedKey = canonicalJson(edited);
5977
+ const attempted = useRef(null);
5978
+ useEffect(() => {
5979
+ if (!autoApply || !dirty || errors.length || busy || attempted.current === editedKey) return;
5980
+ const timer = setTimeout(() => {
5981
+ attempted.current = editedKey;
5982
+ apply();
5983
+ }, AUTO_APPLY_DELAY.model);
5984
+ return () => clearTimeout(timer);
5985
+ }, [
5986
+ autoApply,
5987
+ editedKey,
5988
+ dirty,
5989
+ errors.length,
5990
+ busy
5991
+ ]);
5921
5992
  const previewSave = () => act("save", async () => {
5922
5993
  const result = await fetcher({
5923
5994
  model: modelName,
@@ -6060,9 +6131,11 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
6060
6131
  ml: "auto",
6061
6132
  display: "flex",
6062
6133
  gap: 1,
6063
- flexWrap: "wrap"
6134
+ flexWrap: "wrap",
6135
+ alignItems: "center"
6064
6136
  },
6065
6137
  children: [
6138
+ /* @__PURE__ */ jsx(AutoApplySwitch, { applying: busy === "apply" }),
6066
6139
  /* @__PURE__ */ jsx(Button, {
6067
6140
  size: "small",
6068
6141
  onClick: () => setShowCode(true),
@@ -6082,11 +6155,14 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
6082
6155
  title: "Executa cada pedaço do modelo no banco com poucas chaves",
6083
6156
  children: busy === "validate" ? "Validando…" : "Validar SQL"
6084
6157
  }),
6085
- /* @__PURE__ */ jsx(Button, {
6158
+ /* @__PURE__ */ jsx(ConfirmButton, {
6086
6159
  size: "small",
6087
6160
  disabled: !dirty,
6088
- onClick: () => setEditor(toEditor(reference)),
6161
+ onConfirm: () => setEditor(toEditor(reference)),
6089
6162
  title: "Desfaz as edições ainda não aplicadas",
6163
+ confirmTitle: "Desfazer as edições?",
6164
+ confirmText: "As edições do modelo ainda não aplicadas na página serão perdidas; o editor volta ao modelo que a página usa.",
6165
+ confirmLabel: "Desfazer",
6090
6166
  children: "Desfazer"
6091
6167
  }),
6092
6168
  /* @__PURE__ */ jsx(Button, {
@@ -6105,7 +6181,7 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
6105
6181
  title: `Reescreve src/dashboard/models/${original.name}.ts`,
6106
6182
  children: busy === "save" ? "Gerando…" : "Salvar no arquivo"
6107
6183
  }),
6108
- /* @__PURE__ */ jsx(Button, {
6184
+ !autoApply && /* @__PURE__ */ jsx(Button, {
6109
6185
  size: "small",
6110
6186
  variant: "contained",
6111
6187
  disabled: !dirty || !!errors.length || !!busy,
@@ -6545,6 +6621,7 @@ function QueriesPanel({ devtools, capabilities, selected, onSelect, onHover, onC
6545
6621
  run.status === "loading" ? "carregando…" : `${run.durationMs} ms`,
6546
6622
  run.rows && ` · ${run.rows.length} linhas`,
6547
6623
  run.debug?.cached && " · cache",
6624
+ run.batched && ` · lote de ${run.batched}`,
6548
6625
  view === "active" && ` · ${instance?.runs ?? 1}×`
6549
6626
  ]
6550
6627
  })]
@@ -6627,6 +6704,11 @@ function RunDetail({ run, devtools, capabilities, mounted }) {
6627
6704
  color: "warning",
6628
6705
  label: "rodou no rascunho"
6629
6706
  }),
6707
+ run.batched && /* @__PURE__ */ jsx(Chip, {
6708
+ size: "small",
6709
+ label: `lote de ${run.batched}`,
6710
+ title: "Saiu da mesma SQL que outras consultas (mesmas dimensões, filtros e fatos): o SQL e o tempo são os do conjunto"
6711
+ }),
6630
6712
  mounted && /* @__PURE__ */ jsx(Button, {
6631
6713
  size: "small",
6632
6714
  onClick: () => scrollToQuery(run.instanceId),
@@ -8111,4 +8193,4 @@ function ToggleButton$1({ draft, loading, problems, inspecting, onOpen }) {
8111
8193
  //#endregion
8112
8194
  export { DashboardDevtools as default };
8113
8195
 
8114
- //# sourceMappingURL=DashboardDevtools-xhkgdAkr.js.map
8196
+ //# sourceMappingURL=DashboardDevtools-Osu5V30U.js.map