@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.
@@ -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: ['taskGroup', 'currentFilter'],
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
- // If clicking the same status, clear the filter
69
- if (this.currentFilter === status) {
70
- this.$emit('update:currentFilter', null);
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('update:currentFilter', status);
217
+ this.$emit('back');
73
218
  }
74
219
  },
75
- clearFilter() {
76
- this.$emit('update:currentFilter', null);
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.$emit('update:currentFilter', this.taskGroup.filteredStatus);
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
- <!-- Task Details View -->
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: 'today',
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.selectedTaskGroup = group;
74
- this.showTaskDetails = true;
70
+ this.$router.push({ path: `/tasks/${encodeURIComponent(group.name || '')}` });
75
71
  },
76
72
  openTaskGroupDetailsWithFilter(group, status) {
77
- // Create a filtered version of the task group with only the specified status
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.12",
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
  },