@mongoosejs/studio 0.2.12 → 0.3.0

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 (92) hide show
  1. package/backend/actions/ChatMessage/executeScript.js +5 -1
  2. package/backend/actions/ChatThread/createChatMessage.js +2 -1
  3. package/backend/actions/ChatThread/streamChatMessage.js +2 -2
  4. package/backend/actions/Model/getEstimatedDocumentCounts.js +38 -0
  5. package/backend/actions/Model/index.js +1 -0
  6. package/backend/actions/Model/streamDocumentChanges.js +8 -7
  7. package/backend/actions/Task/getTasks.js +9 -6
  8. package/backend/authorize.js +1 -0
  9. package/backend/index.js +11 -3
  10. package/eslint.config.js +5 -1
  11. package/express.js +1 -0
  12. package/frontend/public/app.js +25235 -662
  13. package/frontend/public/dark-theme.css +365 -0
  14. package/frontend/public/images/mongoose-studio.svg +4 -0
  15. package/frontend/public/index.html +21 -1
  16. package/frontend/public/style.css +5 -7
  17. package/frontend/public/theme-variables.css +294 -0
  18. package/frontend/public/tw.css +461 -239
  19. package/frontend/src/ace-editor/ace-editor.html +4 -0
  20. package/frontend/src/ace-editor/ace-editor.js +89 -0
  21. package/frontend/src/aceEditor.js +69 -0
  22. package/frontend/src/api.js +6 -0
  23. package/frontend/src/chat/chat-message/chat-message.html +1 -1
  24. package/frontend/src/chat/chat-message/chat-message.js +1 -1
  25. package/frontend/src/chat/chat-message-script/chat-message-script.html +51 -34
  26. package/frontend/src/chat/chat-message-script/chat-message-script.js +12 -55
  27. package/frontend/src/chat/chat.html +72 -37
  28. package/frontend/src/chat/chat.js +26 -2
  29. package/frontend/src/clone-document/clone-document.html +7 -2
  30. package/frontend/src/clone-document/clone-document.js +1 -8
  31. package/frontend/src/create-dashboard/create-dashboard.html +11 -6
  32. package/frontend/src/create-dashboard/create-dashboard.js +0 -7
  33. package/frontend/src/create-document/create-document.html +15 -9
  34. package/frontend/src/create-document/create-document.js +5 -12
  35. package/frontend/src/dashboard/dashboard.html +14 -12
  36. package/frontend/src/dashboard/dashboard.js +12 -4
  37. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.html +13 -7
  38. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +13 -21
  39. package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html +19 -17
  40. package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js +97 -2
  41. package/frontend/src/dashboard-result/dashboard-map/dashboard-map.js +27 -3
  42. package/frontend/src/dashboard-result/dashboard-result.html +3 -3
  43. package/frontend/src/dashboards/dashboards.html +101 -109
  44. package/frontend/src/dashboards/dashboards.js +25 -1
  45. package/frontend/src/detail-default/detail-default.html +2 -2
  46. package/frontend/src/detail-default/detail-default.js +24 -3
  47. package/frontend/src/document/confirm-changes/confirm-changes.html +1 -1
  48. package/frontend/src/document/confirm-delete/confirm-delete.html +1 -1
  49. package/frontend/src/document/document.css +1 -1
  50. package/frontend/src/document/document.html +53 -27
  51. package/frontend/src/document/document.js +27 -1
  52. package/frontend/src/document/execute-script/execute-script.html +20 -21
  53. package/frontend/src/document/execute-script/execute-script.js +1 -43
  54. package/frontend/src/document-details/document-details.css +4 -9
  55. package/frontend/src/document-details/document-details.html +34 -33
  56. package/frontend/src/document-details/document-details.js +2 -53
  57. package/frontend/src/document-details/document-property/document-property.html +12 -12
  58. package/frontend/src/edit-array/edit-array.html +7 -6
  59. package/frontend/src/edit-array/edit-array.js +10 -50
  60. package/frontend/src/edit-boolean/edit-boolean.html +12 -12
  61. package/frontend/src/edit-date/edit-date.html +2 -2
  62. package/frontend/src/edit-default/edit-default.html +1 -1
  63. package/frontend/src/edit-string/edit-string.html +3 -3
  64. package/frontend/src/edit-subdocument/edit-subdocument.html +5 -3
  65. package/frontend/src/edit-subdocument/edit-subdocument.js +1 -15
  66. package/frontend/src/export-query-results/export-query-results.html +3 -3
  67. package/frontend/src/json-node/json-node.html +3 -3
  68. package/frontend/src/list-json/json-node.html +1 -1
  69. package/frontend/src/models/document-search/document-search.html +3 -3
  70. package/frontend/src/models/model-switcher/model-switcher.html +53 -0
  71. package/frontend/src/models/model-switcher/model-switcher.js +123 -0
  72. package/frontend/src/models/models.css +3 -10
  73. package/frontend/src/models/models.html +146 -74
  74. package/frontend/src/models/models.js +142 -4
  75. package/frontend/src/navbar/navbar.html +157 -97
  76. package/frontend/src/navbar/navbar.js +32 -13
  77. package/frontend/src/routes.js +20 -4
  78. package/frontend/src/splash/splash.html +5 -5
  79. package/frontend/src/task-by-name/task-by-name.html +15 -0
  80. package/frontend/src/task-by-name/task-by-name.js +78 -0
  81. package/frontend/src/task-single/task-single.html +157 -0
  82. package/frontend/src/task-single/task-single.js +116 -0
  83. package/frontend/src/tasks/task-details/task-details.html +124 -73
  84. package/frontend/src/tasks/task-details/task-details.js +166 -10
  85. package/frontend/src/tasks/tasks.html +37 -48
  86. package/frontend/src/tasks/tasks.js +11 -50
  87. package/frontend/src/team/new-invitation/new-invitation.html +8 -8
  88. package/frontend/src/team/team.html +27 -27
  89. package/frontend/src/update-document/update-document.html +7 -2
  90. package/frontend/src/update-document/update-document.js +2 -11
  91. package/package.json +3 -1
  92. package/tailwind.config.js +75 -11
