@devtable/dashboard 0.4.0 → 1.0.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.
Files changed (29) hide show
  1. package/dist/api-caller/index.d.ts +9 -2
  2. package/dist/contexts/definition-context.d.ts +2 -1
  3. package/dist/contexts/panel-context.d.ts +2 -2
  4. package/dist/dashboard.es.js +1066 -509
  5. package/dist/dashboard.umd.js +16 -5
  6. package/dist/definition-editor/data-source-editor/context-and-snippets.d.ts +5 -0
  7. package/dist/definition-editor/data-source-editor/data-preview.d.ts +4 -0
  8. package/dist/definition-editor/data-source-editor/editor.d.ts +6 -0
  9. package/dist/definition-editor/data-source-editor/form.d.ts +8 -0
  10. package/dist/definition-editor/data-source-editor/index.d.ts +7 -0
  11. package/dist/definition-editor/data-source-editor/select-or-add-data-source.d.ts +7 -0
  12. package/dist/definition-editor/index.d.ts +2 -0
  13. package/dist/{panel/settings/context-info/index.d.ts → definition-editor/sql-snippet-editor/context-info.d.ts} +0 -0
  14. package/dist/definition-editor/sql-snippet-editor/editor.d.ts +5 -0
  15. package/dist/definition-editor/sql-snippet-editor/index.d.ts +7 -0
  16. package/dist/layout/index.d.ts +1 -2
  17. package/dist/main/main.d.ts +3 -1
  18. package/dist/main/read-only.d.ts +3 -1
  19. package/dist/panel/index.d.ts +1 -1
  20. package/dist/panel/settings/pick-data-source/index.d.ts +5 -0
  21. package/dist/panel/settings/viz-config/preview-viz.d.ts +5 -0
  22. package/dist/style.css +1 -1
  23. package/dist/types/dashboard.d.ts +8 -1
  24. package/package.json +2 -2
  25. package/dist/panel/settings/query-editor/index.d.ts +0 -2
  26. package/dist/panel/settings/query-editor/sql-query-editor/index.d.ts +0 -5
  27. package/dist/panel/settings/query-result/index.d.ts +0 -5
  28. package/dist/panel/settings/sql-snippets/form.d.ts +0 -9
  29. package/dist/panel/settings/sql-snippets/index.d.ts +0 -5
