@mongoosejs/studio 0.3.5 → 0.3.7

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 (37) hide show
  1. package/backend/actions/ChatThread/createChatMessage.js +12 -2
  2. package/backend/actions/ChatThread/streamChatMessage.js +12 -2
  3. package/backend/actions/Model/createChatMessage.js +12 -2
  4. package/backend/actions/Model/streamChatMessage.js +13 -2
  5. package/backend/actions/Task/getTasksOverTime.js +66 -0
  6. package/backend/actions/Task/index.js +1 -0
  7. package/frontend/public/app.js +809 -110
  8. package/frontend/public/tw.css +19 -16
  9. package/frontend/src/api.js +6 -0
  10. package/frontend/src/chat/chat.html +19 -12
  11. package/frontend/src/chat/chat.js +9 -3
  12. package/frontend/src/create-document/create-document.js +3 -1
  13. package/frontend/src/dashboard-result/dashboard-primitive/dashboard-primitive.html +2 -2
  14. package/frontend/src/dashboard-result/dashboard-primitive/dashboard-primitive.js +13 -1
  15. package/frontend/src/dashboard-result/dashboard-result.html +1 -1
  16. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +21 -1
  17. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +52 -0
  18. package/frontend/src/detail-date/detail-date.html +1 -0
  19. package/frontend/src/detail-date/detail-date.js +123 -0
  20. package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html +26 -0
  21. package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js +41 -0
  22. package/frontend/src/document-details/document-property/document-property.html +13 -5
  23. package/frontend/src/document-details/document-property/document-property.js +14 -1
  24. package/frontend/src/getCurrentDateTimeContext.js +17 -0
  25. package/frontend/src/list-json/list-json.js +0 -7
  26. package/frontend/src/modal/modal.js +25 -1
  27. package/frontend/src/models/document-search/document-search.js +3 -0
  28. package/frontend/src/models/models.html +25 -5
  29. package/frontend/src/models/models.js +1 -1
  30. package/frontend/src/navbar/navbar.html +6 -1
  31. package/frontend/src/navbar/navbar.js +1 -6
  32. package/frontend/src/pro-upgrade-modal/pro-upgrade-modal.html +38 -0
  33. package/frontend/src/pro-upgrade-modal/pro-upgrade-modal.js +23 -0
  34. package/frontend/src/tasks/tasks.html +34 -20
  35. package/frontend/src/tasks/tasks.js +158 -5
  36. package/local.js +2 -1
  37. package/package.json +2 -1
@@ -28,13 +28,6 @@ module.exports = app => app.component('list-json', {
28
28
  topLevelExpanded: false
29
29
  };
30
30
  },
