@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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div class="p-4 space-y-6">
|
|
2
|
+
<div v-if="status === 'init'">
|
|
3
|
+
<img src="images/loader.gif" alt="Loading" />
|
|
4
|
+
</div>
|
|
5
|
+
<div v-else-if="status === 'error'" class="text-red-600">
|
|
6
|
+
{{ errorMessage }}
|
|
7
|
+
</div>
|
|
8
|
+
<task-details
|
|
9
|
+
v-else-if="taskGroup"
|
|
10
|
+
:task-group="taskGroup"
|
|
11
|
+
:back-to="{ name: 'tasks' }"
|
|
12
|
+
@task-created="onTaskCreated"
|
|
13
|
+
@task-cancelled="onTaskCancelled"
|
|
14
|
+
></task-details>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Page: all tasks with a given name. Reuses task-details to render the list (many tasks).
|
|
4
|
+
const template = require('./task-by-name.html');
|
|
5
|
+
const api = require('../api');
|
|
6
|
+
|
|
7
|
+
function buildTaskGroup(name, tasks) {
|
|
8
|
+
const statusCounts = { pending: 0, succeeded: 0, failed: 0, cancelled: 0, in_progress: 0, unknown: 0 };
|
|
9
|
+
let lastRun = null;
|
|
10
|
+
tasks.forEach(task => {
|
|
11
|
+
const status = task.status || 'unknown';
|
|
12
|
+
if (Object.prototype.hasOwnProperty.call(statusCounts, status)) {
|
|
13
|
+
statusCounts[status]++;
|
|
14
|
+
} else {
|
|
15
|
+
statusCounts.unknown++;
|
|
16
|
+
}
|
|
17
|
+
const taskTime = new Date(task.scheduledAt || task.createdAt || 0);
|
|
18
|
+
if (!lastRun || taskTime > lastRun) lastRun = taskTime;
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
name,
|
|
22
|
+
tasks,
|
|
23
|
+
statusCounts,
|
|
24
|
+
totalCount: tasks.length,
|
|
25
|
+
lastRun
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = app => app.component('task-by-name', {
|
|
30
|
+
template,
|
|
31
|
+
data: () => ({
|
|
32
|
+
status: 'init',
|
|
33
|
+
taskGroup: null,
|
|
34
|
+
errorMessage: ''
|
|
35
|
+
}),
|
|
36
|
+
computed: {
|
|
37
|
+
taskName() {
|
|
38
|
+
return this.$route.params.name || '';
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
watch: {
|
|
42
|
+
taskName: {
|
|
43
|
+
immediate: true,
|
|
44
|
+
handler() {
|
|
45
|
+
this.loadTasks();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
methods: {
|
|
50
|
+
async loadTasks() {
|
|
51
|
+
if (!this.taskName) return;
|
|
52
|
+
this.status = 'init';
|
|
53
|
+
this.taskGroup = null;
|
|
54
|
+
this.errorMessage = '';
|
|
55
|
+
const start = new Date();
|
|
56
|
+
start.setHours(start.getHours() - 1);
|
|
57
|
+
const end = new Date();
|
|
58
|
+
try {
|
|
59
|
+
const { tasks } = await api.Task.getTasks({
|
|
60
|
+
name: this.taskName,
|
|
61
|
+
start,
|
|
62
|
+
end
|
|
63
|
+
});
|
|
64
|
+
this.taskGroup = buildTaskGroup(this.taskName, tasks);
|
|
65
|
+
this.status = 'loaded';
|
|
66
|
+
} catch (err) {
|
|
67
|
+
this.status = 'error';
|
|
68
|
+
this.errorMessage = err?.response?.data?.message || err.message || 'Failed to load tasks';
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async onTaskCreated() {
|
|
72
|
+
await this.loadTasks();
|
|
73
|
+
},
|
|
74
|
+
async onTaskCancelled() {
|
|
75
|
+
await this.loadTasks();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<div class="p-4 space-y-6">
|
|
2
|
+
<div v-if="status === 'init'">
|
|
3
|
+
<img src="images/loader.gif" alt="Loading" />
|
|
4
|
+
</div>
|
|
5
|
+
<div v-else-if="status === 'error'" class="text-red-600">
|
|
6
|
+
{{ errorMessage }}
|
|
7
|
+
</div>
|
|
8
|
+
<div v-else-if="status === 'notfound'" class="text-gray-600">
|
|
9
|
+
Task not found.
|
|
10
|
+
</div>
|
|
11
|
+
<div v-else-if="task" class="max-w-4xl">
|
|
12
|
+
<button @click="goBack" class="text-gray-500 hover:text-gray-700 mb-4 flex items-center gap-1">
|
|
13
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
14
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
|
15
|
+
</svg>
|
|
16
|
+
Back to {{ task.name }}
|
|
17
|
+
</button>
|
|
18
|
+
<h1 class="text-2xl font-bold text-gray-700 mb-1">{{ task.name }}</h1>
|
|
19
|
+
<p class="text-gray-500 mb-6">Task details</p>
|
|
20
|
+
|
|
21
|
+
<div class="bg-white rounded-lg shadow p-6 md:p-8">
|
|
22
|
+
<div class="flex items-center gap-3 mb-6">
|
|
23
|
+
<span class="text-sm font-medium text-gray-900">ID: {{ task.id }}</span>
|
|
24
|
+
<span
|
|
25
|
+
class="text-xs px-2 py-1 rounded-full font-medium"
|
|
26
|
+
:class="getStatusColor(task.status)"
|
|
27
|
+
>
|
|
28
|
+
{{ task?.status }}
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
33
|
+
<div>
|
|
34
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Scheduled At</label>
|
|
35
|
+
<div class="text-sm text-gray-900">{{ formatDate(task.scheduledAt) }}</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div v-if="task?.startedAt">
|
|
38
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Started At</label>
|
|
39
|
+
<div class="text-sm text-gray-900">{{ formatDate(task.startedAt) }}</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div v-if="task?.completedAt">
|
|
42
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Completed At</label>
|
|
43
|
+
<div class="text-sm text-gray-900">{{ formatDate(task.completedAt) }}</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div v-if="task?.params" class="mb-6">
|
|
48
|
+
<label class="block text-sm font-medium text-gray-700 mb-2">Params</label>
|
|
49
|
+
<div class="bg-gray-50 rounded-md p-4">
|
|
50
|
+
<list-json :value="task.params"></list-json>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div v-if="task?.result" class="mb-6">
|
|
55
|
+
<label class="block text-sm font-medium text-gray-700 mb-2">Result</label>
|
|
56
|
+
<div class="bg-gray-50 rounded-md p-4">
|
|
57
|
+
<list-json :value="task.result"></list-json>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div v-if="task?.error" class="mb-6">
|
|
62
|
+
<label class="block text-sm font-medium text-gray-700 mb-2">Error</label>
|
|
63
|
+
<div class="bg-gray-50 rounded-md p-4">
|
|
64
|
+
<list-json :value="task.error"></list-json>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="flex flex-wrap gap-3 pt-4 border-t border-gray-200">
|
|
69
|
+
<button
|
|
70
|
+
@click="showRescheduleConfirmation(task)"
|
|
71
|
+
class="flex items-center justify-center gap-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
72
|
+
:disabled="task.status === 'in_progress'"
|
|
73
|
+
>
|
|
74
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
75
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
76
|
+
</svg>
|
|
77
|
+
Reschedule
|
|
78
|
+
</button>
|
|
79
|
+
<button
|
|
80
|
+
@click="showRunConfirmation(task)"
|
|
81
|
+
class="flex items-center justify-center gap-2 bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-green-600 hover:to-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
82
|
+
:disabled="task.status === 'in_progress'"
|
|
83
|
+
>
|
|
84
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
85
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3l14 9-14 9V3z"></path>
|
|
86
|
+
</svg>
|
|
87
|
+
Run Now
|
|
88
|
+
</button>
|
|
89
|
+
<button
|
|
90
|
+
v-if="task.status === 'pending'"
|
|
91
|
+
@click="showCancelConfirmation(task)"
|
|
92
|
+
class="flex items-center justify-center gap-2 bg-gradient-to-r from-red-500 to-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-red-600 hover:to-red-700"
|
|
93
|
+
>
|
|
94
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
95
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
96
|
+
</svg>
|
|
97
|
+
Cancel Task
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Reschedule Modal -->
|
|
104
|
+
<modal v-if="showRescheduleModal" containerClass="!max-w-md">
|
|
105
|
+
<template #body>
|
|
106
|
+
<div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showRescheduleModal = false" role="button" aria-label="Close modal">×</div>
|
|
107
|
+
<div class="p-6">
|
|
108
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Reschedule Task</h3>
|
|
109
|
+
<p class="text-sm text-gray-600 mb-2">Reschedule task <strong>{{ selectedTask?.id }}</strong>?</p>
|
|
110
|
+
<p class="text-sm text-gray-500 mb-4">This will reset the task's status and schedule it to run again.</p>
|
|
111
|
+
<label for="newScheduledTime" class="block text-sm font-medium text-gray-700 mb-2">New Scheduled Time</label>
|
|
112
|
+
<input
|
|
113
|
+
id="newScheduledTime"
|
|
114
|
+
v-model="newScheduledTime"
|
|
115
|
+
type="datetime-local"
|
|
116
|
+
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 mb-4"
|
|
117
|
+
/>
|
|
118
|
+
<div class="flex gap-3">
|
|
119
|
+
<button @click="confirmRescheduleTask" class="flex-1 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 font-medium">Reschedule</button>
|
|
120
|
+
<button @click="showRescheduleModal = false" class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium">Cancel</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</template>
|
|
124
|
+
</modal>
|
|
125
|
+
|
|
126
|
+
<!-- Run Modal -->
|
|
127
|
+
<modal v-if="showRunModal" containerClass="!max-w-md">
|
|
128
|
+
<template #body>
|
|
129
|
+
<div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showRunModal = false" role="button" aria-label="Close modal">×</div>
|
|
130
|
+
<div class="p-6">
|
|
131
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Run Task Now</h3>
|
|
132
|
+
<p class="text-sm text-gray-600 mb-2">Run task <strong>{{ selectedTask?.id }}</strong> immediately?</p>
|
|
133
|
+
<p class="text-sm text-gray-500 mb-4">This will execute the task right away, bypassing its scheduled time.</p>
|
|
134
|
+
<div class="flex gap-3">
|
|
135
|
+
<button @click="confirmRunTask" class="flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 font-medium">Run Now</button>
|
|
136
|
+
<button @click="showRunModal = false" class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium">Cancel</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</template>
|
|
140
|
+
</modal>
|
|
141
|
+
|
|
142
|
+
<!-- Cancel Task Modal -->
|
|
143
|
+
<modal v-if="showCancelModal" containerClass="!max-w-md">
|
|
144
|
+
<template #body>
|
|
145
|
+
<div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showCancelModal = false" role="button" aria-label="Close modal">×</div>
|
|
146
|
+
<div class="p-6">
|
|
147
|
+
<h3 class="text-lg font-medium text-gray-900 mb-4">Cancel Task</h3>
|
|
148
|
+
<p class="text-sm text-gray-600 mb-2">Cancel task <strong>{{ selectedTask?.id }}</strong>?</p>
|
|
149
|
+
<p class="text-sm text-gray-500 mb-4">This will permanently cancel the task and it cannot be undone.</p>
|
|
150
|
+
<div class="flex gap-3">
|
|
151
|
+
<button @click="confirmCancelTask" class="flex-1 bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 font-medium">Cancel Task</button>
|
|
152
|
+
<button @click="showCancelModal = false" class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium">Keep Task</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</template>
|
|
156
|
+
</modal>
|
|
157
|
+
</div>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Page: one task by id. Dedicated single-task detail UI (not the list).
|
|
4
|
+
const template = require('./task-single.html');
|
|
5
|
+
const api = require('../api');
|
|
6
|
+
|
|
7
|
+
module.exports = app => app.component('task-single', {
|
|
8
|
+
template,
|
|
9
|
+
data: () => ({
|
|
10
|
+
status: 'init',
|
|
11
|
+
task: null,
|
|
12
|
+
errorMessage: '',
|
|
13
|
+
showRescheduleModal: false,
|
|
14
|
+
showRunModal: false,
|
|
15
|
+
showCancelModal: false,
|
|
16
|
+
selectedTask: null,
|
|
17
|
+
newScheduledTime: ''
|
|
18
|
+
}),
|
|
19
|
+
computed: {
|
|
20
|
+
taskId() {
|
|
21
|
+
return this.$route.params.id || '';
|
|
22
|
+
},
|
|
23
|
+
taskByNamePath() {
|
|
24
|
+
const name = this.task?.name ?? this.$route.params.name ?? '';
|
|
25
|
+
const path = `/tasks/${encodeURIComponent(name || '')}`;
|
|
26
|
+
const query = this.$route.query?.status ? { status: this.$route.query.status } : {};
|
|
27
|
+
return { path, query };
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
watch: {
|
|
31
|
+
'$route.params': {
|
|
32
|
+
deep: true,
|
|
33
|
+
handler() {
|
|
34
|
+
this.loadTask();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
methods: {
|
|
39
|
+
getStatusColor(status) {
|
|
40
|
+
if (status === 'succeeded') return 'bg-green-100 text-green-800';
|
|
41
|
+
if (status === 'pending') return 'bg-yellow-100 text-yellow-800';
|
|
42
|
+
if (status === 'cancelled') return 'bg-gray-100 text-gray-800';
|
|
43
|
+
if (status === 'failed') return 'bg-red-100 text-red-800';
|
|
44
|
+
if (status === 'in_progress') return 'bg-blue-100 text-blue-800';
|
|
45
|
+
return 'bg-slate-100 text-slate-800';
|
|
46
|
+
},
|
|
47
|
+
formatDate(dateString) {
|
|
48
|
+
if (!dateString) return 'N/A';
|
|
49
|
+
return new Date(dateString).toLocaleString();
|
|
50
|
+
},
|
|
51
|
+
async loadTask() {
|
|
52
|
+
if (!this.taskId) return;
|
|
53
|
+
this.status = 'init';
|
|
54
|
+
this.task = null;
|
|
55
|
+
this.errorMessage = '';
|
|
56
|
+
try {
|
|
57
|
+
const { doc } = await api.Model.getDocument({ model: 'Task', documentId: this.taskId });
|
|
58
|
+
this.task = doc;
|
|
59
|
+
this.status = 'loaded';
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const status = err?.response?.status;
|
|
62
|
+
const notFound = status === 404 || err?.response?.data?.name === 'DocumentNotFoundError';
|
|
63
|
+
this.status = notFound ? 'notfound' : 'error';
|
|
64
|
+
this.errorMessage = notFound ? '' : (err?.response?.data?.message || err.message || 'Failed to load task');
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
showRescheduleConfirmation(task) {
|
|
68
|
+
this.selectedTask = task;
|
|
69
|
+
const defaultTime = new Date();
|
|
70
|
+
defaultTime.setHours(defaultTime.getHours() + 1);
|
|
71
|
+
this.newScheduledTime = defaultTime.toISOString().slice(0, 16);
|
|
72
|
+
this.showRescheduleModal = true;
|
|
73
|
+
},
|
|
74
|
+
showRunConfirmation(task) {
|
|
75
|
+
this.selectedTask = task;
|
|
76
|
+
this.showRunModal = true;
|
|
77
|
+
},
|
|
78
|
+
showCancelConfirmation(task) {
|
|
79
|
+
this.selectedTask = task;
|
|
80
|
+
this.showCancelModal = true;
|
|
81
|
+
},
|
|
82
|
+
async confirmRescheduleTask() {
|
|
83
|
+
if (!this.newScheduledTime) return;
|
|
84
|
+
await api.Task.rescheduleTask({ taskId: this.selectedTask.id, scheduledAt: this.newScheduledTime });
|
|
85
|
+
this.$toast.success({ title: 'Task Rescheduled', text: `Task ${this.selectedTask.id} has been rescheduled`, });
|
|
86
|
+
this.showRescheduleModal = false;
|
|
87
|
+
this.selectedTask = null;
|
|
88
|
+
this.newScheduledTime = '';
|
|
89
|
+
await this.loadTask();
|
|
90
|
+
},
|
|
91
|
+
async confirmRunTask() {
|
|
92
|
+
await api.Task.runTask({ taskId: this.selectedTask.id });
|
|
93
|
+
this.$toast.success({ title: 'Task Started', text: `Task ${this.selectedTask.id} is now running`, type: 'success' });
|
|
94
|
+
this.showRunModal = false;
|
|
95
|
+
this.selectedTask = null;
|
|
96
|
+
await this.loadTask();
|
|
97
|
+
},
|
|
98
|
+
goBack() {
|
|
99
|
+
if (window.history.length > 1) {
|
|
100
|
+
window.history.back();
|
|
101
|
+
} else {
|
|
102
|
+
this.$router.push(this.taskByNamePath);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
async confirmCancelTask() {
|
|
106
|
+
await api.Task.cancelTask({ taskId: this.selectedTask.id });
|
|
107
|
+
this.$toast.success({ title: 'Task Cancelled', text: `Task ${this.selectedTask.id} has been cancelled` });
|
|
108
|
+
this.showCancelModal = false;
|
|
109
|
+
this.selectedTask = null;
|
|
110
|
+
this.goBack();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
mounted() {
|
|
114
|
+
this.loadTask();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
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-gray-500 hover:text-gray-700 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
10
|
<h1 class="text-2xl font-bold text-gray-700">{{ taskGroup.name }}</h1>
|
|
11
11
|
<p class="text-gray-500">Total: {{ taskGroup.totalCount }} tasks</p>
|
|
@@ -14,64 +14,115 @@
|
|
|
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-gray-700">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-ultramarine-600 text-white border-ultramarine-600' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'"
|
|
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-ultramarine-600 text-white border-ultramarine-600' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'"
|
|
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-gray-50 border border-gray-200 rounded-md p-3 text-center hover:bg-gray-100 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-gray-700">{{ 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-white border border-gray-200 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-gray-500 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-gray-100 text-gray-500 hover:bg-gray-200'"
|
|
88
|
+
@click="filterByStatus(status)"
|
|
89
|
+
>
|
|
90
|
+
{{ statusLabel(status) }}
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- Task List -->
|
|
97
|
+
<div class="bg-white rounded-lg shadow">
|
|
98
|
+
<div class="px-6 py-6 border-b border-gray-200 flex items-center justify-between bg-gray-50">
|
|
99
|
+
<h2 class="text-xl font-bold text-gray-900">
|
|
100
|
+
Individual Tasks
|
|
101
|
+
<span v-if="currentFilter" class="ml-3 text-base font-semibold text-ultramarine-700">
|
|
102
|
+
(Filtered by {{ currentFilter }})
|
|
103
|
+
</span>
|
|
104
|
+
</h2>
|
|
105
|
+
<button
|
|
106
|
+
v-if="currentFilter"
|
|
107
|
+
@click="clearFilter"
|
|
108
|
+
class="text-sm font-semibold text-ultramarine-600 hover:text-ultramarine-700"
|
|
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
118
|
<span class="text-sm font-medium text-gray-900">Task ID: {{ task.id }}</span>
|
|
119
|
+
<router-link
|
|
120
|
+
v-if="backTo"
|
|
121
|
+
:to="taskDetailRoute(task)"
|
|
122
|
+
class="text-sm text-ultramarine-600 hover:text-ultramarine-700 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)"
|