@devtable/dashboard 0.3.1 → 1.1.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 (35) 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 +1165 -462
  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/layout/read-only.d.ts +16 -0
  18. package/dist/main/index.d.ts +2 -9
  19. package/dist/main/main.d.ts +11 -0
  20. package/dist/main/read-only.d.ts +10 -0
  21. package/dist/panel/index.d.ts +3 -3
  22. package/dist/panel/settings/{viz-config → panel-config}/description.d.ts +0 -0
  23. package/dist/panel/settings/panel-config/index.d.ts +5 -0
  24. package/dist/panel/settings/panel-config/preview-panel.d.ts +2 -0
  25. package/dist/panel/settings/{viz-config → panel-config}/title.d.ts +0 -0
  26. package/dist/panel/settings/pick-data-source/index.d.ts +5 -0
  27. package/dist/panel/settings/viz-config/preview-viz.d.ts +5 -0
  28. package/dist/style.css +1 -1
  29. package/dist/types/dashboard.d.ts +8 -1
  30. package/package.json +2 -2
  31. package/dist/panel/settings/query-editor/index.d.ts +0 -2
  32. package/dist/panel/settings/query-editor/sql-query-editor/index.d.ts +0 -5
  33. package/dist/panel/settings/query-result/index.d.ts +0 -5
  34. package/dist/panel/settings/sql-snippets/form.d.ts +0 -9
  35. 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 { Textarea, ActionIcon, Container, Group, Tooltip, Text, TextInput, Box, LoadingOverlay, Table, Select, Button, useMantineTheme, ColorSwatch, Switch, Slider, JsonInput, Modal, AppShell, Tabs, Menu, Divider, 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, InfoCircle, Trash, 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: "",
@@ -173,6 +182,48 @@ reactJsxRuntime_production_min.jsxs = q;
173
182
  const jsx = jsxRuntime.exports.jsx;
174
183
  const jsxs = jsxRuntime.exports.jsxs;
175
184
  const Fragment = jsxRuntime.exports.Fragment;
185
+ function EditDescription() {
186
+ const {
187
+ description,
188
+ setDescription
189
+ } = React.useContext(PanelContext);
190
+ const [localDesc, setLocalDesc] = useInputState(description);
191
+ const changed = description !== localDesc;
192
+ const submit = React.useCallback(() => {
193
+ if (!changed) {
194
+ return;
195
+ }
196
+ setDescription(localDesc);
197
+ }, [changed, localDesc]);
198
+ return /* @__PURE__ */ jsx(Textarea, {
199
+ label: "Panel Description",
200
+ value: localDesc,
201
+ onChange: setLocalDesc,
202
+ minRows: 10,
203
+ autosize: true,
204
+ maxRows: 30,
205
+ rightSection: /* @__PURE__ */ jsx(ActionIcon, {
206
+ disabled: !changed,
207
+ onClick: submit,
208
+ sx: {
209
+ alignSelf: "flex-start",
210
+ marginTop: "4px"
211
+ },
212
+ children: /* @__PURE__ */ jsx(DeviceFloppy, {
213
+ size: 20
214
+ })
215
+ })
216
+ });
217
+ }
218
+ const initialContext = {
219
+ sqlSnippets: [],
220
+ setSQLSnippets: () => {
221
+ },
222
+ dataSources: [],
223
+ setDataSources: () => {
224
+ }
225
+ };
226
+ const DefinitionContext = React.createContext(initialContext);
176
227
  class ErrorBoundary extends React.Component {
177
228
  constructor(props) {
178
229
  super(props);
@@ -194,6 +245,229 @@ class ErrorBoundary extends React.Component {
194
245
  return this.props.children;
195
246
  }
196
247
  }
248
+ function PreviewPanel() {
249
+ const {
250
+ title,
251
+ description
252
+ } = React.useContext(PanelContext);
253
+ return /* @__PURE__ */ jsx(ErrorBoundary, {
254
+ children: /* @__PURE__ */ jsx(Container, {
255
+ mt: "xl",
256
+ p: "5px",
257
+ sx: {
258
+ width: "600px",
259
+ height: "450px",
260
+ background: "transparent",
261
+ borderRadius: "5px",
262
+ boxShadow: "0px 0px 10px 0px rgba(0,0,0,.2)"
263
+ },
264
+ children: /* @__PURE__ */ jsxs(Group, {
265
+ position: "apart",
266
+ noWrap: true,
267
+ sx: {
268
+ borderBottom: "1px solid #eee",
269
+ paddingBottom: "5px"
270
+ },
271
+ children: [/* @__PURE__ */ jsx(Group, {
272
+ children: description && /* @__PURE__ */ jsx(Tooltip, {
273
+ label: description,
274
+ withArrow: true,
275
+ children: /* @__PURE__ */ jsx(InfoCircle, {
276
+ size: 12,
277
+ style: {
278
+ verticalAlign: "baseline",
279
+ cursor: "pointer"
280
+ }
281
+ })
282
+ })
283
+ }), /* @__PURE__ */ jsx(Group, {
284
+ grow: true,
285
+ position: "center",
286
+ children: /* @__PURE__ */ jsx(Text, {
287
+ lineClamp: 1,
288
+ weight: "bold",
289
+ children: title
290
+ })
291
+ }), /* @__PURE__ */ jsx(Group, {
292
+ position: "right",
293
+ spacing: 0,
294
+ sx: {
295
+ height: "28px"
296
+ }
297
+ })]
298
+ })
299
+ })
300
+ });
301
+ }
302
+ function EditTitle() {
303
+ const {
304
+ title,
305
+ setTitle
306
+ } = React.useContext(PanelContext);
307
+ const [localTitle, setLocalTitle] = useInputState(title);
308
+ const changed = title !== localTitle;
309
+ const submit = React.useCallback(() => {
310
+ if (!changed) {
311
+ return;
312
+ }
313
+ setTitle(localTitle);
314
+ }, [changed, localTitle]);
315
+ return /* @__PURE__ */ jsx(TextInput, {
316
+ label: "Panel Title",
317
+ value: localTitle,
318
+ onChange: setLocalTitle,
319
+ rightSection: /* @__PURE__ */ jsx(ActionIcon, {
320
+ disabled: !changed,
321
+ onClick: submit,
322
+ children: /* @__PURE__ */ jsx(DeviceFloppy, {
323
+ size: 20
324
+ })
325
+ })
326
+ });
327
+ }
328
+ function PanelConfig({}) {
329
+ return /* @__PURE__ */ jsxs(Group, {
330
+ direction: "row",
331
+ grow: true,
332
+ noWrap: true,
333
+ align: "stretch",
334
+ sx: {
335
+ height: "100%"
336
+ },
337
+ children: [/* @__PURE__ */ jsxs(Group, {
338
+ grow: true,
339
+ direction: "column",
340
+ sx: {
341
+ width: "40%",
342
+ flexShrink: 0,
343
+ flexGrow: 0
344
+ },
345
+ children: [/* @__PURE__ */ jsx(EditTitle, {}), /* @__PURE__ */ jsx(EditDescription, {})]
346
+ }), /* @__PURE__ */ jsx(Box, {
347
+ sx: {
348
+ height: "100%",
349
+ flexGrow: 1,
350
+ maxWidth: "60%"
351
+ },
352
+ children: /* @__PURE__ */ jsx(PreviewPanel, {})
353
+ })]
354
+ });
355
+ }
356
+ function DataPreview({
357
+ id
358
+ }) {
359
+ const definitions = React.useContext(DefinitionContext);
360
+ const contextInfo = React.useContext(ContextInfoContext);
361
+ const dataSource = React.useMemo(() => {
362
+ return definitions.dataSources.find((d) => d.id === id);
363
+ }, [definitions.dataSources, id]);
364
+ const {
365
+ data = [],
366
+ loading
367
+ } = useRequest(queryBySQL({
368
+ context: contextInfo,
369
+ definitions,
370
+ title: id,
371
+ dataSource
372
+ }), {
373
+ refreshDeps: [contextInfo, definitions, dataSource]
374
+ });
375
+ if (loading) {
376
+ return /* @__PURE__ */ jsx(LoadingOverlay, {
377
+ visible: loading
378
+ });
379
+ }
380
+ if (data.length === 0) {
381
+ return /* @__PURE__ */ jsx(Table, {});
382
+ }
383
+ return /* @__PURE__ */ jsxs(Group, {
384
+ my: "xl",
385
+ direction: "column",
386
+ grow: true,
387
+ sx: {
388
+ border: "1px solid #eee"
389
+ },
390
+ children: [/* @__PURE__ */ jsx(Group, {
391
+ position: "left",
392
+ py: "md",
393
+ pl: "md",
394
+ sx: {
395
+ borderBottom: "1px solid #eee",
396
+ background: "#efefef"
397
+ },
398
+ children: /* @__PURE__ */ jsx(Text, {
399
+ weight: 500,
400
+ children: "Preview Data"
401
+ })
402
+ }), /* @__PURE__ */ jsxs(Table, {
403
+ children: [/* @__PURE__ */ jsx("thead", {
404
+ children: /* @__PURE__ */ jsx("tr", {
405
+ children: Object.keys(data == null ? void 0 : data[0]).map((label) => /* @__PURE__ */ jsx("th", {
406
+ children: label
407
+ }, label))
408
+ })
409
+ }), /* @__PURE__ */ jsx("tbody", {
410
+ children: data.map((row, index2) => /* @__PURE__ */ jsx("tr", {
411
+ children: Object.values(row).map((v, i) => /* @__PURE__ */ jsx("td", {
412
+ children: /* @__PURE__ */ jsx(Group, {
413
+ sx: {
414
+ "&, .mantine-Text-root": {
415
+ fontFamily: "monospace"
416
+ }
417
+ },
418
+ children: /* @__PURE__ */ jsx(Text, {
419
+ children: v
420
+ })
421
+ })
422
+ }, `${v}--${i}`))
423
+ }, `row-${index2}`))
424
+ })]
425
+ })]
426
+ });
427
+ }
428
+ function PickDataSource({}) {
429
+ const {
430
+ dataSources
431
+ } = React.useContext(DefinitionContext);
432
+ const {
433
+ dataSourceID,
434
+ setDataSourceID,
435
+ data,
436
+ loading
437
+ } = React.useContext(PanelContext);
438
+ const options = React.useMemo(() => {
439
+ return dataSources.map((d) => ({
440
+ value: d.id,
441
+ label: d.id
442
+ }));
443
+ }, [dataSources]);
444
+ return /* @__PURE__ */ jsxs(Group, {
445
+ direction: "column",
446
+ grow: true,
447
+ noWrap: true,
448
+ children: [/* @__PURE__ */ jsxs(Group, {
449
+ position: "left",
450
+ sx: {
451
+ maxWidth: "600px",
452
+ alignItems: "baseline"
453
+ },
454
+ children: [/* @__PURE__ */ jsx(Text, {
455
+ children: "Select a Data Source"
456
+ }), /* @__PURE__ */ jsx(Select, {
457
+ data: options,
458
+ value: dataSourceID,
459
+ onChange: setDataSourceID,
460
+ allowDeselect: false,
461
+ clearable: false,
462
+ sx: {
463
+ flexGrow: 1
464
+ }
465
+ })]
466
+ }), /* @__PURE__ */ jsx(DataPreview, {
467
+ id: dataSourceID
468
+ })]
469
+ });
470
+ }
197
471
  echarts.use([SunburstChart, CanvasRenderer]);
198
472
  const defaultOption$1 = {
199
473
  tooltip: {
@@ -244,8 +518,8 @@ function Sunbrust({
244
518
  }
245
519
  }
246
520
  }
247
- }), []);
248
- const option = _.merge(defaultOption$1, labelOption, restConf, {
521
+ }), [max]);
522
+ const option = _.merge({}, defaultOption$1, labelOption, restConf, {
249
523
  series: {
250
524
  data: chartData
251
525
  }
@@ -560,7 +834,7 @@ function VizBar3D({
560
834
  }
561
835
  });
562
836
  }
