@mongoosejs/studio 0.2.13 → 0.3.1

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 (92) hide show
  1. package/backend/actions/ChatMessage/executeScript.js +5 -1
  2. package/backend/actions/ChatThread/createChatMessage.js +4 -1
  3. package/backend/actions/ChatThread/streamChatMessage.js +4 -2
  4. package/backend/actions/Dashboard/updateDashboard.js +2 -2
  5. package/backend/actions/Task/getTaskOverview.js +102 -0
  6. package/backend/actions/Task/getTasks.js +85 -45
  7. package/backend/actions/Task/index.js +1 -0
  8. package/eslint.config.js +4 -1
  9. package/frontend/public/app.js +25025 -762
  10. package/frontend/public/dark-theme.css +365 -0
  11. package/frontend/public/images/mongoose-studio.svg +4 -0
  12. package/frontend/public/index.html +21 -1
  13. package/frontend/public/style.css +5 -7
  14. package/frontend/public/theme-variables.css +294 -0
  15. package/frontend/public/tw.css +348 -213
  16. package/frontend/src/_util/dateRange.js +82 -0
  17. package/frontend/src/ace-editor/ace-editor.html +4 -0
  18. package/frontend/src/ace-editor/ace-editor.js +95 -0
  19. package/frontend/src/aceEditor.js +69 -0
  20. package/frontend/src/api.js +6 -0
  21. package/frontend/src/chat/chat-message/chat-message.html +1 -1
  22. package/frontend/src/chat/chat-message/chat-message.js +1 -1
  23. package/frontend/src/chat/chat-message-script/chat-message-script.html +54 -42
  24. package/frontend/src/chat/chat-message-script/chat-message-script.js +6 -55
  25. package/frontend/src/chat/chat.html +68 -39
  26. package/frontend/src/chat/chat.js +26 -2
  27. package/frontend/src/clone-document/clone-document.html +7 -2
  28. package/frontend/src/clone-document/clone-document.js +1 -8
  29. package/frontend/src/create-dashboard/create-dashboard.html +11 -6
  30. package/frontend/src/create-dashboard/create-dashboard.js +0 -7
  31. package/frontend/src/create-document/create-document.html +15 -9
  32. package/frontend/src/create-document/create-document.js +5 -12
  33. package/frontend/src/dashboard/dashboard.html +14 -12
  34. package/frontend/src/dashboard/dashboard.js +21 -4
  35. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.html +13 -7
  36. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +16 -23
  37. package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html +19 -17
  38. package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js +97 -2
  39. package/frontend/src/dashboard-result/dashboard-map/dashboard-map.js +27 -3
  40. package/frontend/src/dashboard-result/dashboard-result.html +3 -3
  41. package/frontend/src/dashboard-result/dashboard-result.js +3 -0
  42. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +34 -0
  43. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +37 -0
  44. package/frontend/src/dashboards/dashboards.html +101 -109
  45. package/frontend/src/dashboards/dashboards.js +25 -1
  46. package/frontend/src/detail-default/detail-default.html +2 -2
  47. package/frontend/src/detail-default/detail-default.js +24 -3
  48. package/frontend/src/document/confirm-changes/confirm-changes.html +1 -1
  49. package/frontend/src/document/confirm-delete/confirm-delete.html +1 -1
  50. package/frontend/src/document/document.css +1 -1
  51. package/frontend/src/document/document.html +28 -28
  52. package/frontend/src/document/execute-script/execute-script.html +20 -21
  53. package/frontend/src/document/execute-script/execute-script.js +1 -43
  54. package/frontend/src/document-details/document-details.css +4 -9
  55. package/frontend/src/document-details/document-details.html +34 -33
  56. package/frontend/src/document-details/document-details.js +2 -53
  57. package/frontend/src/document-details/document-property/document-property.html +12 -12
  58. package/frontend/src/edit-array/edit-array.html +7 -6
  59. package/frontend/src/edit-array/edit-array.js +10 -50
  60. package/frontend/src/edit-boolean/edit-boolean.html +12 -12
  61. package/frontend/src/edit-date/edit-date.html +2 -2
  62. package/frontend/src/edit-default/edit-default.html +1 -1
  63. package/frontend/src/edit-string/edit-string.html +3 -3
  64. package/frontend/src/edit-subdocument/edit-subdocument.html +5 -3
  65. package/frontend/src/edit-subdocument/edit-subdocument.js +1 -15
  66. package/frontend/src/export-query-results/export-query-results.html +3 -3
  67. package/frontend/src/json-node/json-node.html +3 -3
  68. package/frontend/src/list-json/json-node.html +1 -1
  69. package/frontend/src/models/document-search/document-search.html +3 -3
  70. package/frontend/src/models/model-switcher/model-switcher.html +53 -0
  71. package/frontend/src/models/model-switcher/model-switcher.js +123 -0
  72. package/frontend/src/models/models.css +3 -10
  73. package/frontend/src/models/models.html +146 -80
  74. package/frontend/src/models/models.js +116 -7
  75. package/frontend/src/navbar/navbar.html +157 -97
  76. package/frontend/src/navbar/navbar.js +31 -12
  77. package/frontend/src/routes.js +1 -1
  78. package/frontend/src/splash/splash.html +5 -5
  79. package/frontend/src/task-by-name/task-by-name.html +77 -7
  80. package/frontend/src/task-by-name/task-by-name.js +84 -9
  81. package/frontend/src/task-single/task-single.html +29 -29
  82. package/frontend/src/task-single/task-single.js +10 -10
  83. package/frontend/src/tasks/task-details/task-details.html +43 -43
  84. package/frontend/src/tasks/task-details/task-details.js +9 -3
  85. package/frontend/src/tasks/tasks.html +36 -35
  86. package/frontend/src/tasks/tasks.js +27 -143
  87. package/frontend/src/team/new-invitation/new-invitation.html +8 -8
  88. package/frontend/src/team/team.html +27 -27
  89. package/frontend/src/update-document/update-document.html +7 -2
  90. package/frontend/src/update-document/update-document.js +2 -11
  91. package/package.json +3 -1
  92. package/tailwind.config.js +75 -11
