@mongoosejs/studio 0.3.0 → 0.3.2

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 (32) hide show
  1. package/DEVGUIDE.md +8 -0
  2. package/backend/actions/ChatThread/createChatMessage.js +2 -0
  3. package/backend/actions/ChatThread/streamChatMessage.js +2 -0
  4. package/backend/actions/Dashboard/getDashboard.js +15 -11
  5. package/backend/actions/Dashboard/updateDashboard.js +2 -2
  6. package/backend/actions/Task/getTaskOverview.js +102 -0
  7. package/backend/actions/Task/getTasks.js +85 -45
  8. package/backend/actions/Task/index.js +1 -0
  9. package/frontend/public/app.js +318 -148
  10. package/frontend/public/tw.css +82 -0
  11. package/frontend/src/_util/dateRange.js +82 -0
  12. package/frontend/src/ace-editor/ace-editor.js +6 -0
  13. package/frontend/src/api.js +6 -0
  14. package/frontend/src/chat/chat-message-script/chat-message-script.html +11 -16
  15. package/frontend/src/chat/chat-message-script/chat-message-script.js +0 -6
  16. package/frontend/src/chat/chat.js +2 -0
  17. package/frontend/src/dashboard/dashboard.js +13 -2
  18. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +4 -3
  19. package/frontend/src/dashboard-result/dashboard-result.js +3 -0
  20. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +34 -0
  21. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +37 -0
  22. package/frontend/src/models/models.js +9 -3
  23. package/frontend/src/navbar/navbar.js +3 -2
  24. package/frontend/src/task-by-name/task-by-name.html +77 -7
  25. package/frontend/src/task-by-name/task-by-name.js +84 -9
  26. package/frontend/src/tasks/task-details/task-details.html +8 -8
  27. package/frontend/src/tasks/task-details/task-details.js +2 -1
  28. package/frontend/src/tasks/tasks.js +25 -118
  29. package/local.js +38 -0
  30. package/package.json +4 -1
  31. package/seed/connect.js +23 -0
  32. package/seed/index.js +101 -0
@@ -1260,6 +1260,10 @@ video {
1260
1260
  min-width: 220px;
1261
1261
  }
1262
1262
 
1263
+ .min-w-\[7rem\] {
1264
+ min-width: 7rem;
1265
+ }
1266
+
1263
1267
  .min-w-\[80px\] {
1264
1268
  min-width: 80px;
1265
1269
  }
@@ -1280,6 +1284,10 @@ video {
1280
1284
  max-width: 64rem;
1281
1285
  }
1282
1286
 
1287
+ .max-w-6xl {
1288
+ max-width: 72rem;
1289
+ }
1290
+
1283
1291
  .max-w-7xl {
1284
1292
  max-width: 80rem;
1285
1293
  }
@@ -1300,6 +1308,10 @@ video {
1300
1308
  max-width: 36rem;
1301
1309
  }
1302
1310
 
1311
+ .max-w-xs {
1312
+ max-width: 20rem;
1313
+ }
1314
+
1303
1315
  .flex-1 {
1304
1316
  flex: 1 1 0%;
1305
1317
  }
@@ -1332,6 +1344,16 @@ video {
1332
1344
  flex-grow: 1;
1333
1345
  }
1334
1346
 
1347
+ .border-separate {
1348
+ border-collapse: separate;
1349
+ }
1350
+
1351
+ .border-spacing-0 {
1352
+ --tw-border-spacing-x: 0px;
1353
+ --tw-border-spacing-y: 0px;
1354
+ border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y);
1355
+ }
1356
+
1335
1357
  .origin-top-right {
1336
1358
  transform-origin: top right;
1337
1359
  }
@@ -1762,6 +1784,16 @@ video {
1762
1784
  border-color: rgb(243 244 246 / var(--tw-border-opacity));
1763
1785
  }
1764
1786
 
