@quantumwake/terminal-ux-dashboard-components 0.1.7 → 0.1.10
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/index.cjs +404 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -7
- package/dist/index.d.ts +4 -7
- package/dist/index.js +406 -51
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.d.cts
CHANGED
|
@@ -30,6 +30,7 @@ interface DashboardCapabilities {
|
|
|
30
30
|
newDashboard?: () => void;
|
|
31
31
|
addPanel?: (panel: PanelInput) => void;
|
|
32
32
|
removePanel?: (panelId: string) => void;
|
|
33
|
+
persistPanelData?: (panelId: string, rows: QueryResult['rows'], refreshedAt: string) => void;
|
|
33
34
|
}
|
|
34
35
|
interface DashboardContextValue extends DashboardCapabilities {
|
|
35
36
|
theme: DashboardTheme;
|
|
@@ -52,6 +53,7 @@ declare function useCapabilities(): {
|
|
|
52
53
|
canNew: boolean;
|
|
53
54
|
canAddPanel: boolean;
|
|
54
55
|
canEditPanels: boolean;
|
|
56
|
+
canRefresh: boolean;
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
type ChartType = 'bar' | 'grouped-bar' | 'pie' | 'line' | 'scatter' | 'heatmap';
|
|
@@ -358,6 +360,8 @@ interface PanelConfig {
|
|
|
358
360
|
text?: string;
|
|
359
361
|
label?: string;
|
|
360
362
|
column?: string;
|
|
363
|
+
data?: Row[];
|
|
364
|
+
refreshedAt?: string;
|
|
361
365
|
[key: string]: unknown;
|
|
362
366
|
}
|
|
363
367
|
interface DashboardPanel$1 {
|
|
@@ -404,13 +408,6 @@ interface ChartBuilderProps {
|
|
|
404
408
|
stateId?: string;
|
|
405
409
|
onSave?: (panel: ChartPanel) => void;
|
|
406
410
|
}
|
|
407
|
-
/**
|
|
408
|
-
* ChartBuilder — visual chart authoring over a state's dataset. Generates DuckDB
|
|
409
|
-
* SQL from the visual config (SQL engine mode, exact over the full dataset) or
|
|
410
|
-
* aggregates the loaded sample client-side (Direct mode). The host injects
|
|
411
|
-
* runQuery + theme via <DashboardProvider>; an optional onSave persists the
|
|
412
|
-
* assembled panel (absent ⇒ the save affordance is hidden / read-only).
|
|
413
|
-
*/
|
|
414
411
|
declare function ChartBuilder({ records, columns, stateId, onSave }: ChartBuilderProps): react.JSX.Element;
|
|
415
412
|
|
|
416
413
|
/** A dataset column descriptor: name + DuckDB-ish type tag. */
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ interface DashboardCapabilities {
|
|
|
30
30
|
newDashboard?: () => void;
|
|
31
31
|
addPanel?: (panel: PanelInput) => void;
|
|
32
32
|
removePanel?: (panelId: string) => void;
|
|
33
|
+
persistPanelData?: (panelId: string, rows: QueryResult['rows'], refreshedAt: string) => void;
|
|
33
34
|
}
|
|
34
35
|
interface DashboardContextValue extends DashboardCapabilities {
|
|
35
36
|
theme: DashboardTheme;
|
|
@@ -52,6 +53,7 @@ declare function useCapabilities(): {
|
|
|
52
53
|
canNew: boolean;
|
|
53
54
|
canAddPanel: boolean;
|
|
54
55
|
canEditPanels: boolean;
|
|
56
|
+
canRefresh: boolean;
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
type ChartType = 'bar' | 'grouped-bar' | 'pie' | 'line' | 'scatter' | 'heatmap';
|
|
@@ -358,6 +360,8 @@ interface PanelConfig {
|
|
|
358
360
|
text?: string;
|
|
359
361
|
label?: string;
|
|
360
362
|
column?: string;
|
|
363
|
+
data?: Row[];
|
|
364
|
+
refreshedAt?: string;
|
|
361
365
|
[key: string]: unknown;
|
|
362
366
|
}
|
|
363
367
|
interface DashboardPanel$1 {
|
|
@@ -404,13 +408,6 @@ interface ChartBuilderProps {
|
|
|
404
408
|
stateId?: string;
|
|
405
409
|
onSave?: (panel: ChartPanel) => void;
|
|
406
410
|
}
|
|
407
|
-
/**
|
|
408
|
-
* ChartBuilder — visual chart authoring over a state's dataset. Generates DuckDB
|
|
409
|
-
* SQL from the visual config (SQL engine mode, exact over the full dataset) or
|
|
410
|
-
* aggregates the loaded sample client-side (Direct mode). The host injects
|
|
411
|
-
* runQuery + theme via <DashboardProvider>; an optional onSave persists the
|
|
412
|
-
* assembled panel (absent ⇒ the save affordance is hidden / read-only).
|
|
413
|
-
*/
|
|
414
411
|
declare function ChartBuilder({ records, columns, stateId, onSave }: ChartBuilderProps): react.JSX.Element;
|
|
415
412
|
|
|
416
413
|
/** A dataset column descriptor: name + DuckDB-ish type tag. */
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext, useMemo, useState, useEffect, useRef, Suspense } from 'react';
|
|
1
|
+
import { createContext, useContext, useMemo, useState, useEffect, useRef, Suspense, useCallback } from 'react';
|
|
2
2
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
3
|
import { ResponsiveBar } from '@nivo/bar';
|
|
4
4
|
import { ResponsivePie } from '@nivo/pie';
|
|
@@ -9,7 +9,8 @@ import PivotTableUI from 'react-pivottable/PivotTableUI';
|
|
|
9
9
|
import 'react-pivottable/pivottable.css';
|
|
10
10
|
import { Loader2, Play, ChevronDown, AlertCircle, Rows3, BarChart3, PieChart, TrendingUp, ScatterChart, LayoutGrid, Code, Sparkles, X, Terminal, RefreshCw, Minimize2, Maximize2, Plus, Save, FolderOpen, Trash2, Filter, Database, Send, GripVertical } from 'lucide-react';
|
|
11
11
|
import CodeMirror, { EditorView, Prec, keymap } from '@uiw/react-codemirror';
|
|
12
|
-
import { sql } from '@codemirror/lang-sql';
|
|
12
|
+
import { sql, PostgreSQL, schemaCompletionSource, keywordCompletionSource } from '@codemirror/lang-sql';
|
|
13
|
+
import { snippetCompletion, completeFromList, autocompletion, acceptCompletion } from '@codemirror/autocomplete';
|
|
13
14
|
|
|
14
15
|
// src/context/DashboardContext.tsx
|
|
15
16
|
var DashboardContext = createContext(null);
|
|
@@ -36,7 +37,9 @@ function useCapabilities() {
|
|
|
36
37
|
canRefine: !!c.refineDashboard,
|
|
37
38
|
canNew: !!c.newDashboard,
|
|
38
39
|
canAddPanel: !!c.addPanel,
|
|
39
|
-
canEditPanels: !!c.removePanel
|
|
40
|
+
canEditPanels: !!c.removePanel,
|
|
41
|
+
// Studio can recompute + persist a panel's precomputed result.
|
|
42
|
+
canRefresh: !!c.persistPanelData
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -638,6 +641,280 @@ function ChartStyleControls({ style, onChange }) {
|
|
|
638
641
|
] })
|
|
639
642
|
] });
|
|
640
643
|
}
|
|
644
|
+
var AGGREGATES = [
|
|
645
|
+
"count",
|
|
646
|
+
"count_if",
|
|
647
|
+
"sum",
|
|
648
|
+
"avg",
|
|
649
|
+
"min",
|
|
650
|
+
"max",
|
|
651
|
+
"median",
|
|
652
|
+
"mode",
|
|
653
|
+
"stddev",
|
|
654
|
+
"stddev_pop",
|
|
655
|
+
"stddev_samp",
|
|
656
|
+
"var_pop",
|
|
657
|
+
"var_samp",
|
|
658
|
+
"variance",
|
|
659
|
+
"quantile",
|
|
660
|
+
"quantile_cont",
|
|
661
|
+
"quantile_disc",
|
|
662
|
+
"approx_count_distinct",
|
|
663
|
+
"approx_quantile",
|
|
664
|
+
"arg_max",
|
|
665
|
+
"arg_min",
|
|
666
|
+
"first",
|
|
667
|
+
"last",
|
|
668
|
+
"product",
|
|
669
|
+
"bool_and",
|
|
670
|
+
"bool_or",
|
|
671
|
+
"bit_and",
|
|
672
|
+
"bit_or",
|
|
673
|
+
"bit_xor",
|
|
674
|
+
"string_agg",
|
|
675
|
+
"list",
|
|
676
|
+
"array_agg",
|
|
677
|
+
"histogram",
|
|
678
|
+
"corr",
|
|
679
|
+
"covar_pop",
|
|
680
|
+
"covar_samp",
|
|
681
|
+
"kurtosis",
|
|
682
|
+
"skewness",
|
|
683
|
+
"entropy",
|
|
684
|
+
"row_number",
|
|
685
|
+
"rank",
|
|
686
|
+
"dense_rank",
|
|
687
|
+
"percent_rank",
|
|
688
|
+
"cume_dist",
|
|
689
|
+
"ntile",
|
|
690
|
+
"lag",
|
|
691
|
+
"lead",
|
|
692
|
+
"first_value",
|
|
693
|
+
"last_value",
|
|
694
|
+
"nth_value"
|
|
695
|
+
];
|
|
696
|
+
var SCALARS = [
|
|
697
|
+
// string
|
|
698
|
+
"length",
|
|
699
|
+
"len",
|
|
700
|
+
"lower",
|
|
701
|
+
"upper",
|
|
702
|
+
"trim",
|
|
703
|
+
"ltrim",
|
|
704
|
+
"rtrim",
|
|
705
|
+
"substring",
|
|
706
|
+
"substr",
|
|
707
|
+
"concat",
|
|
708
|
+
"concat_ws",
|
|
709
|
+
"replace",
|
|
710
|
+
"reverse",
|
|
711
|
+
"repeat",
|
|
712
|
+
"lpad",
|
|
713
|
+
"rpad",
|
|
714
|
+
"left",
|
|
715
|
+
"right",
|
|
716
|
+
"contains",
|
|
717
|
+
"starts_with",
|
|
718
|
+
"ends_with",
|
|
719
|
+
"position",
|
|
720
|
+
"split_part",
|
|
721
|
+
"string_split",
|
|
722
|
+
"regexp_matches",
|
|
723
|
+
"regexp_replace",
|
|
724
|
+
"regexp_extract",
|
|
725
|
+
"regexp_full_match",
|
|
726
|
+
"regexp_split_to_array",
|
|
727
|
+
"format",
|
|
728
|
+
"printf",
|
|
729
|
+
"md5",
|
|
730
|
+
"sha256",
|
|
731
|
+
"levenshtein",
|
|
732
|
+
"jaccard",
|
|
733
|
+
"ascii",
|
|
734
|
+
"chr",
|
|
735
|
+
// date / time
|
|
736
|
+
"now",
|
|
737
|
+
"current_date",
|
|
738
|
+
"current_timestamp",
|
|
739
|
+
"today",
|
|
740
|
+
"date_trunc",
|
|
741
|
+
"date_part",
|
|
742
|
+
"date_diff",
|
|
743
|
+
"date_add",
|
|
744
|
+
"date_sub",
|
|
745
|
+
"age",
|
|
746
|
+
"strftime",
|
|
747
|
+
"strptime",
|
|
748
|
+
"epoch",
|
|
749
|
+
"epoch_ms",
|
|
750
|
+
"extract",
|
|
751
|
+
"make_date",
|
|
752
|
+
"make_time",
|
|
753
|
+
"make_timestamp",
|
|
754
|
+
"last_day",
|
|
755
|
+
"dayname",
|
|
756
|
+
"monthname",
|
|
757
|
+
"year",
|
|
758
|
+
"month",
|
|
759
|
+
"day",
|
|
760
|
+
"hour",
|
|
761
|
+
"minute",
|
|
762
|
+
"second",
|
|
763
|
+
"dayofweek",
|
|
764
|
+
"dayofyear",
|
|
765
|
+
"week",
|
|
766
|
+
"quarter",
|
|
767
|
+
"time_bucket",
|
|
768
|
+
// math
|
|
769
|
+
"abs",
|
|
770
|
+
"ceil",
|
|
771
|
+
"ceiling",
|
|
772
|
+
"floor",
|
|
773
|
+
"round",
|
|
774
|
+
"trunc",
|
|
775
|
+
"sign",
|
|
776
|
+
"mod",
|
|
777
|
+
"pow",
|
|
778
|
+
"power",
|
|
779
|
+
"sqrt",
|
|
780
|
+
"cbrt",
|
|
781
|
+
"exp",
|
|
782
|
+
"ln",
|
|
783
|
+
"log",
|
|
784
|
+
"log2",
|
|
785
|
+
"log10",
|
|
786
|
+
"greatest",
|
|
787
|
+
"least",
|
|
788
|
+
"gcd",
|
|
789
|
+
"lcm",
|
|
790
|
+
"factorial",
|
|
791
|
+
"random",
|
|
792
|
+
"pi",
|
|
793
|
+
"degrees",
|
|
794
|
+
"radians",
|
|
795
|
+
"sin",
|
|
796
|
+
"cos",
|
|
797
|
+
"tan",
|
|
798
|
+
"asin",
|
|
799
|
+
"acos",
|
|
800
|
+
"atan",
|
|
801
|
+
"atan2",
|
|
802
|
+
// conditional / null / cast
|
|
803
|
+
"coalesce",
|
|
804
|
+
"ifnull",
|
|
805
|
+
"nullif",
|
|
806
|
+
"nvl",
|
|
807
|
+
"if",
|
|
808
|
+
"try_cast",
|
|
809
|
+
"cast",
|
|
810
|
+
"typeof",
|
|
811
|
+
// list / struct / map / json
|
|
812
|
+
"list_value",
|
|
813
|
+
"list_aggregate",
|
|
814
|
+
"list_distinct",
|
|
815
|
+
"list_sort",
|
|
816
|
+
"list_reverse",
|
|
817
|
+
"list_slice",
|
|
818
|
+
"list_concat",
|
|
819
|
+
"list_contains",
|
|
820
|
+
"list_position",
|
|
821
|
+
"list_extract",
|
|
822
|
+
"list_transform",
|
|
823
|
+
"list_filter",
|
|
824
|
+
"unnest",
|
|
825
|
+
"array_length",
|
|
826
|
+
"array_to_string",
|
|
827
|
+
"struct_pack",
|
|
828
|
+
"struct_extract",
|
|
829
|
+
"map",
|
|
830
|
+
"map_keys",
|
|
831
|
+
"map_values",
|
|
832
|
+
"json_extract",
|
|
833
|
+
"json_extract_string",
|
|
834
|
+
"to_json",
|
|
835
|
+
"from_json",
|
|
836
|
+
"json_array_length",
|
|
837
|
+
"json_keys",
|
|
838
|
+
"generate_series",
|
|
839
|
+
"range"
|
|
840
|
+
];
|
|
841
|
+
var TYPES = [
|
|
842
|
+
"BOOLEAN",
|
|
843
|
+
"TINYINT",
|
|
844
|
+
"SMALLINT",
|
|
845
|
+
"INTEGER",
|
|
846
|
+
"BIGINT",
|
|
847
|
+
"HUGEINT",
|
|
848
|
+
"UTINYINT",
|
|
849
|
+
"USMALLINT",
|
|
850
|
+
"UINTEGER",
|
|
851
|
+
"UBIGINT",
|
|
852
|
+
"FLOAT",
|
|
853
|
+
"REAL",
|
|
854
|
+
"DOUBLE",
|
|
855
|
+
"DECIMAL",
|
|
856
|
+
"NUMERIC",
|
|
857
|
+
"VARCHAR",
|
|
858
|
+
"CHAR",
|
|
859
|
+
"TEXT",
|
|
860
|
+
"BLOB",
|
|
861
|
+
"BIT",
|
|
862
|
+
"DATE",
|
|
863
|
+
"TIME",
|
|
864
|
+
"TIMESTAMP",
|
|
865
|
+
"TIMESTAMPTZ",
|
|
866
|
+
"INTERVAL",
|
|
867
|
+
"UUID",
|
|
868
|
+
"JSON",
|
|
869
|
+
"LIST",
|
|
870
|
+
"ARRAY",
|
|
871
|
+
"STRUCT",
|
|
872
|
+
"MAP",
|
|
873
|
+
"UNION",
|
|
874
|
+
"ENUM"
|
|
875
|
+
];
|
|
876
|
+
var EXTRA_KEYWORDS = [
|
|
877
|
+
"QUALIFY",
|
|
878
|
+
"EXCLUDE",
|
|
879
|
+
"PIVOT",
|
|
880
|
+
"UNPIVOT",
|
|
881
|
+
"ASOF",
|
|
882
|
+
"POSITIONAL",
|
|
883
|
+
"SEMI",
|
|
884
|
+
"ANTI",
|
|
885
|
+
"SUMMARIZE",
|
|
886
|
+
"DISTINCT ON",
|
|
887
|
+
"GROUP BY ALL",
|
|
888
|
+
"ORDER BY ALL",
|
|
889
|
+
"USING SAMPLE"
|
|
890
|
+
];
|
|
891
|
+
function opts(labels, type, boost = 0) {
|
|
892
|
+
return labels.map((label) => ({ label, type, boost }));
|
|
893
|
+
}
|
|
894
|
+
var SNIPPETS = [
|
|
895
|
+
snippetCompletion("SELECT ${col}, count(*) AS n\nFROM data\nGROUP BY ${col}\nORDER BY n DESC", {
|
|
896
|
+
label: "group by count",
|
|
897
|
+
type: "snippet",
|
|
898
|
+
detail: "aggregate by column"
|
|
899
|
+
}),
|
|
900
|
+
snippetCompletion("SELECT *\nFROM data\nORDER BY ${col} DESC\nLIMIT 10", {
|
|
901
|
+
label: "top N",
|
|
902
|
+
type: "snippet",
|
|
903
|
+
detail: "order + limit"
|
|
904
|
+
}),
|
|
905
|
+
snippetCompletion("SELECT DISTINCT ${col}\nFROM data", {
|
|
906
|
+
label: "select distinct",
|
|
907
|
+
type: "snippet"
|
|
908
|
+
})
|
|
909
|
+
];
|
|
910
|
+
var duckdbCompletionSource = completeFromList([
|
|
911
|
+
...opts(AGGREGATES, "function", 1),
|
|
912
|
+
// boost aggregates slightly — common in analysis
|
|
913
|
+
...opts(SCALARS, "function"),
|
|
914
|
+
...opts(TYPES, "type"),
|
|
915
|
+
...opts(EXTRA_KEYWORDS, "keyword"),
|
|
916
|
+
...SNIPPETS
|
|
917
|
+
]);
|
|
641
918
|
var DEFAULT_SQL = "SELECT *\nFROM data\nLIMIT 100";
|
|
642
919
|
var renderCell = (v) => {
|
|
643
920
|
if (v == null) return "";
|
|
@@ -677,18 +954,32 @@ function SqlConsole({ columns = [], stateId, defaultColumnsOpen = false, default
|
|
|
677
954
|
void run();
|
|
678
955
|
};
|
|
679
956
|
const schemaKey = columns.map((c) => c.name).join(",");
|
|
680
|
-
const extensions = useMemo(
|
|
681
|
-
|
|
682
|
-
|
|
957
|
+
const extensions = useMemo(() => {
|
|
958
|
+
const sqlConfig = {
|
|
959
|
+
dialect: PostgreSQL,
|
|
960
|
+
schema: { data: columns.map((c) => c.name) },
|
|
961
|
+
defaultTable: "data",
|
|
962
|
+
upperCaseKeywords: true
|
|
963
|
+
};
|
|
964
|
+
return [
|
|
965
|
+
sql(sqlConfig),
|
|
966
|
+
autocompletion({
|
|
967
|
+
override: [
|
|
968
|
+
schemaCompletionSource(sqlConfig),
|
|
969
|
+
keywordCompletionSource(PostgreSQL, true),
|
|
970
|
+
duckdbCompletionSource
|
|
971
|
+
]
|
|
972
|
+
}),
|
|
683
973
|
EditorView.lineWrapping,
|
|
684
|
-
Prec.highest(keymap.of([
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
974
|
+
Prec.highest(keymap.of([
|
|
975
|
+
{ key: "Mod-Enter", run: () => {
|
|
976
|
+
runRef.current();
|
|
977
|
+
return true;
|
|
978
|
+
} },
|
|
979
|
+
{ key: "Tab", run: acceptCompletion }
|
|
980
|
+
]))
|
|
981
|
+
];
|
|
982
|
+
}, [schemaKey]);
|
|
692
983
|
const resultColumns = result?.columns || [];
|
|
693
984
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
694
985
|
/* @__PURE__ */ jsxs("div", { className: `border-b ${theme.border} bg-midnight-elevated p-3 shrink-0`, children: [
|
|
@@ -718,7 +1009,7 @@ function SqlConsole({ columns = [], stateId, defaultColumnsOpen = false, default
|
|
|
718
1009
|
onChange: setSql,
|
|
719
1010
|
extensions,
|
|
720
1011
|
theme: "dark",
|
|
721
|
-
basicSetup: { lineNumbers: false, foldGutter: false, highlightActiveLine: false },
|
|
1012
|
+
basicSetup: { lineNumbers: false, foldGutter: false, highlightActiveLine: false, autocompletion: false },
|
|
722
1013
|
height: "120px",
|
|
723
1014
|
placeholder: "SELECT ... FROM data",
|
|
724
1015
|
className: "border border-midnight-border text-sm overflow-hidden focus-within:border-midnight-accent"
|
|
@@ -1050,6 +1341,7 @@ var ChartPreview = ({
|
|
|
1050
1341
|
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Select a chart type" });
|
|
1051
1342
|
}
|
|
1052
1343
|
};
|
|
1344
|
+
var MAX_PRECOMPUTE_ROWS = 5e3;
|
|
1053
1345
|
function ChartBuilder({ records, columns, stateId, onSave }) {
|
|
1054
1346
|
const { theme, runQuery } = useDashboard();
|
|
1055
1347
|
const runQueryRef = useRef(runQuery);
|
|
@@ -1178,6 +1470,10 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
|
|
|
1178
1470
|
config.chartType = chartType;
|
|
1179
1471
|
if (filters.length) config.filters = filters;
|
|
1180
1472
|
if (yFields.length) config.yFields = yFields.map((f) => ({ name: f.name, agg: f.agg }));
|
|
1473
|
+
if (sqlRows && sqlRows.length <= MAX_PRECOMPUTE_ROWS) {
|
|
1474
|
+
config.data = sqlRows;
|
|
1475
|
+
config.refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1476
|
+
}
|
|
1181
1477
|
}
|
|
1182
1478
|
config.style = style;
|
|
1183
1479
|
onSave({
|
|
@@ -1382,70 +1678,129 @@ function ViewLoading() {
|
|
|
1382
1678
|
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
|
|
1383
1679
|
}
|
|
1384
1680
|
function SqlPanel({ panel }) {
|
|
1385
|
-
const { runQuery } = useDashboard();
|
|
1681
|
+
const { runQuery, persistPanelData } = useDashboard();
|
|
1682
|
+
const { canRefresh } = useCapabilities();
|
|
1386
1683
|
const runQueryRef = useRef(runQuery);
|
|
1387
1684
|
runQueryRef.current = runQuery;
|
|
1388
1685
|
const config = panel.config || {};
|
|
1389
1686
|
const chartType = config.chartType || panel.type;
|
|
1390
|
-
const
|
|
1687
|
+
const precomputed = config.data;
|
|
1688
|
+
const [rows, setRows] = useState(precomputed ?? null);
|
|
1391
1689
|
const [error, setError] = useState(null);
|
|
1392
|
-
const [loading, setLoading] = useState(
|
|
1690
|
+
const [loading, setLoading] = useState(!precomputed && canRefresh);
|
|
1691
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
1393
1692
|
useEffect(() => {
|
|
1693
|
+
if (precomputed) {
|
|
1694
|
+
setRows(precomputed);
|
|
1695
|
+
setLoading(false);
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (!canRefresh) {
|
|
1699
|
+
setLoading(false);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1394
1702
|
let cancelled = false;
|
|
1395
1703
|
setLoading(true);
|
|
1396
1704
|
setError(null);
|
|
1397
|
-
|
|
1398
|
-
runQueryRef.current(sql).then(
|
|
1705
|
+
runQueryRef.current(config.sql || "").then(
|
|
1399
1706
|
(res) => {
|
|
1400
|
-
if (cancelled)
|
|
1401
|
-
|
|
1402
|
-
|
|
1707
|
+
if (!cancelled) {
|
|
1708
|
+
setLoading(false);
|
|
1709
|
+
setRows(res?.rows || []);
|
|
1710
|
+
}
|
|
1403
1711
|
},
|
|
1404
1712
|
(err) => {
|
|
1405
|
-
if (cancelled)
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1713
|
+
if (!cancelled) {
|
|
1714
|
+
setLoading(false);
|
|
1715
|
+
setRows(null);
|
|
1716
|
+
setError(err instanceof Error ? err.message : String(err) || "Query failed");
|
|
1717
|
+
}
|
|
1409
1718
|
}
|
|
1410
1719
|
);
|
|
1411
1720
|
return () => {
|
|
1412
1721
|
cancelled = true;
|
|
1413
1722
|
};
|
|
1414
|
-
}, [config.sql]);
|
|
1723
|
+
}, [config.sql, canRefresh]);
|
|
1724
|
+
const refresh = useCallback(async () => {
|
|
1725
|
+
setRefreshing(true);
|
|
1726
|
+
setError(null);
|
|
1727
|
+
try {
|
|
1728
|
+
const res = await runQueryRef.current(config.sql || "");
|
|
1729
|
+
const r = res?.rows || [];
|
|
1730
|
+
setRows(r);
|
|
1731
|
+
persistPanelData?.(panel.id, r, (/* @__PURE__ */ new Date()).toISOString());
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
setError(err instanceof Error ? err.message : String(err) || "Query failed");
|
|
1734
|
+
} finally {
|
|
1735
|
+
setRefreshing(false);
|
|
1736
|
+
}
|
|
1737
|
+
}, [config.sql, panel.id, persistPanelData]);
|
|
1738
|
+
const refreshBtn = canRefresh ? /* @__PURE__ */ jsx(
|
|
1739
|
+
"button",
|
|
1740
|
+
{
|
|
1741
|
+
onClick: refresh,
|
|
1742
|
+
disabled: refreshing,
|
|
1743
|
+
title: config.refreshedAt ? `Last computed ${new Date(config.refreshedAt).toLocaleString()} \u2014 click to refresh` : "Compute & save this chart\u2019s data",
|
|
1744
|
+
className: "absolute top-1 right-1 z-10 p-1 bg-midnight-elevated/80 border border-midnight-border text-midnight-text-muted hover:text-midnight-accent transition-colors",
|
|
1745
|
+
children: /* @__PURE__ */ jsx(RefreshCw, { className: `w-3 h-3 ${refreshing ? "animate-spin" : ""}` })
|
|
1746
|
+
}
|
|
1747
|
+
) : null;
|
|
1415
1748
|
if (loading && !rows) {
|
|
1416
1749
|
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Running\u2026" });
|
|
1417
1750
|
}
|
|
1751
|
+
if (!rows && !error && !canRefresh) {
|
|
1752
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-1 px-2 text-center text-xs text-midnight-text-muted", children: [
|
|
1753
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4" }),
|
|
1754
|
+
/* @__PURE__ */ jsx("span", { children: "Chart data not available" }),
|
|
1755
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-midnight-text-subdued", children: "Download the dataset to view this chart." })
|
|
1756
|
+
] });
|
|
1757
|
+
}
|
|
1418
1758
|
if (error) {
|
|
1419
|
-
return /* @__PURE__ */
|
|
1759
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center h-full px-2 text-center text-xs text-red-400", children: [
|
|
1760
|
+
refreshBtn,
|
|
1761
|
+
error
|
|
1762
|
+
] });
|
|
1420
1763
|
}
|
|
1421
1764
|
if (!rows) return null;
|
|
1765
|
+
let chart;
|
|
1422
1766
|
if (chartType === "metric") {
|
|
1423
1767
|
const first = rows[0];
|
|
1424
1768
|
const v = first ? Object.values(first)[0] : 0;
|
|
1425
|
-
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1769
|
+
chart = /* @__PURE__ */ jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
|
|
1770
|
+
} else {
|
|
1771
|
+
const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
|
|
1772
|
+
switch (chartType) {
|
|
1773
|
+
case "bar":
|
|
1774
|
+
chart = /* @__PURE__ */ jsx(BarView, { data: shaped, groupColumn: config.group || "", valueColumn: config.value || "", style: config.style });
|
|
1775
|
+
break;
|
|
1776
|
+
case "grouped-bar": {
|
|
1777
|
+
const gb = shaped;
|
|
1778
|
+
chart = /* @__PURE__ */ jsx(GroupedBarChart, { data: gb.data, keys: gb.keys, yFields: config.yFields || [], style: config.style });
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
case "pie":
|
|
1782
|
+
chart = /* @__PURE__ */ jsx(PieView, { data: shaped, groupColumn: config.group || "", style: config.style });
|
|
1783
|
+
break;
|
|
1784
|
+
case "line":
|
|
1785
|
+
chart = /* @__PURE__ */ jsx(LineView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
|
|
1786
|
+
break;
|
|
1787
|
+
case "scatter":
|
|
1788
|
+
chart = /* @__PURE__ */ jsx(ScatterView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
|
|
1789
|
+
break;
|
|
1790
|
+
case "heatmap":
|
|
1791
|
+
chart = shaped.length ? /* @__PURE__ */ jsx(HeatmapView, { data: shaped, rowColumn: config.row || "", colColumn: config.col || "", rowOrder: config.rowOrder, colOrder: config.colOrder, marginAgg: config.marginAgg, showTotals: config.showTotals, style: config.style }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
|
|
1792
|
+
break;
|
|
1793
|
+
default:
|
|
1794
|
+
chart = /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
|
|
1795
|
+
"Unsupported SQL chart: ",
|
|
1796
|
+
chartType
|
|
1797
|
+
] });
|
|
1434
1798
|
}
|
|
1435
|
-
case "pie":
|
|
1436
|
-
return /* @__PURE__ */ jsx(PieView, { data: shaped, groupColumn: config.group || "", style: config.style });
|
|
1437
|
-
case "line":
|
|
1438
|
-
return /* @__PURE__ */ jsx(LineView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
|
|
1439
|
-
case "scatter":
|
|
1440
|
-
return /* @__PURE__ */ jsx(ScatterView, { data: shaped, xColumn: config.x || "", yColumn: config.y || "", style: config.style });
|
|
1441
|
-
case "heatmap":
|
|
1442
|
-
return shaped.length ? /* @__PURE__ */ jsx(HeatmapView, { data: shaped, rowColumn: config.row || "", colColumn: config.col || "", rowOrder: config.rowOrder, colOrder: config.colOrder, marginAgg: config.marginAgg, showTotals: config.showTotals, style: config.style }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
|
|
1443
|
-
default:
|
|
1444
|
-
return /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
|
|
1445
|
-
"Unsupported SQL chart: ",
|
|
1446
|
-
chartType
|
|
1447
|
-
] });
|
|
1448
1799
|
}
|
|
1800
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative h-full w-full", children: [
|
|
1801
|
+
refreshBtn,
|
|
1802
|
+
chart
|
|
1803
|
+
] });
|
|
1449
1804
|
}
|
|
1450
1805
|
function PanelContent({ panel, records, columns }) {
|
|
1451
1806
|
const { type } = panel;
|