@@ -6,15 +6,20 @@ const template = require('./edit-dashboard.html');
6
6
  module.exports = app => app.component('edit-dashboard', {
7
7
  template: template,
8
8
  props: ['dashboardId', 'code', 'currentDescription', 'currentTitle'],
9
- emits: ['close'],
9
+ emits: ['close', 'update'],
10
10
  data: function() {
11
11
  return {
12
12
  status: 'loaded',
13
- editor: null,
14
13
  title: '',
15
- description: ''
14
+ description: '',
15
+ editCode: ''
16
16
  };
17
17
  },
18
+ mounted() {
19
+ this.editCode = this.code || '';
20
+ this.description = this.currentDescription;
21
+ this.title = this.currentTitle;
22
+ },
18
23
  methods: {
19
24
  closeEditor() {
20
25
  this.$emit('close');
@@ -22,39 +27,27 @@ module.exports = app => app.component('edit-dashboard', {
22
27
  async updateCode() {
23
28
  this.status = 'loading';
24
29
  try {
30
+ const codeToSave = this.$refs.codeEditor?.getValue ? this.$refs.codeEditor.getValue() : this.editCode;
25
31
  const { doc } = await api.Dashboard.updateDashboard({
26
32
  dashboardId: this.dashboardId,
27
- code: this.editor.getValue(),
33
+ code: codeToSave,
28
34
  title: this.title,
29
35
  description: this.description,
30
36
  evaluate: false
31
37
  });
32
38
  this.$emit('update', { doc });
33
- this.editor.setValue(doc.code);
39
+ this.editCode = doc.code;
40
+ if (this.$refs.codeEditor) {
41
+ this.$refs.codeEditor.setValue(doc.code);
42
+ }
34
43
  this.$toast.success('Dashboard updated!');
35
44
  this.closeEditor();
36
45
  } catch (err) {
37
- this.$emit('update', { error: { message: err.message } });
46
+ const message = err?.response?.data?.message || err?.message || 'Dashboard update failed';
47
+ this.$emit('update', { error: { message } });
38
48
  } finally {
39
49
  this.status = 'loaded';
40
50
  }
41
51
  }
42
- },
43
- mounted: async function() {
44
- this.editor = CodeMirror.fromTextArea(this.$refs.codeEditor, {
45
- mode: 'javascript',
46
- lineNumbers: true,
47
- indentUnit: 4,
48
- smartIndent: true,
49
- tabsize: 4,
50
- indentWithTabs: true,
51
- cursorBlinkRate: 300,
52
- lineWrapping: true,
53
- showCursorWhenSelecting: true
54
- });
55
- // this.editor.focus();
56
- // this.editor.refresh(); // if anything weird happens on load, this usually fixes it. However, this breaks it in this case.
57
- this.description = this.currentDescription;
58
- this.title = this.currentTitle;
59
52
  }
60
53
  });
@@ -1,30 +1,32 @@
1
1
  <div :class="responsive ? 'h-full' : ''">
2
2
  <div v-if="header && !fullscreen" class="border-b border-gray-100 px-2 pb-2 flex items-center gap-1">
3
3
  <div class="text-xl font-bold">{{header}}</div>
4
- <button
5
- class="ml-auto px-2 py-1 text-xs bg-ultramarine-600 text-white border-none rounded cursor-pointer hover:bg-ultramarine-500 transition-colors"
6
- @click="exportPNG"
7
- title="Export PNG">
8
- <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em;" viewBox="0 -960 960 960" fill="currentColor"><path d="M280-280h400v-80H280v80Zm200-120 160-160-56-56-64 62v-166h-80v166l-64-62-56 56 160 160Zm0 320q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
9
- </button>
10
- <button
11
- class="px-2 py-1 text-xs bg-ultramarine-600 text-white border-none rounded cursor-pointer hover:bg-ultramarine-500 transition-colors flex items-center"
12
- @click="$emit('fullscreen')"
13
- aria-label="Expand dashboard result">
14
- <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
15
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5" />
16
- </svg>
17
- </button>
4
+ <template v-if="showControls">
5
+ <button
6
+ class="ml-auto px-2 py-1 text-xs bg-primary text-primary-text border-none rounded cursor-pointer hover:bg-primary-hover transition-colors"
7
+ @click="exportPNG"
8
+ title="Export PNG">
9
+ <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em;" viewBox="0 -960 960 960" fill="currentColor"><path d="M280-280h400v-80H280v80Zm200-120 160-160-56-56-64 62v-166h-80v166l-64-62-56 56 160 160Zm0 320q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
10
+ </button>
11
+ <button
12
+ class="px-2 py-1 text-xs bg-primary text-primary-text border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
13
+ @click="$emit('fullscreen')"
14
+ aria-label="Expand dashboard result">
15
+ <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
16
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5" />
17
+ </svg>
18
+ </button>
19
+ </template>
18
20
  </div>
19
- <div v-else-if="!fullscreen" class="pt-1 border-b border-gray-100 px-2 pb-2 text-right flex items-center justify-end gap-1 w-full">
21
+ <div v-else-if="!fullscreen && showControls" class="pt-1 border-b border-gray-100 px-2 pb-2 text-right flex items-center justify-end gap-1 w-full">
20
22
  <button
21
- class="px-2 py-1 text-xs bg-ultramarine-600 text-white border-none rounded cursor-pointer hover:bg-ultramarine-500 transition-colors"
23
+ class="px-2 py-1 text-xs bg-primary text-primary-text border-none rounded cursor-pointer hover:bg-primary-hover transition-colors"
22
24
  @click="exportPNG"
23
25
  title="Export PNG">
24
26
  <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em;" viewBox="0 -960 960 960" fill="currentColor"><path d="M280-280h400v-80H280v80Zm200-120 160-160-56-56-64 62v-166h-80v166l-64-62-56 56 160 160Zm0 320q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
25
27
  </button>
26
28
  <button
27
- class="px-2 py-1 text-xs bg-ultramarine-600 text-white border-none rounded cursor-pointer hover:bg-ultramarine-500 transition-colors flex items-center"
29
+ class="px-2 py-1 text-xs bg-primary text-primary-text border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
28
30
  @click="$emit('fullscreen')"
29
31
  aria-label="Expand dashboard result">
30
32
  <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -2,6 +2,86 @@
2
2
 
3
3
  const template = require('./dashboard-chart.html');
4
4
 
5
+ function getChartThemeColor(cssVar, fallback) {
6
+ if (typeof document === 'undefined') return fallback;
7
+ const val = getComputedStyle(document.documentElement).getPropertyValue(cssVar)?.trim();
8
+ return val || fallback;
9
+ }
10
+
11
+ function getThemeColors() {
12
+ return {
13
+ tickColor: getChartThemeColor('--studio-text-primary', '#111827'),
14
+ gridColor: getChartThemeColor('--studio-border', '#e5e7eb')
15
+ };
16
+ }
17
+
18
+ const SCALELESS_TYPES = ['pie', 'doughnut', 'polarArea', 'radar'];
19
+
20
+ function applyThemeChartOptions(config) {
21
+ const { tickColor, gridColor } = getThemeColors();
22
+ const type = config.type || '';
23
+ const options = config.options || {};
24
+ const plugins = { ...options.plugins };
25
+ const legend = plugins.legend || {};
26
+ plugins.legend = {
27
+ ...legend,
28
+ labels: { ...legend.labels, color: tickColor }
29
+ };
30
+
31
+ // Pie/doughnut/polarArea/radar don't use cartesian scales
32
+ if (SCALELESS_TYPES.includes(type)) {
33
+ return {
34
+ ...config,
35
+ options: { ...options, plugins }
36
+ };
37
+ }
38
+
39
+ const scales = { ...options.scales };
40
+ const applyScaleTheme = (id) => {
41
+ const scale = scales[id] || {};
42
+ scales[id] = {
43
+ ...scale,
44
+ ticks: { ...scale.ticks, color: tickColor },
45
+ grid: { ...scale.grid, color: gridColor }
46
+ };
47
+ };
48
+ ['x', 'y'].forEach(id => {
49
+ if (!scales[id]) scales[id] = {};
50
+ applyScaleTheme(id);
51
+ });
52
+ Object.keys(scales).forEach(id => {
53
+ if (id !== 'x' && id !== 'y') applyScaleTheme(id);
54
+ });
55
+ return {
56
+ ...config,
57
+ options: {
58
+ ...options,
59
+ scales,
60
+ plugins
61
+ }
62
+ };
63
+ }
64
+
65
+ function recreateChartWithTheme(component) {
66
+ if (!component.$refs.chart || !component.value?.$chart) return;
67
+ const canvas = component.$refs.chart;
68
+ const Chart = typeof window !== 'undefined' && window.Chart;
69
+ if (!Chart) return;
70
+ const existing = Chart.getChart(canvas);
71
+ if (existing) {
72
+ try {
73
+ existing.destroy();
74
+ } catch (_) { /* ignore teardown errors */ }
75
+ }
76
+ component.chart = null;
77
+ try {
78
+ const config = applyThemeChartOptions(component.value.$chart);
79
+ component.chart = new Chart(canvas, config);
80
+ } catch (err) {
81
+ console.warn('Dashboard chart recreate failed:', err);
82
+ }
83
+ }
84
+
5
85
  module.exports = app => app.component('dashboard-chart', {
6
86
  template: template,
7
87
  props: ['value', 'fullscreen'],
@@ -11,8 +91,20 @@ module.exports = app => app.component('dashboard-chart', {
11
91
  showDetailModal: false
12
92
  }),
13
93
  mounted() {
14
- const ctx = this.$refs.chart.getContext('2d');
15
- this.chart = new Chart(ctx, this.value.$chart);
94
+ const config = applyThemeChartOptions(this.value.$chart);
95
+ this.chart = new Chart(this.$refs.chart, config);
96
+ this._onStudioThemeChanged = () => recreateChartWithTheme(this);
97
+ document.documentElement.addEventListener('studio-theme-changed', this._onStudioThemeChanged);
98
+ },
99
+ beforeUnmount() {
100
+ document.documentElement.removeEventListener('studio-theme-changed', this._onStudioThemeChanged);
101
+ const existing = typeof window !== 'undefined' && window.Chart && window.Chart.getChart(this.$refs.chart);
102
+ if (existing) {
103
+ try {
104
+ existing.destroy();
105
+ } catch (_) { /* ignore */ }
106
+ }
107
+ this.chart = null;
16
108
  },
17
109
  methods: {
18
110
  exportPNG() {
@@ -34,6 +126,9 @@ module.exports = app => app.component('dashboard-chart', {
34
126
  return this.value.$chart.header;
35
127
  }
36
128
  return null;
129
+ },
130
+ showControls() {
131
+ return this.fullscreen !== undefined;
37
132
  }
38
133
  }
39
134
  });
@@ -3,15 +3,25 @@
3
3
 
4
4
  const template = require('./dashboard-map.html');
5
5
 
6
+ function getMapTileLayerOptions() {
7
+ const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
8
+ return isDark
9
+ ? { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>', subdomains: 'abcd', maxZoom: 20 }
10
+ : { url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '&copy; OpenStreetMap contributors' };
11
+ }
12
+
6
13
  module.exports = app => app.component('dashboard-map', {
7
14
  template: template,
8
15
  props: ['value', 'height'],
16
+ data() {
17
+ return { _map: null, _tileLayer: null };
18
+ },
9
19
  mounted() {
10
20
  const fc = this.value.$featureCollection.featureCollection || this.value.$featureCollection;
11
21
  const map = L.map(this.$refs.map).setView([0, 0], 1);
12
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
13
- attribution: '&copy; OpenStreetMap contributors'
14
- }).addTo(map);
22
+ this._map = map;
23
+ const opts = getMapTileLayerOptions();
24
+ this._tileLayer = L.tileLayer(opts.url, opts).addTo(map);
15
25
  const layer = L.geoJSON(fc).addTo(map);
16
26
 
17
27
  this.$nextTick(() => {
@@ -21,6 +31,20 @@ module.exports = app => app.component('dashboard-map', {
21
31
  map.fitBounds(bounds);
22
32
  }
23
33
  });
34
+ this._onStudioThemeChanged = () => this._updateMapTileLayer();
35
+ document.documentElement.addEventListener('studio-theme-changed', this._onStudioThemeChanged);
36
+ },
37
+ beforeDestroy() {
38
+ document.documentElement.removeEventListener('studio-theme-changed', this._onStudioThemeChanged);
39
+ },
40
+ methods: {
41
+ _updateMapTileLayer() {
42
+ if (!this._map || !this._tileLayer || typeof L === 'undefined') return;
43
+ this._tileLayer.remove();
44
+ this._tileLayer = null;
45
+ const opts = getMapTileLayerOptions();
46
+ this._tileLayer = L.tileLayer(opts.url, opts).addTo(this._map);
47
+ }
24
48
  },
25
49
  computed: {
26
50
  mapStyle() {
@@ -2,7 +2,7 @@
2
2
  <div v-if="Array.isArray(result)">
3
3
  <div v-for="el in result" :key="el._id || el.finishedEvaluatingAt">
4
4
  <component
5
- class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
5
+ class="bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
6
6
  :is="getComponentForValue(el)"
7
7
  :value="el">
8
8
  </component>
@@ -10,14 +10,14 @@
10
10
  </div>
11
11
  <div v-else>
12
12
  <component
13
- class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
13
+ class="bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
14
14
  :is="getComponentForValue(result)"
15
15
  :value="result"
16
16
  :fullscreen="fullscreen"
17
17
  @fullscreen="$emit('fullscreen')">
18
18
  </component>
19
19
  </div>
20
- <div class="text-right text-sm text-gray-700 mt-1" v-if="finishedEvaluatingAt && !fullscreen">
20
+ <div class="text-right text-sm text-content-secondary mt-1" v-if="finishedEvaluatingAt && !fullscreen">
21
21
  Last Evaluated: {{ format.isoToLongDateTime(finishedEvaluatingAt) }}
22
22
  </div>
23
23
  </div>
@@ -34,6 +34,9 @@ module.exports = app => app.component('dashboard-result', {
34
34
  if (value.$grid) {
35
35
  return 'dashboard-grid';
36
36
  }
37
+ if (value.$table) {
38
+ return 'dashboard-table';
39
+ }
37
40
  return 'dashboard-object';
38
41
  }
39
42
  }
@@ -0,0 +1,34 @@
1
+ <div class="overflow-x-auto">
2
+ <table class="min-w-full border-separate border-spacing-0">
3
+ <thead v-if="hasColumns" class="bg-slate-50">
4
+ <tr>
5
+ <th
6
+ v-for="(column, index) in columns"
7
+ :key="'column-' + index"
8
+ class="bg-slate-50 p-3 text-left text-sm font-semibold text-content border-b border-edge"
9
+ >
10
+ {{ column }}
11
+ </th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <tr v-for="(row, rowIndex) in rows" :key="'row-' + rowIndex" class="bg-surface hover:bg-slate-50">
16
+ <td
17
+ v-for="(cell, columnIndex) in row"
18
+ :key="'cell-' + rowIndex + '-' + columnIndex"
19
+ class="p-3 text-sm text-content border-b border-edge"
20
+ >
21
+ {{ displayValue(cell) }}
22
+ </td>
23
+ </tr>
24
+ <tr v-if="!hasRows">
25
+ <td
26
+ :colspan="Math.max(columns.length, 1)"
27
+ class="p-3 text-sm text-content-tertiary border-b border-edge"
28
+ >
29
+ No rows
30
+ </td>
31
+ </tr>
32
+ </tbody>
33
+ </table>
34
+ </div>
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const template = require('./dashboard-table.html');
4
+
5
+ module.exports = app => app.component('dashboard-table', {
6
+ template,
7
+ props: ['value'],
8
+ computed: {
9
+ columns() {
10
+ return Array.isArray(this.value?.$table?.columns) ? this.value.$table.columns : [];
11
+ },
12
+ rows() {
13
+ return Array.isArray(this.value?.$table?.rows) ? this.value.$table.rows : [];
14
+ },
15
+ hasColumns() {
16
+ return this.columns.length > 0;
17
+ },
18
+ hasRows() {
19
+ return this.rows.length > 0;
20
+ }
21
+ },
22
+ methods: {
23
+ displayValue(cell) {
24
+ if (cell == null) {
25
+ return '';
26
+ }
27
+ if (typeof cell === 'object') {
28
+ try {
29
+ return JSON.stringify(cell);
30
+ } catch (err) {
31
+ return String(cell);
32
+ }
33
+ }
34
+ return String(cell);
35
+ }
36
+ }
37
+ });
@@ -1,119 +1,111 @@
1
- <div class="dashboards max-w-5xl mx-auto mt-8">
2
- <div v-if="status === 'loading'" class="text-center mt-4">
3
- <svg
4
- class="inline w-8 h-8 animate-spin text-ultramarine-600"
5
- xmlns="http://www.w3.org/2000/svg"
6
- fill="none"
7
- viewBox="0 0 24 24"
8
- >
9
- <circle
10
- class="opacity-25"
11
- cx="12"
12
- cy="12"
13
- r="10"
14
- stroke="currentColor"
15
- stroke-width="4"
16
- ></circle>
17
- <path
18
- class="opacity-75"
19
- fill="currentColor"
20
- d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
21
- ></path>
22
- </svg>
23
- </div>
24
- <div v-if="status === 'loaded' && dashboards.length === 0">
25
- <div class="text-center">
26
- <h3 class="mt-2 text-sm font-semibold text-gray-900">No dashboards yet</h3>
27
- <p class="mt-1 text-sm text-gray-500">Get started by creating a new dashboard.</p>
28
- <div class="mt-6">
29
- <button type="button" class="inline-flex items-center rounded-md bg-ultramarine-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
30
- <svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
31
- <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
32
- </svg>
33
- New Dashboard
34
- </button>
35
- </div>
36
- </div>
1
+ <div class="dashboards bg-page min-h-[calc(100vh-55px)]">
2
+ <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
3
+ <div class="flex items-center justify-between mb-6">
4
+ <h1 class="text-lg font-semibold text-content">Dashboards</h1>
5
+ <button
6
+ type="button"
7
+ @click="showCreateDashboardModal = true"
8
+ class="rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
9
+ New Dashboard
10
+ </button>
37
11
  </div>
38
12
 
39
- <div class="px-4 sm:px-6 lg:px-8">
40
- <div class="sm:flex sm:items-center">
41
- <div class="sm:flex-auto">
42
- <h1 class="text-base font-semibold leading-6 text-gray-900">Dashboards</h1>
43
- </div>
44
- <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
45
- <button
46
- type="button"
47
- @click="showCreateDashboardModal = true"
48
- class="block rounded-md bg-ultramarine-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">Create New Dashboard</button>
49
- </div>
13
+ <div v-if="status === 'loading'" class="text-center py-12">
14
+ <svg
15
+ class="inline w-8 h-8 animate-spin text-gray-400"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ fill="none"
18
+ viewBox="0 0 24 24"
19
+ >
20
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
21
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
22
+ </svg>
23
+ </div>
24
+
25
+ <div v-else-if="dashboards.length === 0" class="text-center py-12">
26
+ <h3 class="text-sm font-semibold text-content">No dashboards yet</h3>
27
+ <p class="mt-1 text-sm text-content-tertiary">Get started by creating a new dashboard.</p>
28
+ <div class="mt-4">
29
+ <button
30
+ type="button"
31
+ @click="showCreateDashboardModal = true"
32
+ class="inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover">
33
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
34
+ <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
35
+ </svg>
36
+ New Dashboard
37
+ </button>
50
38
  </div>
51
- <div class="mt-8 flow-root">
52
- <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
53
- <div class="inline-block min-w-full py-2 align-middle">
54
- <table class="min-w-full divide-y divide-gray-300">
55
- <thead>
56
- <tr>
57
- <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 lg:pl-8">Title</th>
58
- <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-[50%]">Description</th>
59
- <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6 lg:pr-8">
60
- </th>
61
- <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6 lg:pr-8">
62
- </th>
63
- </tr>
64
- </thead>
65
- <tbody class="divide-y divide-gray-200 bg-white">
66
- <tr v-for="dashboard in dashboards">
67
- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8">{{dashboard.title}}</td>
68
- <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 truncate w-[50%]">{{dashboard.description}}</td>
69
- <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
70
- <router-link
71
- :to="'/dashboard/' + dashboard._id + '?edit=true'"
72
- class="text-ultramarine-600 hover:text-ultramarine-900">
73
- Edit
74
- </router-link>
75
- </td>
76
- <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
77
- <router-link
78
- :to="'/dashboard/' + dashboard._id"
79
- class="text-ultramarine-600 hover:text-ultramarine-900">
80
- View
81
- </router-link>
82
- </td>
83
- <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
84
- <button
85
- @click="showDeleteDashboardModal=dashboard"
86
- class="text-ultramarine-600 hover:text-ultramarine-900">
87
- Delete
88
- </button>
89
- </td>
90
- </tr>
39
+ </div>
91
40
 
92
- <!-- More people... -->
93
- </tbody>
94
- </table>
41
+ <div v-else class="space-y-3">
42
+ <div
43
+ v-for="dashboard in dashboards"
44
+ :key="dashboard._id"
45
+ class="bg-surface rounded-lg border border-edge px-5 py-4 flex items-center justify-between hover:border-edge-strong transition-colors"
46
+ >
47
+ <div class="min-w-0 flex-1 mr-4">
48
+ <router-link
49
+ :to="'/dashboard/' + dashboard._id"
50
+ class="text-sm font-semibold text-content hover:text-primary block truncate">
51
+ {{ dashboard.title }}
52
+ </router-link>
53
+ <p v-if="dashboard.description" class="mt-0.5 text-sm text-content-tertiary truncate">{{ dashboard.description }}</p>
54
+ </div>
55
+ <div class="flex items-center gap-2 shrink-0">
56
+ <router-link
57
+ :to="'/dashboard/' + dashboard._id"
58
+ class="rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover">
59
+ View
60
+ </router-link>
61
+ <div class="relative" v-click-outside="() => closeMenu(dashboard._id)">
62
+ <button
63
+ type="button"
64
+ @click="toggleMenu(dashboard._id)"
65
+ class="rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page">
66
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
67
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
68
+ </svg>
69
+ </button>
70
+ <div
71
+ v-if="openMenuId === dashboard._id"
72
+ class="absolute right-0 mt-1 w-36 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black/5 z-10"
73
+ >
74
+ <div class="py-1">
75
+ <router-link
76
+ :to="'/dashboard/' + dashboard._id + '?edit=true'"
77
+ class="block px-4 py-2 text-sm text-content-secondary hover:bg-muted">
78
+ Edit
79
+ </router-link>
80
+ <button
81
+ @click="showDeleteDashboardModal = dashboard; openMenuId = null"
82
+ type="button"
83
+ class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-muted">
84
+ Delete
85
+ </button>
86
+ </div>
87
+ </div>
95
88
  </div>
96
89
  </div>
97
90
  </div>
98
91
  </div>
92
+ </div>
99
93
 
100
- <modal v-if="showCreateDashboardModal">
101
- <template v-slot:body>
102
- <div class="modal-exit" @click="showCreateDashboardModal = false;">&times;</div>
103
-
104
- <create-dashboard @close="insertNewDashboard"></create-dashboard>
105
- </template>
106
- </modal>
94
+ <modal v-if="showCreateDashboardModal">
95
+ <template v-slot:body>
96
+ <div class="modal-exit" @click="showCreateDashboardModal = false;">&times;</div>
97
+ <create-dashboard @close="insertNewDashboard"></create-dashboard>
98
+ </template>
99
+ </modal>
107
100
 
108
- <modal v-if="showDeleteDashboardModal">
109
- <template v-slot:body>
110
- <div class="modal-exit" @click="showDeleteDashboardModal = null;">&times;</div>
111
- <h2>Are you sure you want to delete this dashboard titled {{showDeleteDashboardModal.title}}?</h2>
112
- <div class="flex space-x-2">
113
- <button class="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500" @click="deleteDashboard(showDeleteDashboardModal)">Yes, delete</button>
114
- <button class="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-600" @click="showDeleteDashboardModal=null;">Cancel</button>
115
- </div>
116
- </template>
117
- </modal>
118
- </div>
119
- </div>
101
+ <modal v-if="showDeleteDashboardModal">
102
+ <template v-slot:body>
103
+ <div class="modal-exit" @click="showDeleteDashboardModal = null;">&times;</div>
104
+ <h2>Are you sure you want to delete "{{showDeleteDashboardModal.title}}"?</h2>
105
+ <div class="flex space-x-2 mt-4">
106
+ <button class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 text-sm font-semibold" @click="deleteDashboard(showDeleteDashboardModal)">Yes, delete</button>
107
+ <button class="px-4 py-2 bg-muted text-content-secondary rounded-md hover:bg-muted text-sm font-semibold" @click="showDeleteDashboardModal=null;">Cancel</button>
108
+ </div>
109
+ </template>
110
+ </modal>
111
+ </div>