@@ -1,77 +1,128 @@
1
1
  <div class="p-4 space-y-6">
2
2
  <div class="flex items-center justify-between">
3
3
  <div>
4
- <button @click="$emit('back')" class="text-gray-500 hover:text-gray-700 mb-2">
4
+ <button @click="goBack" class="text-content-tertiary hover:text-content-secondary mb-2">
5
5
  <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
6
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
7
7
  </svg>
8
- Back to Task Groups
8
+ {{ backLabel }}
9
9
  </button>
10
- <h1 class="text-2xl font-bold text-gray-700">{{ taskGroup.name }}</h1>
11
- <p class="text-gray-500">Total: {{ taskGroup.totalCount }} tasks</p>
10
+ <h1 class="text-2xl font-bold text-content-secondary">{{ taskGroup.name }}</h1>
11
+ <p class="text-content-tertiary">Total: {{ taskGroup.totalCount }} tasks</p>
12
12
  </div>
13
13
 
14
14
  </div>
15
15
 
16
16
  <!-- Status Summary -->
17
- <div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
18
- <button
19
- @click="filterByStatus('pending')"
20
- class="bg-yellow-50 border border-yellow-200 rounded-md p-3 text-center hover:bg-yellow-100 transition-colors cursor-pointer"
21
- :class="{ 'ring-2 ring-yellow-400': currentFilter === 'pending' }"
22
- >
23
- <div class="text-xs text-yellow-600 font-medium">Pending</div>
24
- <div class="text-lg font-bold text-yellow-700">{{ taskGroup.statusCounts.pending || 0 }}</div>
25
- </button>
26
- <button
27
- @click="filterByStatus('succeeded')"
28
- class="bg-green-50 border border-green-200 rounded-md p-3 text-center hover:bg-green-100 transition-colors cursor-pointer"
29
- :class="{ 'ring-2 ring-green-400': currentFilter === 'succeeded' }"
30
- >
31
- <div class="text-xs text-green-600 font-medium">Succeeded</div>
32
- <div class="text-lg font-bold text-green-700">{{ taskGroup.statusCounts.succeeded || 0 }}</div>
33
- </button>
34
- <button
35
- @click="filterByStatus('failed')"
36
- class="bg-red-50 border border-red-200 rounded-md p-3 text-center hover:bg-red-100 transition-colors cursor-pointer"
37
- :class="{ 'ring-2 ring-red-400': currentFilter === 'failed' }"
38
- >
39
- <div class="text-xs text-red-600 font-medium">Failed</div>
40
- <div class="text-lg font-bold text-red-700">{{ taskGroup.statusCounts.failed || 0 }}</div>
41
- </button>
42
- <button
43
- @click="filterByStatus('cancelled')"
44
- class="bg-gray-50 border border-gray-200 rounded-md p-3 text-center hover:bg-gray-100 transition-colors cursor-pointer"
45
- :class="{ 'ring-2 ring-gray-400': currentFilter === 'cancelled' }"
46
- >
47
- <div class="text-xs text-gray-600 font-medium">Cancelled</div>
48
- <div class="text-lg font-bold text-gray-700">{{ taskGroup.statusCounts.cancelled || 0 }}</div>
49
- </button>
50
- </div>
51
-
52
- <!-- Task List -->
53
- <div class="bg-white rounded-lg shadow">
54
- <div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
55
- <h2 class="text-lg font-semibold text-gray-700">
56
- Individual Tasks
57
- <span v-if="currentFilter" class="text-sm font-normal text-gray-500 ml-2">
58
- (Filtered by {{ currentFilter }})
59
- </span>
60
- </h2>
17
+ <div class="space-y-3">
18
+ <div class="flex items-center justify-between">
19
+ <span class="text-sm font-medium text-content-secondary">Status</span>
20
+ <div class="flex rounded-md shadow-sm" role="group">
21
+ <button
22
+ type="button"
23
+ @click="statusView = 'summary'"
24
+ class="px-3 py-1.5 text-sm font-medium rounded-l-md border transition-colors"
25
+ :class="statusView === 'summary' ? 'bg-primary text-primary-text border-primary' : 'bg-surface text-content-secondary border-edge-strong hover:bg-page'"
26
+ >
27
+ Summary
28
+ </button>
29
+ <button
30
+ type="button"
31
+ @click="statusView = 'chart'"
32
+ class="px-3 py-1.5 text-sm font-medium rounded-r-md border border-l-0 transition-colors"
33
+ :class="statusView === 'chart' ? 'bg-primary text-primary-text border-primary' : 'bg-surface text-content-secondary border-edge-strong hover:bg-page'"
34
+ >
35
+ Chart
36
+ </button>
37
+ </div>
38
+ </div>
39
+ <!-- Summary view -->
40
+ <div v-show="statusView === 'summary'" class="grid grid-cols-2 sm:grid-cols-4 gap-4">
41
+ <button
42
+ @click="filterByStatus('pending')"
43
+ class="bg-yellow-50 border border-yellow-200 rounded-md p-3 text-center hover:bg-yellow-100 transition-colors cursor-pointer"
44
+ :class="{ 'ring-2 ring-yellow-400': currentFilter === 'pending' }"
45
+ >
46
+ <div class="text-xs text-yellow-600 font-medium">Pending</div>
47
+ <div class="text-lg font-bold text-yellow-700">{{ taskGroup.statusCounts.pending || 0 }}</div>
48
+ </button>
49
+ <button
50
+ @click="filterByStatus('succeeded')"
51
+ class="bg-green-50 border border-green-200 rounded-md p-3 text-center hover:bg-green-100 transition-colors cursor-pointer"
52
+ :class="{ 'ring-2 ring-green-400': currentFilter === 'succeeded' }"
53
+ >
54
+ <div class="text-xs text-green-600 font-medium">Succeeded</div>
55
+ <div class="text-lg font-bold text-green-700">{{ taskGroup.statusCounts.succeeded || 0 }}</div>
56
+ </button>
61
57
  <button
