@mongoosejs/studio 0.2.12 → 0.2.13
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/Model/getEstimatedDocumentCounts.js +38 -0
- package/backend/actions/Model/index.js +1 -0
- package/backend/actions/Model/streamDocumentChanges.js +8 -7
- package/backend/actions/Task/getTasks.js +9 -6
- package/backend/authorize.js +1 -0
- package/backend/index.js +11 -3
- package/eslint.config.js +2 -1
- package/express.js +1 -0
- package/frontend/public/app.js +535 -61
- package/frontend/public/tw.css +181 -12
- package/frontend/src/api.js +6 -0
- package/frontend/src/chat/chat.html +13 -7
- package/frontend/src/document/document.html +26 -0
- package/frontend/src/document/document.js +27 -1
- package/frontend/src/models/models.html +8 -2
- package/frontend/src/models/models.js +34 -0
- package/frontend/src/navbar/navbar.js +1 -1
- package/frontend/src/routes.js +20 -4
- package/frontend/src/task-by-name/task-by-name.html +15 -0
- package/frontend/src/task-by-name/task-by-name.js +78 -0
- package/frontend/src/task-single/task-single.html +157 -0
- package/frontend/src/task-single/task-single.js +116 -0
- package/frontend/src/tasks/task-details/task-details.html +101 -50
- package/frontend/src/tasks/task-details/task-details.js +161 -10
- package/frontend/src/tasks/tasks.html +1 -13
- package/frontend/src/tasks/tasks.js +9 -25
- package/package.json +2 -1
|
@@ -3,16 +3,30 @@
|
|
|
3
3
|
const template = require('./task-details.html');
|
|
4
4
|
const api = require('../../api');
|
|
5
5
|
|
|
6
|
+
const STATUS_ORDER = ['pending', 'succeeded', 'failed', 'cancelled'];
|
|
7
|
+
const PIE_COLORS = ['#eab308', '#22c55e', '#ef4444', '#6b7280'];
|
|
8
|
+
const PIE_HOVER = ['#ca8a04', '#16a34a', '#dc2626', '#4b5563'];
|
|
9
|
+
|
|
6
10
|
module.exports = app => app.component('task-details', {
|
|
7
|
-
props:
|
|
11
|
+
props: {
|
|
12
|
+
taskGroup: { type: Object, required: true },
|
|
13
|
+
backTo: { type: Object, default: null }
|
|
14
|
+
},
|
|
8
15
|
data: () => ({
|
|
16
|
+
currentFilter: null,
|
|
9
17
|
showRescheduleModal: false,
|
|
10
18
|
showRunModal: false,
|
|
11
19
|
showCancelModal: false,
|
|
12
20
|
selectedTask: null,
|
|
13
|
-
newScheduledTime: ''
|
|
21
|
+
newScheduledTime: '',
|
|
22
|
+
statusView: 'summary',
|
|
23
|
+
statusChart: null
|
|
14
24
|
}),
|
|
15
25
|
computed: {
|
|
26
|
+
backLabel() {
|
|
27
|
+
if (this.backTo?.path?.startsWith('/tasks/') || this.backTo?.name === 'taskByName') return `Back to ${this.taskGroup?.name || 'tasks'}`;
|
|
28
|
+
return 'Back to Task Groups';
|
|
29
|
+
},
|
|
16
30
|
sortedTasks() {
|
|
17
31
|
let tasks = this.taskGroup.tasks;
|
|
18
32
|
|
|
@@ -26,9 +40,123 @@ module.exports = app => app.component('task-details', {
|
|
|
26
40
|
const dateB = new Date(b.scheduledAt || b.createdAt || 0);
|
|
27
41
|
return dateB - dateA; // Most recent first
|
|
28
42
|
});
|
|
43
|
+
},
|
|
44
|
+
pieChartData() {
|
|
45
|
+
const counts = this.taskGroup?.statusCounts || {};
|
|
46
|
+
return {
|
|
47
|
+
labels: ['Pending', 'Succeeded', 'Failed', 'Cancelled'],
|
|
48
|
+
datasets: [{
|
|
49
|
+
data: STATUS_ORDER.map(s => counts[s] || 0),
|
|
50
|
+
backgroundColor: PIE_COLORS,
|
|
51
|
+
hoverBackgroundColor: PIE_HOVER,
|
|
52
|
+
borderWidth: 2,
|
|
53
|
+
borderColor: '#fff'
|
|
54
|
+
}]
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
statusOrderForDisplay() {
|
|
58
|
+
return STATUS_ORDER;
|
|
29
59
|
}
|
|
30
60
|
},
|
|
61
|
+
watch: {
|
|
62
|
+
'$route.query.status': {
|
|
63
|
+
handler(status) {
|
|
64
|
+
this.currentFilter = status || null;
|
|
65
|
+
},
|
|
66
|
+
immediate: true
|
|
67
|
+
},
|
|
68
|
+
statusView(val) {
|
|
69
|
+
if (val !== 'chart') this.destroyStatusChart();
|
|
70
|
+
else {
|
|
71
|
+
this.$nextTick(() => {
|
|
72
|
+
requestAnimationFrame(() => this.ensureStatusChart());
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
taskGroup: {
|
|
77
|
+
deep: true,
|
|
78
|
+
handler() {
|
|
79
|
+
this.$nextTick(() => {
|
|
80
|
+
requestAnimationFrame(() => this.ensureStatusChart());
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
31
85
|
methods: {
|
|
86
|
+
destroyStatusChart() {
|
|
87
|
+
if (this.statusChart) {
|
|
88
|
+
try {
|
|
89
|
+
this.statusChart.destroy();
|
|
90
|
+
} catch (_) {
|
|
91
|
+
// ignore Chart.js teardown errors
|
|
92
|
+
}
|
|
93
|
+
this.statusChart = null;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
isChartCanvasReady(canvas) {
|
|
97
|
+
return canvas && typeof canvas.getContext === 'function' && canvas.isConnected && canvas.offsetParent != null;
|
|
98
|
+
},
|
|
99
|
+
ensureStatusChart() {
|
|
100
|
+
if (this.statusView !== 'chart' || !this.taskGroup || this.taskGroup.totalCount === 0) {
|
|
101
|
+
this.destroyStatusChart();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const canvas = this.$refs.statusPieChart;
|
|
105
|
+
if (!canvas || !this.isChartCanvasReady(canvas)) return;
|
|
106
|
+
const Chart = typeof window !== 'undefined' && window.Chart;
|
|
107
|
+
if (!Chart) return;
|
|
108
|
+
const data = this.pieChartData;
|
|
109
|
+
if (this.statusChart) {
|
|
110
|
+
try {
|
|
111
|
+
this.statusChart.data.labels = data.labels;
|
|
112
|
+
this.statusChart.data.datasets[0].data = data.datasets[0].data;
|
|
113
|
+
this.statusChart.update('none');
|
|
114
|
+
} catch (_) {
|
|
115
|
+
this.destroyStatusChart();
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
this.statusChart = new Chart(canvas, {
|
|
121
|
+
type: 'doughnut',
|
|
122
|
+
data,
|
|
123
|
+
options: {
|
|
124
|
+
responsive: false,
|
|
125
|
+
maintainAspectRatio: false,
|
|
126
|
+
animation: false,
|
|
127
|
+
layout: {
|
|
128
|
+
padding: 8
|
|
129
|
+
},
|
|
130
|
+
onClick: (_evt, elements) => {
|
|
131
|
+
if (elements && elements.length > 0) {
|
|
132
|
+
const status = STATUS_ORDER[elements[0].index];
|
|
133
|
+
this.$nextTick(() => this.filterByStatus(status));
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
plugins: {
|
|
137
|
+
legend: {
|
|
138
|
+
display: true,
|
|
139
|
+
position: 'bottom'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
} catch (_) {
|
|
145
|
+
this.statusChart = null;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
statusLabel(status) {
|
|
149
|
+
return status.charAt(0).toUpperCase() + status.slice(1).replace('_', ' ');
|
|
150
|
+
},
|
|
151
|
+
getStatusPillClass(status) {
|
|
152
|
+
const classes = {
|
|
153
|
+
pending: 'bg-yellow-200 text-yellow-900 ring-2 ring-yellow-500',
|
|
154
|
+
succeeded: 'bg-green-200 text-green-900 ring-2 ring-green-500',
|
|
155
|
+
failed: 'bg-red-200 text-red-900 ring-2 ring-red-500',
|
|
156
|
+
cancelled: 'bg-gray-200 text-gray-900 ring-2 ring-gray-500'
|
|
157
|
+
};
|
|
158
|
+
return classes[status] || 'bg-slate-200 text-slate-900 ring-2 ring-slate-500';
|
|
159
|
+
},
|
|
32
160
|
getStatusColor(status) {
|
|
33
161
|
if (status === 'succeeded') {
|
|
34
162
|
return 'bg-green-100 text-green-800';
|
|
@@ -65,15 +193,35 @@ module.exports = app => app.component('task-details', {
|
|
|
65
193
|
this.$emit('task-cancelled');
|
|
66
194
|
},
|
|
67
195
|
filterByStatus(status) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
196
|
+
const next = this.currentFilter === status ? null : status;
|
|
197
|
+
this.currentFilter = next;
|
|
198
|
+
const query = { ...this.$route.query };
|
|
199
|
+
if (next) query.status = next;
|
|
200
|
+
else delete query.status;
|
|
201
|
+
this.$router.replace({ path: this.$route.path, query });
|
|
202
|
+
},
|
|
203
|
+
clearFilter() {
|
|
204
|
+
this.currentFilter = null;
|
|
205
|
+
const query = { ...this.$route.query };
|
|
206
|
+
delete query.status;
|
|
207
|
+
this.$router.replace({ path: this.$route.path, query });
|
|
208
|
+
},
|
|
209
|
+
goBack() {
|
|
210
|
+
if (this.backTo) {
|
|
211
|
+
if (window.history.length > 1) {
|
|
212
|
+
window.history.back();
|
|
213
|
+
} else {
|
|
214
|
+
this.$router.push(this.backTo);
|
|
215
|
+
}
|
|
71
216
|
} else {
|
|
72
|
-
this.$emit('
|
|
217
|
+
this.$emit('back');
|
|
73
218
|
}
|
|
74
219
|
},
|
|
75
|
-
|
|
76
|
-
|
|
220
|
+
taskDetailRoute(task) {
|
|
221
|
+
const id = String(task.id || task._id);
|
|
222
|
+
const path = `/tasks/${encodeURIComponent(this.taskGroup.name || '')}/${id}`;
|
|
223
|
+
const query = this.currentFilter ? { status: this.currentFilter } : {};
|
|
224
|
+
return { path, query };
|
|
77
225
|
},
|
|
78
226
|
showRescheduleConfirmation(task) {
|
|
79
227
|
this.selectedTask = task;
|
|
@@ -173,10 +321,13 @@ module.exports = app => app.component('task-details', {
|
|
|
173
321
|
|
|
174
322
|
},
|
|
175
323
|
mounted() {
|
|
176
|
-
// Check if the task group was already filtered when passed from parent
|
|
177
324
|
if (this.taskGroup.filteredStatus && !this.currentFilter) {
|
|
178
|
-
this
|
|
325
|
+
this.currentFilter = this.taskGroup.filteredStatus;
|
|
326
|
+
this.$router.replace({ path: this.$route.path, query: { ...this.$route.query, status: this.taskGroup.filteredStatus } });
|
|
179
327
|
}
|
|
180
328
|
},
|
|
329
|
+
beforeUnmount() {
|
|
330
|
+
this.destroyStatusChart();
|
|
331
|
+
},
|
|
181
332
|
template: template
|
|
182
333
|
});
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
<div class="p-4 space-y-6">
|
|
2
|
-
|
|
3
|
-
<task-details
|
|
4
|
-
v-if="showTaskDetails && selectedTaskGroup"
|
|
5
|
-
:task-group="selectedTaskGroup"
|
|
6
|
-
:current-filter="taskDetailsFilter"
|
|
7
|
-
@back="hideTaskDetails"
|
|
8
|
-
@task-created="onTaskCreated"
|
|
9
|
-
@task-cancelled="onTaskCancelled"
|
|
10
|
-
@update:current-filter="taskDetailsFilter = $event"
|
|
11
|
-
></task-details>
|
|
12
|
-
|
|
13
|
-
<!-- Main Tasks View -->
|
|
14
|
-
<div v-else>
|
|
2
|
+
<div>
|
|
15
3
|
<h1 class="text-2xl font-bold text-gray-700 mb-4">Task Overview</h1>
|
|
16
4
|
<div v-if="status == 'init'">
|
|
17
5
|
<img src="images/loader.gif" />
|
|
@@ -8,11 +8,12 @@ module.exports = app => app.component('tasks', {
|
|
|
8
8
|
status: 'init',
|
|
9
9
|
tasks: [],
|
|
10
10
|
groupedTasks: {},
|
|
11
|
-
selectedRange: '
|
|
11
|
+
selectedRange: 'last_hour',
|
|
12
12
|
start: null,
|
|
13
13
|
end: null,
|
|
14
14
|
dateFilters: [
|
|
15
15
|
{ value: 'all', label: 'All Time' },
|
|
16
|
+
{ value: 'last_hour', label: 'Last Hour' },
|
|
16
17
|
{ value: 'today', label: 'Today' },
|
|
17
18
|
{ value: 'yesterday', label: 'Yesterday' },
|
|
18
19
|
{ value: 'thisWeek', label: 'This Week' },
|
|
@@ -31,10 +32,6 @@ module.exports = app => app.component('tasks', {
|
|
|
31
32
|
],
|
|
32
33
|
searchQuery: '',
|
|
33
34
|
searchTimeout: null,
|
|
34
|
-
// Task details view state
|
|
35
|
-
showTaskDetails: false,
|
|
36
|
-
selectedTaskGroup: null,
|
|
37
|
-
taskDetailsFilter: null,
|
|
38
35
|
// Create task modal state
|
|
39
36
|
showCreateTaskModal: false,
|
|
40
37
|
newTask: {
|
|
@@ -70,28 +67,10 @@ module.exports = app => app.component('tasks', {
|
|
|
70
67
|
this.groupedTasks = groupedTasks;
|
|
71
68
|
},
|
|
72
69
|
openTaskGroupDetails(group) {
|
|
73
|
-
this.
|
|
74
|
-
this.showTaskDetails = true;
|
|
70
|
+
this.$router.push({ path: `/tasks/${encodeURIComponent(group.name || '')}` });
|
|
75
71
|
},
|
|
76
72
|
openTaskGroupDetailsWithFilter(group, status) {
|
|
77
|
-
|
|
78
|
-
const filteredGroup = {
|
|
79
|
-
...group,
|
|
80
|
-
tasks: group.tasks.filter(task => task.status === status),
|
|
81
|
-
filteredStatus: status
|
|
82
|
-
};
|
|
83
|
-
this.selectedTaskGroup = filteredGroup;
|
|
84
|
-
this.taskDetailsFilter = status;
|
|
85
|
-
this.showTaskDetails = true;
|
|
86
|
-
},
|
|
87
|
-
async onTaskCancelled() {
|
|
88
|
-
// Refresh the task data when a task is cancelled
|
|
89
|
-
await this.getTasks();
|
|
90
|
-
},
|
|
91
|
-
hideTaskDetails() {
|
|
92
|
-
this.showTaskDetails = false;
|
|
93
|
-
this.selectedTaskGroup = null;
|
|
94
|
-
this.taskDetailsFilter = null;
|
|
73
|
+
this.$router.push({ path: `/tasks/${encodeURIComponent(group.name || '')}`, query: status ? { status } : {} });
|
|
95
74
|
},
|
|
96
75
|
async onTaskCreated() {
|
|
97
76
|
// Refresh the task data when a new task is created
|
|
@@ -255,6 +234,11 @@ module.exports = app => app.component('tasks', {
|
|
|
255
234
|
let start, end;
|
|
256
235
|
|
|
257
236
|
switch (this.selectedRange) {
|
|
237
|
+
case 'last_hour':
|
|
238
|
+
start = new Date();
|
|
239
|
+
start.setHours(start.getHours() - 1);
|
|
240
|
+
end = new Date();
|
|
241
|
+
break;
|
|
258
242
|
case 'today':
|
|
259
243
|
start = new Date();
|
|
260
244
|
start.setHours(0, 0, 0, 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mongoosejs/studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
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": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"eslint": "9.30.0",
|
|
39
39
|
"express": "4.x",
|
|
40
40
|
"mocha": "10.2.0",
|
|
41
|
+
"mongodb-memory-server": "^11.0.1",
|
|
41
42
|
"mongoose": "9.x",
|
|
42
43
|
"sinon": "^21.0.1"
|
|
43
44
|
},
|