@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.
- package/backend/actions/ChatThread/createChatMessage.js +12 -2
- package/backend/actions/ChatThread/streamChatMessage.js +12 -2
- package/backend/actions/Model/createChatMessage.js +12 -2
- package/backend/actions/Model/streamChatMessage.js +13 -2
- package/backend/actions/Task/getTasksOverTime.js +66 -0
- package/backend/actions/Task/index.js +1 -0
- package/frontend/public/app.js +809 -110
- package/frontend/public/tw.css +19 -16
- package/frontend/src/api.js +6 -0
- package/frontend/src/chat/chat.html +19 -12
- package/frontend/src/chat/chat.js +9 -3
- package/frontend/src/create-document/create-document.js +3 -1
- package/frontend/src/dashboard-result/dashboard-primitive/dashboard-primitive.html +2 -2
- package/frontend/src/dashboard-result/dashboard-primitive/dashboard-primitive.js +13 -1
- package/frontend/src/dashboard-result/dashboard-result.html +1 -1
- package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +21 -1
- package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +52 -0
- package/frontend/src/detail-date/detail-date.html +1 -0
- package/frontend/src/detail-date/detail-date.js +123 -0
- package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html +26 -0
- package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js +41 -0
- package/frontend/src/document-details/document-property/document-property.html +13 -5
- package/frontend/src/document-details/document-property/document-property.js +14 -1
- package/frontend/src/getCurrentDateTimeContext.js +17 -0
- package/frontend/src/list-json/list-json.js +0 -7
- package/frontend/src/modal/modal.js +25 -1
- package/frontend/src/models/document-search/document-search.js +3 -0
- package/frontend/src/models/models.html +25 -5
- package/frontend/src/models/models.js +1 -1
- package/frontend/src/navbar/navbar.html +6 -1
- package/frontend/src/navbar/navbar.js +1 -6
- package/frontend/src/pro-upgrade-modal/pro-upgrade-modal.html +38 -0
- package/frontend/src/pro-upgrade-modal/pro-upgrade-modal.js +23 -0
- package/frontend/src/tasks/tasks.html +34 -20
- package/frontend/src/tasks/tasks.js +158 -5
- package/local.js +2 -1
- 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
|
});
|
|
@@ -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
|
-
<
|
|
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
|
-
|
|
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
|
|
451
|
-
</
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
87
|
-
<
|
|
88
|
-
<div class="flex items-
|
|
89
|
-
<div class="flex-1 cursor-pointer" @click="openTaskGroupDetails(group)">
|
|
90
|
-
<div class="flex items-center gap-
|
|
91
|
-
<div class="font-medium text-
|
|
92
|
-
<svg class="w-
|
|
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-
|
|
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-
|
|
102
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
148
|
+
<div class="text-base font-bold text-content-secondary">{{ group.statusCounts.cancelled || 0 }}</div>
|
|
135
149
|
</button>
|
|
136
150
|
</div>
|
|
137
|
-
</
|
|
138
|
-
</
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|