62
- v-if="currentFilter"
63
- @click="clearFilter"
64
- class="text-sm text-ultramarine-600 hover:text-ultramarine-700 font-medium"
58
+ @click="filterByStatus('failed')"
59
+ class="bg-red-50 border border-red-200 rounded-md p-3 text-center hover:bg-red-100 transition-colors cursor-pointer"
60
+ :class="{ 'ring-2 ring-red-400': currentFilter === 'failed' }"
65
61
  >
66
- Show All
62
+ <div class="text-xs text-red-600 font-medium">Failed</div>
63
+ <div class="text-lg font-bold text-red-700">{{ taskGroup.statusCounts.failed || 0 }}</div>
64
+ </button>
65
+ <button
66
+ @click="filterByStatus('cancelled')"
67
+ class="bg-page border border-edge rounded-md p-3 text-center hover:bg-muted transition-colors cursor-pointer"
68
+ :class="{ 'ring-2 ring-gray-400': currentFilter === 'cancelled' }"
69
+ >
70
+ <div class="text-xs text-gray-600 font-medium">Cancelled</div>
71
+ <div class="text-lg font-bold text-content-secondary">{{ taskGroup.statusCounts.cancelled || 0 }}</div>
67
72
  </button>
68
73
  </div>
74
+ <!-- Chart view -->
75
+ <div v-show="statusView === 'chart'" class="flex flex-col items-center justify-center bg-surface border border-edge rounded-lg p-4 gap-3" style="min-height: 280px;">
76
+ <div v-if="taskGroup.totalCount > 0" class="w-[240px] h-[240px] shrink-0">
77
+ <canvas ref="statusPieChart" width="240" height="240" class="block"></canvas>
78
+ </div>
79
+ <p v-else class="text-content-tertiary text-sm py-8">No tasks to display</p>
80
+ <!-- Selection labels: show which segment is selected (click to filter) -->
81
+ <div v-if="taskGroup.totalCount > 0" class="flex flex-wrap justify-center gap-2">
82
+ <button
83
+ v-for="status in statusOrderForDisplay"
84
+ :key="status"
85
+ type="button"
86
+ class="text-xs px-2 py-1 rounded-full font-medium transition-all cursor-pointer"
87
+ :class="currentFilter === status ? getStatusPillClass(status) : 'bg-muted text-content-tertiary hover:bg-muted'"
88
+ @click="filterByStatus(status)"
89
+ >
90
+ {{ statusLabel(status) }}
91
+ </button>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Task List -->
97
+ <div class="bg-surface rounded-lg shadow">
98
+ <div class="px-6 py-6 border-b border-edge flex items-center justify-between bg-page">
99
+ <h2 class="text-xl font-bold text-content">
100
+ Individual Tasks
101
+ <span v-if="currentFilter" class="ml-3 text-base font-semibold text-primary">
102
+ (Filtered by {{ currentFilter }})
103
+ </span>
104
+ </h2>
105
+ <button
106
+ v-if="currentFilter"
107
+ @click="clearFilter"
108
+ class="text-sm font-semibold text-primary hover:text-primary"
109
+ >
110
+ Show All
111
+ </button>
112
+ </div>
69
113
  <div class="divide-y divide-gray-200">
