@bluemarble/bi 0.6.6 → 0.8.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/dist/{DashboardDevtools-I00Iz0Z8.js → DashboardDevtools-wk4vMfhy.js} +1447 -499
- package/dist/DashboardDevtools-wk4vMfhy.js.map +1 -0
- package/dist/{ModelDictionary-DUGFHBo-.js → ModelDictionary-DZiYK1mV.js} +57 -4
- package/dist/ModelDictionary-DZiYK1mV.js.map +1 -0
- package/dist/client/index.d.ts +22 -5
- package/dist/client/index.js +30 -7
- package/dist/client/index.js.map +1 -1
- package/dist/{formatRules-DTKbov47.d.ts → formatRules-DXMHY9tU.d.ts} +41 -2
- package/dist/{layout-CrZ00tB0.js → layout-BS3nO6OT.js} +102 -2
- package/dist/layout-BS3nO6OT.js.map +1 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +17 -1
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/DashboardDevtools-I00Iz0Z8.js.map +0 -1
- package/dist/ModelDictionary-DUGFHBo-.js.map +0 -1
- package/dist/layout-CrZ00tB0.js.map +0 -1
|
@@ -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
|
|
4
|
-
import { C as parseLayoutItemKey,
|
|
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";
|
|
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";
|
|
@@ -2182,6 +2182,11 @@ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters,
|
|
|
2182
2182
|
model,
|
|
2183
2183
|
onChange: (rules) => set(key, rules.length ? rules : void 0)
|
|
2184
2184
|
});
|
|
2185
|
+
case "colorScales": return /* @__PURE__ */ jsx(ColorScalesInput, {
|
|
2186
|
+
scales: value ?? [],
|
|
2187
|
+
columnOptions: columnOptionsOf(columns.filter((name) => model.dimensions[name]?.type !== "string")),
|
|
2188
|
+
onChange: (scales) => set(key, scales.length ? scales : void 0)
|
|
2189
|
+
});
|
|
2185
2190
|
case "formatters": {
|
|
2186
2191
|
const map = value ?? {};
|
|
2187
2192
|
if (!chosenDimensions.length) return /* @__PURE__ */ jsx(Typography, {
|
|
@@ -2254,7 +2259,7 @@ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters,
|
|
|
2254
2259
|
...pairedFormatters
|
|
2255
2260
|
].filter(Boolean));
|
|
2256
2261
|
const sections = [...new Set(entries.filter(([key]) => !hidden.has(key)).map(([, spec]) => spec.section))];
|
|
2257
|
-
const fullRow = (spec) => spec.kind === "dimensions" || spec.kind === "measures" || spec.kind === "formatters" || spec.kind === "formatRules" || spec.kind === "colors" || spec.kind === "valueRule";
|
|
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";
|
|
2258
2263
|
const gridColumn = (spec, key) => fullRow(spec) ? "1 / -1" : key === "title" && formColumns > 1 ? "span 2" : void 0;
|
|
2259
2264
|
return /* @__PURE__ */ jsx(Fragment, { children: sections.map((section) => {
|
|
2260
2265
|
const inSection = entries.filter(([, spec]) => spec.section === section);
|
|
@@ -2590,6 +2595,202 @@ function FormatRulesInput({ rules, fieldOptions, columnOptions, model, onChange
|
|
|
2590
2595
|
})]
|
|
2591
2596
|
});
|
|
2592
2597
|
}
|
|
2598
|
+
const COLOR_SCALE_TARGET_LABEL = {
|
|
2599
|
+
background: "o fundo",
|
|
2600
|
+
text: "o texto"
|
|
2601
|
+
};
|
|
2602
|
+
const DEFAULT_SCALE_COLOR = "#2e7d32";
|
|
2603
|
+
/**
|
|
2604
|
+
* Escalas de cor (gradiente), uma por cartão: "pinta <o fundo | o texto> de <coluna> de <cor
|
|
2605
|
+
* mínima> (em <mínimo>) a <cor máxima> (em <máximo>)". Mínimo/máximo vazios = da própria coluna.
|
|
2606
|
+
*/
|
|
2607
|
+
function ColorScalesInput({ scales, columnOptions, onChange }) {
|
|
2608
|
+
const update = (index, change) => onChange(scales.map((scale, i) => i === index ? change(scale) : scale));
|
|
2609
|
+
const columns = columnOptions.map((option) => option.name);
|
|
2610
|
+
const parseNumber = (text) => {
|
|
2611
|
+
const number = Number(text.trim().replace(",", "."));
|
|
2612
|
+
return text.trim() && Number.isFinite(number) ? number : void 0;
|
|
2613
|
+
};
|
|
2614
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2615
|
+
sx: {
|
|
2616
|
+
display: "flex",
|
|
2617
|
+
flexDirection: "column",
|
|
2618
|
+
gap: .75,
|
|
2619
|
+
maxWidth: 760
|
|
2620
|
+
},
|
|
2621
|
+
children: [scales.map((scale, index) => {
|
|
2622
|
+
const minColor = scale.minColor ?? "#ffffff";
|
|
2623
|
+
const inverted = scale.min !== void 0 && scale.max !== void 0 && scale.min >= scale.max;
|
|
2624
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
2625
|
+
sx: {
|
|
2626
|
+
border: 1,
|
|
2627
|
+
borderColor: "divider",
|
|
2628
|
+
borderRadius: 1,
|
|
2629
|
+
p: .75,
|
|
2630
|
+
display: "flex",
|
|
2631
|
+
flexDirection: "column",
|
|
2632
|
+
gap: .5,
|
|
2633
|
+
"&:hover .row-actions": { visibility: "visible" }
|
|
2634
|
+
},
|
|
2635
|
+
children: [
|
|
2636
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
2637
|
+
sx: {
|
|
2638
|
+
display: "flex",
|
|
2639
|
+
gap: .5,
|
|
2640
|
+
alignItems: "center",
|
|
2641
|
+
flexWrap: "wrap"
|
|
2642
|
+
},
|
|
2643
|
+
children: [
|
|
2644
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
2645
|
+
sx: {
|
|
2646
|
+
fontSize: 12,
|
|
2647
|
+
color: "text.secondary"
|
|
2648
|
+
},
|
|
2649
|
+
children: [index + 1, ". Pinta"]
|
|
2650
|
+
}),
|
|
2651
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
2652
|
+
select: true,
|
|
2653
|
+
size: "small",
|
|
2654
|
+
value: scale.target ?? "background",
|
|
2655
|
+
onChange: (e) => update(index, (current) => withField(current, "target", e.target.value === "background" ? void 0 : e.target.value)),
|
|
2656
|
+
sx: {
|
|
2657
|
+
...inputSx$1,
|
|
2658
|
+
flex: "1 1 100px"
|
|
2659
|
+
},
|
|
2660
|
+
children: COLOR_SCALE_TARGETS.map((target) => /* @__PURE__ */ jsx(MenuItem, {
|
|
2661
|
+
value: target,
|
|
2662
|
+
children: COLOR_SCALE_TARGET_LABEL[target]
|
|
2663
|
+
}, target))
|
|
2664
|
+
}),
|
|
2665
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
2666
|
+
sx: {
|
|
2667
|
+
fontSize: 12,
|
|
2668
|
+
color: "text.secondary"
|
|
2669
|
+
},
|
|
2670
|
+
children: "de"
|
|
2671
|
+
}),
|
|
2672
|
+
/* @__PURE__ */ jsx(Box, {
|
|
2673
|
+
sx: { flex: "2 1 140px" },
|
|
2674
|
+
children: /* @__PURE__ */ jsx(NameSelect, {
|
|
2675
|
+
value: scale.column,
|
|
2676
|
+
options: columnOptions,
|
|
2677
|
+
required: true,
|
|
2678
|
+
onChange: (column) => update(index, (current) => ({
|
|
2679
|
+
...current,
|
|
2680
|
+
column
|
|
2681
|
+
}))
|
|
2682
|
+
})
|
|
2683
|
+
}),
|
|
2684
|
+
/* @__PURE__ */ jsx(Box, {
|
|
2685
|
+
title: "Prévia do gradiente",
|
|
2686
|
+
sx: {
|
|
2687
|
+
flex: "1 1 80px",
|
|
2688
|
+
height: 20,
|
|
2689
|
+
border: 1,
|
|
2690
|
+
borderColor: "divider",
|
|
2691
|
+
borderRadius: .5,
|
|
2692
|
+
background: `linear-gradient(to right, ${minColor}, ${scale.color})`
|
|
2693
|
+
}
|
|
2694
|
+
}),
|
|
2695
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
2696
|
+
className: "row-actions",
|
|
2697
|
+
sx: {
|
|
2698
|
+
display: "flex",
|
|
2699
|
+
visibility: "hidden"
|
|
2700
|
+
},
|
|
2701
|
+
children: [
|
|
2702
|
+
/* @__PURE__ */ jsx(SmallButton, {
|
|
2703
|
+
title: "Subir",
|
|
2704
|
+
disabled: index === 0,
|
|
2705
|
+
onClick: () => onChange(moveTo(scales, index, index - 1)),
|
|
2706
|
+
children: "↑"
|
|
2707
|
+
}),
|
|
2708
|
+
/* @__PURE__ */ jsx(SmallButton, {
|
|
2709
|
+
title: "Descer",
|
|
2710
|
+
disabled: index === scales.length - 1,
|
|
2711
|
+
onClick: () => onChange(moveTo(scales, index, index + 2)),
|
|
2712
|
+
children: "↓"
|
|
2713
|
+
}),
|
|
2714
|
+
/* @__PURE__ */ jsx(SmallButton, {
|
|
2715
|
+
title: "Remover",
|
|
2716
|
+
onClick: () => onChange(scales.filter((_, i) => i !== index)),
|
|
2717
|
+
children: "×"
|
|
2718
|
+
})
|
|
2719
|
+
]
|
|
2720
|
+
})
|
|
2721
|
+
]
|
|
2722
|
+
}),
|
|
2723
|
+
scale.column && !columns.includes(scale.column) && /* @__PURE__ */ jsxs(Typography, {
|
|
2724
|
+
sx: {
|
|
2725
|
+
fontSize: 11,
|
|
2726
|
+
color: "warning.main"
|
|
2727
|
+
},
|
|
2728
|
+
children: [
|
|
2729
|
+
"\"",
|
|
2730
|
+
scale.column,
|
|
2731
|
+
"\" não é coluna numérica do gráfico: nada é pintado"
|
|
2732
|
+
]
|
|
2733
|
+
}),
|
|
2734
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
2735
|
+
sx: {
|
|
2736
|
+
display: "grid",
|
|
2737
|
+
gap: .5,
|
|
2738
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(130px, 1fr))",
|
|
2739
|
+
alignItems: "end"
|
|
2740
|
+
},
|
|
2741
|
+
children: [
|
|
2742
|
+
field("Cor mínima", /* @__PURE__ */ jsx(ColorInput, {
|
|
2743
|
+
value: scale.minColor,
|
|
2744
|
+
fallback: DEFAULT_SCALE_MIN_COLOR,
|
|
2745
|
+
onCommit: (color) => update(index, (current) => withField(current, "minColor", color === "#ffffff" ? void 0 : color))
|
|
2746
|
+
})),
|
|
2747
|
+
field("Mínimo", /* @__PURE__ */ jsx(CommitText, {
|
|
2748
|
+
value: scale.min === void 0 ? "" : String(scale.min),
|
|
2749
|
+
placeholder: "menor da coluna",
|
|
2750
|
+
error: inverted,
|
|
2751
|
+
onCommit: (text) => update(index, (current) => withField(current, "min", parseNumber(text)))
|
|
2752
|
+
})),
|
|
2753
|
+
field("Cor máxima", /* @__PURE__ */ jsx(ColorInput, {
|
|
2754
|
+
value: scale.color,
|
|
2755
|
+
fallback: DEFAULT_SCALE_COLOR,
|
|
2756
|
+
onCommit: (color) => update(index, (current) => ({
|
|
2757
|
+
...current,
|
|
2758
|
+
color: color ?? DEFAULT_SCALE_COLOR
|
|
2759
|
+
}))
|
|
2760
|
+
})),
|
|
2761
|
+
field("Máximo", /* @__PURE__ */ jsx(CommitText, {
|
|
2762
|
+
value: scale.max === void 0 ? "" : String(scale.max),
|
|
2763
|
+
placeholder: "maior da coluna",
|
|
2764
|
+
error: inverted,
|
|
2765
|
+
onCommit: (text) => update(index, (current) => withField(current, "max", parseNumber(text)))
|
|
2766
|
+
}))
|
|
2767
|
+
]
|
|
2768
|
+
}),
|
|
2769
|
+
inverted && /* @__PURE__ */ jsx(Typography, {
|
|
2770
|
+
sx: {
|
|
2771
|
+
fontSize: 11,
|
|
2772
|
+
color: "error.main"
|
|
2773
|
+
},
|
|
2774
|
+
children: "O mínimo precisa ser menor que o máximo"
|
|
2775
|
+
})
|
|
2776
|
+
]
|
|
2777
|
+
}, index);
|
|
2778
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2779
|
+
size: "small",
|
|
2780
|
+
variant: "outlined",
|
|
2781
|
+
disabled: !columnOptions.length,
|
|
2782
|
+
onClick: () => onChange([...scales, {
|
|
2783
|
+
column: columns.find((name) => !scales.some((scale) => scale.column === name)) ?? columns[0] ?? "",
|
|
2784
|
+
color: DEFAULT_SCALE_COLOR
|
|
2785
|
+
}]),
|
|
2786
|
+
sx: {
|
|
2787
|
+
alignSelf: "flex-start",
|
|
2788
|
+
textTransform: "none"
|
|
2789
|
+
},
|
|
2790
|
+
children: "+ escala"
|
|
2791
|
+
})]
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2593
2794
|
/** Campo de texto que só manda o valor no Enter ou ao sair (número parcial não vira valor). */
|
|
2594
2795
|
function CommitText({ value, placeholder, error, onCommit }) {
|
|
2595
2796
|
const [typing, setTyping] = useState(null);
|
|
@@ -3036,7 +3237,7 @@ function ColumnOptions({ kind, name, model, label, onLabel, format, onFormat, fo
|
|
|
3036
3237
|
value: formatter,
|
|
3037
3238
|
options: formatters.map((option) => ({ name: option })),
|
|
3038
3239
|
onChange: onFormatter
|
|
3039
|
-
}), "
|
|
3240
|
+
}), "Built-in (to_month_name, to_date, ...) e os passados ao <DashboardCharts formatters={{ ... }}> da página"),
|
|
3040
3241
|
format !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3041
3242
|
/* @__PURE__ */ jsxs(Box, {
|
|
3042
3243
|
sx: {
|
|
@@ -3620,126 +3821,629 @@ function freeName(prefix, taken) {
|
|
|
3620
3821
|
for (let index = 2;; index++) if (!taken.includes(`${prefix}${index}`)) return `${prefix}${index}`;
|
|
3621
3822
|
}
|
|
3622
3823
|
//#endregion
|
|
3623
|
-
//#region src/devtools/
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
*
|
|
3629
|
-
* O destaque é um `<pre>` colorido por baixo do `<textarea>` de texto transparente: mesma fonte,
|
|
3630
|
-
* mesmo recuo e mesma quebra de linha, com a rolagem sincronizada. O cursor, a seleção e o
|
|
3631
|
-
* autocomplete continuam sendo do textarea.
|
|
3632
|
-
*/
|
|
3633
|
-
const PAD = {
|
|
3634
|
-
top: 8.5,
|
|
3635
|
-
right: 30,
|
|
3636
|
-
bottom: 8.5,
|
|
3637
|
-
left: 12
|
|
3638
|
-
};
|
|
3639
|
-
const LINE_HEIGHT = "18px";
|
|
3640
|
-
const SqlCompletionsContext = createContext({
|
|
3641
|
-
tables: [],
|
|
3642
|
-
modelColumns: {}
|
|
3643
|
-
});
|
|
3644
|
-
const KEYWORDS = new Set("WHERE ON LEFT RIGHT INNER OUTER JOIN GROUP ORDER LIMIT USING AND OR AS SET HAVING UNION".split(" "));
|
|
3645
|
-
/** `alias -> tabela` a partir dos `FROM x a` / `JOIN x AS a` do texto. */
|
|
3646
|
-
function aliases(sql) {
|
|
3647
|
-
const map = /* @__PURE__ */ new Map();
|
|
3648
|
-
for (const match of sql.matchAll(/\b(?:FROM|JOIN)\s+`?(\w+)`?(?:\s+(?:AS\s+)?`?(\w+)`?)?/gi)) {
|
|
3649
|
-
const [, table, alias] = match;
|
|
3650
|
-
map.set(table.toLowerCase(), table);
|
|
3651
|
-
if (alias && !KEYWORDS.has(alias.toUpperCase())) map.set(alias.toLowerCase(), table);
|
|
3652
|
-
}
|
|
3653
|
-
return map;
|
|
3824
|
+
//#region src/devtools/QueryBuilder.tsx
|
|
3825
|
+
const RESERVED = new Set("select from where group order by limit key keys index table desc asc and or not in is null like as on join case when then else end interval range rows".split(" "));
|
|
3826
|
+
const quote = (name) => /^[A-Za-z_]\w*$/.test(name) && !RESERVED.has(name.toLowerCase()) ? name : `\`${name}\``;
|
|
3827
|
+
const unquote = (name) => name.trim().replace(/^`(.*)`$/, "$1");
|
|
3828
|
+
function buildSelect(table, columns) {
|
|
3829
|
+
return `SELECT\n${columns.length ? columns.map((column) => ` ${quote(column)}`).join(",\n") : " *"}\nFROM ${quote(table)}`;
|
|
3654
3830
|
}
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
if (qualified) {
|
|
3666
|
-
const [, qualifier, typed] = qualified;
|
|
3667
|
-
prefix = typed;
|
|
3668
|
-
if (completions.modelColumns[qualifier]) candidates = completions.modelColumns[qualifier].map((text) => ({
|
|
3669
|
-
text,
|
|
3670
|
-
detail: qualifier
|
|
3671
|
-
}));
|
|
3672
|
-
else candidates = columnsOf(aliases(sql).get(qualifier.toLowerCase()) ?? qualifier);
|
|
3673
|
-
} else {
|
|
3674
|
-
const word = /[A-Za-z_]\w*$/.exec(before);
|
|
3675
|
-
if (!word || word[0].length < 2) return null;
|
|
3676
|
-
prefix = word[0];
|
|
3677
|
-
const referenced = [...new Set(aliases(sql).values())];
|
|
3678
|
-
candidates = [
|
|
3679
|
-
...Object.keys(completions.modelColumns).map((text) => ({
|
|
3680
|
-
text,
|
|
3681
|
-
detail: "tabela do modelo"
|
|
3682
|
-
})),
|
|
3683
|
-
...completions.tables.map((table) => ({
|
|
3684
|
-
text: table.name,
|
|
3685
|
-
detail: table.type
|
|
3686
|
-
})),
|
|
3687
|
-
...referenced.flatMap(columnsOf)
|
|
3688
|
-
];
|
|
3689
|
-
}
|
|
3690
|
-
const lower = prefix.toLowerCase();
|
|
3691
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3692
|
-
const items = candidates.filter((item) => {
|
|
3693
|
-
const text = item.text.toLowerCase();
|
|
3694
|
-
if (seen.has(text) || text === lower || !text.includes(lower)) return false;
|
|
3695
|
-
seen.add(text);
|
|
3696
|
-
return true;
|
|
3697
|
-
}).sort((a, b) => Number(!a.text.toLowerCase().startsWith(lower)) - Number(!b.text.toLowerCase().startsWith(lower)) || a.text.localeCompare(b.text)).slice(0, 8);
|
|
3698
|
-
return items.length ? {
|
|
3699
|
-
from: caret - prefix.length,
|
|
3700
|
-
items
|
|
3701
|
-
} : null;
|
|
3831
|
+
/** Tabela e colunas de um `SELECT a, b FROM t` (sem WHERE, JOIN etc.); senão `null`. */
|
|
3832
|
+
function parseSelect(sql) {
|
|
3833
|
+
const match = /^\s*SELECT\s+([\s\S]+?)\s+FROM\s+(`[^`]+`|\w+)\s*;?\s*$/i.exec(sql);
|
|
3834
|
+
if (!match) return null;
|
|
3835
|
+
const columns = match[1].trim() === "*" ? [] : match[1].split(",").map(unquote);
|
|
3836
|
+
if (columns.some((column) => !/^[^\s`().]+$/.test(column))) return null;
|
|
3837
|
+
return {
|
|
3838
|
+
table: unquote(match[2]),
|
|
3839
|
+
columns
|
|
3840
|
+
};
|
|
3702
3841
|
}
|
|
3703
|
-
function
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3842
|
+
function QueryBuilder({ open, onClose, tables, sql, onApply }) {
|
|
3843
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
3844
|
+
open,
|
|
3845
|
+
onClose,
|
|
3846
|
+
maxWidth: "md",
|
|
3847
|
+
fullWidth: true,
|
|
3848
|
+
children: open && /* @__PURE__ */ jsx(Builder, {
|
|
3849
|
+
tables,
|
|
3850
|
+
sql,
|
|
3851
|
+
onClose,
|
|
3852
|
+
onApply
|
|
3853
|
+
})
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
function Builder({ tables, sql, onClose, onApply }) {
|
|
3857
|
+
const [initial] = useState(() => {
|
|
3858
|
+
const parsed = parseSelect(sql);
|
|
3859
|
+
const table = parsed && tables.find((item) => item.name === parsed.table);
|
|
3860
|
+
if (!parsed || !table) return {
|
|
3861
|
+
table: null,
|
|
3862
|
+
columns: []
|
|
3863
|
+
};
|
|
3864
|
+
const known = new Set(table.columns.map((column) => column.name));
|
|
3865
|
+
return {
|
|
3866
|
+
table,
|
|
3867
|
+
columns: parsed.columns.filter((column) => known.has(column))
|
|
3868
|
+
};
|
|
3869
|
+
});
|
|
3870
|
+
const [table, setTable] = useState(initial.table);
|
|
3871
|
+
const [selected, setSelected] = useState(initial.columns);
|
|
3872
|
+
const [search, setSearch] = useState("");
|
|
3873
|
+
const visible = useMemo(() => {
|
|
3874
|
+
const lower = search.trim().toLowerCase();
|
|
3875
|
+
return (table?.columns ?? []).filter((column) => column.name.toLowerCase().includes(lower));
|
|
3876
|
+
}, [table, search]);
|
|
3877
|
+
const ordered = (table?.columns ?? []).map((column) => column.name).filter((name) => selected.includes(name));
|
|
3878
|
+
const result = table ? buildSelect(table.name, ordered) : "";
|
|
3879
|
+
const allVisible = visible.length > 0 && visible.every((column) => selected.includes(column.name));
|
|
3880
|
+
const toggle = (name) => setSelected((current) => current.includes(name) ? current.filter((item) => item !== name) : [...current, name]);
|
|
3881
|
+
const toggleVisible = () => {
|
|
3882
|
+
const names = visible.map((column) => column.name);
|
|
3883
|
+
setSelected((current) => allVisible ? current.filter((name) => !names.includes(name)) : [.../* @__PURE__ */ new Set([...current, ...names])]);
|
|
3721
3884
|
};
|
|
3722
3885
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3723
|
-
/* @__PURE__ */ jsx(
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3886
|
+
/* @__PURE__ */ jsx(DialogTitle, {
|
|
3887
|
+
sx: { fontSize: 14 },
|
|
3888
|
+
children: "Criar query"
|
|
3889
|
+
}),
|
|
3890
|
+
/* @__PURE__ */ jsxs(DialogContent, {
|
|
3891
|
+
sx: {
|
|
3892
|
+
display: "flex",
|
|
3893
|
+
flexDirection: "column",
|
|
3894
|
+
gap: 1.5
|
|
3895
|
+
},
|
|
3896
|
+
children: [
|
|
3897
|
+
/* @__PURE__ */ jsx(Autocomplete, {
|
|
3898
|
+
size: "small",
|
|
3899
|
+
options: tables,
|
|
3900
|
+
value: table,
|
|
3901
|
+
onChange: (_, value) => {
|
|
3902
|
+
setTable(value);
|
|
3903
|
+
setSelected([]);
|
|
3904
|
+
setSearch("");
|
|
3905
|
+
},
|
|
3906
|
+
getOptionLabel: (option) => option.name,
|
|
3907
|
+
isOptionEqualToValue: (option, value) => option.name === value.name,
|
|
3908
|
+
noOptionsText: tables.length ? "Nenhuma tabela" : "Carregando as tabelas do banco…",
|
|
3909
|
+
renderOption: ({ key, ...props }, option) => /* @__PURE__ */ jsxs(Box, {
|
|
3910
|
+
component: "li",
|
|
3911
|
+
...props,
|
|
3912
|
+
sx: {
|
|
3913
|
+
gap: 2,
|
|
3914
|
+
fontFamily: MONO,
|
|
3915
|
+
fontSize: 12
|
|
3916
|
+
},
|
|
3917
|
+
children: [option.name, /* @__PURE__ */ jsxs(Box, {
|
|
3918
|
+
component: "span",
|
|
3919
|
+
sx: {
|
|
3920
|
+
ml: "auto",
|
|
3921
|
+
color: "text.secondary",
|
|
3922
|
+
fontSize: 11
|
|
3923
|
+
},
|
|
3924
|
+
children: [
|
|
3925
|
+
option.type,
|
|
3926
|
+
" · ",
|
|
3927
|
+
option.columns.length,
|
|
3928
|
+
" colunas"
|
|
3929
|
+
]
|
|
3930
|
+
})]
|
|
3931
|
+
}, key),
|
|
3932
|
+
renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
|
|
3933
|
+
...params,
|
|
3934
|
+
label: "tabela",
|
|
3935
|
+
sx: { mt: 1 }
|
|
3936
|
+
})
|
|
3937
|
+
}),
|
|
3938
|
+
table && /* @__PURE__ */ jsxs(Box, {
|
|
3939
|
+
sx: {
|
|
3940
|
+
display: "flex",
|
|
3941
|
+
flexDirection: "column",
|
|
3942
|
+
gap: .5
|
|
3943
|
+
},
|
|
3944
|
+
children: [/* @__PURE__ */ jsxs(Box, {
|
|
3945
|
+
sx: {
|
|
3946
|
+
display: "flex",
|
|
3947
|
+
alignItems: "center",
|
|
3948
|
+
gap: 1
|
|
3949
|
+
},
|
|
3950
|
+
children: [
|
|
3951
|
+
/* @__PURE__ */ jsx(FormControlLabel, {
|
|
3952
|
+
control: /* @__PURE__ */ jsx(Checkbox, {
|
|
3953
|
+
size: "small",
|
|
3954
|
+
checked: allVisible,
|
|
3955
|
+
indeterminate: !allVisible && visible.some((c) => selected.includes(c.name)),
|
|
3956
|
+
onChange: toggleVisible
|
|
3957
|
+
}),
|
|
3958
|
+
label: /* @__PURE__ */ jsx(Typography, {
|
|
3959
|
+
sx: { fontSize: 12 },
|
|
3960
|
+
children: "Todas"
|
|
3961
|
+
})
|
|
3962
|
+
}),
|
|
3963
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
3964
|
+
sx: {
|
|
3965
|
+
fontSize: 12,
|
|
3966
|
+
color: "text.secondary"
|
|
3967
|
+
},
|
|
3968
|
+
children: [
|
|
3969
|
+
selected.length,
|
|
3970
|
+
" de ",
|
|
3971
|
+
table.columns.length,
|
|
3972
|
+
" marcadas",
|
|
3973
|
+
!selected.length && " (nenhuma = *)"
|
|
3974
|
+
]
|
|
3975
|
+
}),
|
|
3976
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
3977
|
+
size: "small",
|
|
3978
|
+
placeholder: "filtrar colunas",
|
|
3979
|
+
value: search,
|
|
3980
|
+
onChange: (event) => setSearch(event.target.value),
|
|
3981
|
+
sx: {
|
|
3982
|
+
ml: "auto",
|
|
3983
|
+
width: 200
|
|
3984
|
+
}
|
|
3985
|
+
})
|
|
3986
|
+
]
|
|
3987
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
3988
|
+
sx: {
|
|
3989
|
+
maxHeight: 280,
|
|
3990
|
+
overflow: "auto",
|
|
3991
|
+
border: 1,
|
|
3992
|
+
borderColor: "divider",
|
|
3993
|
+
borderRadius: 1,
|
|
3994
|
+
px: 1,
|
|
3995
|
+
display: "grid",
|
|
3996
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))"
|
|
3997
|
+
},
|
|
3998
|
+
children: visible.map((column) => /* @__PURE__ */ jsx(FormControlLabel, {
|
|
3999
|
+
control: /* @__PURE__ */ jsx(Checkbox, {
|
|
4000
|
+
size: "small",
|
|
4001
|
+
checked: selected.includes(column.name),
|
|
4002
|
+
onChange: () => toggle(column.name)
|
|
4003
|
+
}),
|
|
4004
|
+
label: /* @__PURE__ */ jsxs(Box, {
|
|
4005
|
+
component: "span",
|
|
4006
|
+
sx: {
|
|
4007
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
4008
|
+
fontSize: 12
|
|
4009
|
+
},
|
|
4010
|
+
children: [
|
|
4011
|
+
column.name,
|
|
4012
|
+
" ",
|
|
4013
|
+
/* @__PURE__ */ jsx(Box, {
|
|
4014
|
+
component: "span",
|
|
4015
|
+
sx: {
|
|
4016
|
+
color: "text.secondary",
|
|
4017
|
+
fontSize: 11
|
|
4018
|
+
},
|
|
4019
|
+
children: column.type
|
|
4020
|
+
})
|
|
4021
|
+
]
|
|
4022
|
+
}),
|
|
4023
|
+
sx: {
|
|
4024
|
+
m: 0,
|
|
4025
|
+
minWidth: 0
|
|
4026
|
+
}
|
|
4027
|
+
}, column.name))
|
|
4028
|
+
})]
|
|
4029
|
+
}),
|
|
4030
|
+
result && /* @__PURE__ */ jsx(Box, {
|
|
4031
|
+
component: "pre",
|
|
4032
|
+
sx: {
|
|
4033
|
+
m: 0,
|
|
4034
|
+
p: 1,
|
|
4035
|
+
border: 1,
|
|
4036
|
+
borderColor: "divider",
|
|
4037
|
+
borderRadius: 1,
|
|
4038
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
4039
|
+
fontSize: 12,
|
|
4040
|
+
maxHeight: 200,
|
|
4041
|
+
overflow: "auto"
|
|
4042
|
+
},
|
|
4043
|
+
children: /* @__PURE__ */ jsx(HighlightedSql, { code: result })
|
|
4044
|
+
})
|
|
4045
|
+
]
|
|
4046
|
+
}),
|
|
4047
|
+
/* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
|
|
4048
|
+
onClick: onClose,
|
|
4049
|
+
children: "Cancelar"
|
|
4050
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
4051
|
+
variant: "contained",
|
|
4052
|
+
disabled: !table,
|
|
4053
|
+
onClick: () => {
|
|
4054
|
+
onApply(result);
|
|
4055
|
+
onClose();
|
|
4056
|
+
},
|
|
4057
|
+
title: "Substitui o SQL atual",
|
|
4058
|
+
children: "Usar no SQL"
|
|
4059
|
+
})] })
|
|
4060
|
+
] });
|
|
4061
|
+
}
|
|
4062
|
+
//#endregion
|
|
4063
|
+
//#region src/devtools/queryTemplates.ts
|
|
4064
|
+
/**
|
|
4065
|
+
* `WITH` da origem: a tabela do banco entra pelo nome; o SQL de uma tabela do modelo vira o CTE
|
|
4066
|
+
* `origem`, antes dos CTEs do template.
|
|
4067
|
+
*/
|
|
4068
|
+
function withSource(source) {
|
|
4069
|
+
if (source.kind === "database") return {
|
|
4070
|
+
ctes: [],
|
|
4071
|
+
from: quote(source.table)
|
|
4072
|
+
};
|
|
4073
|
+
return {
|
|
4074
|
+
ctes: [`origem AS (\n${source.sql.trim().replace(/;+\s*$/, "").replace(/^/gm, " ")}\n)`],
|
|
4075
|
+
from: "origem"
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* Um dia por linha, do menor ao maior valor da coluna, com ano e mês. É a tabela "Calendário" do
|
|
4080
|
+
* Power BI: relacione a coluna de data da fato com `dia`.
|
|
4081
|
+
*
|
|
4082
|
+
* Não é um `WITH RECURSIVE` de um dia por iteração: o MySQL (`cte_max_recursion_depth`) e o MariaDB
|
|
4083
|
+
* (`max_recursive_iterations`) param em 1000 iterações, e o MariaDB corta em silêncio — o calendário
|
|
4084
|
+
* acabaria ~2,7 anos depois do início. A recursão só gera 0–999 e o dia sai de `mil * 1000 + n`.
|
|
4085
|
+
*/
|
|
4086
|
+
function calendarSql(source, column) {
|
|
4087
|
+
const { ctes, from } = withSource(source);
|
|
4088
|
+
const col = quote(column);
|
|
4089
|
+
return [
|
|
4090
|
+
`WITH RECURSIVE ${[
|
|
4091
|
+
...ctes,
|
|
4092
|
+
[
|
|
4093
|
+
"limites AS (",
|
|
4094
|
+
` SELECT DATE(MIN(${col})) AS inicio, DATE(MAX(${col})) AS fim`,
|
|
4095
|
+
` FROM ${from}`,
|
|
4096
|
+
")"
|
|
4097
|
+
],
|
|
4098
|
+
[
|
|
4099
|
+
"numeros AS (",
|
|
4100
|
+
" SELECT 0 AS n",
|
|
4101
|
+
" UNION ALL",
|
|
4102
|
+
" SELECT n + 1 FROM numeros WHERE n < 999",
|
|
4103
|
+
")"
|
|
4104
|
+
],
|
|
4105
|
+
[
|
|
4106
|
+
"dias AS (",
|
|
4107
|
+
" SELECT inicio + INTERVAL (mil.n * 1000 + numeros.n) DAY AS dia",
|
|
4108
|
+
" FROM limites, numeros mil, numeros",
|
|
4109
|
+
" WHERE mil.n <= DATEDIFF(fim, inicio) DIV 1000",
|
|
4110
|
+
" AND mil.n * 1000 + numeros.n <= DATEDIFF(fim, inicio)",
|
|
4111
|
+
")"
|
|
4112
|
+
]
|
|
4113
|
+
].map((part) => Array.isArray(part) ? part.join("\n") : part).join(",\n")}`,
|
|
4114
|
+
"SELECT",
|
|
4115
|
+
" dia,",
|
|
4116
|
+
" YEAR(dia) AS ano,",
|
|
4117
|
+
" MONTH(dia) AS mes",
|
|
4118
|
+
"FROM dias"
|
|
4119
|
+
].join("\n");
|
|
4120
|
+
}
|
|
4121
|
+
const QUERY_TEMPLATES = [{
|
|
4122
|
+
id: "calendar",
|
|
4123
|
+
label: "Calendário",
|
|
4124
|
+
description: "Um dia por linha (com ano e mês) entre a menor e a maior data da coluna. Relacione a data da fato com `dia`.",
|
|
4125
|
+
columnLabel: "coluna de data",
|
|
4126
|
+
preferredTypes: /date|time/i,
|
|
4127
|
+
build: calendarSql
|
|
4128
|
+
}];
|
|
4129
|
+
//#endregion
|
|
4130
|
+
//#region src/devtools/QueryTemplates.tsx
|
|
4131
|
+
function QueryTemplates({ open, onClose, ...props }) {
|
|
4132
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
4133
|
+
open,
|
|
4134
|
+
onClose,
|
|
4135
|
+
maxWidth: "md",
|
|
4136
|
+
fullWidth: true,
|
|
4137
|
+
children: open && /* @__PURE__ */ jsx(Templates, {
|
|
4138
|
+
onClose,
|
|
4139
|
+
...props
|
|
4140
|
+
})
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4143
|
+
function Templates({ tables, modelTables, onClose, onApply }) {
|
|
4144
|
+
const [templateId, setTemplateId] = useState(QUERY_TEMPLATES[0].id);
|
|
4145
|
+
const [source, setSource] = useState(null);
|
|
4146
|
+
const [column, setColumn] = useState(null);
|
|
4147
|
+
const template = QUERY_TEMPLATES.find((item) => item.id === templateId) ?? QUERY_TEMPLATES[0];
|
|
4148
|
+
const sources = useMemo(() => [...Object.entries(modelTables).map(([name, table]) => ({
|
|
4149
|
+
key: `model:${name}`,
|
|
4150
|
+
name,
|
|
4151
|
+
group: "Tabelas do modelo",
|
|
4152
|
+
columns: table.columns.map((column) => ({ name: column })),
|
|
4153
|
+
source: {
|
|
4154
|
+
kind: "model",
|
|
4155
|
+
sql: table.sql
|
|
4156
|
+
}
|
|
4157
|
+
})), ...tables.map((table) => ({
|
|
4158
|
+
key: `database:${table.name}`,
|
|
4159
|
+
name: table.name,
|
|
4160
|
+
group: "Tabelas do banco",
|
|
4161
|
+
columns: table.columns,
|
|
4162
|
+
source: {
|
|
4163
|
+
kind: "database",
|
|
4164
|
+
table: table.name
|
|
4165
|
+
}
|
|
4166
|
+
}))], [tables, modelTables]);
|
|
4167
|
+
const columns = useMemo(() => {
|
|
4168
|
+
const list = source?.columns ?? [];
|
|
4169
|
+
const preferred = (type) => !!type && !!template.preferredTypes?.test(type);
|
|
4170
|
+
return [...list.filter((c) => preferred(c.type)), ...list.filter((c) => !preferred(c.type))];
|
|
4171
|
+
}, [source, template]);
|
|
4172
|
+
const result = source && column ? template.build(source.source, column) : "";
|
|
4173
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4174
|
+
/* @__PURE__ */ jsx(DialogTitle, {
|
|
4175
|
+
sx: { fontSize: 14 },
|
|
4176
|
+
children: "Importar template"
|
|
4177
|
+
}),
|
|
4178
|
+
/* @__PURE__ */ jsxs(DialogContent, {
|
|
4179
|
+
sx: {
|
|
4180
|
+
display: "flex",
|
|
4181
|
+
flexDirection: "column",
|
|
4182
|
+
gap: 1.5
|
|
4183
|
+
},
|
|
4184
|
+
children: [
|
|
4185
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
4186
|
+
select: true,
|
|
4187
|
+
size: "small",
|
|
4188
|
+
label: "template",
|
|
4189
|
+
value: template.id,
|
|
4190
|
+
onChange: (event) => setTemplateId(event.target.value),
|
|
4191
|
+
helperText: template.description,
|
|
4192
|
+
sx: { mt: 1 },
|
|
4193
|
+
children: QUERY_TEMPLATES.map((item) => /* @__PURE__ */ jsx(MenuItem, {
|
|
4194
|
+
value: item.id,
|
|
4195
|
+
children: item.label
|
|
4196
|
+
}, item.id))
|
|
4197
|
+
}),
|
|
4198
|
+
/* @__PURE__ */ jsx(Autocomplete, {
|
|
4199
|
+
size: "small",
|
|
4200
|
+
options: sources,
|
|
4201
|
+
value: source,
|
|
4202
|
+
onChange: (_, value) => {
|
|
4203
|
+
setSource(value);
|
|
4204
|
+
setColumn(null);
|
|
4205
|
+
},
|
|
4206
|
+
groupBy: (option) => option.group,
|
|
4207
|
+
getOptionLabel: (option) => option.name,
|
|
4208
|
+
isOptionEqualToValue: (option, value) => option.key === value.key,
|
|
4209
|
+
noOptionsText: "Nenhuma tabela",
|
|
4210
|
+
renderOption: ({ key, ...props }, option) => /* @__PURE__ */ jsxs(Box, {
|
|
4211
|
+
component: "li",
|
|
4212
|
+
...props,
|
|
4213
|
+
sx: {
|
|
4214
|
+
gap: 2,
|
|
4215
|
+
fontFamily: MONO,
|
|
4216
|
+
fontSize: 12
|
|
4217
|
+
},
|
|
4218
|
+
children: [option.name, /* @__PURE__ */ jsxs(Box, {
|
|
4219
|
+
component: "span",
|
|
4220
|
+
sx: {
|
|
4221
|
+
ml: "auto",
|
|
4222
|
+
color: "text.secondary",
|
|
4223
|
+
fontSize: 11
|
|
4224
|
+
},
|
|
4225
|
+
children: [option.columns.length, " colunas"]
|
|
4226
|
+
})]
|
|
4227
|
+
}, key),
|
|
4228
|
+
renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
|
|
4229
|
+
...params,
|
|
4230
|
+
label: "tabela"
|
|
4231
|
+
})
|
|
4232
|
+
}),
|
|
4233
|
+
source && /* @__PURE__ */ jsx(Autocomplete, {
|
|
4234
|
+
size: "small",
|
|
4235
|
+
options: columns.map((item) => item.name),
|
|
4236
|
+
value: column,
|
|
4237
|
+
onChange: (_, value) => setColumn(value),
|
|
4238
|
+
noOptionsText: source.source.kind === "model" ? "Sem colunas: valide o SQL dessa tabela" : "Nenhuma coluna",
|
|
4239
|
+
renderOption: ({ key, ...props }, name) => /* @__PURE__ */ jsxs(Box, {
|
|
4240
|
+
component: "li",
|
|
4241
|
+
...props,
|
|
4242
|
+
sx: {
|
|
4243
|
+
gap: 2,
|
|
4244
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
4245
|
+
fontSize: 12
|
|
4246
|
+
},
|
|
4247
|
+
children: [name, /* @__PURE__ */ jsx(Box, {
|
|
4248
|
+
component: "span",
|
|
4249
|
+
sx: {
|
|
4250
|
+
ml: "auto",
|
|
4251
|
+
color: "text.secondary",
|
|
4252
|
+
fontSize: 11
|
|
4253
|
+
},
|
|
4254
|
+
children: columns.find((item) => item.name === name)?.type
|
|
4255
|
+
})]
|
|
4256
|
+
}, key),
|
|
4257
|
+
renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
|
|
4258
|
+
...params,
|
|
4259
|
+
label: template.columnLabel
|
|
4260
|
+
})
|
|
4261
|
+
}),
|
|
4262
|
+
result ? /* @__PURE__ */ jsx(Box, {
|
|
4263
|
+
component: "pre",
|
|
4264
|
+
sx: {
|
|
4265
|
+
m: 0,
|
|
4266
|
+
p: 1,
|
|
4267
|
+
border: 1,
|
|
4268
|
+
borderColor: "divider",
|
|
4269
|
+
borderRadius: 1,
|
|
4270
|
+
fontFamily: MONO,
|
|
4271
|
+
fontSize: 12,
|
|
4272
|
+
maxHeight: 280,
|
|
4273
|
+
overflow: "auto"
|
|
4274
|
+
},
|
|
4275
|
+
children: /* @__PURE__ */ jsx(HighlightedSql, { code: result })
|
|
4276
|
+
}) : /* @__PURE__ */ jsx(Typography, {
|
|
4277
|
+
sx: {
|
|
4278
|
+
fontSize: 12,
|
|
4279
|
+
color: "text.secondary"
|
|
4280
|
+
},
|
|
4281
|
+
children: "Escolha a tabela e a coluna para ver o SQL."
|
|
4282
|
+
})
|
|
4283
|
+
]
|
|
4284
|
+
}),
|
|
4285
|
+
/* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
|
|
4286
|
+
onClick: onClose,
|
|
4287
|
+
children: "Cancelar"
|
|
4288
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
4289
|
+
variant: "contained",
|
|
4290
|
+
disabled: !result,
|
|
4291
|
+
onClick: () => {
|
|
4292
|
+
onApply(result);
|
|
4293
|
+
onClose();
|
|
4294
|
+
},
|
|
4295
|
+
title: "Substitui o SQL atual",
|
|
4296
|
+
children: "Usar no SQL"
|
|
4297
|
+
})] })
|
|
4298
|
+
] });
|
|
4299
|
+
}
|
|
4300
|
+
//#endregion
|
|
4301
|
+
//#region src/devtools/SqlEditor.tsx
|
|
4302
|
+
/**
|
|
4303
|
+
* Campo de SQL com destaque de sintaxe e autocomplete de tabelas e colunas. As colunas vêm do
|
|
4304
|
+
* `information_schema` (tabelas/views do banco) e das colunas de cada tabela do modelo (lidas do banco).
|
|
4305
|
+
* O ⤢ abre o mesmo SQL num editor grande; com `queryTools`, ele traz "Criar query" (escolher tabela e
|
|
4306
|
+
* colunas do banco), "Importar template" (calendário etc.) e "Validar SQL" (executa no banco e
|
|
4307
|
+
* mostra uma amostra).
|
|
4308
|
+
*
|
|
4309
|
+
* O destaque é um `<pre>` colorido por baixo do `<textarea>` de texto transparente: mesma fonte,
|
|
4310
|
+
* mesmo recuo e mesma quebra de linha, com a rolagem sincronizada. O cursor, a seleção e o
|
|
4311
|
+
* autocomplete continuam sendo do textarea.
|
|
4312
|
+
*/
|
|
4313
|
+
const PAD = {
|
|
4314
|
+
top: 8.5,
|
|
4315
|
+
right: 30,
|
|
4316
|
+
bottom: 8.5,
|
|
4317
|
+
left: 12
|
|
4318
|
+
};
|
|
4319
|
+
const LINE_HEIGHT = "18px";
|
|
4320
|
+
const SqlCompletionsContext = createContext({
|
|
4321
|
+
tables: [],
|
|
4322
|
+
modelColumns: {}
|
|
4323
|
+
});
|
|
4324
|
+
const KEYWORDS = new Set("WHERE ON LEFT RIGHT INNER OUTER JOIN GROUP ORDER LIMIT USING AND OR AS SET HAVING UNION".split(" "));
|
|
4325
|
+
/** `alias -> tabela` a partir dos `FROM x a` / `JOIN x AS a` do texto. */
|
|
4326
|
+
function aliases(sql) {
|
|
4327
|
+
const map = /* @__PURE__ */ new Map();
|
|
4328
|
+
for (const match of sql.matchAll(/\b(?:FROM|JOIN)\s+`?(\w+)`?(?:\s+(?:AS\s+)?`?(\w+)`?)?/gi)) {
|
|
4329
|
+
const [, table, alias] = match;
|
|
4330
|
+
map.set(table.toLowerCase(), table);
|
|
4331
|
+
if (alias && !KEYWORDS.has(alias.toUpperCase())) map.set(alias.toLowerCase(), table);
|
|
4332
|
+
}
|
|
4333
|
+
return map;
|
|
4334
|
+
}
|
|
4335
|
+
function suggest(sql, caret, completions) {
|
|
4336
|
+
const before = sql.slice(0, caret);
|
|
4337
|
+
const tables = new Map(completions.tables.map((table) => [table.name.toLowerCase(), table]));
|
|
4338
|
+
const columnsOf = (table) => tables.get(table.toLowerCase())?.columns.map((column) => ({
|
|
4339
|
+
text: column.name,
|
|
4340
|
+
detail: column.type
|
|
4341
|
+
})) ?? [];
|
|
4342
|
+
let prefix;
|
|
4343
|
+
let candidates;
|
|
4344
|
+
const qualified = /([A-Za-z_]\w*)\.(\w*)$/.exec(before);
|
|
4345
|
+
if (qualified) {
|
|
4346
|
+
const [, qualifier, typed] = qualified;
|
|
4347
|
+
prefix = typed;
|
|
4348
|
+
if (completions.modelColumns[qualifier]) candidates = completions.modelColumns[qualifier].map((text) => ({
|
|
4349
|
+
text,
|
|
4350
|
+
detail: qualifier
|
|
4351
|
+
}));
|
|
4352
|
+
else candidates = columnsOf(aliases(sql).get(qualifier.toLowerCase()) ?? qualifier);
|
|
4353
|
+
} else {
|
|
4354
|
+
const word = /[A-Za-z_]\w*$/.exec(before);
|
|
4355
|
+
if (!word || word[0].length < 2) return null;
|
|
4356
|
+
prefix = word[0];
|
|
4357
|
+
const referenced = [...new Set(aliases(sql).values())];
|
|
4358
|
+
candidates = [
|
|
4359
|
+
...Object.keys(completions.modelColumns).map((text) => ({
|
|
4360
|
+
text,
|
|
4361
|
+
detail: "tabela do modelo"
|
|
4362
|
+
})),
|
|
4363
|
+
...completions.tables.map((table) => ({
|
|
4364
|
+
text: table.name,
|
|
4365
|
+
detail: table.type
|
|
4366
|
+
})),
|
|
4367
|
+
...referenced.flatMap(columnsOf)
|
|
4368
|
+
];
|
|
4369
|
+
}
|
|
4370
|
+
const lower = prefix.toLowerCase();
|
|
4371
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4372
|
+
const items = candidates.filter((item) => {
|
|
4373
|
+
const text = item.text.toLowerCase();
|
|
4374
|
+
if (seen.has(text) || text === lower || !text.includes(lower)) return false;
|
|
4375
|
+
seen.add(text);
|
|
4376
|
+
return true;
|
|
4377
|
+
}).sort((a, b) => Number(!a.text.toLowerCase().startsWith(lower)) - Number(!b.text.toLowerCase().startsWith(lower)) || a.text.localeCompare(b.text)).slice(0, 8);
|
|
4378
|
+
return items.length ? {
|
|
4379
|
+
from: caret - prefix.length,
|
|
4380
|
+
items
|
|
4381
|
+
} : null;
|
|
4382
|
+
}
|
|
4383
|
+
function SqlEditor({ value, onChange, label, placeholder = "SQL", minRows, maxRows = 6, error, warning, expandable = true, queryTools = false }) {
|
|
4384
|
+
const completions = useContext(SqlCompletionsContext);
|
|
4385
|
+
const anchor = useRef(null);
|
|
4386
|
+
const input = useRef(null);
|
|
4387
|
+
const highlight = useRef(null);
|
|
4388
|
+
const [expanded, setExpanded] = useState(false);
|
|
4389
|
+
const [building, setBuilding] = useState(false);
|
|
4390
|
+
const [importing, setImporting] = useState(false);
|
|
4391
|
+
const [test, setTest] = useState(null);
|
|
4392
|
+
const [testing, setTesting] = useState(false);
|
|
4393
|
+
const currentTest = test?.sql === value ? test : null;
|
|
4394
|
+
const testSql = completions.testSql;
|
|
4395
|
+
const runTest = async () => {
|
|
4396
|
+
if (!testSql) return;
|
|
4397
|
+
const sql = value;
|
|
4398
|
+
setTesting(true);
|
|
4399
|
+
try {
|
|
4400
|
+
setTest({
|
|
4401
|
+
sql,
|
|
4402
|
+
result: await testSql(sql)
|
|
4403
|
+
});
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
setTest({
|
|
4406
|
+
sql,
|
|
4407
|
+
error: errorMessage(error)
|
|
4408
|
+
});
|
|
4409
|
+
} finally {
|
|
4410
|
+
setTesting(false);
|
|
4411
|
+
}
|
|
4412
|
+
};
|
|
4413
|
+
const [state, setState] = useState(null);
|
|
4414
|
+
const accept = (item) => {
|
|
4415
|
+
const element = input.current;
|
|
4416
|
+
if (!element || !state) return;
|
|
4417
|
+
const caret = element.selectionStart;
|
|
4418
|
+
onChange(value.slice(0, state.from) + item.text + value.slice(caret));
|
|
4419
|
+
setState(null);
|
|
4420
|
+
const position = state.from + item.text.length;
|
|
4421
|
+
requestAnimationFrame(() => {
|
|
4422
|
+
element.focus();
|
|
4423
|
+
element.setSelectionRange(position, position);
|
|
4424
|
+
});
|
|
4425
|
+
};
|
|
4426
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4427
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
4428
|
+
ref: anchor,
|
|
4429
|
+
inputRef: input,
|
|
4430
|
+
size: "small",
|
|
4431
|
+
fullWidth: true,
|
|
4432
|
+
multiline: true,
|
|
4433
|
+
label,
|
|
4434
|
+
minRows,
|
|
4435
|
+
maxRows,
|
|
4436
|
+
value,
|
|
4437
|
+
placeholder,
|
|
4438
|
+
error: !value.trim() || !!error,
|
|
4439
|
+
helperText: error ?? warning,
|
|
4440
|
+
onChange: (event) => {
|
|
4441
|
+
onChange(event.target.value);
|
|
4442
|
+
const found = suggest(event.target.value, event.target.selectionStart ?? 0, completions);
|
|
4443
|
+
setState(found && {
|
|
4444
|
+
...found,
|
|
4445
|
+
index: 0
|
|
4446
|
+
});
|
|
3743
4447
|
},
|
|
3744
4448
|
onKeyDown: (event) => {
|
|
3745
4449
|
if (!state) return;
|
|
@@ -3845,25 +4549,90 @@ function SqlEditor({ value, onChange, label, placeholder = "SQL", minRows, maxRo
|
|
|
3845
4549
|
sx: { fontSize: 14 },
|
|
3846
4550
|
children: label ?? "SQL"
|
|
3847
4551
|
}),
|
|
3848
|
-
/* @__PURE__ */
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
4552
|
+
/* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
4553
|
+
/* @__PURE__ */ jsx(Box, {
|
|
4554
|
+
sx: { pt: 1 },
|
|
4555
|
+
children: /* @__PURE__ */ jsx(SqlEditor, {
|
|
4556
|
+
value,
|
|
4557
|
+
onChange,
|
|
4558
|
+
placeholder,
|
|
4559
|
+
minRows: 20,
|
|
4560
|
+
maxRows: 36,
|
|
4561
|
+
error,
|
|
4562
|
+
warning,
|
|
4563
|
+
expandable: false
|
|
4564
|
+
})
|
|
4565
|
+
}),
|
|
4566
|
+
currentTest?.error && /* @__PURE__ */ jsx(Alert, {
|
|
4567
|
+
severity: "error",
|
|
4568
|
+
sx: {
|
|
4569
|
+
mt: 1,
|
|
4570
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
4571
|
+
fontSize: 12
|
|
4572
|
+
},
|
|
4573
|
+
children: currentTest.error
|
|
4574
|
+
}),
|
|
4575
|
+
currentTest?.result && /* @__PURE__ */ jsxs(Box, {
|
|
4576
|
+
sx: {
|
|
4577
|
+
mt: 1,
|
|
4578
|
+
display: "flex",
|
|
4579
|
+
flexDirection: "column",
|
|
4580
|
+
gap: 1
|
|
4581
|
+
},
|
|
4582
|
+
children: [/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Chip, {
|
|
4583
|
+
color: "success",
|
|
4584
|
+
size: "small",
|
|
4585
|
+
label: `SQL válido · ${currentTest.result.columns.length} colunas · ${currentTest.result.durationMs} ms`
|
|
4586
|
+
}) }), /* @__PURE__ */ jsx(ResultTable, {
|
|
4587
|
+
rows: currentTest.result.rows,
|
|
4588
|
+
maxHeight: 240
|
|
4589
|
+
})]
|
|
3859
4590
|
})
|
|
3860
|
-
|
|
3861
|
-
/* @__PURE__ */
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
4591
|
+
] }),
|
|
4592
|
+
/* @__PURE__ */ jsxs(DialogActions, { children: [
|
|
4593
|
+
queryTools && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4594
|
+
/* @__PURE__ */ jsx(Button, {
|
|
4595
|
+
onClick: () => setBuilding(true),
|
|
4596
|
+
title: "Escolher a tabela e as colunas",
|
|
4597
|
+
children: "Criar query"
|
|
4598
|
+
}),
|
|
4599
|
+
/* @__PURE__ */ jsx(Button, {
|
|
4600
|
+
onClick: () => setImporting(true),
|
|
4601
|
+
title: "Calendário e outras queries prontas",
|
|
4602
|
+
children: "Importar template"
|
|
4603
|
+
}),
|
|
4604
|
+
testSql && /* @__PURE__ */ jsx(Button, {
|
|
4605
|
+
disabled: !value.trim() || testing,
|
|
4606
|
+
onClick: runTest,
|
|
4607
|
+
title: "Executa o SQL no banco e mostra as primeiras linhas",
|
|
4608
|
+
children: testing ? "Validando…" : "Validar SQL"
|
|
4609
|
+
})
|
|
4610
|
+
] }),
|
|
4611
|
+
/* @__PURE__ */ jsx(Box, { sx: { flex: 1 } }),
|
|
4612
|
+
/* @__PURE__ */ jsx(Button, {
|
|
4613
|
+
onClick: () => setExpanded(false),
|
|
4614
|
+
children: "Fechar"
|
|
4615
|
+
})
|
|
4616
|
+
] })
|
|
3865
4617
|
]
|
|
3866
4618
|
}),
|
|
4619
|
+
queryTools && /* @__PURE__ */ jsx(QueryBuilder, {
|
|
4620
|
+
open: building,
|
|
4621
|
+
onClose: () => setBuilding(false),
|
|
4622
|
+
tables: completions.tables,
|
|
4623
|
+
sql: value,
|
|
4624
|
+
onApply: onChange
|
|
4625
|
+
}),
|
|
4626
|
+
queryTools && /* @__PURE__ */ jsx(QueryTemplates, {
|
|
4627
|
+
open: importing,
|
|
4628
|
+
onClose: () => setImporting(false),
|
|
4629
|
+
tables: completions.tables,
|
|
4630
|
+
modelTables: Object.fromEntries(Object.entries(completions.modelSql ?? {}).map(([name, sql]) => [name, {
|
|
4631
|
+
sql,
|
|
4632
|
+
columns: completions.modelColumns[name] ?? []
|
|
4633
|
+
}])),
|
|
4634
|
+
onApply: onChange
|
|
4635
|
+
}),
|
|
3867
4636
|
/* @__PURE__ */ jsx(Popper, {
|
|
3868
4637
|
open: !!state,
|
|
3869
4638
|
anchorEl: anchor.current,
|
|
@@ -4323,6 +5092,28 @@ function DimensionSource({ row, editor, columns, error, warning, onPatch }) {
|
|
|
4323
5092
|
}
|
|
4324
5093
|
//#endregion
|
|
4325
5094
|
//#region src/devtools/ModelDiagram.tsx
|
|
5095
|
+
const TARGET_NAME = {
|
|
5096
|
+
table: "tabela",
|
|
5097
|
+
dimension: "dimensão",
|
|
5098
|
+
measure: "medida"
|
|
5099
|
+
};
|
|
5100
|
+
/** Põe uma medida calculada (fórmula) nova, com nome livre, no fim; devolve o `uid` dela. */
|
|
5101
|
+
function addFormulaTo(setEditor) {
|
|
5102
|
+
const uid = newUid();
|
|
5103
|
+
setEditor((current) => {
|
|
5104
|
+
const taken = [...current.dimensions, ...current.measures].map((item) => item.name);
|
|
5105
|
+
const measure = emptyMeasure({
|
|
5106
|
+
uid,
|
|
5107
|
+
type: "formula",
|
|
5108
|
+
name: freeName("novaMedida", taken)
|
|
5109
|
+
});
|
|
5110
|
+
return {
|
|
5111
|
+
...current,
|
|
5112
|
+
measures: [...current.measures, measure]
|
|
5113
|
+
};
|
|
5114
|
+
});
|
|
5115
|
+
return uid;
|
|
5116
|
+
}
|
|
4326
5117
|
const CARD_WIDTH = 230;
|
|
4327
5118
|
const HEADER = 34;
|
|
4328
5119
|
const ROW = 22;
|
|
@@ -4489,6 +5280,12 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
|
|
|
4489
5280
|
});
|
|
4490
5281
|
const [selection, setSelection] = useState(null);
|
|
4491
5282
|
const [panel, setPanel] = useState(loadPanel);
|
|
5283
|
+
const [renaming, setRenaming] = useState(null);
|
|
5284
|
+
const [menu, setMenu] = useState(null);
|
|
5285
|
+
const closeMenu = () => setMenu((current) => current && {
|
|
5286
|
+
...current,
|
|
5287
|
+
open: false
|
|
5288
|
+
});
|
|
4492
5289
|
useEffect(() => {
|
|
4493
5290
|
try {
|
|
4494
5291
|
localStorage.setItem(PANEL_KEY, JSON.stringify(panel));
|
|
@@ -4609,15 +5406,16 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
|
|
|
4609
5406
|
const rect = canvas.current?.getBoundingClientRect();
|
|
4610
5407
|
return toWorld((rect?.left ?? 0) + (rect?.width ?? 600) / 2 - CARD_WIDTH / 2, (rect?.top ?? 0) + (rect?.height ?? 400) / 3);
|
|
4611
5408
|
};
|
|
4612
|
-
|
|
5409
|
+
/** `at`: canto do card (o ponto do botão direito); sem ele, perto do centro da tela. */
|
|
5410
|
+
const addTable = (at) => {
|
|
4613
5411
|
const uid = newUid();
|
|
4614
5412
|
const name = freeName("novaTabela", editor.tables.map((table) => table.name));
|
|
4615
|
-
const
|
|
5413
|
+
const position = at ?? visibleCenter();
|
|
4616
5414
|
setEditor((current) => ({
|
|
4617
5415
|
...current,
|
|
4618
5416
|
positions: {
|
|
4619
5417
|
...current.positions,
|
|
4620
|
-
[uid]:
|
|
5418
|
+
[uid]: position
|
|
4621
5419
|
},
|
|
4622
5420
|
tables: [...current.tables, {
|
|
4623
5421
|
uid,
|
|
@@ -4633,6 +5431,20 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
|
|
|
4633
5431
|
uid
|
|
4634
5432
|
});
|
|
4635
5433
|
};
|
|
5434
|
+
const organize = () => {
|
|
5435
|
+
const layout = autoLayout(nodes, sizes);
|
|
5436
|
+
setPositions((current) => Object.fromEntries(Object.entries(layout).map(([id, point]) => [id, {
|
|
5437
|
+
...current[id],
|
|
5438
|
+
...point
|
|
5439
|
+
}])));
|
|
5440
|
+
fitToScreen(layout);
|
|
5441
|
+
};
|
|
5442
|
+
const addFormula = () => {
|
|
5443
|
+
setSelection({
|
|
5444
|
+
kind: "measure",
|
|
5445
|
+
uid: addFormulaTo(setEditor)
|
|
5446
|
+
});
|
|
5447
|
+
};
|
|
4636
5448
|
const removeTable = (uid) => {
|
|
4637
5449
|
setEditor((current) => ({
|
|
4638
5450
|
...current,
|
|
@@ -4641,6 +5453,19 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
|
|
|
4641
5453
|
}));
|
|
4642
5454
|
setSelection(null);
|
|
4643
5455
|
};
|
|
5456
|
+
const remove = (target) => {
|
|
5457
|
+
if (target.kind === "table") return removeTable(target.uid);
|
|
5458
|
+
const list = {
|
|
5459
|
+
relationship: "relationships",
|
|
5460
|
+
dimension: "dimensions",
|
|
5461
|
+
measure: "measures"
|
|
5462
|
+
}[target.kind];
|
|
5463
|
+
setEditor((current) => ({
|
|
5464
|
+
...current,
|
|
5465
|
+
[list]: current[list].filter((row) => row.uid !== target.uid)
|
|
5466
|
+
}));
|
|
5467
|
+
setSelection((current) => current?.uid === target.uid ? null : current);
|
|
5468
|
+
};
|
|
4644
5469
|
const commitRename = (uid, name) => setEditor((current) => renameTable(current, uid, name));
|
|
4645
5470
|
const connect = (from, fromColumn, to, toColumn) => {
|
|
4646
5471
|
if (from.uid === to.uid) return;
|
|
@@ -4701,20 +5526,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
|
|
|
4701
5526
|
});
|
|
4702
5527
|
};
|
|
4703
5528
|
const removeSelection = () => {
|
|
4704
|
-
if (
|
|
4705
|
-
if (selection.kind === "table") removeTable(selection.uid);
|
|
4706
|
-
else {
|
|
4707
|
-
const list = {
|
|
4708
|
-
relationship: "relationships",
|
|
4709
|
-
dimension: "dimensions",
|
|
4710
|
-
measure: "measures"
|
|
4711
|
-
}[selection.kind];
|
|
4712
|
-
setEditor((current) => ({
|
|
4713
|
-
...current,
|
|
4714
|
-
[list]: current[list].filter((row) => row.uid !== selection.uid)
|
|
4715
|
-
}));
|
|
4716
|
-
setSelection(null);
|
|
4717
|
-
}
|
|
5529
|
+
if (selection) remove(selection);
|
|
4718
5530
|
};
|
|
4719
5531
|
const startPan = (event) => {
|
|
4720
5532
|
if (event.button !== 0) return;
|
|
@@ -4902,305 +5714,392 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
|
|
|
4902
5714
|
height: "100%",
|
|
4903
5715
|
minHeight: 0
|
|
4904
5716
|
},
|
|
4905
|
-
children: [
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
}
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
5717
|
+
children: [
|
|
5718
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
5719
|
+
ref: canvas,
|
|
5720
|
+
tabIndex: -1,
|
|
5721
|
+
onMouseDown: startPan,
|
|
5722
|
+
onMouseDownCapture: () => canvas.current?.focus({ preventScroll: true }),
|
|
5723
|
+
onKeyDown: (event) => {
|
|
5724
|
+
if ((event.key === "Delete" || event.key === "Backspace") && event.target === canvas.current) removeSelection();
|
|
5725
|
+
},
|
|
5726
|
+
onWheel,
|
|
5727
|
+
onContextMenu: (event) => {
|
|
5728
|
+
event.preventDefault();
|
|
5729
|
+
setMenu({
|
|
5730
|
+
open: true,
|
|
5731
|
+
x: event.clientX,
|
|
5732
|
+
y: event.clientY,
|
|
5733
|
+
target: null
|
|
5734
|
+
});
|
|
5735
|
+
},
|
|
5736
|
+
sx: {
|
|
5737
|
+
outline: "none",
|
|
5738
|
+
flex: 1,
|
|
5739
|
+
position: "relative",
|
|
5740
|
+
overflow: "hidden",
|
|
5741
|
+
cursor: "grab",
|
|
5742
|
+
bgcolor: "#15171b",
|
|
5743
|
+
backgroundImage: "radial-gradient(rgba(255,255,255,.07) 1px, transparent 1px)",
|
|
5744
|
+
backgroundSize: `${20 * view.zoom}px ${20 * view.zoom}px`,
|
|
5745
|
+
backgroundPosition: `${view.x}px ${view.y}px`
|
|
5746
|
+
},
|
|
5747
|
+
children: [
|
|
5748
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
5749
|
+
sx: {
|
|
5750
|
+
position: "absolute",
|
|
5751
|
+
left: 0,
|
|
5752
|
+
top: 0,
|
|
5753
|
+
transformOrigin: "0 0",
|
|
5754
|
+
transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`
|
|
5755
|
+
},
|
|
5756
|
+
children: [/* @__PURE__ */ jsxs("svg", {
|
|
5757
|
+
width: 1,
|
|
5758
|
+
height: 1,
|
|
5759
|
+
style: {
|
|
5760
|
+
position: "absolute",
|
|
5761
|
+
overflow: "visible",
|
|
5762
|
+
pointerEvents: "none"
|
|
5763
|
+
},
|
|
5764
|
+
"aria-hidden": true,
|
|
5765
|
+
children: [lines.map((line) => /* @__PURE__ */ jsx(RelationshipLine, {
|
|
5766
|
+
line,
|
|
5767
|
+
hovered: hoveredEdge === line.id,
|
|
5768
|
+
onHover: (hovered) => setHoveredEdge(hovered ? line.id : null)
|
|
5769
|
+
}, line.id)), linkPreview && /* @__PURE__ */ jsx("path", {
|
|
5770
|
+
d: linkPreview,
|
|
5771
|
+
fill: "none",
|
|
5772
|
+
stroke: "#F58220",
|
|
5773
|
+
strokeWidth: 2,
|
|
5774
|
+
strokeDasharray: "5 4"
|
|
5775
|
+
})]
|
|
5776
|
+
}), nodes.map((node) => /* @__PURE__ */ jsx(TableCard, {
|
|
5777
|
+
node,
|
|
5778
|
+
position: placed(node),
|
|
5779
|
+
size: sizeOf(node),
|
|
5780
|
+
collapsed: !!collapsed[node.id],
|
|
5781
|
+
selected: !!isSelected(node),
|
|
5782
|
+
linking: !!linking && linking.node.id !== node.id,
|
|
5783
|
+
selectedField: selection?.kind === "dimension" || selection?.kind === "measure" ? selection.uid : null,
|
|
5784
|
+
onToggle: () => setCollapsed((current) => ({
|
|
5785
|
+
...current,
|
|
5786
|
+
[node.id]: !current[node.id]
|
|
5787
|
+
})),
|
|
5788
|
+
onScroll: (top) => setScrolls((current) => ({
|
|
5789
|
+
...current,
|
|
5790
|
+
[node.id]: top
|
|
5791
|
+
})),
|
|
5792
|
+
onHeaderDown: (event) => startMove(event, node),
|
|
5793
|
+
onResizeDown: (event, edges) => startResize(event, node, edges),
|
|
5794
|
+
onResizeReset: () => resetSize(node),
|
|
5795
|
+
onColumnDown: (event, column) => startLink(event, node, column),
|
|
5796
|
+
onFieldClick: (item) => setSelection({
|
|
5797
|
+
kind: item.kind,
|
|
5798
|
+
uid: item.uid
|
|
5799
|
+
}),
|
|
5800
|
+
onAddField: (kind, column) => addField(kind, node.name, column),
|
|
5801
|
+
renaming: renaming === node.uid,
|
|
5802
|
+
onRenameStart: () => setRenaming(node.uid),
|
|
5803
|
+
onRename: (name) => {
|
|
5804
|
+
setRenaming(null);
|
|
5805
|
+
if (name !== null && name !== node.name) commitRename(node.uid, name);
|
|
5806
|
+
},
|
|
5807
|
+
onContextMenu: (event, target) => {
|
|
5808
|
+
event.preventDefault();
|
|
5809
|
+
event.stopPropagation();
|
|
5810
|
+
setMenu({
|
|
5811
|
+
open: true,
|
|
5812
|
+
x: event.clientX,
|
|
5813
|
+
y: event.clientY,
|
|
5814
|
+
target
|
|
5815
|
+
});
|
|
5816
|
+
}
|
|
5817
|
+
}, node.id))]
|
|
5818
|
+
}),
|
|
5819
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
5820
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
5821
|
+
onContextMenu: (event) => event.stopPropagation(),
|
|
5822
|
+
sx: {
|
|
5823
|
+
position: "absolute",
|
|
5824
|
+
top: 8,
|
|
5825
|
+
left: 8,
|
|
5826
|
+
right: 8,
|
|
5827
|
+
display: "flex",
|
|
5828
|
+
gap: .75,
|
|
5829
|
+
flexWrap: "wrap",
|
|
5830
|
+
alignItems: "center",
|
|
5831
|
+
pointerEvents: "none",
|
|
5832
|
+
"& > *": { pointerEvents: "auto" }
|
|
5833
|
+
},
|
|
5834
|
+
children: [
|
|
5835
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5836
|
+
size: "small",
|
|
5837
|
+
variant: "outlined",
|
|
5838
|
+
onClick: () => addTable(),
|
|
5839
|
+
children: "+ Tabela"
|
|
5840
|
+
}),
|
|
5841
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5842
|
+
size: "small",
|
|
5843
|
+
onClick: onLoadColumns,
|
|
5844
|
+
disabled: loadingColumns,
|
|
5845
|
+
title: "Lê uma linha de cada tabela para listar as colunas",
|
|
5846
|
+
sx: { bgcolor: "#1b1e23" },
|
|
5847
|
+
children: loadingColumns ? "Lendo colunas…" : "Recarregar colunas"
|
|
5848
|
+
}),
|
|
5849
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
5850
|
+
sx: {
|
|
5851
|
+
ml: "auto",
|
|
5852
|
+
display: "flex",
|
|
5853
|
+
gap: .5,
|
|
5854
|
+
alignItems: "center"
|
|
5855
|
+
},
|
|
5856
|
+
children: [
|
|
5857
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5858
|
+
size: "small",
|
|
5859
|
+
onClick: organize,
|
|
5860
|
+
title: "Dimensões em cima, fatos embaixo (troca as posições salvas)",
|
|
5861
|
+
sx: { bgcolor: "#1b1e23" },
|
|
5862
|
+
children: "Organizar"
|
|
5863
|
+
}),
|
|
5864
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5865
|
+
size: "small",
|
|
5866
|
+
onClick: () => fitToScreen(),
|
|
5867
|
+
sx: { bgcolor: "#1b1e23" },
|
|
5868
|
+
children: "Ajustar à tela"
|
|
5869
|
+
}),
|
|
5870
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
5871
|
+
size: "small",
|
|
5872
|
+
"aria-label": "Diminuir zoom",
|
|
5873
|
+
onClick: () => setView((current) => ({
|
|
5874
|
+
...current,
|
|
5875
|
+
zoom: Math.max(.3, current.zoom - .1)
|
|
5876
|
+
})),
|
|
5877
|
+
children: "−"
|
|
5878
|
+
}),
|
|
5879
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
5880
|
+
sx: {
|
|
5881
|
+
fontSize: 11,
|
|
5882
|
+
fontFamily: MONO,
|
|
5883
|
+
width: 36,
|
|
5884
|
+
textAlign: "center"
|
|
5885
|
+
},
|
|
5886
|
+
children: [Math.round(view.zoom * 100), "%"]
|
|
5887
|
+
}),
|
|
5888
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
5889
|
+
size: "small",
|
|
5890
|
+
"aria-label": "Aumentar zoom",
|
|
5891
|
+
onClick: () => setView((current) => ({
|
|
5892
|
+
...current,
|
|
5893
|
+
zoom: Math.min(2, current.zoom + .1)
|
|
5894
|
+
})),
|
|
5895
|
+
children: "+"
|
|
5896
|
+
})
|
|
5897
|
+
]
|
|
5898
|
+
})
|
|
5899
|
+
]
|
|
5900
|
+
}),
|
|
5901
|
+
!nodes.length && /* @__PURE__ */ jsx(Box, {
|
|
5902
|
+
sx: {
|
|
5903
|
+
position: "absolute",
|
|
5904
|
+
inset: 0,
|
|
5905
|
+
display: "flex",
|
|
5906
|
+
alignItems: "center",
|
|
5907
|
+
justifyContent: "center",
|
|
5908
|
+
pointerEvents: "none",
|
|
5909
|
+
color: "text.secondary",
|
|
5910
|
+
fontSize: 13,
|
|
5911
|
+
textAlign: "center",
|
|
5912
|
+
px: 4
|
|
5913
|
+
},
|
|
5914
|
+
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."
|
|
5915
|
+
}),
|
|
5916
|
+
!!nodes.length && /* @__PURE__ */ jsx(Typography, {
|
|
5917
|
+
sx: {
|
|
4938
5918
|
position: "absolute",
|
|
4939
|
-
|
|
5919
|
+
left: 10,
|
|
5920
|
+
bottom: 6,
|
|
5921
|
+
fontSize: 11,
|
|
5922
|
+
color: "text.disabled",
|
|
4940
5923
|
pointerEvents: "none"
|
|
4941
5924
|
},
|
|
4942
|
-
"
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
[node.id]: !current[node.id]
|
|
4965
|
-
})),
|
|
4966
|
-
onScroll: (top) => setScrolls((current) => ({
|
|
4967
|
-
...current,
|
|
4968
|
-
[node.id]: top
|
|
4969
|
-
})),
|
|
4970
|
-
onHeaderDown: (event) => startMove(event, node),
|
|
4971
|
-
onResizeDown: (event, edges) => startResize(event, node, edges),
|
|
4972
|
-
onResizeReset: () => resetSize(node),
|
|
4973
|
-
onColumnDown: (event, column) => startLink(event, node, column),
|
|
4974
|
-
onFieldClick: (item) => setSelection({
|
|
4975
|
-
kind: item.kind,
|
|
4976
|
-
uid: item.uid
|
|
4977
|
-
}),
|
|
4978
|
-
onAddField: (kind, column) => addField(kind, node.name, column)
|
|
4979
|
-
}, node.id))]
|
|
4980
|
-
}),
|
|
4981
|
-
/* @__PURE__ */ jsxs(Box, {
|
|
4982
|
-
onMouseDown: (event) => event.stopPropagation(),
|
|
4983
|
-
sx: {
|
|
4984
|
-
position: "absolute",
|
|
4985
|
-
top: 8,
|
|
4986
|
-
left: 8,
|
|
4987
|
-
right: 8,
|
|
4988
|
-
display: "flex",
|
|
4989
|
-
gap: .75,
|
|
4990
|
-
flexWrap: "wrap",
|
|
4991
|
-
alignItems: "center",
|
|
4992
|
-
pointerEvents: "none",
|
|
4993
|
-
"& > *": { pointerEvents: "auto" }
|
|
4994
|
-
},
|
|
4995
|
-
children: [
|
|
4996
|
-
/* @__PURE__ */ jsx(Button, {
|
|
4997
|
-
size: "small",
|
|
4998
|
-
variant: "outlined",
|
|
4999
|
-
onClick: addTable,
|
|
5000
|
-
children: "+ Tabela"
|
|
5001
|
-
}),
|
|
5002
|
-
/* @__PURE__ */ jsx(Button, {
|
|
5003
|
-
size: "small",
|
|
5004
|
-
onClick: onLoadColumns,
|
|
5005
|
-
disabled: loadingColumns,
|
|
5006
|
-
title: "Lê uma linha de cada tabela para listar as colunas",
|
|
5007
|
-
sx: { bgcolor: "#1b1e23" },
|
|
5008
|
-
children: loadingColumns ? "Lendo colunas…" : "Recarregar colunas"
|
|
5009
|
-
}),
|
|
5010
|
-
/* @__PURE__ */ jsxs(Box, {
|
|
5011
|
-
sx: {
|
|
5012
|
-
ml: "auto",
|
|
5013
|
-
display: "flex",
|
|
5014
|
-
gap: .5,
|
|
5015
|
-
alignItems: "center"
|
|
5016
|
-
},
|
|
5017
|
-
children: [
|
|
5018
|
-
/* @__PURE__ */ jsx(Button, {
|
|
5019
|
-
size: "small",
|
|
5020
|
-
onClick: () => {
|
|
5021
|
-
const layout = autoLayout(nodes, sizes);
|
|
5022
|
-
setPositions((current) => Object.fromEntries(Object.entries(layout).map(([id, point]) => [id, {
|
|
5023
|
-
...current[id],
|
|
5024
|
-
...point
|
|
5025
|
-
}])));
|
|
5026
|
-
fitToScreen(layout);
|
|
5027
|
-
},
|
|
5028
|
-
title: "Dimensões em cima, fatos embaixo (troca as posições salvas)",
|
|
5029
|
-
sx: { bgcolor: "#1b1e23" },
|
|
5030
|
-
children: "Organizar"
|
|
5031
|
-
}),
|
|
5032
|
-
/* @__PURE__ */ jsx(Button, {
|
|
5033
|
-
size: "small",
|
|
5034
|
-
onClick: () => fitToScreen(),
|
|
5035
|
-
sx: { bgcolor: "#1b1e23" },
|
|
5036
|
-
children: "Ajustar à tela"
|
|
5037
|
-
}),
|
|
5038
|
-
/* @__PURE__ */ jsx(IconButton, {
|
|
5039
|
-
size: "small",
|
|
5040
|
-
"aria-label": "Diminuir zoom",
|
|
5041
|
-
onClick: () => setView((current) => ({
|
|
5042
|
-
...current,
|
|
5043
|
-
zoom: Math.max(.3, current.zoom - .1)
|
|
5044
|
-
})),
|
|
5045
|
-
children: "−"
|
|
5046
|
-
}),
|
|
5047
|
-
/* @__PURE__ */ jsxs(Typography, {
|
|
5048
|
-
sx: {
|
|
5049
|
-
fontSize: 11,
|
|
5050
|
-
fontFamily: MONO,
|
|
5051
|
-
width: 36,
|
|
5052
|
-
textAlign: "center"
|
|
5053
|
-
},
|
|
5054
|
-
children: [Math.round(view.zoom * 100), "%"]
|
|
5055
|
-
}),
|
|
5056
|
-
/* @__PURE__ */ jsx(IconButton, {
|
|
5057
|
-
size: "small",
|
|
5058
|
-
"aria-label": "Aumentar zoom",
|
|
5059
|
-
onClick: () => setView((current) => ({
|
|
5060
|
-
...current,
|
|
5061
|
-
zoom: Math.min(2, current.zoom + .1)
|
|
5062
|
-
})),
|
|
5063
|
-
children: "+"
|
|
5064
|
-
})
|
|
5065
|
-
]
|
|
5066
|
-
})
|
|
5067
|
-
]
|
|
5068
|
-
}),
|
|
5069
|
-
!nodes.length && /* @__PURE__ */ jsx(Box, {
|
|
5070
|
-
sx: {
|
|
5071
|
-
position: "absolute",
|
|
5072
|
-
inset: 0,
|
|
5073
|
-
display: "flex",
|
|
5074
|
-
alignItems: "center",
|
|
5075
|
-
justifyContent: "center",
|
|
5076
|
-
pointerEvents: "none",
|
|
5077
|
-
color: "text.secondary",
|
|
5078
|
-
fontSize: 13,
|
|
5079
|
-
textAlign: "center",
|
|
5080
|
-
px: 4
|
|
5925
|
+
children: "Arraste uma coluna até a coluna de outra tabela para relacionar · +◇/+Σ na coluna cria dimensão/medida · Duplo clique no nome renomeia · Botão direito: editar/remover (no fundo: nova tabela/medida, recarregar, organizar) · Delete remove · Ctrl+roda: zoom"
|
|
5926
|
+
})
|
|
5927
|
+
]
|
|
5928
|
+
}),
|
|
5929
|
+
/* @__PURE__ */ jsx(Menu, {
|
|
5930
|
+
open: !!menu?.open,
|
|
5931
|
+
onClose: closeMenu,
|
|
5932
|
+
anchorReference: "anchorPosition",
|
|
5933
|
+
anchorPosition: menu ? {
|
|
5934
|
+
top: menu.y,
|
|
5935
|
+
left: menu.x
|
|
5936
|
+
} : void 0,
|
|
5937
|
+
transitionDuration: {
|
|
5938
|
+
enter: 90,
|
|
5939
|
+
exit: 60
|
|
5940
|
+
},
|
|
5941
|
+
onContextMenu: (event) => event.preventDefault(),
|
|
5942
|
+
children: menu?.target ? [/* @__PURE__ */ jsxs(MenuItem, {
|
|
5943
|
+
dense: true,
|
|
5944
|
+
onClick: () => {
|
|
5945
|
+
setSelection(menu.target);
|
|
5946
|
+
closeMenu();
|
|
5081
5947
|
},
|
|
5082
|
-
children: "
|
|
5083
|
-
}),
|
|
5084
|
-
|
|
5085
|
-
sx: {
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
fontSize: 11,
|
|
5090
|
-
color: "text.disabled",
|
|
5091
|
-
pointerEvents: "none"
|
|
5948
|
+
children: ["Editar ", TARGET_NAME[menu.target.kind]]
|
|
5949
|
+
}, "edit"), /* @__PURE__ */ jsxs(MenuItem, {
|
|
5950
|
+
dense: true,
|
|
5951
|
+
sx: { color: "error.main" },
|
|
5952
|
+
onClick: () => {
|
|
5953
|
+
if (menu.target) remove(menu.target);
|
|
5954
|
+
closeMenu();
|
|
5092
5955
|
},
|
|
5093
|
-
children: "
|
|
5094
|
-
})
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
"
|
|
5120
|
-
|
|
5121
|
-
|
|
5956
|
+
children: ["Remover ", TARGET_NAME[menu.target.kind]]
|
|
5957
|
+
}, "remove")] : [
|
|
5958
|
+
/* @__PURE__ */ jsx(MenuItem, {
|
|
5959
|
+
dense: true,
|
|
5960
|
+
onClick: () => {
|
|
5961
|
+
if (menu) addTable(toWorld(menu.x, menu.y));
|
|
5962
|
+
closeMenu();
|
|
5963
|
+
},
|
|
5964
|
+
children: "Nova Tabela"
|
|
5965
|
+
}, "table"),
|
|
5966
|
+
/* @__PURE__ */ jsx(MenuItem, {
|
|
5967
|
+
dense: true,
|
|
5968
|
+
onClick: () => {
|
|
5969
|
+
addFormula();
|
|
5970
|
+
closeMenu();
|
|
5971
|
+
},
|
|
5972
|
+
children: "Nova Medida Calculada"
|
|
5973
|
+
}, "formula"),
|
|
5974
|
+
/* @__PURE__ */ jsx(Divider, {}, "divider"),
|
|
5975
|
+
/* @__PURE__ */ jsx(MenuItem, {
|
|
5976
|
+
dense: true,
|
|
5977
|
+
disabled: loadingColumns,
|
|
5978
|
+
onClick: () => {
|
|
5979
|
+
onLoadColumns();
|
|
5980
|
+
closeMenu();
|
|
5981
|
+
},
|
|
5982
|
+
children: "Recarregar colunas"
|
|
5983
|
+
}, "columns"),
|
|
5984
|
+
/* @__PURE__ */ jsx(MenuItem, {
|
|
5985
|
+
dense: true,
|
|
5986
|
+
onClick: () => {
|
|
5987
|
+
organize();
|
|
5988
|
+
closeMenu();
|
|
5989
|
+
},
|
|
5990
|
+
children: "Organizar"
|
|
5991
|
+
}, "organize")
|
|
5992
|
+
]
|
|
5993
|
+
}),
|
|
5994
|
+
panel.open ? /* @__PURE__ */ jsxs(Box, {
|
|
5995
|
+
sx: {
|
|
5996
|
+
width: panel.width,
|
|
5997
|
+
flexShrink: 0,
|
|
5998
|
+
borderLeft: 1,
|
|
5999
|
+
borderColor: "divider",
|
|
6000
|
+
position: "relative",
|
|
6001
|
+
display: "flex",
|
|
6002
|
+
flexDirection: "column",
|
|
6003
|
+
minHeight: 0
|
|
6004
|
+
},
|
|
6005
|
+
children: [
|
|
6006
|
+
/* @__PURE__ */ jsx(Box, {
|
|
6007
|
+
onMouseDown: startPanelResize,
|
|
6008
|
+
title: "Arraste para redimensionar",
|
|
6009
|
+
sx: {
|
|
6010
|
+
position: "absolute",
|
|
6011
|
+
left: -3,
|
|
6012
|
+
top: 0,
|
|
6013
|
+
bottom: 0,
|
|
6014
|
+
width: 6,
|
|
6015
|
+
cursor: "ew-resize",
|
|
6016
|
+
zIndex: 2,
|
|
6017
|
+
"&:hover": {
|
|
6018
|
+
bgcolor: "primary.main",
|
|
6019
|
+
opacity: .4
|
|
6020
|
+
}
|
|
5122
6021
|
}
|
|
5123
|
-
}
|
|
5124
|
-
|
|
5125
|
-
/* @__PURE__ */ jsxs(Box, {
|
|
5126
|
-
sx: {
|
|
5127
|
-
display: "flex",
|
|
5128
|
-
alignItems: "center",
|
|
5129
|
-
px: 1.5,
|
|
5130
|
-
py: .5,
|
|
5131
|
-
borderBottom: 1,
|
|
5132
|
-
borderColor: "divider"
|
|
5133
|
-
},
|
|
5134
|
-
children: [/* @__PURE__ */ jsx(Typography, {
|
|
6022
|
+
}),
|
|
6023
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
5135
6024
|
sx: {
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
6025
|
+
display: "flex",
|
|
6026
|
+
alignItems: "center",
|
|
6027
|
+
px: 1.5,
|
|
6028
|
+
py: .5,
|
|
6029
|
+
borderBottom: 1,
|
|
6030
|
+
borderColor: "divider"
|
|
5140
6031
|
},
|
|
5141
|
-
children:
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
})
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
6032
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
6033
|
+
sx: {
|
|
6034
|
+
fontSize: 12,
|
|
6035
|
+
fontWeight: 700,
|
|
6036
|
+
color: "text.secondary",
|
|
6037
|
+
flex: 1
|
|
6038
|
+
},
|
|
6039
|
+
children: "Propriedades"
|
|
6040
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
6041
|
+
size: "small",
|
|
6042
|
+
"aria-label": "Recolher propriedades",
|
|
6043
|
+
title: "Recolher",
|
|
6044
|
+
onClick: () => setPanel((current) => ({
|
|
6045
|
+
...current,
|
|
6046
|
+
open: false
|
|
6047
|
+
})),
|
|
6048
|
+
children: "⟩"
|
|
6049
|
+
})]
|
|
6050
|
+
}),
|
|
6051
|
+
/* @__PURE__ */ jsx(Box, {
|
|
6052
|
+
sx: {
|
|
6053
|
+
overflow: "auto",
|
|
6054
|
+
flex: 1,
|
|
6055
|
+
p: 1.5
|
|
6056
|
+
},
|
|
6057
|
+
children: /* @__PURE__ */ jsx(Properties, {
|
|
6058
|
+
editor,
|
|
6059
|
+
selection,
|
|
6060
|
+
columns,
|
|
6061
|
+
errors,
|
|
6062
|
+
warnings,
|
|
6063
|
+
usage,
|
|
6064
|
+
onSelect: setSelection,
|
|
6065
|
+
onTry,
|
|
6066
|
+
setEditor,
|
|
6067
|
+
patchTable,
|
|
6068
|
+
patchRelationship,
|
|
6069
|
+
patchDimension,
|
|
6070
|
+
patchMeasure,
|
|
6071
|
+
removeTable,
|
|
6072
|
+
commitRename,
|
|
6073
|
+
addField,
|
|
6074
|
+
unplaced
|
|
6075
|
+
})
|
|
5177
6076
|
})
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
6077
|
+
]
|
|
6078
|
+
}) : /* @__PURE__ */ jsx(Box, {
|
|
6079
|
+
component: "button",
|
|
6080
|
+
type: "button",
|
|
6081
|
+
title: "Mostrar as propriedades",
|
|
6082
|
+
onClick: () => setPanel((current) => ({
|
|
6083
|
+
...current,
|
|
6084
|
+
open: true
|
|
6085
|
+
})),
|
|
6086
|
+
sx: {
|
|
6087
|
+
width: 28,
|
|
6088
|
+
flexShrink: 0,
|
|
6089
|
+
border: 0,
|
|
6090
|
+
borderLeft: 1,
|
|
6091
|
+
borderColor: "divider",
|
|
6092
|
+
bgcolor: "background.paper",
|
|
6093
|
+
color: "text.secondary",
|
|
6094
|
+
cursor: "pointer",
|
|
6095
|
+
writingMode: "vertical-rl",
|
|
6096
|
+
fontSize: 12,
|
|
6097
|
+
py: 1,
|
|
6098
|
+
"&:hover": { color: "primary.main" }
|
|
6099
|
+
},
|
|
6100
|
+
children: "⟨ Propriedades"
|
|
6101
|
+
})
|
|
6102
|
+
]
|
|
5204
6103
|
});
|
|
5205
6104
|
}
|
|
5206
6105
|
/** Curva com tangentes horizontais (saída e chegada pelas laterais dos cards). */
|
|
@@ -5269,11 +6168,15 @@ function RelationshipLine({ line, hovered, onHover }) {
|
|
|
5269
6168
|
]
|
|
5270
6169
|
});
|
|
5271
6170
|
}
|
|
5272
|
-
function TableCard({ node, position, size, collapsed, selected, linking, selectedField, onToggle, onScroll, onHeaderDown, onResizeDown, onResizeReset, onColumnDown, onFieldClick, onAddField }) {
|
|
6171
|
+
function TableCard({ node, position, size, collapsed, selected, linking, selectedField, onToggle, onScroll, onHeaderDown, onResizeDown, onResizeReset, onColumnDown, onFieldClick, onAddField, renaming, onRenameStart, onRename, onContextMenu }) {
|
|
5273
6172
|
const style = ROLE_STYLE[node.role];
|
|
5274
6173
|
const columnActions = ["dimension", "measure"];
|
|
5275
6174
|
return /* @__PURE__ */ jsxs(Box, {
|
|
5276
6175
|
onMouseDown: (event) => event.stopPropagation(),
|
|
6176
|
+
onContextMenu: (event) => onContextMenu(event, {
|
|
6177
|
+
kind: "table",
|
|
6178
|
+
uid: node.uid
|
|
6179
|
+
}),
|
|
5277
6180
|
sx: {
|
|
5278
6181
|
position: "absolute",
|
|
5279
6182
|
left: position.x,
|
|
@@ -5314,7 +6217,12 @@ function TableCard({ node, position, size, collapsed, selected, linking, selecte
|
|
|
5314
6217
|
},
|
|
5315
6218
|
children: "▦"
|
|
5316
6219
|
}),
|
|
5317
|
-
/* @__PURE__ */ jsx(
|
|
6220
|
+
renaming ? /* @__PURE__ */ jsx(TableNameInput, {
|
|
6221
|
+
value: node.name,
|
|
6222
|
+
onDone: onRename
|
|
6223
|
+
}) : /* @__PURE__ */ jsx(Typography, {
|
|
6224
|
+
onDoubleClick: onRenameStart,
|
|
6225
|
+
title: "Duplo clique para renomear",
|
|
5318
6226
|
sx: {
|
|
5319
6227
|
fontWeight: 700,
|
|
5320
6228
|
fontSize: 13,
|
|
@@ -5442,6 +6350,10 @@ function TableCard({ node, position, size, collapsed, selected, linking, selecte
|
|
|
5442
6350
|
}, `c:${item.name}`) : /* @__PURE__ */ jsxs(Box, {
|
|
5443
6351
|
onMouseDown: (event) => event.stopPropagation(),
|
|
5444
6352
|
onClick: () => onFieldClick(item),
|
|
6353
|
+
onContextMenu: (event) => onContextMenu(event, {
|
|
6354
|
+
kind: item.kind,
|
|
6355
|
+
uid: item.uid
|
|
6356
|
+
}),
|
|
5445
6357
|
sx: {
|
|
5446
6358
|
height: ROW,
|
|
5447
6359
|
display: "flex",
|
|
@@ -5537,23 +6449,60 @@ const resizeHandleSx = {
|
|
|
5537
6449
|
position: "absolute",
|
|
5538
6450
|
zIndex: 1
|
|
5539
6451
|
};
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
6452
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6453
|
+
/** Nome da tabela editado no cabeçalho do card: Enter/sair confirma, Esc cancela. */
|
|
6454
|
+
function TableNameInput({ value, onDone }) {
|
|
6455
|
+
const [text, setText] = useState(value);
|
|
6456
|
+
const valid = IDENTIFIER.test(text);
|
|
6457
|
+
const done = useRef(false);
|
|
6458
|
+
const finish = (name) => {
|
|
6459
|
+
if (done.current) return;
|
|
6460
|
+
done.current = true;
|
|
6461
|
+
onDone(name);
|
|
6462
|
+
};
|
|
6463
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
6464
|
+
component: "input",
|
|
6465
|
+
autoFocus: true,
|
|
6466
|
+
value: text,
|
|
6467
|
+
"aria-label": "Nome da tabela",
|
|
6468
|
+
onFocus: (event) => event.currentTarget.select(),
|
|
6469
|
+
onChange: (event) => setText(event.target.value),
|
|
6470
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
6471
|
+
onKeyDown: (event) => {
|
|
6472
|
+
event.stopPropagation();
|
|
6473
|
+
if (event.key === "Enter") finish(valid ? text : null);
|
|
6474
|
+
if (event.key === "Escape") finish(null);
|
|
5547
6475
|
},
|
|
5548
|
-
|
|
5549
|
-
|
|
6476
|
+
onBlur: () => finish(valid ? text : null),
|
|
6477
|
+
sx: {
|
|
6478
|
+
flex: 1,
|
|
6479
|
+
minWidth: 0,
|
|
6480
|
+
font: "inherit",
|
|
6481
|
+
fontSize: 13,
|
|
6482
|
+
fontWeight: 700,
|
|
6483
|
+
color: "text.primary",
|
|
6484
|
+
bgcolor: "#1b1e23",
|
|
6485
|
+
border: "1px solid",
|
|
6486
|
+
borderColor: valid ? "primary.main" : "error.main",
|
|
6487
|
+
borderRadius: .5,
|
|
6488
|
+
px: .5,
|
|
6489
|
+
outline: "none"
|
|
6490
|
+
}
|
|
6491
|
+
});
|
|
6492
|
+
}
|
|
6493
|
+
/** Bloco do painel; com `collapseKey`, o título recolhe/expande (lembrado no navegador). */
|
|
6494
|
+
function PropertyBlock({ title, collapseKey, children }) {
|
|
6495
|
+
return /* @__PURE__ */ jsx(Section, {
|
|
6496
|
+
title,
|
|
6497
|
+
collapseKey,
|
|
6498
|
+
children: /* @__PURE__ */ jsx(Box, {
|
|
5550
6499
|
sx: {
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
6500
|
+
display: "flex",
|
|
6501
|
+
flexDirection: "column",
|
|
6502
|
+
gap: 1
|
|
5554
6503
|
},
|
|
5555
|
-
children
|
|
5556
|
-
})
|
|
6504
|
+
children
|
|
6505
|
+
})
|
|
5557
6506
|
});
|
|
5558
6507
|
}
|
|
5559
6508
|
function ColumnSelect({ label, value, options, onChange }) {
|
|
@@ -5617,29 +6566,23 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
|
|
|
5617
6566
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5618
6567
|
/* @__PURE__ */ jsxs(PropertyBlock, {
|
|
5619
6568
|
title: "Tabela",
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
}),
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
minRows: 3,
|
|
5634
|
-
maxRows: 12,
|
|
5635
|
-
value: table.sql,
|
|
5636
|
-
error: errors[`table:${table.name}`],
|
|
5637
|
-
onChange: (sql) => patchTable(table.uid, { sql })
|
|
5638
|
-
})
|
|
5639
|
-
]
|
|
6569
|
+
collapseKey: "diagram:table",
|
|
6570
|
+
children: [/* @__PURE__ */ jsx(RenameField, {
|
|
6571
|
+
value: table.name,
|
|
6572
|
+
onCommit: (name) => commitRename(table.uid, name)
|
|
6573
|
+
}, table.uid), /* @__PURE__ */ jsx(SqlEditor, {
|
|
6574
|
+
label: "sql",
|
|
6575
|
+
minRows: 3,
|
|
6576
|
+
maxRows: 12,
|
|
6577
|
+
value: table.sql,
|
|
6578
|
+
error: errors[`table:${table.name}`],
|
|
6579
|
+
onChange: (sql) => patchTable(table.uid, { sql }),
|
|
6580
|
+
queryTools: true
|
|
6581
|
+
})]
|
|
5640
6582
|
}),
|
|
5641
6583
|
/* @__PURE__ */ jsx(PropertyBlock, {
|
|
5642
6584
|
title: "Dimensões (marque as colunas)",
|
|
6585
|
+
collapseKey: "diagram:dimensions",
|
|
5643
6586
|
children: /* @__PURE__ */ jsx(DimensionPicker, {
|
|
5644
6587
|
editor,
|
|
5645
6588
|
setEditor,
|
|
@@ -5653,6 +6596,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
|
|
|
5653
6596
|
}),
|
|
5654
6597
|
/* @__PURE__ */ jsx(PropertyBlock, {
|
|
5655
6598
|
title: "Dicionário",
|
|
6599
|
+
collapseKey: "diagram:dictionary",
|
|
5656
6600
|
children: /* @__PURE__ */ jsx(DictionaryFields, {
|
|
5657
6601
|
row: table,
|
|
5658
6602
|
onPatch: (changes) => patchTable(table.uid, changes)
|
|
@@ -5839,6 +6783,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
|
|
|
5839
6783
|
}),
|
|
5840
6784
|
/* @__PURE__ */ jsx(PropertyBlock, {
|
|
5841
6785
|
title: "Dicionário",
|
|
6786
|
+
collapseKey: "diagram:dictionary",
|
|
5842
6787
|
children: /* @__PURE__ */ jsx(DictionaryFields, {
|
|
5843
6788
|
row,
|
|
5844
6789
|
onPatch: (changes) => patchDimension(row.uid, changes)
|
|
@@ -5891,6 +6836,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
|
|
|
5891
6836
|
}),
|
|
5892
6837
|
/* @__PURE__ */ jsx(PropertyBlock, {
|
|
5893
6838
|
title: "Dicionário",
|
|
6839
|
+
collapseKey: "diagram:dictionary",
|
|
5894
6840
|
children: /* @__PURE__ */ jsx(DictionaryFields, {
|
|
5895
6841
|
row,
|
|
5896
6842
|
onPatch: (changes) => patchMeasure(row.uid, changes)
|
|
@@ -5917,6 +6863,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
|
|
|
5917
6863
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5918
6864
|
/* @__PURE__ */ jsx(PropertyBlock, {
|
|
5919
6865
|
title: "Modelo",
|
|
6866
|
+
collapseKey: "diagram:model",
|
|
5920
6867
|
children: /* @__PURE__ */ jsx(Typography, {
|
|
5921
6868
|
sx: {
|
|
5922
6869
|
fontSize: 12,
|
|
@@ -5927,6 +6874,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
|
|
|
5927
6874
|
}),
|
|
5928
6875
|
/* @__PURE__ */ jsxs(PropertyBlock, {
|
|
5929
6876
|
title: "Relacionamentos",
|
|
6877
|
+
collapseKey: "diagram:relationships",
|
|
5930
6878
|
children: [!editor.relationships.length && /* @__PURE__ */ jsx(Typography, {
|
|
5931
6879
|
sx: {
|
|
5932
6880
|
fontSize: 12,
|
|
@@ -5995,6 +6943,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
|
|
|
5995
6943
|
}),
|
|
5996
6944
|
/* @__PURE__ */ jsxs(PropertyBlock, {
|
|
5997
6945
|
title: "Medidas calculadas",
|
|
6946
|
+
collapseKey: "diagram:formulas",
|
|
5998
6947
|
children: [formulas.map((row) => /* @__PURE__ */ jsxs(Button, {
|
|
5999
6948
|
size: "small",
|
|
6000
6949
|
onClick: () => onSelect({
|
|
@@ -6012,22 +6961,10 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
|
|
|
6012
6961
|
]
|
|
6013
6962
|
}, row.uid)), /* @__PURE__ */ jsx(Button, {
|
|
6014
6963
|
size: "small",
|
|
6015
|
-
onClick: () => {
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
...current,
|
|
6020
|
-
measures: [...current.measures, emptyMeasure({
|
|
6021
|
-
uid,
|
|
6022
|
-
type: "formula",
|
|
6023
|
-
name: freeName("novaMedida", taken)
|
|
6024
|
-
})]
|
|
6025
|
-
}));
|
|
6026
|
-
onSelect({
|
|
6027
|
-
kind: "measure",
|
|
6028
|
-
uid
|
|
6029
|
-
});
|
|
6030
|
-
},
|
|
6964
|
+
onClick: () => onSelect({
|
|
6965
|
+
kind: "measure",
|
|
6966
|
+
uid: addFormulaTo(setEditor)
|
|
6967
|
+
}),
|
|
6031
6968
|
children: "+ Medida calculada"
|
|
6032
6969
|
})]
|
|
6033
6970
|
}),
|
|
@@ -6050,7 +6987,7 @@ function RenameField({ value, onCommit }) {
|
|
|
6050
6987
|
value: text,
|
|
6051
6988
|
onChange: setText,
|
|
6052
6989
|
onBlur: () => {
|
|
6053
|
-
if (
|
|
6990
|
+
if (IDENTIFIER.test(text)) onCommit(text);
|
|
6054
6991
|
else setText(value);
|
|
6055
6992
|
}
|
|
6056
6993
|
});
|
|
@@ -6228,11 +7165,22 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
|
|
|
6228
7165
|
});
|
|
6229
7166
|
const completions = useMemo(() => ({
|
|
6230
7167
|
tables,
|
|
6231
|
-
modelColumns: Object.fromEntries(editor.tables.map((row) => [row.name, columns?.tables[row.name] ?? []]))
|
|
7168
|
+
modelColumns: Object.fromEntries(editor.tables.map((row) => [row.name, columns?.tables[row.name] ?? []])),
|
|
7169
|
+
modelSql: Object.fromEntries(editor.tables.map((row) => [row.name, row.sql])),
|
|
7170
|
+
testSql: async (sql) => {
|
|
7171
|
+
const result = await fetcher({
|
|
7172
|
+
model: modelName,
|
|
7173
|
+
devtools: { testSql: sql }
|
|
7174
|
+
});
|
|
7175
|
+
if (!result.devtools?.sqlTest) throw new Error("O servidor não devolveu o teste do SQL");
|
|
7176
|
+
return result.devtools.sqlTest;
|
|
7177
|
+
}
|
|
6232
7178
|
}), [
|
|
6233
7179
|
tables,
|
|
6234
7180
|
columns,
|
|
6235
|
-
editor.tables
|
|
7181
|
+
editor.tables,
|
|
7182
|
+
fetcher,
|
|
7183
|
+
modelName
|
|
6236
7184
|
]);
|
|
6237
7185
|
const validationErrors = validation ? Object.entries(validation.errors) : [];
|
|
6238
7186
|
return /* @__PURE__ */ jsxs(SqlCompletionsContext.Provider, {
|
|
@@ -8381,4 +9329,4 @@ function ToggleButton$1({ draft, loading, problems, inspecting, onOpen }) {
|
|
|
8381
9329
|
//#endregion
|
|
8382
9330
|
export { DashboardDevtools as default };
|
|
8383
9331
|
|
|
8384
|
-
//# sourceMappingURL=DashboardDevtools-
|
|
9332
|
+
//# sourceMappingURL=DashboardDevtools-wk4vMfhy.js.map
|