563
- var index$3 = "";
837
+ var index$2 = "";
564
838
  function renderViz(width, height, data, viz) {
565
839
  const props = {
566
840
  width,
@@ -613,311 +887,47 @@ function Viz({
613
887
  }), !empty && renderViz(width, height, data, viz)]
614
888
  });
615
889
  }
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({}) {
650
- const {
651
- sql,
652
- setSQL
653
- } = 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
- })]
693
- });
694
- }
695
- const QueryEditor = SQLQueryEditor;
696
- function QueryResult({}) {
890
+ function PreviewViz({}) {
697
891
  const {
698
- data
892
+ data,
893
+ loading,
894
+ viz
699
895
  } = 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
- })]
896
+ return /* @__PURE__ */ jsx(ErrorBoundary, {
897
+ children: /* @__PURE__ */ jsx(Viz, {
898
+ viz,
899
+ data,
900
+ loading
901
+ })
713
902
  });
714
903
  }
715
- const initialContext = {
716
- sqlSnippets: [],
717
- setSQLSnippets: () => {
718
- }
719
- };
720
- const DefinitionContext = React.createContext(initialContext);
721
- function SQLSnippetsForm({
722
- sqlSnippets,
723
- setSQLSnippets
904
+ function VizBar3DPanel({
905
+ conf,
906
+ setConf
724
907
  }) {
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: ""
734
- });
735
- const changed = React.useMemo(() => !_.isEqual(form.values, initialValues), [form.values, initialValues]);
736
- const submit = ({
737
- snippets
738
- }) => {
739
- setSQLSnippets(snippets);
740
- };
741
- return /* @__PURE__ */ jsx(Group, {
742
- direction: "column",
743
- sx: {
744
- width: "100%",
745
- position: "relative"
746
- },
747
- grow: true,
748
- children: /* @__PURE__ */ jsxs("form", {
749
- onSubmit: form.onSubmit(submit),
750
- children: [form.values.snippets.map((_item, index2) => /* @__PURE__ */ jsxs(Group, {
751
- direction: "column",
752
- grow: true,
753
- my: 0,
754
- 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
- 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
908
+ const defaultValues = _.assign({}, {
909
+ "x_axis_data_key": "x",
910
+ "y_axis_data_key": "y",
911
+ "z_axis_data_key": "z",
912
+ "xAxis3D": {
913
+ "type": "value",
914
+ "name": "X Axis Name"
915
+ },
916
+ "yAxis3D": {
917
+ "type": "value",
918
+ "name": "Y Axis Name"
919
+ },
920
+ "zAxis3D": {
921
+ "type": "value",
922
+ "name": "Z Axis Name"
923
+ }
924
+ }, conf);
925
+ const {
926
+ control,
927
+ handleSubmit,
928
+ formState
929
+ } = useForm({
930
+ defaultValues
921
931
  });
922
932
  return /* @__PURE__ */ jsx(Group, {
923
933
  direction: "column",
@@ -1119,7 +1129,7 @@ function VizLineBarChartPanel({
1119
1129
  const initialValues = React.useMemo(() => __spreadValues({
1120
1130
  series: formList(series != null ? series : [])
1121
1131
  }, restConf), [series, restConf]);
1122
- const form = useForm({
1132
+ const form = useForm$1({
1123
1133
  initialValues
1124
1134
  });
1125
1135
  const addSeries = () => form.addListItem("series", {
@@ -1231,7 +1241,7 @@ function SunburstPanel({
1231
1241
  },
1232
1242
  setConf
1233
1243
  }) {
1234
- const form = useForm({
1244
+ const form = useForm$1({
1235
1245
  initialValues: {
1236
1246
  label_field,
1237
1247
  value_field
@@ -1318,7 +1328,7 @@ function VizTablePanel(_a) {
1318
1328
  ]), {
1319
1329
  setConf
1320
1330
  } = _b;
1321
- const form = useForm({
1331
+ const form = useForm$1({
1322
1332
  initialValues: __spreadValues({
1323
1333
  id_field: "id",
1324
1334
  use_raw_columns: true,
@@ -1593,7 +1603,7 @@ function VizTextPanel({
1593
1603
  setConf
1594
1604
  }) {
1595
1605
  var _a;
1596
- const form = useForm({
1606
+ const form = useForm$1({
1597
1607
  initialValues: {
1598
1608
  paragraphs: formList((_a = conf.paragraphs) != null ? _a : sampleParagraphs)
1599
1609
  }
@@ -1788,9 +1798,30 @@ function EditVizConf() {
1788
1798
  }
1789
1799
  function VizConfig({}) {
1790
1800
  return /* @__PURE__ */ jsxs(Group, {
1801
+ direction: "row",
1791
1802
  grow: true,
1792
- direction: "column",
1793
- children: [/* @__PURE__ */ jsx(EditTitle, {}), /* @__PURE__ */ jsx(EditDescription, {}), /* @__PURE__ */ jsx(Divider, {}), /* @__PURE__ */ jsx(EditVizConf, {})]
1803
+ noWrap: true,
1804
+ align: "stretch",
1805
+ sx: {
1806
+ height: "100%"
1807
+ },
1808
+ children: [/* @__PURE__ */ jsx(Group, {
1809
+ grow: true,
1810
+ direction: "column",
1811
+ sx: {
1812
+ width: "40%",
1813
+ flexShrink: 0,
1814
+ flexGrow: 0
1815
+ },
1816
+ children: /* @__PURE__ */ jsx(EditVizConf, {})
1817
+ }), /* @__PURE__ */ jsx(Box, {
1818
+ sx: {
1819
+ height: "100%",
1820
+ flexGrow: 1,
1821
+ maxWidth: "60%"
1822
+ },
1823
+ children: /* @__PURE__ */ jsx(PreviewViz, {})
1824
+ })]
1794
1825
  });
1795
1826
  }
1796
1827
  function PanelSettingsModal({
@@ -1832,40 +1863,20 @@ function PanelSettingsModal({
1832
1863
  }
1833
1864
  },
1834
1865
  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
- })
1866
+ children: /* @__PURE__ */ jsxs(Tabs, {
1867
+ initialTab: 2,
1868
+ children: [/* @__PURE__ */ jsxs(Tabs.Tab, {
1869
+ label: "Data Source",
1870
+ children: [/* @__PURE__ */ jsx(LoadingOverlay, {
1871
+ visible: loading
1872
+ }), /* @__PURE__ */ jsx(PickDataSource, {})]
1873
+ }), /* @__PURE__ */ jsx(Tabs.Tab, {
1874
+ label: "Panel",
1875
+ children: /* @__PURE__ */ jsx(PanelConfig, {})
1876
+ }), /* @__PURE__ */ jsx(Tabs.Tab, {
1877
+ label: "Visualization",
1878
+ children: /* @__PURE__ */ jsx(VizConfig, {})
1879
+ })]
1869
1880
  })
1870
1881
  })
1871
1882
  });
@@ -1947,7 +1958,7 @@ function PanelTitleBar({}) {
1947
1958
  var index$1 = "";
1948
1959
  function Panel({
1949
1960
  viz: initialViz,
1950
- sql: initialSQL,
1961
+ dataSourceID: initialDataSourceID,
1951
1962
  title: initialTitle,
1952
1963
  description: initialDesc,
1953
1964
  update,
@@ -1958,24 +1969,35 @@ function Panel({
1958
1969
  const definitions = React.useContext(DefinitionContext);
1959
1970
  const [title, setTitle] = React.useState(initialTitle);
1960
1971
  const [description, setDescription] = React.useState(initialDesc);
1961
- const [sql, setSQL] = React.useState(initialSQL);
1972
+ const [dataSourceID, setDataSourceID] = React.useState(initialDataSourceID);
1962
1973
  const [viz, setViz] = React.useState(initialViz);
1974
+ const dataSource = React.useMemo(() => {
1975
+ if (!dataSourceID) {
1976
+ return void 0;
1977
+ }
1978
+ return definitions.dataSources.find((d) => d.id === dataSourceID);
1979
+ }, [dataSourceID, definitions.dataSources]);
1963
1980
  React.useEffect(() => {
1964
- update({
1981
+ update == null ? void 0 : update({
1965
1982
  id,
1966
1983
  layout,
1967
1984
  title,
1968
1985
  description,
1969
- sql,
1986
+ dataSourceID,
1970
1987
  viz
1971
1988
  });
1972
- }, [title, description, sql, viz, id, layout]);
1989
+ }, [title, description, dataSource, viz, id, layout, dataSourceID]);
1973
1990
  const {
1974
1991
  data = [],
1975
1992
  loading,
1976
1993
  refresh
1977
- } = useRequest(queryBySQL(sql, contextInfo, definitions, title), {
1978
- refreshDeps: [contextInfo, definitions]
1994
+ } = useRequest(queryBySQL({
1995
+ context: contextInfo,
1996
+ definitions,
1997
+ title,
1998
+ dataSource
1999
+ }), {
2000
+ refreshDeps: [contextInfo, definitions, dataSource]
1979
2001
  });
1980
2002
  const refreshData = refresh;
1981
2003
  return /* @__PURE__ */ jsx(PanelContext.Provider, {
@@ -1986,8 +2008,8 @@ function Panel({
1986
2008
  setTitle,
1987
2009
  description,
1988
2010
  setDescription,
1989
- sql,
1990
- setSQL,
2011
+ dataSourceID,
2012
+ setDataSourceID,
1991
2013
  viz,
1992
2014
  setViz,
1993
2015
  refreshData
@@ -2005,7 +2027,7 @@ function Panel({
2005
2027
  });
2006
2028
  }
2007
2029
  var index = "";
2008
- const ResponsiveReactGridLayout = WidthProvider(Responsive);
2030
+ const ResponsiveReactGridLayout$1 = WidthProvider(Responsive);
2009
2031
  function DashboardLayout({
2010
2032
  panels,
2011
2033
  setPanels,
@@ -2041,9 +2063,9 @@ function DashboardLayout({
2041
2063
  const newPanels = panels.map((p2) => __spreadProps(__spreadValues({}, p2), {
2042
2064
  layout: m2.get(p2.id)
2043
2065
  }));
2044
- setPanels.setState(newPanels);
2066
+ setPanels(newPanels);
2045
2067
  }, [panels, setPanels]);
2046
- return /* @__PURE__ */ jsx(ResponsiveReactGridLayout, {
2068
+ return /* @__PURE__ */ jsx(ResponsiveReactGridLayout$1, {
2047
2069
  onBreakpointChange,
2048
2070
  onLayoutChange,
2049
2071
  className,
@@ -2064,7 +2086,10 @@ function DashboardLayout({
2064
2086
  }, rest), {
2065
2087
  destroy: () => onRemoveItem(id),
2066
2088
  update: (panel) => {
2067
- setPanels.setItem(index2, panel);
2089
+ setPanels((prevs) => {
2090
+ prevs.splice(index2, 1, panel);
2091
+ return prevs;
2092
+ });
2068
2093
  }
2069
2094
  }))
2070
2095
  }, id);
@@ -2101,73 +2126,673 @@ function ModeToggler({
2101
2126
  }]
2102
2127
  });
2103
2128
  }
2104
- function DashboardActions({
2105
- mode,
2106
- setMode,
2107
- hasChanges,
2108
- addPanel,
2109
- saveChanges
2110
- }) {
2111
- const inEditMode = mode === DashboardMode.Edit;
2129
+ const example = `
2130
+ -- You may reference context data or SQL snippets *by name*
2131
+ -- in SQL or VizConfig.
2132
+ SELECT *
2133
+ FROM commit
2134
+ WHERE
2135
+ -- context data
2136
+ author_time BETWEEN '\${timeRange?.[0].toISOString()}' AND '\${timeRange?.[1].toISOString()}'
2137
+ -- SQL snippets
2138
+ AND \${author_email_condition}
2139
+ \${order_by_clause}
2140
+ `;
2141
+ function ContextAndSnippets({}) {
2142
+ const contextInfo = React.useContext(ContextInfoContext);
2143
+ const {
2144
+ sqlSnippets
2145
+ } = React.useContext(DefinitionContext);
2146
+ const snippets = React.useMemo(() => {
2147
+ const snippets2 = sqlSnippets.reduce((prev, curr) => {
2148
+ prev[curr.key] = curr.value;
2149
+ return prev;
2150
+ }, {});
2151
+ return JSON.stringify(snippets2, null, 2);
2152
+ }, [sqlSnippets]);
2153
+ const context = React.useMemo(() => {
2154
+ return JSON.stringify(contextInfo, null, 2);
2155
+ }, [contextInfo]);
2112
2156
  return /* @__PURE__ */ jsxs(Group, {
2113
- position: "apart",
2114
- pt: "sm",
2115
- pb: "xs",
2157
+ direction: "column",
2158
+ grow: true,
2159
+ sx: {
2160
+ border: "1px solid #eee",
2161
+ maxWidth: "48%",
2162
+ overflow: "hidden"
2163
+ },
2116
2164
  children: [/* @__PURE__ */ jsx(Group, {
2117
2165
  position: "left",
2118
- children: /* @__PURE__ */ jsx(ModeToggler, {
2119
- mode,
2120
- setMode
2166
+ pl: "md",
2167
+ py: "md",
2168
+ sx: {
2169
+ borderBottom: "1px solid #eee",
2170
+ background: "#efefef",
2171
+ flexGrow: 0
2172
+ },
2173
+ children: /* @__PURE__ */ jsx(Text, {
2174
+ weight: 500,
2175
+ children: "Context"
2121
2176
  })
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"
2177
+ }), /* @__PURE__ */ jsxs(Group, {
2178
+ direction: "column",
2179
+ px: "md",
2180
+ pb: "md",
2181
+ sx: {
2182
+ width: "100%"
2183
+ },
2184
+ children: [/* @__PURE__ */ jsx(Prism, {
2185
+ language: "sql",
2186
+ sx: {
2187
+ width: "100%"
2188
+ },
2189
+ noCopy: true,
2190
+ colorScheme: "dark",
2191
+ children: example
2192
+ }), /* @__PURE__ */ jsx(Text, {
2193
+ weight: 500,
2194
+ sx: {
2195
+ flexGrow: 0
2196
+ },
2197
+ children: "Avaiable context"
2198
+ }), /* @__PURE__ */ jsx(Prism, {
2199
+ language: "json",
2200
+ sx: {
2201
+ width: "100%"
2202
+ },
2203
+ noCopy: true,
2204
+ colorScheme: "dark",
2205
+ children: context
2206
+ }), /* @__PURE__ */ jsx(Text, {
2207
+ weight: 500,
2208
+ sx: {
2209
+ flexGrow: 0
2210
+ },
2211
+ children: "Avaiable SQL Snippets"
2212
+ }), /* @__PURE__ */ jsx(Prism, {
2213
+ language: "json",
2214
+ sx: {
2215
+ width: "100%"
2216
+ },
2217
+ noCopy: true,
2218
+ colorScheme: "dark",
2219
+ children: snippets
2149
2220
  })]
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
2221
  })]
2159
2222
  });
2160
2223
  }
2161
- function Dashboard({
2162
- dashboard,
2224
+ function DataSourceForm({
2225
+ value,
2226
+ onChange
2227
+ }) {
2228
+ const form = useForm$1({
2229
+ initialValues: value
2230
+ });
2231
+ const submit = React.useCallback((values) => {
2232
+ onChange(values);
2233
+ }, [onChange]);
2234
+ const changed = React.useMemo(() => {
2235
+ return !_.isEqual(value, form.values);
2236
+ }, [value, form.values]);
2237
+ React.useEffect(() => {
2238
+ form.reset();
2239
+ }, [value]);
2240
+ return /* @__PURE__ */ jsx(Group, {
2241
+ direction: "column",
2242
+ grow: true,
2243
+ sx: {
2244
+ border: "1px solid #eee",
2245
+ flexGrow: 1
2246
+ },
2247
+ children: /* @__PURE__ */ jsxs("form", {
2248
+ onSubmit: form.onSubmit(submit),
2249
+ children: [/* @__PURE__ */ jsxs(Group, {
2250
+ position: "left",
2251
+ py: "md",
2252
+ pl: "md",
2253
+ sx: {
2254
+ borderBottom: "1px solid #eee",
2255
+ background: "#efefef"
2256
+ },
2257
+ children: [/* @__PURE__ */ jsx(Text, {
2258
+ weight: 500,
2259
+ children: "Data Source Configuration"
2260
+ }), /* @__PURE__ */ jsx(ActionIcon, {
2261
+ type: "submit",
2262
+ mr: 5,
2263
+ variant: "filled",
2264
+ color: "blue",
2265
+ disabled: !changed,
2266
+ children: /* @__PURE__ */ jsx(DeviceFloppy, {
2267
+ size: 20
2268
+ })
2269
+ })]
2270
+ }), /* @__PURE__ */ jsxs(Group, {
2271
+ direction: "column",
2272
+ grow: true,
2273
+ my: 0,
2274
+ p: "md",
2275
+ pr: 40,
2276
+ children: [/* @__PURE__ */ jsxs(Group, {
2277
+ grow: true,
2278
+ children: [/* @__PURE__ */ jsx(TextInput, __spreadValues({
2279
+ placeholder: "An ID unique in this dashboard",
2280
+ label: "ID",
2281
+ required: true,
2282
+ sx: {
2283
+ flex: 1
2284
+ }
2285
+ }, form.getInputProps("id"))), /* @__PURE__ */ jsx(TextInput, __spreadValues({
2286
+ placeholder: "TODO: use a dedicated UI component for this",
2287
+ label: "Data Source Key",
2288
+ required: true,
2289
+ sx: {
2290
+ flex: 1
2291
+ }
2292
+ }, form.getInputProps("key"))), /* @__PURE__ */ jsx(TextInput, __spreadValues({
2293
+ placeholder: "Type of the data source",
2294
+ label: "Type",
2295
+ disabled: true,
2296
+ sx: {
2297
+ flex: 1
2298
+ }
2299
+ }, form.getInputProps("type")))]
2300
+ }), /* @__PURE__ */ jsx(Textarea, __spreadValues({
2301
+ autosize: true,
2302
+ minRows: 12,
2303
+ maxRows: 24
2304
+ }, form.getInputProps("sql")))]
2305
+ })]
2306
+ })
2307
+ });
2308
+ }
2309
+ function DataSourceEditor({
2310
+ id
2311
+ }) {
2312
+ const {
2313
+ dataSources,
2314
+ setDataSources
2315
+ } = React.useContext(DefinitionContext);
2316
+ const dataSource = React.useMemo(() => {
2317
+ return dataSources.find((d) => d.id === id);
2318
+ }, [dataSources, id]);
2319
+ const update = React.useCallback((value) => {
2320
+ const index2 = dataSources.findIndex((d) => d.id === value.id);
2321
+ if (!index2) {
2322
+ console.error(new Error("Invalid data source id when updating by id"));
2323
+ return;
2324
+ }
2325
+ setDataSources((prevs) => {
2326
+ return prevs.map((p2) => {
2327
+ if (p2.id === value.id) {
2328
+ return value;
2329
+ }
2330
+ return p2;
2331
+ });
2332
+ });
2333
+ }, [setDataSources]);
2334
+ if (!dataSource) {
2335
+ return /* @__PURE__ */ jsx("span", {
2336
+ children: "Invalid Data Source ID"
2337
+ });
2338
+ }
2339
+ return /* @__PURE__ */ jsxs(Group, {
2340
+ direction: "row",
2341
+ position: "apart",
2342
+ grow: true,
2343
+ align: "stretch",
2344
+ noWrap: true,
2345
+ children: [/* @__PURE__ */ jsx(DataSourceForm, {
2346
+ value: dataSource,
2347
+ onChange: update
2348
+ }), /* @__PURE__ */ jsx(ContextAndSnippets, {})]
2349
+ });
2350
+ }
2351
+ function SelectOrAddDataSource({
2352
+ id,
2353
+ setID
2354
+ }) {
2355
+ const {
2356
+ dataSources,
2357
+ setDataSources
2358
+ } = React.useContext(DefinitionContext);
2359
+ React.useEffect(() => {
2360
+ var _a, _b;
2361
+ if (!id) {
2362
+ setID((_b = (_a = dataSources[0]) == null ? void 0 : _a.id) != null ? _b : "");
2363
+ }
2364
+ }, [id, setID, dataSources]);
2365
+ const options = React.useMemo(() => {
2366
+ return dataSources.map((d) => ({
2367
+ value: d.id,
2368
+ label: d.id
2369
+ }));
2370
+ }, [dataSources]);
2371
+ const add = React.useCallback(() => {
2372
+ const newDataSource = {
2373
+ id: randomId(),
2374
+ type: "postgresql",
2375
+ key: "",
2376
+ sql: ""
2377
+ };
2378
+ setDataSources((prevs) => [...prevs, newDataSource]);
2379
+ }, [setDataSources]);
2380
+ return /* @__PURE__ */ jsx(Group, {
2381
+ pb: "xl",
2382
+ children: /* @__PURE__ */ jsxs(Group, {
2383
+ position: "left",
2384
+ sx: {
2385
+ maxWidth: "600px",
2386
+ alignItems: "baseline"
2387
+ },
2388
+ children: [/* @__PURE__ */ jsx(Text, {
2389
+ children: "Select a Data Source"
2390
+ }), /* @__PURE__ */ jsx(Select, {
2391
+ data: options,
2392
+ value: id,
2393
+ onChange: setID,
2394
+ allowDeselect: false,
2395
+ clearable: false,
2396
+ sx: {
2397
+ flexGrow: 1
2398
+ }
2399
+ }), /* @__PURE__ */ jsx(Text, {
2400
+ children: "or"
2401
+ }), /* @__PURE__ */ jsx(Group, {
2402
+ position: "center",
2403
+ mt: "md",
2404
+ children: /* @__PURE__ */ jsx(Button, {
2405
+ onClick: add,
2406
+ children: "Add a Data Source"
2407
+ })
2408
+ })]
2409
+ })
2410
+ });
2411
+ }
2412
+ function EditDataSourcesModal({
2413
+ opened,
2414
+ close
2415
+ }) {
2416
+ const [id, setID] = React.useState("");
2417
+ const {
2418
+ freezeLayout
2419
+ } = React.useContext(LayoutStateContext);
2420
+ React.useEffect(() => {
2421
+ freezeLayout(opened);
2422
+ }, [opened]);
2423
+ return /* @__PURE__ */ jsx(Modal, {
2424
+ size: "96vw",
2425
+ overflow: "inside",
2426
+ opened,
2427
+ onClose: close,
2428
+ title: "Data Sources",
2429
+ trapFocus: true,
2430
+ onDragStart: (e) => {
2431
+ e.stopPropagation();
2432
+ },
2433
+ children: /* @__PURE__ */ jsxs(AppShell, {
2434
+ sx: {
2435
+ height: "90vh",
2436
+ maxHeight: "calc(100vh - 185px)",
2437
+ ".mantine-AppShell-body": {
2438
+ height: "100%"
2439
+ },
2440
+ main: {
2441
+ height: "100%",
2442
+ width: "100%",
2443
+ padding: 0,
2444
+ margin: 0
2445
+ }
2446
+ },
2447
+ padding: "md",
2448
+ header: /* @__PURE__ */ jsx(SelectOrAddDataSource, {
2449
+ id,
2450
+ setID
2451
+ }),
2452
+ children: [/* @__PURE__ */ jsx(DataSourceEditor, {
2453
+ id
2454
+ }), /* @__PURE__ */ jsx(DataPreview, {
2455
+ id
2456
+ })]
2457
+ })
2458
+ });
2459
+ }
2460
+ function ContextInfo({}) {
2461
+ const contextInfo = React.useContext(ContextInfoContext);
2462
+ const sampleSQL = `SELECT *
2463
+ FROM commit
2464
+ WHERE author_time BETWEEN '\${timeRange?.[0].toISOString()}' AND '\${timeRange?.[1].toISOString()}'`;
2465
+ return /* @__PURE__ */ jsxs(Group, {
2466
+ direction: "column",
2467
+ grow: true,
2468
+ sx: {
2469
+ border: "1px solid #eee",
2470
+ maxWidth: "48%",
2471
+ overflow: "hidden"
2472
+ },
2473
+ children: [/* @__PURE__ */ jsx(Group, {
2474
+ position: "left",
2475
+ pl: "md",
2476
+ py: "md",
2477
+ sx: {
2478
+ borderBottom: "1px solid #eee",
2479
+ background: "#efefef",
2480
+ flexGrow: 0
2481
+ },
2482
+ children: /* @__PURE__ */ jsx(Text, {
2483
+ weight: 500,
2484
+ children: "Context"
2485
+ })
2486
+ }), /* @__PURE__ */ jsxs(Group, {
2487
+ direction: "column",
2488
+ px: "md",
2489
+ pb: "md",
2490
+ sx: {
2491
+ width: "100%"
2492
+ },
2493
+ children: [/* @__PURE__ */ jsx(Prism, {
2494
+ language: "sql",
2495
+ sx: {
2496
+ width: "100%"
2497
+ },
2498
+ noCopy: true,
2499
+ colorScheme: "dark",
2500
+ children: `-- You may refer context data *by name*
2501
+ -- in SQL or VizConfig.
2502
+
2503
+ ${sampleSQL}`
2504
+ }), /* @__PURE__ */ jsx(Text, {
2505
+ weight: 500,
2506
+ sx: {
2507
+ flexGrow: 0
2508
+ },
2509
+ children: "Avaiable context entries"
2510
+ }), /* @__PURE__ */ jsx(Prism, {
2511
+ language: "json",
2512
+ sx: {
2513
+ width: "100%"
2514
+ },
2515
+ noCopy: true,
2516
+ colorScheme: "dark",
2517
+ children: JSON.stringify(contextInfo, null, 2)
2518
+ })]
2519
+ })]
2520
+ });
2521
+ }
2522
+ function SQLSnippetsEditor({}) {
2523
+ const {
2524
+ sqlSnippets,
2525
+ setSQLSnippets
2526
+ } = React.useContext(DefinitionContext);
2527
+ const sampleSQL = `SELECT *
2528
+ FROM commit
2529
+ WHERE \${author_time_condition}`;
2530
+ const initialValues = React.useMemo(() => ({
2531
+ snippets: formList(sqlSnippets != null ? sqlSnippets : [])
2532
+ }), [sqlSnippets]);
2533
+ const form = useForm$1({
2534
+ initialValues
2535
+ });
2536
+ const addSnippet = () => form.addListItem("snippets", {
2537
+ key: randomId(),
2538
+ value: ""
2539
+ });
2540
+ const changed = React.useMemo(() => !_.isEqual(form.values, initialValues), [form.values, initialValues]);
2541
+ const submit = ({
2542
+ snippets
2543
+ }) => {
2544
+ setSQLSnippets(snippets);
2545
+ };
2546
+ return /* @__PURE__ */ jsxs(Group, {
2547
+ direction: "column",
2548
+ grow: true,
2549
+ sx: {
2550
+ border: "1px solid #eee"
2551
+ },
2552
+ children: [/* @__PURE__ */ jsxs(Group, {
2553
+ position: "left",
2554
+ pl: "md",
2555
+ py: "md",
2556
+ sx: {
2557
+ borderBottom: "1px solid #eee",
2558
+ background: "#efefef",
2559
+ flexGrow: 0
2560
+ },
2561
+ children: [/* @__PURE__ */ jsx(Text, {
2562
+ weight: 500,
2563
+ children: "SQL Snippets"
2564
+ }), /* @__PURE__ */ jsx(ActionIcon, {
2565
+ type: "submit",
2566
+ mr: 5,
2567
+ variant: "filled",
2568
+ color: "blue",
2569
+ disabled: !changed,
2570
+ children: /* @__PURE__ */ jsx(DeviceFloppy, {
2571
+ size: 20
2572
+ })
2573
+ })]
2574
+ }), /* @__PURE__ */ jsxs(Group, {
2575
+ px: "md",
2576
+ pb: "md",
2577
+ children: [/* @__PURE__ */ jsx(Prism, {
2578
+ language: "sql",
2579
+ sx: {
2580
+ width: "100%"
2581
+ },
2582
+ noCopy: true,
2583
+ trim: false,
2584
+ colorScheme: "dark",
2585
+ children: `-- You may refer context data *by name*
2586
+ -- in SQL or VizConfig.
2587
+
2588
+ ${sampleSQL}
2589
+
2590
+ -- where author_time_condition is:
2591
+ author_time BETWEEN '\${timeRange?.[0].toISOString()}' AND '\${timeRange?.[1].toISOString()}'
2592
+ `
2593
+ }), /* @__PURE__ */ jsx(Group, {
2594
+ direction: "column",
2595
+ sx: {
2596
+ width: "100%",
2597
+ position: "relative"
2598
+ },
2599
+ grow: true,
2600
+ children: /* @__PURE__ */ jsxs("form", {
2601
+ onSubmit: form.onSubmit(submit),
2602
+ children: [form.values.snippets.map((_item, index2) => /* @__PURE__ */ jsxs(Group, {
2603
+ direction: "column",
2604
+ grow: true,
2605
+ my: 0,
2606
+ p: "md",
2607
+ pr: 40,
2608
+ sx: {
2609
+ border: "1px solid #eee",
2610
+ position: "relative"
2611
+ },
2612
+ children: [/* @__PURE__ */ jsx(TextInput, __spreadValues({
2613
+ label: "Key",
2614
+ required: true
2615
+ }, form.getListInputProps("snippets", index2, "key"))), /* @__PURE__ */ jsx(Textarea, __spreadValues({
2616
+ minRows: 3,
2617
+ label: "Value",
2618
+ required: true
2619
+ }, form.getListInputProps("snippets", index2, "value"))), /* @__PURE__ */ jsx(ActionIcon, {
2620
+ color: "red",
2621
+ variant: "hover",
2622
+ onClick: () => form.removeListItem("snippets", index2),
2623
+ sx: {
2624
+ position: "absolute",
2625
+ top: 15,
2626
+ right: 5
2627
+ },
2628
+ children: /* @__PURE__ */ jsx(Trash, {
2629
+ size: 16
2630
+ })
2631
+ })]
2632
+ }, index2)), /* @__PURE__ */ jsx(Group, {
2633
+ position: "center",
2634
+ mt: "xl",
2635
+ grow: true,
2636
+ sx: {
2637
+ width: "40%"
2638
+ },
2639
+ mx: "auto",
2640
+ children: /* @__PURE__ */ jsx(Button, {
2641
+ variant: "default",
2642
+ onClick: addSnippet,
2643
+ children: "Add a snippet"
2644
+ })
2645
+ })]
2646
+ })
2647
+ })]
2648
+ })]
2649
+ });
2650
+ }
2651
+ function EditSQLSnippetsModal({
2652
+ opened,
2653
+ close
2654
+ }) {
2655
+ const {
2656
+ freezeLayout
2657
+ } = React.useContext(LayoutStateContext);
2658
+ React.useEffect(() => {
2659
+ freezeLayout(opened);
2660
+ }, [opened]);
2661
+ return /* @__PURE__ */ jsx(Modal, {
2662
+ size: "96vw",
2663
+ overflow: "inside",
2664
+ opened,
2665
+ onClose: close,
2666
+ title: "SQL Snippets",
2667
+ trapFocus: true,
2668
+ onDragStart: (e) => {
2669
+ e.stopPropagation();
2670
+ },
2671
+ children: /* @__PURE__ */ jsx(AppShell, {
2672
+ sx: {
2673
+ height: "90vh",
2674
+ maxHeight: "calc(100vh - 185px)",
2675
+ ".mantine-AppShell-body": {
2676
+ height: "100%"
2677
+ },
2678
+ main: {
2679
+ height: "100%",
2680
+ width: "100%",
2681
+ padding: 0,
2682
+ margin: 0
2683
+ }
2684
+ },
2685
+ padding: "md",
2686
+ children: /* @__PURE__ */ jsxs(Group, {
2687
+ direction: "row",
2688
+ position: "apart",
2689
+ grow: true,
2690
+ align: "stretch",
2691
+ noWrap: true,
2692
+ children: [/* @__PURE__ */ jsx(SQLSnippetsEditor, {}), /* @__PURE__ */ jsx(ContextInfo, {})]
2693
+ })
2694
+ })
2695
+ });
2696
+ }
2697
+ function DashboardActions({
2698
+ mode,
2699
+ setMode,
2700
+ hasChanges,
2701
+ addPanel,
2702
+ saveChanges
2703
+ }) {
2704
+ const [dataSourcesOpened, setDataSourcesOpened] = React.useState(false);
2705
+ const openDataSources = () => setDataSourcesOpened(true);
2706
+ const closeDataSources = () => setDataSourcesOpened(false);
2707
+ const [sqlSnippetsOpened, setSQLSnippetsOpened] = React.useState(false);
2708
+ const openSQLSnippets = () => setSQLSnippetsOpened(true);
2709
+ const closeSQLSnippets = () => setSQLSnippetsOpened(false);
2710
+ const inEditMode = mode === DashboardMode.Edit;
2711
+ return /* @__PURE__ */ jsxs(Group, {
2712
+ position: "apart",
2713
+ pt: "sm",
2714
+ pb: "xs",
2715
+ children: [/* @__PURE__ */ jsx(Group, {
2716
+ position: "left",
2717
+ children: /* @__PURE__ */ jsx(ModeToggler, {
2718
+ mode,
2719
+ setMode
2720
+ })
2721
+ }), inEditMode && /* @__PURE__ */ jsxs(Fragment, {
2722
+ children: [/* @__PURE__ */ jsxs(Group, {
2723
+ position: "right",
2724
+ children: [/* @__PURE__ */ jsx(Button, {
2725
+ variant: "default",
2726
+ size: "sm",
2727
+ onClick: addPanel,
2728
+ leftIcon: /* @__PURE__ */ jsx(PlaylistAdd, {
2729
+ size: 20
2730
+ }),
2731
+ children: "Add a Panel"
2732
+ }), /* @__PURE__ */ jsx(Button, {
2733
+ variant: "default",
2734
+ size: "sm",
2735
+ onClick: openSQLSnippets,
2736
+ leftIcon: /* @__PURE__ */ jsx(ClipboardText, {
2737
+ size: 20
2738
+ }),
2739
+ children: "SQL Snippets"
2740
+ }), /* @__PURE__ */ jsx(Button, {
2741
+ variant: "default",
2742
+ size: "sm",
2743
+ onClick: openDataSources,
2744
+ leftIcon: /* @__PURE__ */ jsx(Database, {
2745
+ size: 20
2746
+ }),
2747
+ children: "Data Sources"
2748
+ }), /* @__PURE__ */ jsx(Button, {
2749
+ variant: "default",
2750
+ size: "sm",
2751
+ onClick: saveChanges,
2752
+ disabled: !hasChanges,
2753
+ leftIcon: /* @__PURE__ */ jsx(DeviceFloppy, {
2754
+ size: 20
2755
+ }),
2756
+ children: "Save Changes"
2757
+ }), /* @__PURE__ */ jsx(Button, {
2758
+ color: "red",
2759
+ size: "sm",
2760
+ disabled: !hasChanges,
2761
+ leftIcon: /* @__PURE__ */ jsx(Recycle, {
2762
+ size: 20
2763
+ }),
2764
+ children: "Revert Changes"
2765
+ })]
2766
+ }), /* @__PURE__ */ jsx(EditDataSourcesModal, {
2767
+ opened: dataSourcesOpened,
2768
+ close: closeDataSources
2769
+ }), /* @__PURE__ */ jsx(EditSQLSnippetsModal, {
2770
+ opened: sqlSnippetsOpened,
2771
+ close: closeSQLSnippets
2772
+ })]
2773
+ }), !inEditMode && /* @__PURE__ */ jsx(Button, {
2774
+ variant: "default",
2775
+ size: "sm",
2776
+ disabled: true,
2777
+ leftIcon: /* @__PURE__ */ jsx(Share, {
2778
+ size: 20
2779
+ }),
2780
+ children: "Share"
2781
+ })]
2782
+ });
2783
+ }
2784
+ function Dashboard({
2785
+ context,
2786
+ dashboard,
2163
2787
  update,
2164
2788
  className = "dashboard"
2165
2789
  }) {
2166
2790
  const [layoutFrozen, freezeLayout] = React.useState(false);
2167
2791
  const [breakpoint, setBreakpoint] = React.useState();
2168
2792
  const [localCols, setLocalCols] = React.useState();
2169
- const [panels, setPanels] = useListState(dashboard.panels);
2793
+ const [panels, setPanels] = React.useState(dashboard.panels);
2170
2794
  const [sqlSnippets, setSQLSnippets] = React.useState(dashboard.definition.sqlSnippets);
2795
+ const [dataSources, setDataSources] = React.useState(dashboard.definition.dataSources);
2171
2796
  const [mode, setMode] = React.useState(DashboardMode.Edit);
2172
2797
  const hasChanges = React.useMemo(() => {
2173
2798
  const cleanJSON = (v) => JSON.parse(JSON.stringify(v));
@@ -2175,8 +2800,11 @@ function Dashboard({
2175
2800
  if (!panelsEqual) {
2176
2801
  return true;
2177
2802
  }
2178
- return !_.isEqual(sqlSnippets, dashboard.definition.sqlSnippets);
2179
- }, [dashboard, panels, sqlSnippets]);
2803
+ if (!_.isEqual(sqlSnippets, dashboard.definition.sqlSnippets)) {
2804
+ return true;
2805
+ }
2806
+ return !_.isEqual(dataSources, dashboard.definition.dataSources);
2807
+ }, [dashboard, panels, sqlSnippets, dataSources]);
2180
2808
  const saveDashboardChanges = async () => {
2181
2809
  const d = _.merge({}, dashboard, {
2182
2810
  panels
@@ -2199,51 +2827,126 @@ function Dashboard({
2199
2827
  },
2200
2828
  title: `New Panel - ${id}`,
2201
2829
  description: "description goes here",
2202
- sql: "",
2830
+ dataSourceID: "",
2203
2831
  viz: {
2204
2832
  type: "table",
2205
2833
  conf: {}
2206
2834
  }
2207
2835
  };
2208
- setPanels.append(newItem);
2836
+ setPanels((prevs) => [...prevs, newItem]);
2209
2837
  };
2210
2838
  const removePanelByID = (id) => {
2211
2839
  const index2 = panels.findIndex((p2) => p2.id === id);
2212
- setPanels.remove(index2);
2840
+ setPanels((prevs) => {
2841
+ prevs.splice(index2, 1);
2842
+ return prevs;
2843
+ });
2213
2844
  };
2214
2845
  const inEditMode = mode === DashboardMode.Edit;
2215
2846
  const definitions = React.useMemo(() => ({
2216
2847
  sqlSnippets,
2217
- setSQLSnippets
2218
- }), [sqlSnippets, setSQLSnippets]);
2219
- return /* @__PURE__ */ jsx("div", {
2848
+ setSQLSnippets,
2849
+ dataSources,
2850
+ setDataSources
2851
+ }), [sqlSnippets, setSQLSnippets, dataSources, setDataSources]);
2852
+ return /* @__PURE__ */ jsx(ContextInfoContext.Provider, {
2853
+ value: context,
2854
+ children: /* @__PURE__ */ jsx("div", {
2855
+ className,
2856
+ children: /* @__PURE__ */ jsx(DefinitionContext.Provider, {
2857
+ value: definitions,
2858
+ children: /* @__PURE__ */ jsxs(LayoutStateContext.Provider, {
2859
+ value: {
2860
+ layoutFrozen,
2861
+ freezeLayout,
2862
+ mode,
2863
+ inEditMode
2864
+ },
2865
+ children: [/* @__PURE__ */ jsx(DashboardActions, {
2866
+ mode,
2867
+ setMode,
2868
+ hasChanges,
2869
+ addPanel,
2870
+ saveChanges: saveDashboardChanges
2871
+ }), /* @__PURE__ */ jsx(DashboardLayout, {
2872
+ panels,
2873
+ setPanels,
2874
+ isDraggable: inEditMode && !layoutFrozen,
2875
+ isResizable: inEditMode && !layoutFrozen,
2876
+ onRemoveItem: removePanelByID,
2877
+ setLocalCols,
2878
+ setBreakpoint
2879
+ })]
2880
+ })
2881
+ })
2882
+ })
2883
+ });
2884
+ }
2885
+ const ResponsiveReactGridLayout = WidthProvider(Responsive);
2886
+ function ReadOnlyDashboardLayout({
2887
+ panels,
2888
+ className = "layout",
2889
+ cols = {
2890
+ lg: 12,
2891
+ md: 10,
2892
+ sm: 8,
2893
+ xs: 6,
2894
+ xxs: 4
2895
+ },
2896
+ rowHeight = 10
2897
+ }) {
2898
+ return /* @__PURE__ */ jsx(ResponsiveReactGridLayout, {
2220
2899
  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
- })]
2900
+ cols,
2901
+ rowHeight,
2902
+ isDraggable: false,
2903
+ isResizable: false,
2904
+ children: panels.map((_a) => {
2905
+ var _b = _a, {
2906
+ id
2907
+ } = _b, rest = __objRest(_b, [
2908
+ "id"
2909
+ ]);
2910
+ return /* @__PURE__ */ jsx("div", {
2911
+ "data-grid": rest.layout,
2912
+ children: /* @__PURE__ */ jsx(Panel, __spreadValues({
2913
+ id
2914
+ }, rest))
2915
+ }, id);
2916
+ })
2917
+ });
2918
+ }
2919
+ function ReadOnlyDashboard({
2920
+ context,
2921
+ dashboard,
2922
+ className = "dashboard"
2923
+ }) {
2924
+ const definition = React.useMemo(() => __spreadProps(__spreadValues({}, dashboard.definition), {
2925
+ setSQLSnippets: () => {
2926
+ },
2927
+ setDataSources: () => {
2928
+ }
2929
+ }), [dashboard]);
2930
+ return /* @__PURE__ */ jsx(ContextInfoContext.Provider, {
2931
+ value: context,
2932
+ children: /* @__PURE__ */ jsx("div", {
2933
+ className,
2934
+ children: /* @__PURE__ */ jsx(DefinitionContext.Provider, {
2935
+ value: definition,
2936
+ children: /* @__PURE__ */ jsx(LayoutStateContext.Provider, {
2937
+ value: {
2938
+ layoutFrozen: true,
2939
+ freezeLayout: () => {
2940
+ },
2941
+ mode: DashboardMode.Use,
2942
+ inEditMode: false
2943
+ },
2944
+ children: /* @__PURE__ */ jsx(ReadOnlyDashboardLayout, {
2945
+ panels: dashboard.panels
2946
+ })
2947
+ })
2245
2948
  })
2246
2949
  })
2247
2950
  });
2248
2951
  }
2249
- export { ContextInfoContext, Dashboard, DashboardLayout, DashboardMode, DefinitionContext, LayoutStateContext, Panel, PanelContext, initialContextInfoContext };
2952
+ export { ContextInfoContext, Dashboard, DashboardLayout, DashboardMode, DefinitionContext, LayoutStateContext, Panel, PanelContext, ReadOnlyDashboard, initialContextInfoContext };