70
114
  <div v-for="task in sortedTasks" :key="task.id" class="p-6">
71
115
  <div class="flex items-start justify-between">
72
116
  <div class="flex-1">
73
117
  <div class="flex items-center gap-3 mb-2">
74
- <span class="text-sm font-medium text-gray-900">Task ID: {{ task.id }}</span>
118
+ <span class="text-sm font-medium text-content">Task ID: {{ task.id }}</span>
119
+ <router-link
120
+ v-if="backTo"
121
+ :to="taskDetailRoute(task)"
122
+ class="text-sm text-primary hover:text-primary font-medium"
123
+ >
124
+ View details
125
+ </router-link>
75
126
  <span
76
127
  class="text-xs px-2 py-1 rounded-full font-medium"
77
128
  :class="getStatusColor(task.status)"
@@ -82,27 +133,27 @@
82
133
 
83
134
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
84
135
  <div>
85
- <label class="block text-sm font-medium text-gray-700 mb-1">Scheduled At</label>
86
- <div class="text-sm text-gray-900">{{ formatDate(task.scheduledAt) }}</div>
136
+ <label class="block text-sm font-medium text-content-secondary mb-1">Scheduled At</label>
137
+ <div class="text-sm text-content">{{ formatDate(task.scheduledAt) }}</div>
87
138
  </div>
88
139
  <div v-if="task.startedAt">
89
- <label class="block text-sm font-medium text-gray-700 mb-1">Started At</label>
90
- <div class="text-sm text-gray-900">{{ formatDate(task.startedAt) }}</div>
140
+ <label class="block text-sm font-medium text-content-secondary mb-1">Started At</label>
141
+ <div class="text-sm text-content">{{ formatDate(task.startedAt) }}</div>
91
142
  </div>
92
143
  <div v-if="task.completedAt">
93
- <label class="block text-sm font-medium text-gray-700 mb-1">Completed At</label>
94
- <div class="text-sm text-gray-900">{{ formatDate(task.completedAt) }}</div>
144
+ <label class="block text-sm font-medium text-content-secondary mb-1">Completed At</label>
145
+ <div class="text-sm text-content">{{ formatDate(task.completedAt) }}</div>
95
146
  </div>
96
147
  <div v-if="task.error">
97
- <label class="block text-sm font-medium text-gray-700 mb-1">Error</label>
148
+ <label class="block text-sm font-medium text-content-secondary mb-1">Error</label>
98
149
  <div class="text-sm text-red-600">{{ task.error }}</div>