31
- watch: {
32
- value: {
33
- handler() {
34
- this.resetCollapse();
35
- }
36
- }
37
- },
38
31
  created() {
39
32
  this.resetCollapse();
40
33
  for (const field of this.expandedFields) {
@@ -7,5 +7,29 @@ appendCSS(require('./modal.css'));
7
7
 
8
8
  module.exports = app => app.component('modal', {
9
9
  template,
10
- props: ['containerClass']
10
+ props: ['containerClass'],
11
+ mounted() {
12
+ window.addEventListener('keydown', this.onEscape);
13
+ },
14
+ beforeUnmount() {
15
+ window.removeEventListener('keydown', this.onEscape);
16
+ },
17
+ methods: {
18
+ onEscape(event) {
19
+ if (event.key !== 'Escape') {
20
+ return;
21
+ }
22
+
23
+ const modalMasks = Array.from(document.querySelectorAll('.modal-mask'));
24
+ const currentMask = this.$el?.classList?.contains('modal-mask') ? this.$el : this.$el?.querySelector('.modal-mask') || this.$el;
25
+ const isTopMostModal = modalMasks.length > 0 && modalMasks[modalMasks.length - 1] === currentMask;
26
+
27
+ if (!isTopMostModal) {
28
+ return;
29
+ }
30
+
31
+ const closeButton = currentMask.querySelector('.modal-exit, [data-modal-close]');
32
+ closeButton?.click();
33
+ }
34
+ }
11
35
  });
@@ -41,6 +41,9 @@ module.exports = app => app.component('document-search', {
41
41
  created() {
42
42
  this.buildAutocompleteTrie();
43
43
  },
44
+ mounted() {
45
+ this.$refs.searchInput.focus();
46
+ },
44
47
  methods: {
45
48
  emitSearch() {
46
49
  this.$emit('input', this.searchText);
@@ -280,6 +280,24 @@
280
280
  <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 relative m-4 rounded-md" role="alert">
281
281
  <span class="block font-bold">Error</span>
282
282
  <span class="block">{{ error }}</span>
283
+ <span class="block mt-2">
284
+ Need help?
285
+ <a
286
+ href="https://discord.gg/P3YCfKYxpy"
287
+ target="_blank"
288
+ rel="noopener noreferrer"
289
+ class="underline font-medium text-red-800 hover:text-red-900"
290
+ >Ask in Discord</a>
291
+ or
292
+ <a
293
+ href="https://github.com/mongoosejs/studio/issues"
294
+ target="_blank"
295
+ rel="noopener noreferrer"
296
+ class="underline font-medium text-red-800 hover:text-red-900"
297
+ >
298
+ open a GitHub issue.
299
+ </a>
300
+ </span>
283
301
  </div>
284
302
  </div>
285
303
  <div v-else-if="outputType === 'table'" class="flex-1 min-h-0 flex flex-col overflow-hidden">
@@ -442,13 +460,15 @@
442
460
  selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:shadow-sm hover:border-slate-300 bg-surface'
443
461
  ]"
444
462
  >
445
- <button
446
- type="button"
463
+ <router-link
447
464
  class="absolute top-2 right-2 z-10 inline-flex items-center rounded bg-primary px-2 py-1 text-xs font-semibold text-primary-text shadow-sm transition-opacity duration-150 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
448
- @click.stop="openDocument(document)"
465
+ :to="{ path: '/model/' + currentModel + '/document/' + document._id, query: $route.query }"
466
+ target="_blank"
467
+ rel="noopener noreferrer"
468
+ @click.stop
449
469
  >
450
- Open this Document
451
- </button>
470
+ Open
471
+ </router-link>
452
472
  <list-json :value="filterDocument(document)" :references="referenceMap">
453
473
  </list-json>
454
474
  </div>
@@ -261,7 +261,7 @@ module.exports = app => app.component('models', {
261
261
  computed: {
262
262
  referenceMap() {
263
263
  const map = {};
264
- for (const path of this.filteredPaths) {
264
+ for (const path of this.schemaPaths) {
265
265
  if (path?.ref) {
266
266
  map[path.path] = path.ref;
267
267
  }
@@ -42,7 +42,8 @@
42
42
  </svg>
43
43
  </span>
44
44
  <a
45
- :href="hasTaskVisualizer"
45
+ v-if="hasTaskVisualizer"
46
+ href="#/tasks"
46
47
  class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
47
48
  :class="taskView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">
48
49
  Tasks
@@ -210,6 +211,10 @@
210
211
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
211
212
  </svg>
212
213
  </span>
214
+ <a v-if="hasTaskVisualizer"
215
+ href="#/tasks"
216
+ class="block px-3 py-2 rounded-md text-base font-medium"
217
+ :class="taskView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'">Tasks</a>
213
218
  <div v-if="!user && hasAPIKey" class="mt-4">
214
219
  <button
215
220
  type="button"
@@ -67,12 +67,7 @@ module.exports = app => app.component('navbar', {
67
67
  return this.roles && this.roles[0] === 'dashboards' ? 'dashboards' : 'root';
68
68
  },
69
69
  hasTaskVisualizer() {
70
- if (window.MONGOOSE_STUDIO_CONFIG.enableTaskVisualizer) {
71
- return '#/tasks';
72
- } else {
73
- return 'https://www.npmjs.com/package/@mongoosejs/task';
74
- }
75
-
70
+ return !!window.MONGOOSE_STUDIO_CONFIG.enableTaskVisualizer;
76
71
  }
77
72
  },
78
73
  methods: {
@@ -0,0 +1,38 @@
1
+ <modal v-if="show" containerClass="!max-w-md">
2
+ <template v-slot:body>
3
+ <div @keydown.esc="$emit('close')" tabindex="0" ref="overlay">
4
+ <div class="flex items-center justify-between mb-4">
5
+ <h3 class="text-lg font-semibold text-gray-900">Pro Feature</h3>
6
+ <button
7
+ type="button"
8
+ @click="$emit('close')"
9
+ class="text-gray-400 hover:text-gray-600 cursor-pointer"
10
+ aria-label="Close"
11
+ >
12
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
13
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
14
+ </svg>
15
+ </button>
16
+ </div>
17
+ <p class="text-gray-600 mb-6">
18
+ {{ featureDescription }} Upgrade to a Pro workspace to unlock this feature.
19
+ </p>
20
+ <div class="flex justify-end gap-3">
21
+ <button
22
+ type="button"
23
+ @click="$emit('close')"
24
+ class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 cursor-pointer"
25
+ >
26
+ Cancel
27
+ </button>
28
+ <a
29
+ href="https://studio.mongoosejs.io/pro"
30
+ target="_blank"
31
+ class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-md hover:bg-primary-hover inline-flex items-center"
32
+ >
33
+ Upgrade to Pro
34
+ </a>
35
+ </div>
36
+ </div>
37
+ </template>
38
+ </modal>
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const template = require('./pro-upgrade-modal.html');
4
+
5
+ module.exports = app => app.component('pro-upgrade-modal', {
6
+ template,
7
+ props: {
8
+ show: { type: Boolean, default: false },
9
+ featureDescription: { type: String, default: 'This feature is available on the Pro plan.' }
10
+ },
11
+ emits: ['close'],
12
+ watch: {
13
+ show(val) {
14
+ if (val) {
15
+ this.$nextTick(() => {
16
+ if (this.$refs.overlay) {
17
+ this.$refs.overlay.focus();
18
+ }
19
+ });
20
+ }
21
+ }
22
+ }
23
+ });
@@ -79,63 +79,77 @@
79
79
  <div class="text-2xl font-bold">{{cancelledCount}}</div>
80
80
  </button>
81
81
  </div>
82
+
83
+ <!-- Tasks Over Time Chart -->
84
+ <!--
85
+ Canvas is gated by showOverTimeChart (v-if) so Chart.js is destroyed
86
+ and the DOM node is removed before each refresh. In-place Chart.js
87
+ updates during Vue re-renders from filter changes could freeze the UI
88
+ (dropdowns stuck, chart not updating). See tasks.js getTasks().
89
+ -->
90
+ <div class="mt-6">
91
+ <h2 class="text-lg font-semibold text-content-secondary mb-3">Tasks Over Time</h2>
92
+ <div class="bg-page border border-edge rounded-lg p-4" style="height: 260px;">
93
+ <canvas v-if="showOverTimeChart && overTimeBuckets.length > 0" ref="overTimeChart" style="width:100%;height:100%;"></canvas>
94
+ <div v-else class="flex items-center justify-center h-full text-content-tertiary text-sm">
95
+ No task activity in the selected window
96
+ </div>
97
+ </div>
98
+ </div>
82
99
 
83
100
  <!-- Grouped Task List -->
84
101
  <div class="mt-6">
85
102
  <h2 class="text-lg font-semibold text-content-secondary mb-4">Tasks by Name</h2>
86
- <ul class="divide-y divide-gray-200">
87
- <li v-for="group in tasksByName" :key="group.name" class="p-4 group hover:border hover:rounded-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200">
88
- <div class="flex items-center justify-between mb-3 ">
89
- <div class="flex-1 cursor-pointer" @click="openTaskGroupDetails(group)">
90
- <div class="flex items-center gap-2">
91
- <div class="font-medium text-lg group-hover:text-primary transition-colors">{{ group.name }}</div>
92
- <svg class="w-4 h-4 text-gray-400 group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
103
+ <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
104
+ <div v-for="group in tasksByName" :key="group.name" class="border border-edge rounded-lg p-4 group hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200">
105
+ <div class="flex items-start justify-between mb-3">
106
+ <div class="flex-1 cursor-pointer min-w-0 mr-2" @click="openTaskGroupDetails(group)">
107
+ <div class="flex items-center gap-1">
108
+ <div class="font-medium text-sm group-hover:text-primary transition-colors truncate">{{ group.name }}</div>
109
+ <svg class="w-3 h-3 text-gray-400 group-hover:text-primary transition-colors flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
93
110
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
94
111
  </svg>
95
112
  </div>
96
- <div class="text-sm text-content-tertiary group-hover:text-content-secondary transition-colors">Total: {{ group.totalCount }} tasks</div>
97
- <div class="text-xs text-primary opacity-0 group-hover:opacity-100 transition-opacity mt-1">
98
- Click to view details
99
- </div>
113
+ <div class="text-xs text-content-tertiary">Total: {{ group.totalCount }} tasks</div>
100
114
  </div>
101
- <div class="text-sm text-content-tertiary">
102
- Last run: {{ group.lastRun ? new Date(group.lastRun).toLocaleString() : 'Never' }}
115
+ <div class="text-xs text-content-tertiary flex-shrink-0">
116
+ {{ group.lastRun ? new Date(group.lastRun).toLocaleString() : 'Never' }}
103
117
  </div>
104
118
  </div>
105
119
 
106
120
  <!-- Status Counts -->
107
- <div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
121
+ <div class="grid grid-cols-2 gap-1.5">
108
122
  <button
109
123
  @click.stop="openTaskGroupDetailsWithFilter(group, 'pending')"
110
124
  class="bg-yellow-50 border border-yellow-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-yellow-300"
111
125
  >
112
126
  <div class="text-xs text-yellow-600 font-medium">Pending</div>
113
- <div class="text-lg font-bold text-yellow-700">{{ group.statusCounts.pending || 0 }}</div>
127
+ <div class="text-base font-bold text-yellow-700">{{ group.statusCounts.pending || 0 }}</div>
114
128
  </button>
115
129
  <button
116
130
  @click.stop="openTaskGroupDetailsWithFilter(group, 'succeeded')"
117
131
  class="bg-green-50 border border-green-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-green-300"
118
132
  >
119
133
  <div class="text-xs text-green-600 font-medium">Succeeded</div>
120
- <div class="text-lg font-bold text-green-700">{{ group.statusCounts.succeeded || 0 }}</div>
134
+ <div class="text-base font-bold text-green-700">{{ group.statusCounts.succeeded || 0 }}</div>
121
135
  </button>
122
136
  <button
123
137
  @click.stop="openTaskGroupDetailsWithFilter(group, 'failed')"
124
138
  class="bg-red-50 border border-red-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-red-300"
125
139
  >
126
140
  <div class="text-xs text-red-600 font-medium">Failed</div>
127
- <div class="text-lg font-bold text-red-700">{{ group.statusCounts.failed || 0 }}</div>
141
+ <div class="text-base font-bold text-red-700">{{ group.statusCounts.failed || 0 }}</div>
128
142
  </button>
129
143
  <button
130
144
  @click.stop="openTaskGroupDetailsWithFilter(group, 'cancelled')"
131
145
  class="bg-page border border-edge rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-edge-strong"
132
146
  >
133
147
  <div class="text-xs text-gray-600 font-medium">Cancelled</div>
134
- <div class="text-lg font-bold text-content-secondary">{{ group.statusCounts.cancelled || 0 }}</div>
148
+ <div class="text-base font-bold text-content-secondary">{{ group.statusCounts.cancelled || 0 }}</div>
135
149
  </button>
136
150
  </div>
137
- </li>
138
- </ul>
151
+ </div>
152
+ </div>
139
153
  </div>
140
154
  </div>
141
155
  </div>
@@ -4,6 +4,39 @@ const template = require('./tasks.html');
4
4
  const api = require('../api');
5
5
  const { DATE_FILTERS, getDateRangeForRange } = require('../_util/dateRange');
6
6
 
7
+ /** Returns the bucket size in ms for the given date range. */
8
+ function getBucketSizeMs(range) {
9
+ switch (range) {
10
+ case 'last_hour': return 5 * 60 * 1000; // 5 minutes
11
+ case 'today':
12
+ case 'yesterday': return 60 * 60 * 1000; // 1 hour
13
+ case 'thisWeek':
14
+ case 'lastWeek': return 24 * 60 * 60 * 1000; // 1 day
15
+ case 'thisMonth':
16
+ case 'lastMonth': return 24 * 60 * 60 * 1000; // 1 day
17
+ default: return 5 * 60 * 1000;
18
+ }
19
+ }
20
+
21
+ /** Formats a bucket timestamp for the x-axis label based on the date range. */
22
+ function formatBucketLabel(timestamp, range) {
23
+ const date = new Date(timestamp);
24
+ switch (range) {
25
+ case 'last_hour':
26
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
27
+ case 'today':
28
+ case 'yesterday':
29
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
30
+ case 'thisWeek':
31
+ case 'lastWeek':
32
+ case 'thisMonth':
33
+ case 'lastMonth':
34
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
35
+ default:
36
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
37
+ }
38
+ }
39
+
7
40
  module.exports = app => app.component('tasks', {
8
41
  data: () => ({
9
42
  status: 'init',
@@ -31,10 +64,22 @@ module.exports = app => app.component('tasks', {
31
64
  scheduledAt: '',
32
65
  parameters: '',
33
66
  repeatInterval: ''
34
- }
67
+ },
68
+ // Chart over time
69
+ overTimeChart: null,
70
+ overTimeBuckets: [],
71
+ // Toggled with v-if on the canvas so Chart.js is torn down and remounted on
72
+ // filter changes. Updating Chart.js in place during a big Vue re-render was
73
+ // freezing the page (dropdowns unresponsive, chart stale).
74
+ showOverTimeChart: true
35
75
  }),
36
76
  methods: {
37
77
  async getTasks() {
78
+ // Hide chart canvas + teardown Chart.js immediately on filter changes
79
+ // (see showOverTimeChart + v-if on the canvas in tasks.html).
80
+ this.showOverTimeChart = false;
81
+ this.destroyOverTimeChart();
82
+
38
83
  const params = {};
39
84
  if (this.selectedStatus == 'all') {
40
85
  params.status = null;
@@ -53,9 +98,105 @@ module.exports = app => app.component('tasks', {
53
98
  params.name = this.searchQuery.trim();
54
99
  }
55
100
 
56
- const { statusCounts, tasksByName } = await api.Task.getTaskOverview(params);
57
- this.statusCounts = statusCounts || this.statusCounts;
58
- this.tasksByName = tasksByName || [];
101
+ const [overviewResult, overTimeResult] = await Promise.all([
102
+ api.Task.getTaskOverview(params),
103
+ api.Task.getTasksOverTime({
104
+ start: params.start,
105
+ end: params.end,
106
+ bucketSizeMs: getBucketSizeMs(this.selectedRange)
107
+ })
108
+ ]);
109
+
110
+ this.statusCounts = overviewResult.statusCounts || this.statusCounts;
111
+ this.tasksByName = overviewResult.tasksByName || [];
112
+ this.overTimeBuckets = overTimeResult || [];
113
+ if (this.overTimeBuckets.length === 0) {
114
+ this.showOverTimeChart = false;
115
+ this.destroyOverTimeChart();
116
+ } else {
117
+ this.showOverTimeChart = true;
118
+ await this.$nextTick();
119
+ this.renderOverTimeChart();
120
+ }
121
+ },
122
+
123
+ /** Build or update the stacked bar chart showing tasks over time. */
124
+ renderOverTimeChart() {
125
+ const Chart = typeof window !== 'undefined' && window.Chart;
126
+ if (!Chart) {
127
+ throw new Error('Chart.js not found');
128
+ }
129
+ const canvas = this.$refs.overTimeChart;
130
+ if (!canvas || typeof canvas.getContext !== 'function') return;
131
+
132
+ const buckets = this.overTimeBuckets;
133
+ const labels = buckets.map(b => formatBucketLabel(b.timestamp, this.selectedRange));
134
+ const succeeded = buckets.map(b => b.succeeded || 0);
135
+ const failed = buckets.map(b => b.failed || 0);
136
+ const cancelled = buckets.map(b => b.cancelled || 0);
137
+
138
+ const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
139
+ const tickColor = isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)';
140
+ const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
141
+
142
+ const chartData = {
143
+ labels,
144
+ datasets: [
145
+ { label: 'Succeeded', data: succeeded, backgroundColor: '#22c55e', stack: 'tasks' },
146
+ { label: 'Failed', data: failed, backgroundColor: '#ef4444', stack: 'tasks' },
147
+ { label: 'Cancelled', data: cancelled, backgroundColor: '#6b7280', stack: 'tasks' }
148
+ ]
149
+ };
150
+
151
+ if (this.overTimeChart) {
152
+ try {
153
+ this.overTimeChart.data.labels = labels;
154
+ this.overTimeChart.data.datasets[0].data = succeeded;
155
+ this.overTimeChart.data.datasets[1].data = failed;
156
+ this.overTimeChart.data.datasets[2].data = cancelled;
157
+ this.overTimeChart.update('none');
158
+ } finally {
159
+ this.destroyOverTimeChart();
160
+ }
161
+ }
162
+
163
+ this.overTimeChart = new Chart(canvas, {
164
+ type: 'bar',
165
+ data: chartData,
166
+ options: {
167
+ responsive: true,
168
+ maintainAspectRatio: false,
169
+ animation: false,
170
+ scales: {
171
+ x: {
172
+ stacked: true,
173
+ ticks: { color: tickColor, maxRotation: 45, minRotation: 0 },
174
+ grid: { color: gridColor }
175
+ },
176
+ y: {
177
+ stacked: true,
178
+ beginAtZero: true,
179
+ ticks: { color: tickColor, precision: 0 },
180
+ grid: { color: gridColor }
181
+ }
182
+ },
183
+ plugins: {
184
+ legend: {
185
+ display: true,
186
+ position: 'top',
187
+ labels: { color: tickColor }
188
+ },
189
+ tooltip: { mode: 'index', intersect: false }
190
+ }
191
+ }
192
+ });
193
+ },
194
+
195
+ destroyOverTimeChart() {
196
+ if (this.overTimeChart) {
197
+ this.overTimeChart.destroy();
198
+ this.overTimeChart = null;
199
+ }
59
200
  },
60
201
  openTaskGroupDetails(group) {
61
202
  const query = { dateRange: this.selectedRange || 'last_hour' };
@@ -231,10 +372,22 @@ module.exports = app => app.component('tasks', {
231
372
  }
232
373
  },
233
374
  mounted: async function() {
375
+ // Load initial data while showing the loader state.
234
376
  await this.updateDateRange();
235
- await this.getTasks();
377
+
378
+ // Once data is loaded, switch to the main view.
236
379
  this.status = 'loaded';
380
+ await this.$nextTick();
381
+
382
+ // Ensure the chart renders now that the canvas exists in the DOM.
383
+ if (this.showOverTimeChart && this.overTimeBuckets.length > 0) {
384
+ this.renderOverTimeChart();
385
+ }
386
+
237
387
  this.setDefaultCreateTaskValues();
238
388
  },
389
+ beforeUnmount() {
390
+ this.destroyOverTimeChart();
391
+ },
239
392
  template: template
240
393
  });
package/local.js CHANGED
@@ -29,7 +29,8 @@ async function run() {
29
29
  __watch: process.env.WATCH,
30
30
  _mothershipUrl: 'http://localhost:7777/.netlify/functions',
31
31
  apiKey: 'TACO',
32
- openAIAPIKey: process.env.OPENAI_API_KEY
32
+ openAIAPIKey: process.env.OPENAI_API_KEY,
33
+ googleGeminiAPIKey: process.env.GEMINI_API_KEY
33
34
  })
34
35
  );
35
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {
@@ -25,6 +25,7 @@
25
25
  "vue": "3.x",
26
26
  "vue-toastification": "^2.0.0-rc.5",
27
27
  "webpack": "5.x",
28
+ "time-commando": "1.0.1",
28
29
  "xss": "^1.0.15"
29
30
  },
30
31
  "peerDependencies": {