@@ -32,11 +32,11 @@ var __objRest = (source, exclude) => {
32
32
  import React from "react";
33
33
  import _ from "lodash";
34
34
  import { WidthProvider, Responsive } from "react-grid-layout";
35
- import { Text, Table, Group, LoadingOverlay, Box, Textarea, Button, TextInput, ActionIcon, useMantineTheme, ColorSwatch, Select, Switch, Slider, JsonInput, Divider, Modal, AppShell, Navbar, Tabs, Tooltip, Menu, Container, SegmentedControl } from "@mantine/core";
35
+ import { LoadingOverlay, Table, Group, Text, Select, Textarea, ActionIcon, TextInput, Button, useMantineTheme, ColorSwatch, Switch, Slider, JsonInput, Divider, Box, Modal, AppShell, Tabs, Tooltip, Menu, Container, SegmentedControl } from "@mantine/core";
36
36
  import { useRequest } from "ahooks";
37
37
  import axios from "axios";
38
- import { Trash, DeviceFloppy, InfoCircle, Refresh, Settings, Paint, PlayerPlay, PlaylistAdd, Recycle, Share } from "tabler-icons-react";
39
- import { useElementSize, randomId, useInputState, useListState } from "@mantine/hooks";
38
+ import { DeviceFloppy, Trash, InfoCircle, Refresh, Settings, Paint, PlayerPlay, PlaylistAdd, ClipboardText, Database, Recycle, Share } from "tabler-icons-react";
39
+ import { useInputState, useElementSize, randomId } from "@mantine/hooks";
40
40
  import ReactEChartsCore from "echarts-for-react/lib/core";
41
41
  import * as echarts from "echarts/core";
42
42
  import { SunburstChart, BarChart, LineChart } from "echarts/charts";
@@ -44,9 +44,9 @@ import { CanvasRenderer } from "echarts/renderers";
44
44
  import { GridComponent, LegendComponent, TooltipComponent, VisualMapComponent } from "echarts/components";
45
45
  import numbro from "numbro";
46
46
  import "echarts-gl";
47
+ import { useForm, Controller } from "react-hook-form";
48
+ import { formList, useForm as useForm$1 } from "@mantine/form";
47
49
  import { Prism } from "@mantine/prism";
48
- import { formList, useForm } from "@mantine/form";
49
- import { useForm as useForm$1, Controller } from "react-hook-form";
50
50
  var DashboardMode = /* @__PURE__ */ ((DashboardMode2) => {
51
51
  DashboardMode2["Use"] = "use";
52
52
  DashboardMode2["Edit"] = "edit";
@@ -87,7 +87,14 @@ const post = getRequest("POST");
87
87
  function formatSQL(sql, params) {
88
88
  const names = Object.keys(params);
89
89
  const vals = Object.values(params);
90
- return new Function(...names, `return \`${sql}\`;`)(...vals);
90
+ try {
91
+ return new Function(...names, `return \`${sql}\`;`)(...vals);
92
+ } catch (error) {
93
+ if (names.length === 0 && sql.includes("$")) {
94
+ throw new Error("[formatSQL] insufficient params");
95
+ }
96
+ throw error;
97
+ }
91
98
  }
92
99
  function getSQLParams(context, definitions) {
93
100
  const sqlSnippetRecord = definitions.sqlSnippets.reduce((ret, curr) => {
@@ -96,24 +103,26 @@ function getSQLParams(context, definitions) {
96
103
  }, {});
97
104
  return _.merge({}, sqlSnippetRecord, context);
98
105
  }
99
- const queryBySQL = (sql, context, definitions, title) => async () => {
100
- if (!sql) {
106
+ const queryBySQL = ({ context, definitions, title, dataSource }) => async () => {
107
+ if (!dataSource || !dataSource.sql) {
101
108
  return [];
102
109
  }
110
+ const { type, key, sql } = dataSource;
103
111
  const needParams = sql.includes("$");
104
- const params = getSQLParams(context, definitions);
105
- if (needParams && Object.keys(params).length === 0) {
106
- console.error(`[queryBySQL] insufficient params for {${title}}'s SQL`);
112
+ try {
113
+ const params = getSQLParams(context, definitions);
114
+ const formattedSQL = formatSQL(sql, params);
115
+ if (needParams) {
116
+ console.groupCollapsed(`Final SQL for: ${title}`);
117
+ console.log(formattedSQL);
118
+ console.groupEnd();
119
+ }
120
+ const res = await post("/query", { type, key, sql: formattedSQL });
121
+ return res;
122
+ } catch (error) {
123
+ console.error(error);
107
124
  return [];
108
125
  }
109
- const formattedSQL = formatSQL(sql, params);
110
- if (needParams) {
111
- console.groupCollapsed(`Final SQL for: ${title}`);
112
- console.log(formattedSQL);
113
- console.groupEnd();
114
- }
115
- const res = await post("/query", { sql: formattedSQL });
116
- return res;
117
126
  };
118
127
  const initialContext$2 = {};
119
128
  const initialContextInfoContext = initialContext$2;
@@ -127,8 +136,8 @@ const initialContext$1 = {
127
136
  description: "",
128
137
  setDescription: () => {
129
138
  },
130
- sql: "",
131
- setSQL: () => {
139
+ dataSourceID: "",
140
+ setDataSourceID: () => {
132
141
  },
133
142
  viz: {
134
143
  type: "",
@@ -140,6 +149,15 @@ const initialContext$1 = {
140
149
  }
141
150
  };
142
151
  const PanelContext = React.createContext(initialContext$1);
152
+ const initialContext = {
153
+ sqlSnippets: [],
154
+ setSQLSnippets: () => {
155
+ },
156
+ dataSources: [],
157
+ setDataSources: () => {
158
+ }
159
+ };
160
+ const DefinitionContext = React.createContext(initialContext);
143
161
  var jsxRuntime = { exports: {} };
144
162
  var reactJsxRuntime_production_min = {};
145
163
  /**
@@ -173,6 +191,152 @@ reactJsxRuntime_production_min.jsxs = q;
173
191
  const jsx = jsxRuntime.exports.jsx;
174
192
  const jsxs = jsxRuntime.exports.jsxs;
175
193
  const Fragment = jsxRuntime.exports.Fragment;
194
+ function DataPreview({
195
+ id
196
+ }) {
197
+ const definitions = React.useContext(DefinitionContext);
198
+ const contextInfo = React.useContext(ContextInfoContext);
199
+ const dataSource = React.useMemo(() => {
200
+ return definitions.dataSources.find((d) => d.id === id);
201
+ }, [definitions.dataSources, id]);
202
+ const {
203
+ data = [],
204
+ loading
205
+ } = useRequest(queryBySQL({
206
+ context: contextInfo,
207
+ definitions,
208
+ title: id,
209
+ dataSource
210
+ }), {
211
+ refreshDeps: [contextInfo, definitions, dataSource]
212
+ });
213
+ if (loading) {
214
+ return /* @__PURE__ */ jsx(LoadingOverlay, {
215
+ visible: loading
216
+ });
217
+ }
218
+ if (data.length === 0) {
219
+ return /* @__PURE__ */ jsx(Table, {});
220
+ }
221
+ return /* @__PURE__ */ jsxs(Group, {
222
+ my: "xl",
223
+ direction: "column",
224
+ grow: true,
225
+ sx: {
226
+ border: "1px solid #eee"
227
+ },
228
+ children: [/* @__PURE__ */ jsx(Group, {
229
+ position: "left",
230
+ py: "md",
231
+ pl: "md",
232
+ sx: {
233
+ borderBottom: "1px solid #eee",
234
+ background: "#efefef"
235
+ },
236
+ children: /* @__PURE__ */ jsx(Text, {
237
+ weight: 500,
238
+ children: "Preview Data"
239
+ })
240
+ }), /* @__PURE__ */ jsxs(Table, {
241
+ children: [/* @__PURE__ */ jsx("thead", {
242
+ children: /* @__PURE__ */ jsx("tr", {
243
+ children: Object.keys(data == null ? void 0 : data[0]).map((label) => /* @__PURE__ */ jsx("th", {
244
+ children: label
245
+ }, label))
246
+ })
247
+ }), /* @__PURE__ */ jsx("tbody", {
248
+ children: data.map((row, index2) => /* @__PURE__ */ jsx("tr", {
249
+ children: Object.values(row).map((v, i) => /* @__PURE__ */ jsx("td", {
250
+ children: /* @__PURE__ */ jsx(Group, {
251
+ sx: {
252
+ "&, .mantine-Text-root": {
253
+ fontFamily: "monospace"
254
+ }
255
+ },
256
+ children: /* @__PURE__ */ jsx(Text, {
257
+ children: v
258
+ })
259
+ })
260
+ }, `${v}--${i}`))
261
+ }, `row-${index2}`))
262
+ })]
263
+ })]
264
+ });
265
+ }
266
+ function PickDataSource({}) {
267
+ const {
268
+ dataSources
269
+ } = React.useContext(DefinitionContext);
270
+ const {
271
+ dataSourceID,
272
+ setDataSourceID,
273
+ data,
274
+ loading
275
+ } = React.useContext(PanelContext);
276
+ const options = React.useMemo(() => {
277
+ return dataSources.map((d) => ({
278
+ value: d.id,
279
+ label: d.id
280
+ }));
281
+ }, [dataSources]);
282
+ return /* @__PURE__ */ jsxs(Group, {
283
+ direction: "column",
284
+ grow: true,
285
+ noWrap: true,
286
+ children: [/* @__PURE__ */ jsxs(Group, {
287
+ position: "left",
288
+ sx: {
289
+ maxWidth: "600px",
290
+ alignItems: "baseline"
291
+ },
292
+ children: [/* @__PURE__ */ jsx(Text, {
293
+ children: "Select a Data Source"
294
+ }), /* @__PURE__ */ jsx(Select, {
295
+ data: options,
296
+ value: dataSourceID,
297
+ onChange: setDataSourceID,
298
+ allowDeselect: false,
299
+ clearable: false,
300
+ sx: {
301
+ flexGrow: 1
302
+ }
303
+ })]
304
+ }), /* @__PURE__ */ jsx(DataPreview, {
305
+ id: dataSourceID
306
+ })]
307
+ });
308
+ }
309
+ function EditDescription() {
310
+ const {
311
+ description,
312
+ setDescription
313
+ } = React.useContext(PanelContext);
314
+ const [localDesc, setLocalDesc] = useInputState(description);
315
+ const changed = description !== localDesc;
316
+ const submit = React.useCallback(() => {
317
+ if (!changed) {
318
+ return;
319
+ }
320
+ setDescription(localDesc);
321
+ }, [changed, localDesc]);
322
+ return /* @__PURE__ */ jsx(Textarea, {
323
+ label: "Panel Description",
324
+ value: localDesc,
325
+ onChange: setLocalDesc,
326
+ minRows: 2,
327
+ rightSection: /* @__PURE__ */ jsx(ActionIcon, {
328
+ disabled: !changed,
329
+ onClick: submit,
330
+ sx: {
331
+ alignSelf: "flex-start",
332
+ marginTop: "4px"
333
+ },
334
+ children: /* @__PURE__ */ jsx(DeviceFloppy, {
335
+ size: 20
336
+ })
337
+ })
338
+ });
339
+ }
176
340
  class ErrorBoundary extends React.Component {
177
341
  constructor(props) {
178
342
  super(props);
@@ -244,8 +408,8 @@ function Sunbrust({
244
408
  }
245
409
  }
246
410
  }
247
- }), []);
248
- const option = _.merge(defaultOption$1, labelOption, restConf, {
411
+ }), [max]);
412
+ const option = _.merge({}, defaultOption$1, labelOption, restConf, {
249
413
  series: {
250
414
  data: chartData
251
415
  }
@@ -560,7 +724,7 @@ function VizBar3D({
560
724
  }
561
725
  });
562
726
  }
563
- var index$3 = "";
727
+ var index$2 = "";
564
728
  function renderViz(width, height, data, viz) {
565
729
  const props = {
566
730
  width,
@@ -613,328 +777,90 @@ function Viz({
613
777
  }), !empty && renderViz(width, height, data, viz)]
614
778
  });
615
779
  }
616
- function ContextInfo({}) {
617
- const contextInfo = React.useContext(ContextInfoContext);
618
- const sampleSQL = `SELECT *
619
- FROM commit
620
- WHERE author_time BETWEEN '\${timeRange?.[0].toISOString()}' AND '\${timeRange?.[1].toISOString()}'`;
621
- return /* @__PURE__ */ jsxs(Group, {
622
- direction: "column",
623
- children: [/* @__PURE__ */ jsx(Prism, {
624
- language: "sql",
625
- sx: {
626
- width: "100%"
627
- },
628
- noCopy: true,
629
- colorScheme: "dark",
630
- children: `-- You may refer context data *by name*
631
- -- in SQL or VizConfig.
632
-
633
- ${sampleSQL}`
634
- }), /* @__PURE__ */ jsx(Text, {
635
- weight: 700,
636
- children: "Avaiable context entries"
637
- }), /* @__PURE__ */ jsx(Prism, {
638
- language: "json",
639
- sx: {
640
- width: "100%"
641
- },
642
- noCopy: true,
643
- colorScheme: "dark",
644
- children: JSON.stringify(contextInfo, null, 2)
645
- })]
646
- });
647
- }
648
- var index$2 = "";
649
- function SQLQueryEditor({}) {
780
+ function PreviewViz({}) {
650
781
  const {
651
- sql,
652
- setSQL
782
+ data,
783
+ loading,
784
+ viz
653
785
  } = React.useContext(PanelContext);
654
- const [showPreview, setShowPreview] = React.useState(true);
655
- const handleChange = (e) => {
656
- setSQL(e.target.value);
657
- };
658
- const togglePreview = React.useCallback(() => {
659
- setShowPreview((v) => !v);
660
- }, []);
661
- const format = React.useCallback(() => {
662
- setSQL((sql2) => sql2.trim());
663
- }, [setSQL]);
664
- return /* @__PURE__ */ jsxs(Box, {
665
- className: "sql-query-editor-root",
666
- sx: {
667
- height: "100%"
668
- },
669
- children: [/* @__PURE__ */ jsx(Textarea, {
670
- value: sql,
671
- onChange: handleChange,
672
- minRows: 20,
673
- maxRows: 20
674
- }), /* @__PURE__ */ jsxs(Group, {
675
- m: "md",
676
- position: "right",
677
- children: [/* @__PURE__ */ jsx(Button, {
678
- color: "blue",
679
- onClick: format,
680
- children: "Format"
681
- }), /* @__PURE__ */ jsx(Button, {
682
- variant: "default",
683
- onClick: togglePreview,
684
- children: "Toggle Preview"
685
- })]
686
- }), showPreview && /* @__PURE__ */ jsx(Prism, {
687
- language: "sql",
688
- withLineNumbers: true,
689
- noCopy: true,
690
- colorScheme: "dark",
691
- children: sql
692
- })]
786
+ return /* @__PURE__ */ jsx(ErrorBoundary, {
787
+ children: /* @__PURE__ */ jsx(Viz, {
788
+ viz,
789
+ data,
790
+ loading
791
+ })
693
792
  });
694
793
  }
695
- const QueryEditor = SQLQueryEditor;
696
- function QueryResult({}) {
794
+ function EditTitle() {
697
795
  const {
698
- data
796
+ title,
797
+ setTitle
699
798
  } = React.useContext(PanelContext);
700
- return /* @__PURE__ */ jsxs("div", {
701
- className: "query-result-root",
702
- children: [/* @__PURE__ */ jsxs(Group, {
703
- mb: "xs",
704
- children: [/* @__PURE__ */ jsx(Text, {
705
- weight: "bold",
706
- children: "Data Length: "
707
- }), data.length]
708
- }), /* @__PURE__ */ jsx(Prism, {
709
- language: "json",
710
- colorScheme: "dark",
711
- children: JSON.stringify(data, null, 2)
712
- })]
799
+ const [localTitle, setLocalTitle] = useInputState(title);
800
+ const changed = title !== localTitle;
801
+ const submit = React.useCallback(() => {
802
+ if (!changed) {
803
+ return;
804
+ }
805
+ setTitle(localTitle);
806
+ }, [changed, localTitle]);
807
+ return /* @__PURE__ */ jsx(TextInput, {
808
+ label: "Panel Title",
809
+ value: localTitle,
810
+ onChange: setLocalTitle,
811
+ rightSection: /* @__PURE__ */ jsx(ActionIcon, {
812
+ disabled: !changed,
813
+ onClick: submit,
814
+ children: /* @__PURE__ */ jsx(DeviceFloppy, {
815
+ size: 20
816
+ })
817
+ })
713
818
  });
714
819
  }
715
- const initialContext = {
716
- sqlSnippets: [],
717
- setSQLSnippets: () => {
718
- }
719
- };
720
- const DefinitionContext = React.createContext(initialContext);
721
- function SQLSnippetsForm({
722
- sqlSnippets,
723
- setSQLSnippets
820
+ function VizBar3DPanel({
821
+ conf,
822
+ setConf
724
823
  }) {
725
- const initialValues = React.useMemo(() => ({
726
- snippets: formList(sqlSnippets != null ? sqlSnippets : [])
727
- }), [sqlSnippets]);
728
- const form = useForm({
729
- initialValues
730
- });
731
- const addSnippet = () => form.addListItem("snippets", {
732
- key: randomId(),
733
- value: ""
824
+ const defaultValues = _.assign({}, {
825
+ "x_axis_data_key": "x",
826
+ "y_axis_data_key": "y",
827
+ "z_axis_data_key": "z",
828
+ "xAxis3D": {
829
+ "type": "value",
830
+ "name": "X Axis Name"
831
+ },
832
+ "yAxis3D": {
833
+ "type": "value",
834
+ "name": "Y Axis Name"
835
+ },
836
+ "zAxis3D": {
837
+ "type": "value",
838
+ "name": "Z Axis Name"
839
+ }
840
+ }, conf);
841
+ const {
842
+ control,
843
+ handleSubmit,
844
+ formState
845
+ } = useForm({
846
+ defaultValues
734
847
  });
735
- const changed = React.useMemo(() => !_.isEqual(form.values, initialValues), [form.values, initialValues]);
736
- const submit = ({
737
- snippets
738
- }) => {
739
- setSQLSnippets(snippets);
740
- };
741
848
  return /* @__PURE__ */ jsx(Group, {
742
849
  direction: "column",
743
- sx: {
744
- width: "100%",
745
- position: "relative"
746
- },
850
+ mt: "md",
851
+ spacing: "xs",
747
852
  grow: true,
748
853
  children: /* @__PURE__ */ jsxs("form", {
749
- onSubmit: form.onSubmit(submit),
750
- children: [form.values.snippets.map((_item, index2) => /* @__PURE__ */ jsxs(Group, {
751
- direction: "column",
854
+ onSubmit: handleSubmit(setConf),
855
+ children: [/* @__PURE__ */ jsx(Text, {
856
+ children: "X Axis"
857
+ }), /* @__PURE__ */ jsxs(Group, {
858
+ position: "apart",
752
859
  grow: true,
753
- my: 0,
754
860
  p: "md",
755
- pr: 40,
756
- sx: {
757
- border: "1px solid #eee",
758
- position: "relative"
759
- },
760
- children: [/* @__PURE__ */ jsx(TextInput, __spreadValues({
761
- label: "Key",
762
- required: true
763
- }, form.getListInputProps("snippets", index2, "key"))), /* @__PURE__ */ jsx(Textarea, __spreadValues({
764
- minRows: 3,
765
- label: "Value",
766
- required: true
767
- }, form.getListInputProps("snippets", index2, "value"))), /* @__PURE__ */ jsx(ActionIcon, {
768
- color: "red",
769
- variant: "hover",
770
- onClick: () => form.removeListItem("snippets", index2),
771
- sx: {
772
- position: "absolute",
773
- top: 15,
774
- right: 5
775
- },
776
- children: /* @__PURE__ */ jsx(Trash, {
777
- size: 16
778
- })
779
- })]
780
- }, index2)), /* @__PURE__ */ jsxs(Group, {
781
- position: "center",
782
- mt: "xl",
783
- grow: true,
784
861
  sx: {
785
- width: "80%"
786
- },
787
- mx: "auto",
788
- children: [/* @__PURE__ */ jsx(Button, {
789
- variant: "default",
790
- onClick: addSnippet,
791
- children: "Add a snippet"
792
- }), /* @__PURE__ */ jsx(Button, {
793
- color: "blue",
794
- type: "submit",
795
- disabled: !changed,
796
- children: "Submit"
797
- })]
798
- })]
799
- })
800
- });
801
- }
802
- function SQLSnippetsTab({}) {
803
- const {
804
- sqlSnippets,
805
- setSQLSnippets
806
- } = React.useContext(DefinitionContext);
807
- const sampleSQL = `SELECT *
808
- FROM commit
809
- WHERE \${author_time_condition}`;
810
- return /* @__PURE__ */ jsxs(Group, {
811
- direction: "column",
812
- children: [/* @__PURE__ */ jsx(Prism, {
813
- language: "sql",
814
- sx: {
815
- width: "100%"
816
- },
817
- noCopy: true,
818
- trim: false,
819
- colorScheme: "dark",
820
- children: `-- You may refer context data *by name*
821
- -- in SQL or VizConfig.
822
-
823
- ${sampleSQL}
824
-
825
- -- where author_time_condition is:
826
- author_time BETWEEN '\${timeRange?.[0].toISOString()}' AND '\${timeRange?.[1].toISOString()}'
827
- `
828
- }), /* @__PURE__ */ jsx(Text, {
829
- weight: 700,
830
- children: "SQL Snippets"
831
- }), /* @__PURE__ */ jsx(SQLSnippetsForm, {
832
- sqlSnippets,
833
- setSQLSnippets
834
- })]
835
- });
836
- }
837
- function EditDescription() {
838
- const {
839
- description,
840
- setDescription
841
- } = React.useContext(PanelContext);
842
- const [localDesc, setLocalDesc] = useInputState(description);
843
- const changed = description !== localDesc;
844
- const submit = React.useCallback(() => {
845
- if (!changed) {
846
- return;
847
- }
848
- setDescription(localDesc);
849
- }, [changed, localDesc]);
850
- return /* @__PURE__ */ jsx(Textarea, {
851
- label: "Panel Description",
852
- value: localDesc,
853
- onChange: setLocalDesc,
854
- minRows: 2,
855
- rightSection: /* @__PURE__ */ jsx(ActionIcon, {
856
- disabled: !changed,
857
- onClick: submit,
858
- sx: {
859
- alignSelf: "flex-start",
860
- marginTop: "4px"
861
- },
862
- children: /* @__PURE__ */ jsx(DeviceFloppy, {
863
- size: 20
864
- })
865
- })
866
- });
867
- }
868
- function EditTitle() {
869
- const {
870
- title,
871
- setTitle
872
- } = React.useContext(PanelContext);
873
- const [localTitle, setLocalTitle] = useInputState(title);
874
- const changed = title !== localTitle;
875
- const submit = React.useCallback(() => {
876
- if (!changed) {
877
- return;
878
- }
879
- setTitle(localTitle);
880
- }, [changed, localTitle]);
881
- return /* @__PURE__ */ jsx(TextInput, {
882
- label: "Panel Title",
883
- value: localTitle,
884
- onChange: setLocalTitle,
885
- rightSection: /* @__PURE__ */ jsx(ActionIcon, {
886
- disabled: !changed,
887
- onClick: submit,
888
- children: /* @__PURE__ */ jsx(DeviceFloppy, {
889
- size: 20
890
- })
891
- })
892
- });
893
- }
894
- function VizBar3DPanel({
895
- conf,
896
- setConf
897
- }) {
898
- const defaultValues = _.assign({}, {
899
- "x_axis_data_key": "x",
900
- "y_axis_data_key": "y",
901
- "z_axis_data_key": "z",
902
- "xAxis3D": {
903
- "type": "value",
904
- "name": "X Axis Name"
905
- },
906
- "yAxis3D": {
907
- "type": "value",
908
- "name": "Y Axis Name"
909
- },
910
- "zAxis3D": {
911
- "type": "value",
912
- "name": "Z Axis Name"
913
- }
914
- }, conf);
915
- const {
916
- control,
917
- handleSubmit,
918
- formState
919
- } = useForm$1({
920
- defaultValues
921
- });
922
- return /* @__PURE__ */ jsx(Group, {
923
- direction: "column",
924
- mt: "md",
925
- spacing: "xs",
926
- grow: true,
927
- children: /* @__PURE__ */ jsxs("form", {
928
- onSubmit: handleSubmit(setConf),
929
- children: [/* @__PURE__ */ jsx(Text, {
930
- children: "X Axis"
931
- }), /* @__PURE__ */ jsxs(Group, {
932
- position: "apart",
933
- grow: true,
934
- p: "md",
935
- sx: {
936
- position: "relative",
937
- border: "1px solid #eee"
862
+ position: "relative",
863
+ border: "1px solid #eee"
938
864
  },
939
865
  children: [/* @__PURE__ */ jsx(Controller, {
940
866
  name: "x_axis_data_key",
@@ -1119,7 +1045,7 @@ function VizLineBarChartPanel({
1119
1045
  const initialValues = React.useMemo(() => __spreadValues({
1120
1046
  series: formList(series != null ? series : [])
1121
1047
  }, restConf), [series, restConf]);
1122
- const form = useForm({
1048
+ const form = useForm$1({
1123
1049
  initialValues
1124
1050
  });
1125
1051
  const addSeries = () => form.addListItem("series", {
@@ -1231,7 +1157,7 @@ function SunburstPanel({
1231
1157
  },
1232
1158
  setConf
1233
1159
  }) {
1234
- const form = useForm({
1160
+ const form = useForm$1({
1235
1161
  initialValues: {
1236
1162
  label_field,
1237
1163
  value_field
@@ -1318,7 +1244,7 @@ function VizTablePanel(_a) {
1318
1244
  ]), {
1319
1245
  setConf
1320
1246
  } = _b;
1321
- const form = useForm({
1247
+ const form = useForm$1({
1322
1248
  initialValues: __spreadValues({
1323
1249
  id_field: "id",
1324
1250
  use_raw_columns: true,
@@ -1593,7 +1519,7 @@ function VizTextPanel({
1593
1519
  setConf
1594
1520
  }) {
1595
1521
  var _a;
1596
- const form = useForm({
1522
+ const form = useForm$1({
1597
1523
  initialValues: {
1598
1524
  paragraphs: formList((_a = conf.paragraphs) != null ? _a : sampleParagraphs)
1599
1525
  }
@@ -1788,9 +1714,34 @@ function EditVizConf() {
1788
1714
  }
1789
1715
  function VizConfig({}) {
1790
1716
  return /* @__PURE__ */ jsxs(Group, {
1717
+ direction: "row",
1791
1718
  grow: true,
1792
- direction: "column",
1793
- children: [/* @__PURE__ */ jsx(EditTitle, {}), /* @__PURE__ */ jsx(EditDescription, {}), /* @__PURE__ */ jsx(Divider, {}), /* @__PURE__ */ jsx(EditVizConf, {})]
1719
+ noWrap: true,
1720
+ align: "stretch",
1721
+ sx: {
1722
+ height: "100%"
1723
+ },
1724
+ children: [/* @__PURE__ */ jsxs(Group, {
1725
+ grow: true,
1726
+ direction: "column",
1727
+ sx: {
1728
+ width: "40%",
1729
+ flexShrink: 0,
1730
+ flexGrow: 0
1731
+ },
1732
+ children: [/* @__PURE__ */ jsx(EditTitle, {}), /* @__PURE__ */ jsx(EditDescription, {}), /* @__PURE__ */ jsx(Divider, {
1733
+ sx: {
1734
+ flexGrow: 0
1735
+ }
1736
+ }), /* @__PURE__ */ jsx(EditVizConf, {})]
1737
+ }), /* @__PURE__ */ jsx(Box, {
1738
+ sx: {
1739
+ height: "100%",
1740
+ flexGrow: 1,
1741
+ maxWidth: "60%"
1742
+ },
1743
+ children: /* @__PURE__ */ jsx(PreviewViz, {})
1744
+ })]
1794
1745
  });
1795
1746
  }
1796
1747
  function PanelSettingsModal({
@@ -1832,40 +1783,17 @@ function PanelSettingsModal({
1832
1783
  }
1833
1784
  },
1834
1785
  padding: "md",
1835
- navbar: /* @__PURE__ */ jsx(Navbar, {
1836
- width: {
1837
- base: "40%"
1838
- },
1839
- height: "100%",
1840
- p: "xs",
1841
- children: /* @__PURE__ */ jsxs(Tabs, {
1842
- initialTab: 1,
1843
- children: [/* @__PURE__ */ jsx(Tabs.Tab, {
1844
- label: "Context",
1845
- children: /* @__PURE__ */ jsx(ContextInfo, {})
1846
- }), /* @__PURE__ */ jsx(Tabs.Tab, {
1847
- label: "SQL Snippets",
1848
- children: /* @__PURE__ */ jsx(SQLSnippetsTab, {})
1849
- }), /* @__PURE__ */ jsx(Tabs.Tab, {
1850
- label: "SQL",
1851
- children: /* @__PURE__ */ jsx(QueryEditor, {})
1852
- }), /* @__PURE__ */ jsxs(Tabs.Tab, {
1853
- label: "Data",
1854
- children: [/* @__PURE__ */ jsx(LoadingOverlay, {
1855
- visible: loading
1856
- }), /* @__PURE__ */ jsx(QueryResult, {})]
1857
- }), /* @__PURE__ */ jsx(Tabs.Tab, {
1858
- label: "Viz Config",
1859
- children: /* @__PURE__ */ jsx(VizConfig, {})
1860
- })]
1861
- })
1862
- }),
1863
- children: /* @__PURE__ */ jsx(ErrorBoundary, {
1864
- children: /* @__PURE__ */ jsx(Viz, {
1865
- viz,
1866
- data,
1867
- loading
1868
- })
1786
+ children: /* @__PURE__ */ jsxs(Tabs, {
1787
+ initialTab: 2,
1788
+ children: [/* @__PURE__ */ jsxs(Tabs.Tab, {
1789
+ label: "Data Source",
1790
+ children: [/* @__PURE__ */ jsx(LoadingOverlay, {
1791
+ visible: loading
1792
+ }), /* @__PURE__ */ jsx(PickDataSource, {})]
1793
+ }), /* @__PURE__ */ jsx(Tabs.Tab, {
1794
+ label: "Viz Config",
1795
+ children: /* @__PURE__ */ jsx(VizConfig, {})
1796
+ })]
1869
1797
  })
1870
1798
  })
1871
1799
  });
@@ -1947,7 +1875,7 @@ function PanelTitleBar({}) {
1947
1875
  var index$1 = "";
1948
1876
  function Panel({
1949
1877
  viz: initialViz,
1950
- sql: initialSQL,
1878
+ dataSourceID: initialDataSourceID,
1951
1879
  title: initialTitle,
1952
1880
  description: initialDesc,
1953
1881
  update,
@@ -1958,24 +1886,35 @@ function Panel({
1958
1886
  const definitions = React.useContext(DefinitionContext);
1959
1887
  const [title, setTitle] = React.useState(initialTitle);
1960
1888
  const [description, setDescription] = React.useState(initialDesc);
1961
- const [sql, setSQL] = React.useState(initialSQL);
1889
+ const [dataSourceID, setDataSourceID] = React.useState(initialDataSourceID);
1962
1890
  const [viz, setViz] = React.useState(initialViz);
1891
+ const dataSource = React.useMemo(() => {
1892
+ if (!dataSourceID) {
1893
+ return void 0;
1894
+ }
1895
+ return definitions.dataSources.find((d) => d.id === dataSourceID);
1896
+ }, [dataSourceID, definitions.dataSources]);
1963
1897
  React.useEffect(() => {
1964
1898
  update == null ? void 0 : update({
1965
1899
  id,
1966
1900
  layout,
1967
1901
  title,
1968
1902
  description,
1969
- sql,
1903
+ dataSourceID,
1970
1904
  viz
1971
1905
  });
1972
- }, [title, description, sql, viz, id, layout]);
1906
+ }, [title, description, dataSource, viz, id, layout, dataSourceID]);
1973
1907
  const {
1974
1908
  data = [],
1975
1909
  loading,
1976
1910
  refresh
1977
- } = useRequest(queryBySQL(sql, contextInfo, definitions, title), {
1978
- refreshDeps: [contextInfo, definitions]
1911
+ } = useRequest(queryBySQL({
1912
+ context: contextInfo,
1913
+ definitions,
1914
+ title,
1915
+ dataSource
1916
+ }), {
1917
+ refreshDeps: [contextInfo, definitions, dataSource]
1979
1918
  });
1980
1919
  const refreshData = refresh;
1981
1920
  return /* @__PURE__ */ jsx(PanelContext.Provider, {
@@ -1986,8 +1925,8 @@ function Panel({
1986
1925
  setTitle,
1987
1926
  description,
1988
1927
  setDescription,
1989
- sql,
1990
- setSQL,
1928
+ dataSourceID,
1929
+ setDataSourceID,
1991
1930
  viz,
1992
1931
  setViz,
1993
1932
  refreshData
@@ -2041,7 +1980,7 @@ function DashboardLayout({
2041
1980
  const newPanels = panels.map((p2) => __spreadProps(__spreadValues({}, p2), {
2042
1981
  layout: m2.get(p2.id)
2043
1982
  }));
2044
- setPanels.setState(newPanels);
1983
+ setPanels(newPanels);
2045
1984
  }, [panels, setPanels]);
2046
1985
  return /* @__PURE__ */ jsx(ResponsiveReactGridLayout$1, {
2047
1986
  onBreakpointChange,
@@ -2064,7 +2003,10 @@ function DashboardLayout({
2064
2003
  }, rest), {
2065
2004
  destroy: () => onRemoveItem(id),
2066
2005
  update: (panel) => {
2067
- setPanels.setItem(index2, panel);
2006
+ setPanels((prevs) => {
2007
+ prevs.splice(index2, 1, panel);
2008
+ return prevs;
2009
+ });
2068
2010
  }
2069
2011
  }))
2070
2012
  }, id);
@@ -2101,95 +2043,698 @@ function ModeToggler({
2101
2043
  }]
2102
2044
  });
2103
2045
  }
2104
- function DashboardActions({
2105
- mode,
2106
- setMode,
2107
- hasChanges,
2108
- addPanel,
2109
- saveChanges
2110
- }) {
2111
- const inEditMode = mode === DashboardMode.Edit;
2046
+ const example = `
2047
+ -- You may reference context data or SQL snippets *by name*
2048
+ -- in SQL or VizConfig.
2049
+ SELECT *
2050
+ FROM commit
2051
+ WHERE
2052
+ -- context data
2053
+ author_time BETWEEN '\${timeRange?.[0].toISOString()}' AND '\${timeRange?.[1].toISOString()}'
2054
+ -- SQL snippets
2055
+ AND \${author_email_condition}
2056
+ \${order_by_clause}
2057
+ `;
2058
+ function ContextAndSnippets({}) {
2059
+ const contextInfo = React.useContext(ContextInfoContext);
2060
+ const {
2061
+ sqlSnippets
2062
+ } = React.useContext(DefinitionContext);
2063
+ const snippets = React.useMemo(() => {
2064
+ const snippets2 = sqlSnippets.reduce((prev, curr) => {
2065
+ prev[curr.key] = curr.value;
2066
+ return prev;
2067
+ }, {});
2068
+ return JSON.stringify(snippets2, null, 2);
2069
+ }, [sqlSnippets]);
2070
+ const context = React.useMemo(() => {
2071
+ return JSON.stringify(contextInfo, null, 2);
2072
+ }, [contextInfo]);
2112
2073
  return /* @__PURE__ */ jsxs(Group, {
2113
- position: "apart",
2114
- pt: "sm",
2115
- pb: "xs",
2074
+ direction: "column",
2075
+ grow: true,
2076
+ sx: {
2077
+ border: "1px solid #eee",
2078
+ maxWidth: "48%",
2079
+ overflow: "hidden"
2080
+ },
2116
2081
  children: [/* @__PURE__ */ jsx(Group, {
2117
2082
  position: "left",
2118
- children: /* @__PURE__ */ jsx(ModeToggler, {
2119
- mode,
2120
- setMode
2083
+ pl: "md",
2084
+ py: "md",
2085
+ sx: {
2086
+ borderBottom: "1px solid #eee",
2087
+ background: "#efefef",
2088
+ flexGrow: 0
2089
+ },
2090
+ children: /* @__PURE__ */ jsx(Text, {
2091
+ weight: 500,
2092
+ children: "Context"
2121
2093
  })
2122
- }), inEditMode && /* @__PURE__ */ jsxs(Group, {
2123
- position: "right",
2124
- children: [/* @__PURE__ */ jsx(Button, {
2125
- variant: "default",
2126
- size: "sm",
2127
- onClick: addPanel,
2128
- leftIcon: /* @__PURE__ */ jsx(PlaylistAdd, {
2129
- size: 20
2130
- }),
2131
- children: "Add a Panel"
2132
- }), /* @__PURE__ */ jsx(Button, {
2133
- variant: "default",
2134
- size: "sm",
2135
- onClick: saveChanges,
2136
- disabled: !hasChanges,
2137
- leftIcon: /* @__PURE__ */ jsx(DeviceFloppy, {
2138
- size: 20
2139
- }),
2140
- children: "Save Changes"
2141
- }), /* @__PURE__ */ jsx(Button, {
2142
- color: "red",
2143
- size: "sm",
2144
- disabled: !hasChanges,
2145
- leftIcon: /* @__PURE__ */ jsx(Recycle, {
2146
- size: 20
2147
- }),
2148
- children: "Revert Changes"
2094
+ }), /* @__PURE__ */ jsxs(Group, {
2095
+ direction: "column",
2096
+ px: "md",
2097
+ pb: "md",
2098
+ sx: {
2099
+ width: "100%"
2100
+ },
2101
+ children: [/* @__PURE__ */ jsx(Prism, {
2102
+ language: "sql",
2103
+ sx: {
2104
+ width: "100%"
2105
+ },
2106
+ noCopy: true,
2107
+ colorScheme: "dark",
2108
+ children: example
2109
+ }), /* @__PURE__ */ jsx(Text, {
2110
+ weight: 500,
2111
+ sx: {
2112
+ flexGrow: 0
2113
+ },
2114
+ children: "Avaiable context"
2115
+ }), /* @__PURE__ */ jsx(Prism, {
2116
+ language: "json",
2117
+ sx: {
2118
+ width: "100%"
2119
+ },
2120
+ noCopy: true,
2121
+ colorScheme: "dark",
2122
+ children: context
2123
+ }), /* @__PURE__ */ jsx(Text, {
2124
+ weight: 500,
2125
+ sx: {
2126
+ flexGrow: 0
2127
+ },
2128
+ children: "Avaiable SQL Snippets"
2129
+ }), /* @__PURE__ */ jsx(Prism, {
2130
+ language: "json",
2131
+ sx: {
2132
+ width: "100%"
2133
+ },
2134
+ noCopy: true,
2135
+ colorScheme: "dark",
2136
+ children: snippets
2149
2137
  })]
2150
- }), !inEditMode && /* @__PURE__ */ jsx(Button, {
2151
- variant: "default",
2152
- size: "sm",
2153
- disabled: true,
2154
- leftIcon: /* @__PURE__ */ jsx(Share, {
2155
- size: 20
2156
- }),
2157
- children: "Share"
2158
2138
  })]
2159
2139
  });
2160
2140
  }
2161
- function Dashboard({
2162
- dashboard,
2163
- update,
2164
- className = "dashboard"
2141
+ function DataSourceForm({
2142
+ value,
2143
+ onChange
2165
2144
  }) {
2166
- const [layoutFrozen, freezeLayout] = React.useState(false);
2167
- const [breakpoint, setBreakpoint] = React.useState();
2168
- const [localCols, setLocalCols] = React.useState();
2169
- const [panels, setPanels] = useListState(dashboard.panels);
2170
- const [sqlSnippets, setSQLSnippets] = React.useState(dashboard.definition.sqlSnippets);
2171
- const [mode, setMode] = React.useState(DashboardMode.Edit);
2172
- const hasChanges = React.useMemo(() => {
2173
- const cleanJSON = (v) => JSON.parse(JSON.stringify(v));
2174
- const panelsEqual = _.isEqual(cleanJSON(panels), cleanJSON(dashboard.panels));
2175
- if (!panelsEqual) {
2176
- return true;
2177
- }
2178
- return !_.isEqual(sqlSnippets, dashboard.definition.sqlSnippets);
2179
- }, [dashboard, panels, sqlSnippets]);
2180
- const saveDashboardChanges = async () => {
2181
- const d = _.merge({}, dashboard, {
2182
- panels
2183
- }, {
2184
- definition: {
2185
- sqlSnippets
2186
- }
2187
- });
2188
- await update(d);
2189
- };
2190
- const addPanel = () => {
2191
- const id = randomId();
2192
- const newItem = {
2145
+ const form = useForm$1({
2146
+ initialValues: value
2147
+ });
2148
+ const submit = React.useCallback((values) => {
2149
+ onChange(values);
2150
+ }, [onChange]);
2151
+ const changed = React.useMemo(() => {
2152
+ return !_.isEqual(value, form.values);
2153
+ }, [value, form.values]);
2154
+ React.useEffect(() => {
2155
+ form.reset();
2156
+ }, [value]);
2157
+ return /* @__PURE__ */ jsx(Group, {
2158
+ direction: "column",
2159
+ grow: true,
2160
+ sx: {
2161
+ border: "1px solid #eee",
2162
+ flexGrow: 1
2163
+ },
2164
+ children: /* @__PURE__ */ jsxs("form", {
2165
+ onSubmit: form.onSubmit(submit),
2166
+ children: [/* @__PURE__ */ jsxs(Group, {
2167
+ position: "left",
2168
+ py: "md",
2169
+ pl: "md",
2170
+ sx: {
2171
+ borderBottom: "1px solid #eee",
2172
+ background: "#efefef"
2173
+ },
2174
+ children: [/* @__PURE__ */ jsx(Text, {
2175
+ weight: 500,
2176
+ children: "Data Source Configuration"
2177
+ }), /* @__PURE__ */ jsx(ActionIcon, {
2178
+ type: "submit",
2179
+ mr: 5,
2180
+ variant: "filled",
2181
+ color: "blue",
2182
+ disabled: !changed,
2183
+ children: /* @__PURE__ */ jsx(DeviceFloppy, {
2184
+ size: 20
2185
+ })
2186
+ })]
2187
+ }), /* @__PURE__ */ jsxs(Group, {
2188
+ direction: "column",
2189
+ grow: true,
2190
+ my: 0,
2191
+ p: "md",
2192
+ pr: 40,
2193
+ children: [/* @__PURE__ */ jsxs(Group, {
2194
+ grow: true,
2195
+ children: [/* @__PURE__ */ jsx(TextInput, __spreadValues({
2196
+ placeholder: "An ID unique in this dashboard",
2197
+ label: "ID",
2198
+ required: true,
2199
+ sx: {
2200
+ flex: 1
2201
+ }
2202
+ }, form.getInputProps("id"))), /* @__PURE__ */ jsx(TextInput, __spreadValues({
2203
+ placeholder: "TODO: use a dedicated UI component for this",
2204
+ label: "Data Source Key",
2205
+ required: true,
2206
+ sx: {
2207
+ flex: 1
2208
+ }
2209
+ }, form.getInputProps("key"))), /* @__PURE__ */ jsx(TextInput, __spreadValues({
2210
+ placeholder: "Type of the data source",
2211
+ label: "Type",
2212
+ disabled: true,
2213
+ sx: {
2214
+ flex: 1
2215
+ }
2216
+ }, form.getInputProps("type")))]
2217
+ }), /* @__PURE__ */ jsx(Textarea, __spreadValues({
2218
+ autosize: true,
2219
+ minRows: 12,
2220
+ maxRows: 24
2221
+ }, form.getInputProps("sql")))]
2222
+ })]
2223
+ })
2224
+ });
2225
+ }
2226
+ function DataSourceEditor({
2227
+ id
2228
+ }) {
2229
+ const {
2230
+ dataSources,
2231
+ setDataSources
2232
+ } = React.useContext(DefinitionContext);
2233
+ const dataSource = React.useMemo(() => {
2234
+ return dataSources.find((d) => d.id === id);
2235
+ }, [dataSources, id]);
2236
+ const update = React.useCallback((value) => {
2237
+ const index2 = dataSources.findIndex((d) => d.id === value.id);
2238
+ if (!index2) {
2239
+ console.error(new Error("Invalid data source id when updating by id"));
2240
+ return;
2241
+ }
2242
+ setDataSources((prevs) => {
2243
+ return prevs.map((p2) => {
2244
+ if (p2.id === value.id) {
2245
+ return value;
2246
+ }
2247
+ return p2;
2248
+ });
2249
+ });
2250
+ }, [setDataSources]);
2251
+ if (!dataSource) {
2252
+ return /* @__PURE__ */ jsx("span", {
2253
+ children: "Invalid Data Source ID"
2254
+ });
2255
+ }
2256
+ return /* @__PURE__ */ jsxs(Group, {
2257
+ direction: "row",
2258
+ position: "apart",
2259
+ grow: true,
2260
+ align: "stretch",
2261
+ noWrap: true,
2262
+ children: [/* @__PURE__ */ jsx(DataSourceForm, {
2263
+ value: dataSource,
2264
+ onChange: update
2265
+ }), /* @__PURE__ */ jsx(ContextAndSnippets, {})]
2266
+ });
2267
+ }
2268
+ function SelectOrAddDataSource({
2269
+ id,
2270
+ setID
2271
+ }) {
2272
+ const {
2273
+ dataSources,
2274
+ setDataSources
2275
+ } = React.useContext(DefinitionContext);
2276
+ React.useEffect(() => {
2277
+ var _a, _b;
2278
+ if (!id) {
2279
+ setID((_b = (_a = dataSources[0]) == null ? void 0 : _a.id) != null ? _b : "");
2280
+ }
2281
+ }, [id, setID, dataSources]);
2282
+ const options = React.useMemo(() => {
2283
+ return dataSources.map((d) => ({
2284
+ value: d.id,
2285
+ label: d.id
2286
+ }));
2287
+ }, [dataSources]);
2288
+ const add = React.useCallback(() => {
2289
+ const newDataSource = {
2290
+ id: randomId(),
2291
+ type: "postgresql",
2292
+ key: "",
2293
+ sql: ""
2294
+ };
2295
+ setDataSources((prevs) => [...prevs, newDataSource]);
2296
+ }, [setDataSources]);
2297
+ return /* @__PURE__ */ jsx(Group, {
2298
+ pb: "xl",
2299
+ children: /* @__PURE__ */ jsxs(Group, {
2300
+ position: "left",
2301
+ sx: {
2302
+ maxWidth: "600px",
2303
+ alignItems: "baseline"
2304
+ },
2305
+ children: [/* @__PURE__ */ jsx(Text, {
2306
+ children: "Select a Data Source"
2307
+ }), /* @__PURE__ */ jsx(Select, {
2308
+ data: options,
2309
+ value: id,
2310
+ onChange: setID,
2311
+ allowDeselect: false,
2312
+ clearable: false,
2313
+ sx: {
2314
+ flexGrow: 1
2315
+ }
2316
+ }), /* @__PURE__ */ jsx(Text, {
2317
+ children: "or"
2318
+ }), /* @__PURE__ */ jsx(Group, {
2319
+ position: "center",
2320
+ mt: "md",
2321
+ children: /* @__PURE__ */ jsx(Button, {
2322
+ onClick: add,
2323
+ children: "Add a Data Source"
2324
+ })
2325
+ })]
2326
+ })
2327
+ });
2328
+ }
2329
+ function EditDataSourcesModal({
2330
+ opened,
2331
+ close
2332
+ }) {
2333
+ const [id, setID] = React.useState("");
2334
+ const {
2335
+ freezeLayout
2336
+ } = React.useContext(LayoutStateContext);
2337
+ React.useEffect(() => {
2338
+ freezeLayout(opened);
2339
+ }, [opened]);
2340
+ return /* @__PURE__ */ jsx(Modal, {
2341
+ size: "96vw",
2342
+ overflow: "inside",
2343
+ opened,
2344
+ onClose: close,
2345
+ title: "Data Sources",
2346
+ trapFocus: true,
2347
+ onDragStart: (e) => {
2348
+ e.stopPropagation();
2349
+ },
2350
+ children: /* @__PURE__ */ jsxs(AppShell, {
2351
+ sx: {
2352
+ height: "90vh",
2353
+ maxHeight: "calc(100vh - 185px)",
2354
+ ".mantine-AppShell-body": {
2355
+ height: "100%"
2356
+ },
2357
+ main: {
2358
+ height: "100%",
2359
+ width: "100%",
2360
+ padding: 0,
2361
+ margin: 0
2362
+ }
2363
+ },
2364
+ padding: "md",
2365
+ header: /* @__PURE__ */ jsx(SelectOrAddDataSource, {
2366
+ id,
2367
+ setID
2368
+ }),
2369
+ children: [/* @__PURE__ */ jsx(DataSourceEditor, {
2370
+ id
2371
+ }), /* @__PURE__ */ jsx(DataPreview, {
2372
+ id
2373
+ })]
2374
+ })
2375
+ });
2376
+ }
2377
+ function ContextInfo({}) {
2378
+ const contextInfo = React.useContext(ContextInfoContext);
2379
+ const sampleSQL = `SELECT *
2380
+ FROM commit
2381
+ WHERE author_time BETWEEN '\${timeRange?.[0].toISOString()}' AND '\${timeRange?.[1].toISOString()}'`;
2382
+ return /* @__PURE__ */ jsxs(Group, {
2383
+ direction: "column",
2384
+ grow: true,
2385
+ sx: {
2386
+ border: "1px solid #eee",
2387
+ maxWidth: "48%",
2388
+ overflow: "hidden"
2389
+ },
2390
+ children: [/* @__PURE__ */ jsx(Group, {
2391
+ position: "left",
2392
+ pl: "md",
2393
+ py: "md",
2394
+ sx: {
2395
+ borderBottom: "1px solid #eee",
2396
+ background: "#efefef",
2397
+ flexGrow: 0
2398
+ },
2399
+ children: /* @__PURE__ */ jsx(Text, {
2400
+ weight: 500,
2401
+ children: "Context"
2402
+ })
2403
+ }), /* @__PURE__ */ jsxs(Group, {
2404
+ direction: "column",
2405
+ px: "md",
2406
+ pb: "md",
2407
+ sx: {
2408
+ width: "100%"
2409
+ },
2410
+ children: [/* @__PURE__ */ jsx(Prism, {
2411
+ language: "sql",
2412
+ sx: {
2413
+ width: "100%"
2414
+ },
2415
+ noCopy: true,
2416
+ colorScheme: "dark",
2417
+ children: `-- You may refer context data *by name*
2418
+ -- in SQL or VizConfig.
2419
+
2420
+ ${sampleSQL}`
2421
+ }), /* @__PURE__ */ jsx(Text, {
2422
+ weight: 500,
2423
+ sx: {
2424
+ flexGrow: 0
2425
+ },
2426
+ children: "Avaiable context entries"
2427
+ }), /* @__PURE__ */ jsx(Prism, {
2428
+ language: "json",
2429
+ sx: {
2430
+ width: "100%"
2431
+ },
2432
+ noCopy: true,
2433
+ colorScheme: "dark",
2434
+ children: JSON.stringify(contextInfo, null, 2)
2435
+ })]
2436
+ })]
2437
+ });
2438
+ }
2439
+ function SQLSnippetsEditor({}) {
2440
+ const {
2441
+ sqlSnippets,
2442
+ setSQLSnippets
2443
+ } = React.useContext(DefinitionContext);
2444
+ const sampleSQL = `SELECT *
2445
+ FROM commit
2446
+ WHERE \${author_time_condition}`;
2447
+ const initialValues = React.useMemo(() => ({
2448
+ snippets: formList(sqlSnippets != null ? sqlSnippets : [])
2449
+ }), [sqlSnippets]);
2450
+ const form = useForm$1({
2451
+ initialValues
2452
+ });
2453
+ const addSnippet = () => form.addListItem("snippets", {
2454
+ key: randomId(),
2455
+ value: ""
2456
+ });
2457
+ const changed = React.useMemo(() => !_.isEqual(form.values, initialValues), [form.values, initialValues]);
2458
+ const submit = ({
2459
+ snippets
2460
+ }) => {
2461
+ setSQLSnippets(snippets);
2462
+ };
2463
+ return /* @__PURE__ */ jsxs(Group, {
2464
+ direction: "column",
2465
+ grow: true,
2466
+ sx: {
2467
+ border: "1px solid #eee"
2468
+ },
2469
+ children: [/* @__PURE__ */ jsxs(Group, {
2470
+ position: "left",
2471
+ pl: "md",
2472
+ py: "md",
2473
+ sx: {
2474
+ borderBottom: "1px solid #eee",
2475
+ background: "#efefef",
2476
+ flexGrow: 0
2477
+ },
2478
+ children: [/* @__PURE__ */ jsx(Text, {
2479
+ weight: 500,
2480
+ children: "SQL Snippets"
2481
+ }), /* @__PURE__ */ jsx(ActionIcon, {
2482
+ type: "submit",
2483
+ mr: 5,
2484
+ variant: "filled",
2485
+ color: "blue",
2486
+ disabled: !changed,
2487
+ children: /* @__PURE__ */ jsx(DeviceFloppy, {
2488
+ size: 20
2489
+ })
2490
+ })]
2491
+ }), /* @__PURE__ */ jsxs(Group, {
2492
+ px: "md",
2493
+ pb: "md",
2494
+ children: [/* @__PURE__ */ jsx(Prism, {
2495
+ language: "sql",
2496
+ sx: {
2497
+ width: "100%"
2498
+ },
2499
+ noCopy: true,
2500
+ trim: false,
2501
+ colorScheme: "dark",
2502
+ children: `-- You may refer context data *by name*
2503
+ -- in SQL or VizConfig.
2504
+
2505
+ ${sampleSQL}
2506
+
2507
+ -- where author_time_condition is:
2508
+ author_time BETWEEN '\${timeRange?.[0].toISOString()}' AND '\${timeRange?.[1].toISOString()}'
2509
+ `
2510
+ }), /* @__PURE__ */ jsx(Group, {
2511
+ direction: "column",
2512
+ sx: {
2513
+ width: "100%",
2514
+ position: "relative"
2515
+ },
2516
+ grow: true,
2517
+ children: /* @__PURE__ */ jsxs("form", {
2518
+ onSubmit: form.onSubmit(submit),
2519
+ children: [form.values.snippets.map((_item, index2) => /* @__PURE__ */ jsxs(Group, {
2520
+ direction: "column",
2521
+ grow: true,
2522
+ my: 0,
2523
+ p: "md",
2524
+ pr: 40,
2525
+ sx: {
2526
+ border: "1px solid #eee",
2527
+ position: "relative"
2528
+ },
2529
+ children: [/* @__PURE__ */ jsx(TextInput, __spreadValues({
2530
+ label: "Key",
2531
+ required: true
2532
+ }, form.getListInputProps("snippets", index2, "key"))), /* @__PURE__ */ jsx(Textarea, __spreadValues({
2533
+ minRows: 3,
2534
+ label: "Value",
2535
+ required: true
2536
+ }, form.getListInputProps("snippets", index2, "value"))), /* @__PURE__ */ jsx(ActionIcon, {
2537
+ color: "red",
2538
+ variant: "hover",
2539
+ onClick: () => form.removeListItem("snippets", index2),
2540
+ sx: {
2541
+ position: "absolute",
2542
+ top: 15,
2543
+ right: 5
2544
+ },
2545
+ children: /* @__PURE__ */ jsx(Trash, {
2546
+ size: 16
2547
+ })
2548
+ })]
2549
+ }, index2)), /* @__PURE__ */ jsx(Group, {
2550
+ position: "center",
2551
+ mt: "xl",
2552
+ grow: true,
2553
+ sx: {
2554
+ width: "40%"
2555
+ },
2556
+ mx: "auto",
2557
+ children: /* @__PURE__ */ jsx(Button, {
2558
+ variant: "default",
2559
+ onClick: addSnippet,
2560
+ children: "Add a snippet"
2561
+ })
2562
+ })]
2563
+ })
2564
+ })]
2565
+ })]
2566
+ });
2567
+ }
2568
+ function EditSQLSnippetsModal({
2569
+ opened,
2570
+ close
2571
+ }) {
2572
+ const {
2573
+ freezeLayout
2574
+ } = React.useContext(LayoutStateContext);
2575
+ React.useEffect(() => {
2576
+ freezeLayout(opened);
2577
+ }, [opened]);
2578
+ return /* @__PURE__ */ jsx(Modal, {
2579
+ size: "96vw",
2580
+ overflow: "inside",
2581
+ opened,
2582
+ onClose: close,
2583
+ title: "SQL Snippets",
2584
+ trapFocus: true,
2585
+ onDragStart: (e) => {
2586
+ e.stopPropagation();
2587
+ },
2588
+ children: /* @__PURE__ */ jsx(AppShell, {
2589
+ sx: {
2590
+ height: "90vh",
2591
+ maxHeight: "calc(100vh - 185px)",
2592
+ ".mantine-AppShell-body": {
2593
+ height: "100%"
2594
+ },
2595
+ main: {
2596
+ height: "100%",
2597
+ width: "100%",
2598
+ padding: 0,
2599
+ margin: 0
2600
+ }
2601
+ },
2602
+ padding: "md",
2603
+ children: /* @__PURE__ */ jsxs(Group, {
2604
+ direction: "row",
2605
+ position: "apart",
2606
+ grow: true,
2607
+ align: "stretch",
2608
+ noWrap: true,
2609
+ children: [/* @__PURE__ */ jsx(SQLSnippetsEditor, {}), /* @__PURE__ */ jsx(ContextInfo, {})]
2610
+ })
2611
+ })
2612
+ });
2613
+ }
2614
+ function DashboardActions({
2615
+ mode,
2616
+ setMode,
2617
+ hasChanges,
2618
+ addPanel,
2619
+ saveChanges
2620
+ }) {
2621
+ const [dataSourcesOpened, setDataSourcesOpened] = React.useState(false);
2622
+ const openDataSources = () => setDataSourcesOpened(true);
2623
+ const closeDataSources = () => setDataSourcesOpened(false);
2624
+ const [sqlSnippetsOpened, setSQLSnippetsOpened] = React.useState(false);
2625
+ const openSQLSnippets = () => setSQLSnippetsOpened(true);
2626
+ const closeSQLSnippets = () => setSQLSnippetsOpened(false);
2627
+ const inEditMode = mode === DashboardMode.Edit;
2628
+ return /* @__PURE__ */ jsxs(Group, {
2629
+ position: "apart",
2630
+ pt: "sm",
2631
+ pb: "xs",
2632
+ children: [/* @__PURE__ */ jsx(Group, {
2633
+ position: "left",
2634
+ children: /* @__PURE__ */ jsx(ModeToggler, {
2635
+ mode,
2636
+ setMode
2637
+ })
2638
+ }), inEditMode && /* @__PURE__ */ jsxs(Fragment, {
2639
+ children: [/* @__PURE__ */ jsxs(Group, {
2640
+ position: "right",
2641
+ children: [/* @__PURE__ */ jsx(Button, {
2642
+ variant: "default",
2643
+ size: "sm",
2644
+ onClick: addPanel,
2645
+ leftIcon: /* @__PURE__ */ jsx(PlaylistAdd, {
2646
+ size: 20
2647
+ }),
2648
+ children: "Add a Panel"
2649
+ }), /* @__PURE__ */ jsx(Button, {
2650
+ variant: "default",
2651
+ size: "sm",
2652
+ onClick: openSQLSnippets,
2653
+ leftIcon: /* @__PURE__ */ jsx(ClipboardText, {
2654
+ size: 20
2655
+ }),
2656
+ children: "SQL Snippets"
2657
+ }), /* @__PURE__ */ jsx(Button, {
2658
+ variant: "default",
2659
+ size: "sm",
2660
+ onClick: openDataSources,
2661
+ leftIcon: /* @__PURE__ */ jsx(Database, {
2662
+ size: 20
2663
+ }),
2664
+ children: "Data Sources"
2665
+ }), /* @__PURE__ */ jsx(Button, {
2666
+ variant: "default",
2667
+ size: "sm",
2668
+ onClick: saveChanges,
2669
+ disabled: !hasChanges,
2670
+ leftIcon: /* @__PURE__ */ jsx(DeviceFloppy, {
2671
+ size: 20
2672
+ }),
2673
+ children: "Save Changes"
2674
+ }), /* @__PURE__ */ jsx(Button, {
2675
+ color: "red",
2676
+ size: "sm",
2677
+ disabled: !hasChanges,
2678
+ leftIcon: /* @__PURE__ */ jsx(Recycle, {
2679
+ size: 20
2680
+ }),
2681
+ children: "Revert Changes"
2682
+ })]
2683
+ }), /* @__PURE__ */ jsx(EditDataSourcesModal, {
2684
+ opened: dataSourcesOpened,
2685
+ close: closeDataSources
2686
+ }), /* @__PURE__ */ jsx(EditSQLSnippetsModal, {
2687
+ opened: sqlSnippetsOpened,
2688
+ close: closeSQLSnippets
2689
+ })]
2690
+ }), !inEditMode && /* @__PURE__ */ jsx(Button, {
2691
+ variant: "default",
2692
+ size: "sm",
2693
+ disabled: true,
2694
+ leftIcon: /* @__PURE__ */ jsx(Share, {
2695
+ size: 20
2696
+ }),
2697
+ children: "Share"
2698
+ })]
2699
+ });
2700
+ }
2701
+ function Dashboard({
2702
+ context,
2703
+ dashboard,
2704
+ update,
2705
+ className = "dashboard"
2706
+ }) {
2707
+ const [layoutFrozen, freezeLayout] = React.useState(false);
2708
+ const [breakpoint, setBreakpoint] = React.useState();
2709
+ const [localCols, setLocalCols] = React.useState();
2710
+ const [panels, setPanels] = React.useState(dashboard.panels);
2711
+ const [sqlSnippets, setSQLSnippets] = React.useState(dashboard.definition.sqlSnippets);
2712
+ const [dataSources, setDataSources] = React.useState(dashboard.definition.dataSources);
2713
+ const [mode, setMode] = React.useState(DashboardMode.Edit);
2714
+ const hasChanges = React.useMemo(() => {
2715
+ const cleanJSON = (v) => JSON.parse(JSON.stringify(v));
2716
+ const panelsEqual = _.isEqual(cleanJSON(panels), cleanJSON(dashboard.panels));
2717
+ if (!panelsEqual) {
2718
+ return true;
2719
+ }
2720
+ if (!_.isEqual(sqlSnippets, dashboard.definition.sqlSnippets)) {
2721
+ return true;
2722
+ }
2723
+ return !_.isEqual(dataSources, dashboard.definition.dataSources);
2724
+ }, [dashboard, panels, sqlSnippets, dataSources]);
2725
+ const saveDashboardChanges = async () => {
2726
+ const d = _.merge({}, dashboard, {
2727
+ panels
2728
+ }, {
2729
+ definition: {
2730
+ sqlSnippets
2731
+ }
2732
+ });
2733
+ await update(d);
2734
+ };
2735
+ const addPanel = () => {
2736
+ const id = randomId();
2737
+ const newItem = {
2193
2738
  id,
2194
2739
  layout: {
2195
2740
  x: 0,
@@ -2199,49 +2744,57 @@ function Dashboard({
2199
2744
  },
2200
2745
  title: `New Panel - ${id}`,
2201
2746
  description: "description goes here",
2202
- sql: "",
2747
+ dataSourceID: "",
2203
2748
  viz: {
2204
2749
  type: "table",
2205
2750
  conf: {}
2206
2751
  }
2207
2752
  };
2208
- setPanels.append(newItem);
2753
+ setPanels((prevs) => [...prevs, newItem]);
2209
2754
  };
2210
2755
  const removePanelByID = (id) => {
2211
2756
  const index2 = panels.findIndex((p2) => p2.id === id);
2212
- setPanels.remove(index2);
2757
+ setPanels((prevs) => {
2758
+ prevs.splice(index2, 1);
2759
+ return prevs;
2760
+ });
2213
2761
  };
2214
2762
  const inEditMode = mode === DashboardMode.Edit;
2215
2763
  const definitions = React.useMemo(() => ({
2216
2764
  sqlSnippets,
2217
- setSQLSnippets
2218
- }), [sqlSnippets, setSQLSnippets]);
2219
- return /* @__PURE__ */ jsx("div", {
2220
- className,
2221
- children: /* @__PURE__ */ jsx(DefinitionContext.Provider, {
2222
- value: definitions,
2223
- children: /* @__PURE__ */ jsxs(LayoutStateContext.Provider, {
2224
- value: {
2225
- layoutFrozen,
2226
- freezeLayout,
2227
- mode,
2228
- inEditMode
2229
- },
2230
- children: [/* @__PURE__ */ jsx(DashboardActions, {
2231
- mode,
2232
- setMode,
2233
- hasChanges,
2234
- addPanel,
2235
- saveChanges: saveDashboardChanges
2236
- }), /* @__PURE__ */ jsx(DashboardLayout, {
2237
- panels,
2238
- setPanels,
2239
- isDraggable: inEditMode && !layoutFrozen,
2240
- isResizable: inEditMode && !layoutFrozen,
2241
- onRemoveItem: removePanelByID,
2242
- setLocalCols,
2243
- setBreakpoint
2244
- })]
2765
+ setSQLSnippets,
2766
+ dataSources,
2767
+ setDataSources
2768
+ }), [sqlSnippets, setSQLSnippets, dataSources, setDataSources]);
2769
+ return /* @__PURE__ */ jsx(ContextInfoContext.Provider, {
2770
+ value: context,
2771
+ children: /* @__PURE__ */ jsx("div", {
2772
+ className,
2773
+ children: /* @__PURE__ */ jsx(DefinitionContext.Provider, {
2774
+ value: definitions,
2775
+ children: /* @__PURE__ */ jsxs(LayoutStateContext.Provider, {
2776
+ value: {
2777
+ layoutFrozen,
2778
+ freezeLayout,
2779
+ mode,
2780
+ inEditMode
2781
+ },
2782
+ children: [/* @__PURE__ */ jsx(DashboardActions, {
2783
+ mode,
2784
+ setMode,
2785
+ hasChanges,
2786
+ addPanel,
2787
+ saveChanges: saveDashboardChanges
2788
+ }), /* @__PURE__ */ jsx(DashboardLayout, {
2789
+ panels,
2790
+ setPanels,
2791
+ isDraggable: inEditMode && !layoutFrozen,
2792
+ isResizable: inEditMode && !layoutFrozen,
2793
+ onRemoveItem: removePanelByID,
2794
+ setLocalCols,
2795
+ setBreakpoint
2796
+ })]
2797
+ })
2245
2798
  })
2246
2799
  })
2247
2800
  });
@@ -2281,29 +2834,33 @@ function ReadOnlyDashboardLayout({
2281
2834
  });
2282
2835
  }
2283
2836
  function ReadOnlyDashboard({
2837
+ context,
2284
2838
  dashboard,
2285
2839
  className = "dashboard"
2286
2840
  }) {
2287
- const [panels, setPanels] = useListState(dashboard.panels);
2288
- const [sqlSnippets, setSQLSnippets] = React.useState(dashboard.definition.sqlSnippets);
2289
- const definitions = React.useMemo(() => ({
2290
- sqlSnippets,
2291
- setSQLSnippets
2292
- }), [sqlSnippets, setSQLSnippets]);
2293
- return /* @__PURE__ */ jsx("div", {
2294
- className,
2295
- children: /* @__PURE__ */ jsx(DefinitionContext.Provider, {
2296
- value: definitions,
2297
- children: /* @__PURE__ */ jsx(LayoutStateContext.Provider, {
2298
- value: {
2299
- layoutFrozen: true,
2300
- freezeLayout: () => {
2841
+ const definition = React.useMemo(() => __spreadProps(__spreadValues({}, dashboard.definition), {
2842
+ setSQLSnippets: () => {
2843
+ },
2844
+ setDataSources: () => {
2845
+ }
2846
+ }), [dashboard]);
2847
+ return /* @__PURE__ */ jsx(ContextInfoContext.Provider, {
2848
+ value: context,
2849
+ children: /* @__PURE__ */ jsx("div", {
2850
+ className,
2851
+ children: /* @__PURE__ */ jsx(DefinitionContext.Provider, {
2852
+ value: definition,
2853
+ children: /* @__PURE__ */ jsx(LayoutStateContext.Provider, {
2854
+ value: {
2855
+ layoutFrozen: true,
2856
+ freezeLayout: () => {
2857
+ },
2858
+ mode: DashboardMode.Use,
2859
+ inEditMode: false
2301
2860
  },
2302
- mode: DashboardMode.Use,
2303
- inEditMode: false
2304
- },
2305
- children: /* @__PURE__ */ jsx(ReadOnlyDashboardLayout, {
2306
- panels
2861
+ children: /* @__PURE__ */ jsx(ReadOnlyDashboardLayout, {
2862
+ panels: dashboard.panels
2863
+ })
2307
2864
  })
2308
2865
  })
2309
2866
  })