99
150
  </div>
100
151
  </div>
101
152
 
102
153
  <!-- Task Parameters -->
103
154
  <div v-if="task.parameters && Object.keys(task.parameters).length > 0">
104
- <label class="block text-sm font-medium text-gray-700 mb-2">Parameters</label>
105
- <div class="bg-gray-50 rounded-md p-3">
155
+ <label class="block text-sm font-medium text-content-secondary mb-2">Parameters</label>
156
+ <div class="bg-page rounded-md p-3">
106
157
  <pre class="text-sm text-gray-800 whitespace-pre-wrap">{{ JSON.stringify(task.parameters, null, 2) }}</pre>
107
158
  </div>
108
159
  </div>
@@ -157,26 +208,26 @@
157
208
  </svg>
158
209
  </div>
159
210
  <div class="ml-3">
160
- <h3 class="text-lg font-medium text-gray-900">Reschedule Task</h3>
211
+ <h3 class="text-lg font-medium text-content">Reschedule Task</h3>
161
212
  </div>
162
213
  </div>
163
214
  <div class="mb-4">
164
215
  <p class="text-sm text-gray-600">
165
216
  Are you sure you want to reschedule task <strong>{{ selectedTask?.id }}</strong>?
166
217
  </p>
167
- <p class="text-sm text-gray-500 mt-2">
218
+ <p class="text-sm text-content-tertiary mt-2">
168
219
  This will reset the task's status and schedule it to run again.
169
220
  </p>
170
221
 
171
222
  <div class="mt-4">
172
- <label for="newScheduledTime" class="block text-sm font-medium text-gray-700 mb-2">
223
+ <label for="newScheduledTime" class="block text-sm font-medium text-content-secondary mb-2">
173
224
  New Scheduled Time
174
225
  </label>
175
226
  <input
176
227
  id="newScheduledTime"
177
228
  v-model="newScheduledTime"
178
229
  type="datetime-local"
179
- class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
230
+ class="w-full px-3 py-2 border border-edge-strong rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
180
231
  required
181
232
  />
182
233
  </div>
@@ -190,7 +241,7 @@
190
241
  </button>
191
242
  <button
192
243
  @click="showRescheduleModal = false"
193
- class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium"
244
+ class="flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium"
194
245
  >
195
246
  Cancel
196
247
  </button>
@@ -211,14 +262,14 @@
211
262
  </svg>
212
263
  </div>
213
264
  <div class="ml-3">
214
- <h3 class="text-lg font-medium text-gray-900">Run Task Now</h3>
265
+ <h3 class="text-lg font-medium text-content">Run Task Now</h3>
215
266
  </div>
216
267
  </div>
217
268
  <div class="mb-4">
218
269
  <p class="text-sm text-gray-600">
219
270
  Are you sure you want to run task <strong>{{ selectedTask?.id }}</strong> immediately?
220
271
  </p>
221
- <p class="text-sm text-gray-500 mt-2">
272
+ <p class="text-sm text-content-tertiary mt-2">
222
273
  This will execute the task right away, bypassing its scheduled time.
223
274
  </p>
224
275
  </div>
@@ -231,7 +282,7 @@
231
282
  </button>
232
283
  <button
233
284
  @click="showRunModal = false"
234
- class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium"
285
+ class="flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium"
235
286
  >
236
287
  Cancel
237
288
  </button>
@@ -252,14 +303,14 @@
252
303
  </svg>
253
304
  </div>
254
305
  <div class="ml-3">
255
- <h3 class="text-lg font-medium text-gray-900">Cancel Task</h3>
306
+ <h3 class="text-lg font-medium text-content">Cancel Task</h3>
256
307
  </div>
257
308
  </div>
258
309
  <div class="mb-4">
259
310
  <p class="text-sm text-gray-600">
260
311
  Are you sure you want to cancel task <strong>{{ selectedTask?.id }}</strong>?
261
312
  </p>
262
- <p class="text-sm text-gray-500 mt-2">
313
+ <p class="text-sm text-content-tertiary mt-2">
263
314
  This will permanently cancel the task and it cannot be undone.
264
315
  </p>
265
316
  </div>
@@ -272,7 +323,7 @@
272
323
  </button>
273
324
  <button
274
325
  @click="showCancelModal = false"
275
- class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium"
326
+ class="flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium"
276
327
  >
277
328
  Keep Task
278
329
  </button>