1787
+ .border-gray-200 {
1788
+ --tw-border-opacity: 1;
1789
+ border-color: rgb(229 231 235 / var(--tw-border-opacity));
1790
+ }
1791
+
1792
+ .border-gray-300 {
1793
+ --tw-border-opacity: 1;
1794
+ border-color: rgb(209 213 219 / var(--tw-border-opacity));
1795
+ }
1796
+
1765
1797
  .border-green-200 {
1766
1798
  --tw-border-opacity: 1;
1767
1799
  border-color: rgb(187 247 208 / var(--tw-border-opacity));
@@ -2033,6 +2065,11 @@ video {
2033
2065
  background-color: rgb(202 56 56 / var(--tw-bg-opacity));
2034
2066
  }
2035
2067
 
2068
+ .bg-white {
2069
+ --tw-bg-opacity: 1;
2070
+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
2071
+ }
2072
+
2036
2073
  .bg-yellow-100 {
2037
2074
  --tw-bg-opacity: 1;
2038
2075
  background-color: rgb(254 249 195 / var(--tw-bg-opacity));
@@ -2253,6 +2290,10 @@ video {
2253
2290
  padding-bottom: 0.5rem;
2254
2291
  }
2255
2292
 
2293
+ .pb-24 {
2294
+ padding-bottom: 6rem;
2295
+ }
2296
+
2256
2297
  .pl-1 {
2257
2298
  padding-left: 0.25rem;
2258
2299
  }
@@ -2484,6 +2525,11 @@ video {
2484
2525
  color: rgb(75 85 99 / var(--tw-text-opacity));
2485
2526
  }
2486
2527
 
2528
+ .text-gray-700 {
2529
+ --tw-text-opacity: 1;
2530
+ color: rgb(55 65 81 / var(--tw-text-opacity));
2531
+ }
2532
+
2487
2533
  .text-gray-800 {
2488
2534
  --tw-text-opacity: 1;
2489
2535
  color: rgb(31 41 55 / var(--tw-text-opacity));
@@ -2695,6 +2741,12 @@ video {
2695
2741
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
2696
2742
  }
2697
2743
 
2744
+ .shadow-\[0_-4px_6px_-1px_rgba\(0\2c 0\2c 0\2c 0\.1\)\] {
2745
+ --tw-shadow: 0 -4px 6px -1px rgba(0,0,0,0.1);
2746
+ --tw-shadow-colored: 0 -4px 6px -1px var(--tw-shadow-color);
2747
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
2748
+ }
2749
+
2698
2750
  .shadow-lg {
2699
2751
  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
2700
2752
  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
@@ -2974,6 +3026,11 @@ video {
2974
3026
  border-color: var(--color-edge-strong);
2975
3027
  }
2976
3028
 
3029
+ .hover\:border-gray-400:hover {
3030
+ --tw-border-opacity: 1;
3031
+ border-color: rgb(156 163 175 / var(--tw-border-opacity));
3032
+ }
3033
+
2977
3034
  .hover\:border-green-300:hover {
2978
3035
  --tw-border-opacity: 1;
2979
3036
  border-color: rgb(134 239 172 / var(--tw-border-opacity));
@@ -3039,6 +3096,11 @@ video {
3039
3096
  background-color: rgb(156 163 175 / var(--tw-bg-opacity));
3040
3097
  }
3041
3098
 
3099
+ .hover\:bg-gray-50:hover {
3100
+ --tw-bg-opacity: 1;
3101
+ background-color: rgb(249 250 251 / var(--tw-bg-opacity));
3102
+ }
3103
+
3042
3104
  .hover\:bg-gray-600:hover {
3043
3105
  --tw-bg-opacity: 1;
3044
3106
  background-color: rgb(75 85 99 / var(--tw-bg-opacity));
@@ -3203,6 +3265,11 @@ video {
3203
3265
  color: rgb(75 85 99 / var(--tw-text-opacity));
3204
3266
  }
3205
3267
 
3268
+ .hover\:text-gray-700:hover {
3269
+ --tw-text-opacity: 1;
3270
+ color: rgb(55 65 81 / var(--tw-text-opacity));
3271
+ }
3272
+
3206
3273
  .hover\:text-gray-800:hover {
3207
3274
  --tw-text-opacity: 1;
3208
3275
  color: rgb(31 41 55 / var(--tw-text-opacity));
@@ -3266,6 +3333,11 @@ video {
3266
3333
  border-color: transparent;
3267
3334
  }
3268
3335
 
3336
+ .focus\:border-ultramarine-500:focus {
3337
+ --tw-border-opacity: 1;
3338
+ border-color: rgb(63 83 255 / var(--tw-border-opacity));
3339
+ }
3340
+
3269
3341
  .focus\:opacity-100:focus {
3270
3342
  opacity: 1;
3271
3343
  }
@@ -3346,6 +3418,11 @@ video {
3346
3418
  --tw-ring-color: rgb(2 132 199 / var(--tw-ring-opacity));
3347
3419
  }
3348
3420
 
3421
+ .focus\:ring-ultramarine-500:focus {
3422
+ --tw-ring-opacity: 1;
3423
+ --tw-ring-color: rgb(63 83 255 / var(--tw-ring-opacity));
3424
+ }
3425
+
3349
3426
  .focus\:ring-offset-0:focus {
3350
3427
  --tw-ring-offset-width: 0px;
3351
3428
  }
@@ -3461,6 +3538,11 @@ video {
3461
3538
  opacity: 0.5;
3462
3539
  }
3463
3540
 
3541
+ .disabled\:hover\:bg-white:hover:disabled {
3542
+ --tw-bg-opacity: 1;
3543
+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
3544
+ }
3545
+
3464
3546
  .group:focus-within .group-focus-within\:pointer-events-auto {
3465
3547
  pointer-events: auto;
3466
3548
  }
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const DATE_FILTERS = [
4
+ { value: 'last_hour', label: 'Last Hour' },
5
+ { value: 'today', label: 'Today' },
6
+ { value: 'yesterday', label: 'Yesterday' },
7
+ { value: 'thisWeek', label: 'This Week' },
8
+ { value: 'lastWeek', label: 'Last Week' },
9
+ { value: 'thisMonth', label: 'This Month' },
10
+ { value: 'lastMonth', label: 'Last Month' }
11
+ ];
12
+
13
+ const DATE_FILTER_VALUES = DATE_FILTERS.map(f => f.value);
14
+
15
+ /**
16
+ * Returns { start, end } Date objects for a given range key (e.g. 'last_hour', 'today').
17
+ * Month ranges use UTC boundaries.
18
+ * @param {string} selectedRange - One of DATE_FILTER_VALUES
19
+ * @returns {{ start: Date, end: Date }}
20
+ */
21
+ function getDateRangeForRange(selectedRange) {
22
+ const now = new Date();
23
+ let start, end;
24
+ switch (selectedRange) {
25
+ case 'last_hour':
26
+ start = new Date();
27
+ start.setHours(start.getHours() - 1);
28
+ end = new Date();
29
+ break;
30
+ case 'today':
31
+ start = new Date();
32
+ start.setHours(0, 0, 0, 0);
33
+ end = new Date();
34
+ end.setHours(23, 59, 59, 999);
35
+ break;
36
+ case 'yesterday':
37
+ start = new Date(now);
38
+ start.setDate(start.getDate() - 1);
39
+ start.setHours(0, 0, 0, 0);
40
+ end = new Date(start);
41
+ end.setHours(23, 59, 59, 999);
42
+ break;
43
+ case 'thisWeek':
44
+ start = new Date(now.getTime() - (7 * 86400000));
45
+ start.setHours(0, 0, 0, 0);
46
+ end = new Date();
47
+ end.setHours(23, 59, 59, 999);
48
+ break;
49
+ case 'lastWeek':
50
+ start = new Date(now.getTime() - (14 * 86400000));
51
+ start.setHours(0, 0, 0, 0);
52
+ end = new Date(now.getTime() - (7 * 86400000));
53
+ end.setHours(23, 59, 59, 999);
54
+ break;
55
+ case 'thisMonth': {
56
+ const y = now.getUTCFullYear();
57
+ const m = now.getUTCMonth();
58
+ start = new Date(Date.UTC(y, m, 1, 0, 0, 0, 0));
59
+ end = new Date(Date.UTC(y, m + 1, 0, 23, 59, 59, 999));
60
+ break;
61
+ }
62
+ case 'lastMonth': {
63
+ const y = now.getUTCFullYear();
64
+ const m = now.getUTCMonth();
65
+ start = new Date(Date.UTC(y, m - 1, 1, 0, 0, 0, 0));
66
+ end = new Date(Date.UTC(y, m, 0, 23, 59, 59, 999));
67
+ break;
68
+ }
69
+ default:
70
+ start = new Date();
71
+ start.setHours(start.getHours() - 1);
72
+ end = new Date();
73
+ break;
74
+ }
75
+ return { start, end };
76
+ }
77
+
78
+ module.exports = {
79
+ DATE_FILTERS,
80
+ DATE_FILTER_VALUES,
81
+ getDateRangeForRange
82
+ };
@@ -80,6 +80,12 @@ module.exports = app => app.component('ace-editor', {
80
80
  }
81
81
  },
82
82
  methods: {
83
+ getValue() {
84
+ if (this.editor) {
85
+ return this.editor.getValue();
86
+ }
87
+ return this.modelValue !== '' ? this.modelValue : this.value;
88
+ },
83
89
  setValue(val) {
84
90
  if (this.editor) {
85
91
  this.editor.setValue(val ?? '', -1);
@@ -185,6 +185,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
185
185
  getTasks: function getTasks(params) {
186
186
  return client.post('', { action: 'Task.getTasks', ...params }).then(res => res.data);
187
187
  },
188
+ getTaskOverview: function getTaskOverview(params) {
189
+ return client.post('', { action: 'Task.getTaskOverview', ...params }).then(res => res.data);
190
+ },
188
191
  rescheduleTask: function rescheduleTask(params) {
189
192
  return client.post('', { action: 'Task.rescheduleTask', ...params }).then(res => res.data);
190
193
  },
@@ -519,6 +522,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
519
522
  getTasks: function getTasks(params) {
520
523
  return client.post('/Task/getTasks', params).then(res => res.data);
521
524
  },
525
+ getTaskOverview: function getTaskOverview(params) {
526
+ return client.post('/Task/getTaskOverview', params).then(res => res.data);
527
+ },
522
528
  rescheduleTask: function rescheduleTask(params) {
523
529
  return client.post('/Task/rescheduleTask', params).then(res => res.data);
524
530
  },
@@ -23,13 +23,6 @@
23
23
  Output
24
24
  </button>
25
25
  <div class="ml-auto mr-1 flex items-center">
26
- <button
27
- v-if="activeTab === 'output' && isChartOutput"
28
- class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
29
- @click="exportChartPNG"
30
- title="Export PNG">
31
- <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.2em;" 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>
32
- </button>
33
26
  <button
34
27
  v-if="activeTab === 'output'"
35
28
  class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
@@ -116,9 +109,11 @@
116
109
  </div>
117
110
 
118
111
  <div class="p-3 whitespace-pre-wrap max-h-[50vh] overflow-y-auto bg-surface border-t max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] relative" v-show="activeTab === 'output'">
119
- <dashboard-chart v-if="message.executionResult?.output?.$chart" :value="message.executionResult?.output" ref="chartOutput" />
120
- <dashboard-map v-else-if="message.executionResult?.output?.$featureCollection" :value="message.executionResult?.output" />
121
- <pre v-else>{{ message.executionResult?.output || 'No output' }}</pre>
112
+ <dashboard-result
113
+ v-if="message.executionResult?.output != null"
114
+ :result="message.executionResult.output">
115
+ </dashboard-result>
116
+ <pre v-else>No output</pre>
122
117
 
123
118
  <div v-if="message.executionResult?.logs?.length" class="mt-3 pt-3 border-t border-edge">
124
119
  <div class="text-xs font-semibold text-gray-600 uppercase tracking-wide">Console</div>
@@ -130,12 +125,12 @@
130
125
  <template #body>
131
126
  <div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showDetailModal = false;">&times;</div>
132
127
  <div class="h-full overflow-auto">
133
- <dashboard-chart v-if="message.executionResult?.output?.$chart" :value="message.executionResult?.output" :responsive="true" />
134
- <dashboard-map
135
- v-else-if="message.executionResult?.output?.$featureCollection"
136
- :value="message.executionResult?.output"
137
- height="80vh" />
138
- <pre v-else class="whitespace-pre-wrap">{{ message.executionResult?.output || 'No output' }}</pre>
128
+ <dashboard-result
129
+ v-if="message.executionResult?.output != null"
130
+ :result="message.executionResult.output"
131
+ :fullscreen="true">
132
+ </dashboard-result>
133
+ <pre v-else class="whitespace-pre-wrap">No output</pre>
139
134
  </div>
140
135
  </template>
141
136
  </modal>
@@ -30,15 +30,9 @@ module.exports = app => app.component('chat-message-script', {
30
30
  },
31
31
  canOverwriteDashboard() {
32
32
  return !!this.targetDashboardId;
33
- },
34
- isChartOutput() {
35
- return !!this.message.executionResult?.output?.$chart;
36
33
  }
37
34
  },
38
35
  methods: {
39
- exportChartPNG() {
40
- this.$refs.chartOutput?.exportPNG?.();
41
- },
42
36
  async executeScript() {
43
37
  const scriptToRun = this.isEditing ? this.editedScript : this.script;
44
38
  this.editedScript = scriptToRun;
@@ -192,6 +192,8 @@ module.exports = {
192
192
  }
193
193
  },
194
194
  async mounted() {
195
+ window.pageState = this;
196
+
195
197
  this.chatThreadId = this.threadId;
196
198
  const { chatThreads } = await api.ChatThread.listChatThreads();
197
199
  this.chatThreads = chatThreads;
@@ -28,6 +28,13 @@ module.exports = {
28
28
  this.showEditor = !this.showEditor;
29
29
  },
30
30
  async updateCode(update) {
31
+ if (!update?.doc) {
32
+ const message = update?.error?.message || 'Dashboard update failed';
33
+ console.error(update?.error || new Error(message));
34
+ this.$toast.error(message);
35
+ return;
36
+ }
37
+
31
38
  this.code = update.doc.code;
32
39
  this.title = update.doc.title;
33
40
  this.description = update.doc.description;
@@ -52,7 +59,9 @@ module.exports = {
52
59
  this.dashboardResults.unshift({ error: { message: error.message || 'Evaluation failed' }, finishedEvaluatingAt: new Date() });
53
60
  }
54
61
  } catch (err) {
55
- this.$toast.error(err?.response?.data?.message || err?.message || 'Dashboard evaluation failed');
62
+ const message = err?.response?.data?.message || err?.message || 'Dashboard evaluation failed';
63
+ console.error(err || new Error(message));
64
+ this.$toast.error(message);
56
65
  } finally {
57
66
  this.status = 'loaded';
58
67
  }
@@ -148,7 +157,9 @@ module.exports = {
148
157
  return this.dashboardResults.length > 0 ? this.dashboardResults[0] : null;
149
158
  }
150
159
  },
151
- mounted: async function() {
160
+ mounted: async function () {
161
+ window.pageState = this;
162
+
152
163
  document.addEventListener('click', this.handleDocumentClick);
153
164
  this.showEditor = this.$route.query.edit;
154
165
  await this.loadInitial();
@@ -6,7 +6,7 @@ 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',
@@ -27,7 +27,7 @@ module.exports = app => app.component('edit-dashboard', {
27
27
  async updateCode() {
28
28
  this.status = 'loading';
29
29
  try {
30
- const codeToSave = this.$refs.codeEditor ? this.$refs.codeEditor.getValue() : this.editCode;
30
+ const codeToSave = this.$refs.codeEditor?.getValue ? this.$refs.codeEditor.getValue() : this.editCode;
31
31
  const { doc } = await api.Dashboard.updateDashboard({
32
32
  dashboardId: this.dashboardId,
33
33
  code: codeToSave,
@@ -43,7 +43,8 @@ module.exports = app => app.component('edit-dashboard', {
43
43
  this.$toast.success('Dashboard updated!');
44
44
  this.closeEditor();
45
45
  } catch (err) {
46
- 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 } });
47
48
  } finally {
48
49
  this.status = 'loaded';
49
50
  }
@@ -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
+ });
@@ -65,6 +65,7 @@ module.exports = app => app.component('models', {
65
65
  }),
66
66
  created() {
67
67
  this.currentModel = this.model;
68
+ this.setSearchTextFromRoute();
68
69
  this.loadOutputPreference();
69
70
  this.loadSelectedGeoField();
70
71
  this.loadRecentlyViewedModels();
@@ -78,6 +79,7 @@ module.exports = app => app.component('models', {
78
79
  this.destroyMap();
79
80
  },
80
81
  async mounted() {
82
+ window.pageState = this;
81
83
  this.onScroll = () => this.checkIfScrolledToBottom();
82
84
  document.addEventListener('scroll', this.onScroll, true);
83
85
  this.onPopState = () => this.initSearchFromUrl();
@@ -101,6 +103,7 @@ module.exports = app => app.component('models', {
101
103
  }
102
104
  };
103
105
  document.addEventListener('keydown', this.onCtrlP, true);
106
+ this.query = Object.assign({}, this.$route.query);
104
107
  const { models, readyState } = await api.Model.listModels();
105
108
  this.models = models;
106
109
  await this.loadModelCounts();
@@ -483,14 +486,17 @@ module.exports = app => app.component('models', {
483
486
 
484
487
  return params;
485
488
  },
486
- async initSearchFromUrl() {
487
- this.status = 'loading';
488
- this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
489
+ setSearchTextFromRoute() {
489
490
  if (this.$route.query?.search) {
490
491
  this.searchText = this.$route.query.search;
491
492
  } else {
492
493
  this.searchText = '';
493
494
  }
495
+ },
496
+ async initSearchFromUrl() {
497
+ this.status = 'loading';
498
+ this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
499
+ this.setSearchTextFromRoute();
494
500
  if (this.$route.query?.sort) {
495
501
  const sort = eval(`(${this.$route.query.sort})`);
496
502
  const path = Object.keys(sort)[0];
@@ -17,7 +17,8 @@ module.exports = app => app.component('navbar', {
17
17
  showFlyout: false,
18
18
  darkMode: typeof localStorage !== 'undefined' && localStorage.getItem('studio-theme') === 'dark'
19
19
  }),
20
- mounted: function() {
20
+ mounted: function () {
21
+ window.navbar = this;
21
22
  const mobileMenuMask = document.querySelector('#mobile-menu-mask');
22
23
  const mobileMenu = document.querySelector('#mobile-menu');
23
24
  const openBtn = document.querySelector('#open-mobile-menu');
@@ -71,7 +72,7 @@ module.exports = app => app.component('navbar', {
71
72
  } else {
72
73
  return 'https://www.npmjs.com/package/@mongoosejs/task';
73
74
  }
74
-
75
+
75
76
  }
76
77
  },
77
78
  methods: {
@@ -5,11 +5,81 @@
5
5
  <div v-else-if="status === 'error'" class="text-red-600">
6
6
  {{ errorMessage }}
7
7
  </div>
8
- <task-details
9
- v-else-if="taskGroup"
10
- :task-group="taskGroup"
11
- :back-to="{ name: 'tasks' }"
12
- @task-created="onTaskCreated"
13
- @task-cancelled="onTaskCancelled"
14
- ></task-details>
8
+ <template v-else-if="taskGroup">
9
+ <div class="pb-24">
10
+ <router-link :to="{ name: 'tasks' }" class="inline-flex items-center gap-1 text-gray-500 hover:text-gray-700 mb-4">
11
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
13
+ </svg>
14
+ Back to Task Groups
15
+ </router-link>
16
+ <div class="mb-4">
17
+ <label class="block text-sm font-medium text-gray-700 mb-1">Filter by Date:</label>
18
+ <select v-model="selectedRange" @change="updateDateRange" class="border-gray-300 rounded-md shadow-sm w-full p-2 max-w-xs">
19
+ <option v-for="option in dateFilters" :key="option.value" :value="option.value">
20
+ {{ option.label }}
21
+ </option>
22
+ </select>
23
+ </div>
24
+ <task-details
25
+ :task-group="taskGroup"
26
+ :back-to="{ name: 'tasks' }"
27
+ :show-back-button="false"
28
+ @task-created="onTaskCreated"
29
+ @task-cancelled="onTaskCancelled"
30
+ ></task-details>
31
+ </div>
32
+ <div
33
+ v-if="numDocs > 0"
34
+ class="fixed bottom-0 left-0 right-0 z-10 px-4 py-4 bg-white border-t border-gray-200 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]"
35
+ >
36
+ <div class="flex flex-wrap items-center justify-between gap-4 max-w-6xl mx-auto">
37
+ <div class="flex items-center gap-6">
38
+ <p class="text-sm text-gray-600">
39
+ <span class="font-medium text-gray-900">{{ Math.min((page - 1) * pageSize + 1, numDocs) }}–{{ Math.min(page * pageSize, numDocs) }}</span>
40
+ <span class="mx-1">of</span>
41
+ <span class="font-medium text-gray-900">{{ numDocs }}</span>
42
+ <span class="ml-1 text-gray-500">tasks</span>
43
+ </p>
44
+ <div class="flex items-center gap-2">
45
+ <label class="text-sm font-medium text-gray-700">Per page</label>
46
+ <select
47
+ v-model.number="pageSize"
48
+ @change="onPageSizeChange"
49
+ class="border border-gray-300 rounded-md shadow-sm px-3 py-2 text-sm text-gray-700 bg-white hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-ultramarine-500 focus:border-ultramarine-500"
50
+ >
51
+ <option v-for="n in pageSizeOptions" :key="n" :value="n">{{ n }}</option>
52
+ </select>
53
+ </div>
54
+ </div>
55
+ <div class="flex items-center gap-1">
56
+ <button
57
+ type="button"
58
+ :disabled="page <= 1"
59
+ @click="goToPage(page - 1)"
60
+ class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white bg-white text-gray-700 border-gray-300 hover:bg-gray-50 hover:border-gray-400"
61
+ >
62
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
63
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
64
+ </svg>
65
+ Previous
66
+ </button>
67
+ <span class="px-4 py-2 text-sm text-gray-600 min-w-[7rem] text-center">
68
+ Page <span class="font-semibold text-gray-900">{{ page }}</span> of <span class="font-semibold text-gray-900">{{ Math.max(1, Math.ceil(numDocs / pageSize)) }}</span>
69
+ </span>
70
+ <button
71
+ type="button"
72
+ :disabled="page >= Math.ceil(numDocs / pageSize)"
73
+ @click="goToPage(page + 1)"
74
+ class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white bg-white text-gray-700 border-gray-300 hover:bg-gray-50 hover:border-gray-400"
75
+ >
76
+ Next
77
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
79
+ </svg>
80
+ </button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </template>
15
85
  </div>