@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.
- package/backend/actions/ChatMessage/executeScript.js +5 -1
- package/backend/actions/ChatThread/createChatMessage.js +2 -1
- package/backend/actions/ChatThread/streamChatMessage.js +2 -2
- 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 +5 -1
- package/express.js +1 -0
- package/frontend/public/app.js +25235 -662
- package/frontend/public/dark-theme.css +365 -0
- package/frontend/public/images/mongoose-studio.svg +4 -0
- package/frontend/public/index.html +21 -1
- package/frontend/public/style.css +5 -7
- package/frontend/public/theme-variables.css +294 -0
- package/frontend/public/tw.css +461 -239
- package/frontend/src/ace-editor/ace-editor.html +4 -0
- package/frontend/src/ace-editor/ace-editor.js +89 -0
- package/frontend/src/aceEditor.js +69 -0
- package/frontend/src/api.js +6 -0
- package/frontend/src/chat/chat-message/chat-message.html +1 -1
- package/frontend/src/chat/chat-message/chat-message.js +1 -1
- package/frontend/src/chat/chat-message-script/chat-message-script.html +51 -34
- package/frontend/src/chat/chat-message-script/chat-message-script.js +12 -55
- package/frontend/src/chat/chat.html +72 -37
- package/frontend/src/chat/chat.js +26 -2
- package/frontend/src/clone-document/clone-document.html +7 -2
- package/frontend/src/clone-document/clone-document.js +1 -8
- package/frontend/src/create-dashboard/create-dashboard.html +11 -6
- package/frontend/src/create-dashboard/create-dashboard.js +0 -7
- package/frontend/src/create-document/create-document.html +15 -9
- package/frontend/src/create-document/create-document.js +5 -12
- package/frontend/src/dashboard/dashboard.html +14 -12
- package/frontend/src/dashboard/dashboard.js +12 -4
- package/frontend/src/dashboard/edit-dashboard/edit-dashboard.html +13 -7
- package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +13 -21
- package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html +19 -17
- package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js +97 -2
- package/frontend/src/dashboard-result/dashboard-map/dashboard-map.js +27 -3
- package/frontend/src/dashboard-result/dashboard-result.html +3 -3
- package/frontend/src/dashboards/dashboards.html +101 -109
- package/frontend/src/dashboards/dashboards.js +25 -1
- package/frontend/src/detail-default/detail-default.html +2 -2
- package/frontend/src/detail-default/detail-default.js +24 -3
- package/frontend/src/document/confirm-changes/confirm-changes.html +1 -1
- package/frontend/src/document/confirm-delete/confirm-delete.html +1 -1
- package/frontend/src/document/document.css +1 -1
- package/frontend/src/document/document.html +53 -27
- package/frontend/src/document/document.js +27 -1
- package/frontend/src/document/execute-script/execute-script.html +20 -21
- package/frontend/src/document/execute-script/execute-script.js +1 -43
- package/frontend/src/document-details/document-details.css +4 -9
- package/frontend/src/document-details/document-details.html +34 -33
- package/frontend/src/document-details/document-details.js +2 -53
- package/frontend/src/document-details/document-property/document-property.html +12 -12
- package/frontend/src/edit-array/edit-array.html +7 -6
- package/frontend/src/edit-array/edit-array.js +10 -50
- package/frontend/src/edit-boolean/edit-boolean.html +12 -12
- package/frontend/src/edit-date/edit-date.html +2 -2
- package/frontend/src/edit-default/edit-default.html +1 -1
- package/frontend/src/edit-string/edit-string.html +3 -3
- package/frontend/src/edit-subdocument/edit-subdocument.html +5 -3
- package/frontend/src/edit-subdocument/edit-subdocument.js +1 -15
- package/frontend/src/export-query-results/export-query-results.html +3 -3
- package/frontend/src/json-node/json-node.html +3 -3
- package/frontend/src/list-json/json-node.html +1 -1
- package/frontend/src/models/document-search/document-search.html +3 -3
- package/frontend/src/models/model-switcher/model-switcher.html +53 -0
- package/frontend/src/models/model-switcher/model-switcher.js +123 -0
- package/frontend/src/models/models.css +3 -10
- package/frontend/src/models/models.html +146 -74
- package/frontend/src/models/models.js +142 -4
- package/frontend/src/navbar/navbar.html +157 -97
- package/frontend/src/navbar/navbar.js +32 -13
- package/frontend/src/routes.js +20 -4
- package/frontend/src/splash/splash.html +5 -5
- 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 +124 -73
- package/frontend/src/tasks/task-details/task-details.js +166 -10
- package/frontend/src/tasks/tasks.html +37 -48
- package/frontend/src/tasks/tasks.js +11 -50
- package/frontend/src/team/new-invitation/new-invitation.html +8 -8
- package/frontend/src/team/team.html +27 -27
- package/frontend/src/update-document/update-document.html +7 -2
- package/frontend/src/update-document/update-document.js +2 -11
- package/package.json +3 -1
- 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="
|
|
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
|
-
|
|
8
|
+
{{ backLabel }}
|
|
9
9
|
</button>
|
|
10
|
-
<h1 class="text-2xl font-bold text-
|
|
11
|
-
<p class="text-
|
|
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="
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
class="
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
class="
|
|
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
|
-
|
|
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-
|
|
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-
|
|
86
|
-
<div class="text-sm text-
|
|
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-
|
|
90
|
-
<div class="text-sm text-
|
|
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-
|
|
94
|
-
<div class="text-sm text-
|
|
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-
|
|
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-
|
|
105
|
-
<div class="bg-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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('
|
|
222
|
+
this.$emit('back');
|
|
73
223
|
}
|
|
74
224
|
},
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
});
|