@@ -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,128 @@ 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;
59
+ }
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
+ }
29
83
  }
30
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
+ const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
121
+ const legendColor = isDark
122
+ ? (getComputedStyle(document.documentElement).getPropertyValue('--studio-text-primary')?.trim() || 'rgba(255,255,255,0.9)')
123
+ : undefined;
124
+ this.statusChart = new Chart(canvas, {
125
+ type: 'doughnut',
126
+ data,
127
+ options: {
128
+ responsive: false,
129
+ maintainAspectRatio: false,
130
+ animation: false,
131
+ layout: {
132
+ padding: 8
133
+ },
134
+ onClick: (_evt, elements) => {
135
+ if (elements && elements.length > 0) {
136
+ const status = STATUS_ORDER[elements[0].index];
137
+ this.$nextTick(() => this.filterByStatus(status));
138
+ }
139
+ },
140
+ plugins: {
141
+ legend: {
142
+ display: true,
143
+ position: 'bottom',
144
+ ...(legendColor && { labels: { color: legendColor } })
145
+ }
146
+ }
147
+ }
148
+ });
149
+ } catch (_) {
150
+ this.statusChart = null;
151
+ }
152
+ },
153
+ statusLabel(status) {
154
+ return status.charAt(0).toUpperCase() + status.slice(1).replace('_', ' ');
155
+ },
156
+ getStatusPillClass(status) {
157
+ const classes = {
158
+ pending: 'bg-yellow-200 text-yellow-900 ring-2 ring-yellow-500',
159
+ succeeded: 'bg-green-200 text-green-900 ring-2 ring-green-500',
160
+ failed: 'bg-red-200 text-red-900 ring-2 ring-red-500',
161
+ cancelled: 'bg-gray-200 text-gray-900 ring-2 ring-gray-500'
162
+ };
163
+ return classes[status] || 'bg-slate-200 text-slate-900 ring-2 ring-slate-500';
164
+ },
32
165
  getStatusColor(status) {
33
166
  if (status === 'succeeded') {
34
167
  return 'bg-green-100 text-green-800';
@@ -65,15 +198,35 @@ module.exports = app => app.component('task-details', {
65
198
  this.$emit('task-cancelled');
66
199
  },
67
200
  filterByStatus(status) {
68
- // If clicking the same status, clear the filter
69
- if (this.currentFilter === status) {
70
- this.$emit('update:currentFilter', null);
201
+ const next = this.currentFilter === status ? null : status;
202
+ this.currentFilter = next;
203
+ const query = { ...this.$route.query };
204
+ if (next) query.status = next;
205
+ else delete query.status;
206
+ this.$router.replace({ path: this.$route.path, query });
207
+ },
208
+ clearFilter() {
209
+ this.currentFilter = null;
210
+ const query = { ...this.$route.query };
211
+ delete query.status;
212
+ this.$router.replace({ path: this.$route.path, query });
213
+ },
214
+ goBack() {
215
+ if (this.backTo) {
216
+ if (window.history.length > 1) {
217
+ window.history.back();
218
+ } else {
219
+ this.$router.push(this.backTo);
220
+ }
71
221
  } else {
72
- this.$emit('update:currentFilter', status);
222
+ this.$emit('back');
73
223
  }
74
224
  },
75
- clearFilter() {
76
- this.$emit('update:currentFilter', null);
225
+ taskDetailRoute(task) {
226
+ const id = String(task.id || task._id);
227
+ const path = `/tasks/${encodeURIComponent(this.taskGroup.name || '')}/${id}`;
228
+ const query = this.currentFilter ? { status: this.currentFilter } : {};
229
+ return { path, query };
77
230
  },
78
231
  showRescheduleConfirmation(task) {
79
232
  this.selectedTask = task;
@@ -173,10 +326,13 @@ module.exports = app => app.component('task-details', {
173
326
 
174
327
  },
175
328
  mounted() {
176
- // Check if the task group was already filtered when passed from parent
177
329
  if (this.taskGroup.filteredStatus && !this.currentFilter) {
178
- this.$emit('update:currentFilter', this.taskGroup.filteredStatus);
330
+ this.currentFilter = this.taskGroup.filteredStatus;
331
+ this.$router.replace({ path: this.$route.path, query: { ...this.$route.query, status: this.taskGroup.filteredStatus } });
179
332
  }
180
333
  },
334
+ beforeUnmount() {
335
+ this.destroyStatusChart();
336
+ },
181
337
  template: template
182
338
  });