@mongoosejs/studio 0.2.9 → 0.2.11
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/executeDocumentScript.js +61 -0
- package/backend/actions/Model/index.js +2 -0
- package/backend/actions/Model/listModels.js +36 -1
- package/backend/actions/Model/streamDocumentChanges.js +123 -0
- package/backend/actions/Task/cancelTask.js +24 -0
- package/backend/actions/Task/createTask.js +33 -0
- package/backend/actions/Task/getTasks.js +62 -0
- package/backend/actions/Task/index.js +7 -0
- package/backend/actions/Task/rescheduleTask.js +39 -0
- package/backend/actions/Task/runTask.js +25 -0
- package/backend/actions/index.js +1 -0
- package/backend/authorize.js +2 -0
- package/backend/index.js +7 -1
- package/eslint.config.js +4 -1
- package/express.js +4 -2
- package/frontend/public/app.js +14590 -13420
- package/frontend/public/tw.css +357 -4
- package/frontend/src/api.js +100 -0
- package/frontend/src/dashboard-result/dashboard-document/dashboard-document.html +4 -5
- package/frontend/src/dashboard-result/dashboard-document/dashboard-document.js +13 -14
- package/frontend/src/document/document.html +80 -0
- package/frontend/src/document/document.js +206 -19
- package/frontend/src/document/execute-script/execute-script.css +35 -0
- package/frontend/src/document/execute-script/execute-script.html +67 -0
- package/frontend/src/document/execute-script/execute-script.js +142 -0
- package/frontend/src/index.js +48 -4
- package/frontend/src/navbar/navbar.html +15 -2
- package/frontend/src/navbar/navbar.js +11 -0
- package/frontend/src/routes.js +13 -5
- package/frontend/src/tasks/task-details/task-details.html +284 -0
- package/frontend/src/tasks/task-details/task-details.js +182 -0
- package/frontend/src/tasks/tasks.css +0 -0
- package/frontend/src/tasks/tasks.html +220 -0
- package/frontend/src/tasks/tasks.js +372 -0
- package/package.json +4 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../../api');
|
|
4
|
+
const template = require('./execute-script.html');
|
|
5
|
+
const appendCSS = require('../../appendCSS');
|
|
6
|
+
|
|
7
|
+
appendCSS(require('./execute-script.css'));
|
|
8
|
+
|
|
9
|
+
module.exports = app => app.component('execute-script', {
|
|
10
|
+
template,
|
|
11
|
+
props: ['model', 'documentId', 'editting', 'visible'],
|
|
12
|
+
data() {
|
|
13
|
+
return {
|
|
14
|
+
scriptText: '',
|
|
15
|
+
scriptResult: null,
|
|
16
|
+
scriptLogs: '',
|
|
17
|
+
scriptError: null,
|
|
18
|
+
scriptHasRun: false,
|
|
19
|
+
scriptRunning: false,
|
|
20
|
+
scriptEditor: null,
|
|
21
|
+
isOpen: false,
|
|
22
|
+
isClosing: false
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
watch: {
|
|
26
|
+
visible(newVal) {
|
|
27
|
+
if (newVal) {
|
|
28
|
+
this.isOpen = true;
|
|
29
|
+
this.isClosing = false;
|
|
30
|
+
} else if (this.isOpen) {
|
|
31
|
+
this.isClosing = true;
|
|
32
|
+
this.destroyScriptEditor();
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
this.isOpen = false;
|
|
35
|
+
this.isClosing = false;
|
|
36
|
+
}, 200);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
isOpen(newVal) {
|
|
40
|
+
if (newVal) {
|
|
41
|
+
this.$nextTick(() => {
|
|
42
|
+
this.initializeScriptEditor();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
mounted() {
|
|
48
|
+
if (this.visible) {
|
|
49
|
+
this.isOpen = true;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
beforeDestroy() {
|
|
53
|
+
this.destroyScriptEditor();
|
|
54
|
+
},
|
|
55
|
+
methods: {
|
|
56
|
+
close() {
|
|
57
|
+
this.$emit('close');
|
|
58
|
+
},
|
|
59
|
+
initializeScriptEditor() {
|
|
60
|
+
if (!this.$refs.scriptEditor || this.scriptEditor || typeof CodeMirror === 'undefined') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.$refs.scriptEditor.value = this.scriptText;
|
|
65
|
+
this.scriptEditor = CodeMirror.fromTextArea(this.$refs.scriptEditor, {
|
|
66
|
+
mode: 'javascript',
|
|
67
|
+
lineNumbers: true,
|
|
68
|
+
lineWrapping: true
|
|
69
|
+
});
|
|
70
|
+
this.scriptEditor.on('change', () => {
|
|
71
|
+
this.scriptText = this.scriptEditor.getValue();
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
destroyScriptEditor() {
|
|
75
|
+
if (this.scriptEditor) {
|
|
76
|
+
this.scriptEditor.toTextArea();
|
|
77
|
+
this.scriptEditor = null;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
updateScriptText(event) {
|
|
81
|
+
this.scriptText = event.target.value;
|
|
82
|
+
},
|
|
83
|
+
clearScript() {
|
|
84
|
+
this.scriptText = '';
|
|
85
|
+
this.scriptResult = null;
|
|
86
|
+
this.scriptLogs = '';
|
|
87
|
+
this.scriptError = null;
|
|
88
|
+
this.scriptHasRun = false;
|
|
89
|
+
if (this.scriptEditor) {
|
|
90
|
+
this.scriptEditor.setValue('');
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
formatScriptOutput(value) {
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
return 'undefined';
|
|
96
|
+
}
|
|
97
|
+
if (value === null) {
|
|
98
|
+
return 'null';
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === 'string') {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return JSON.stringify(value, null, 2);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return String(value);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async runDocumentScript() {
|
|
110
|
+
const scriptToRun = (this.scriptEditor ? this.scriptEditor.getValue() : this.scriptText || '').trim();
|
|
111
|
+
if (!scriptToRun) {
|
|
112
|
+
this.$toast.error('Script cannot be empty.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.scriptRunning = true;
|
|
117
|
+
this.scriptError = null;
|
|
118
|
+
this.scriptHasRun = false;
|
|
119
|
+
try {
|
|
120
|
+
const { result, logs } = await api.Model.executeDocumentScript({
|
|
121
|
+
model: this.model,
|
|
122
|
+
documentId: this.documentId,
|
|
123
|
+
script: scriptToRun
|
|
124
|
+
});
|
|
125
|
+
this.scriptText = scriptToRun;
|
|
126
|
+
if (this.scriptEditor) {
|
|
127
|
+
this.scriptEditor.setValue(scriptToRun);
|
|
128
|
+
}
|
|
129
|
+
this.scriptResult = result;
|
|
130
|
+
this.scriptLogs = logs;
|
|
131
|
+
this.scriptHasRun = true;
|
|
132
|
+
this.$emit('refresh');
|
|
133
|
+
this.$toast.success('Script executed successfully!');
|
|
134
|
+
} catch (err) {
|
|
135
|
+
this.scriptError = err?.message || String(err);
|
|
136
|
+
this.$toast.error('Script execution failed.');
|
|
137
|
+
} finally {
|
|
138
|
+
this.scriptRunning = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
package/frontend/src/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const api = require('./api');
|
|
|
11
11
|
const format = require('./format');
|
|
12
12
|
const arrayUtils = require('./array-utils');
|
|
13
13
|
const mothership = require('./mothership');
|
|
14
|
-
const { routes } = require('./routes');
|
|
14
|
+
const { routes, hasAccess } = require('./routes');
|
|
15
15
|
const Toast = require('vue-toastification').default;
|
|
16
16
|
const { useToast } = require('vue-toastification');
|
|
17
17
|
const appendCSS = require('./appendCSS');
|
|
@@ -130,8 +130,12 @@ app.component('app-component', {
|
|
|
130
130
|
const { user, roles } = await mothership.me();
|
|
131
131
|
|
|
132
132
|
try {
|
|
133
|
-
const { nodeEnv } = await
|
|
133
|
+
const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
|
|
134
|
+
api.status(),
|
|
135
|
+
api.Model.listModels()
|
|
136
|
+
]);
|
|
134
137
|
this.nodeEnv = nodeEnv;
|
|
138
|
+
this.modelSchemaPaths = modelSchemaPaths;
|
|
135
139
|
} catch (err) {
|
|
136
140
|
this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
|
|
137
141
|
this.status = 'loaded';
|
|
@@ -144,8 +148,12 @@ app.component('app-component', {
|
|
|
144
148
|
}
|
|
145
149
|
} else {
|
|
146
150
|
try {
|
|
147
|
-
const { nodeEnv } = await
|
|
151
|
+
const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
|
|
152
|
+
api.status(),
|
|
153
|
+
api.Model.listModels()
|
|
154
|
+
]);
|
|
148
155
|
this.nodeEnv = nodeEnv;
|
|
156
|
+
this.modelSchemaPaths = modelSchemaPaths;
|
|
149
157
|
} catch (err) {
|
|
150
158
|
this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
|
|
151
159
|
}
|
|
@@ -159,8 +167,9 @@ app.component('app-component', {
|
|
|
159
167
|
const status = Vue.ref('init');
|
|
160
168
|
const nodeEnv = Vue.ref(null);
|
|
161
169
|
const authError = Vue.ref(null);
|
|
170
|
+
const modelSchemaPaths = Vue.ref(null);
|
|
162
171
|
|
|
163
|
-
const state = Vue.reactive({ user, roles, status, nodeEnv, authError });
|
|
172
|
+
const state = Vue.reactive({ user, roles, status, nodeEnv, authError, modelSchemaPaths });
|
|
164
173
|
Vue.provide('state', state);
|
|
165
174
|
|
|
166
175
|
return state;
|
|
@@ -176,6 +185,41 @@ const router = VueRouter.createRouter({
|
|
|
176
185
|
}))
|
|
177
186
|
});
|
|
178
187
|
|
|
188
|
+
// Add global navigation guard
|
|
189
|
+
router.beforeEach((to, from, next) => {
|
|
190
|
+
// Skip auth check for authorized (public) routes
|
|
191
|
+
if (to.meta.authorized) {
|
|
192
|
+
next();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get roles from the app state
|
|
197
|
+
const roles = window.state?.roles;
|
|
198
|
+
|
|
199
|
+
// Check if user has access to the route
|
|
200
|
+
if (!hasAccess(roles, to.name)) {
|
|
201
|
+
// Find all routes the user has access to
|
|
202
|
+
const allowedRoutes = routes.filter(route => hasAccess(roles, route.name));
|
|
203
|
+
|
|
204
|
+
// If user has no allowed routes, redirect to splash/login
|
|
205
|
+
if (allowedRoutes.length === 0) {
|
|
206
|
+
next({ name: 'root' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Redirect to first allowed route
|
|
211
|
+
const firstAllowedRoute = allowedRoutes[0].name;
|
|
212
|
+
next({ name: firstAllowedRoute });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (to.name === 'root' && roles && roles[0] === 'dashboards') {
|
|
217
|
+
return next({ name: 'dashboards' });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
next();
|
|
221
|
+
});
|
|
222
|
+
|
|
179
223
|
router.beforeEach((to, from, next) => {
|
|
180
224
|
if (to.name === 'root' && window.state.roles && window.state.roles[0] === 'dashboards') {
|
|
181
225
|
return next({ name: 'dashboards' });
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
href="#/dashboards"
|
|
33
33
|
class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
|
|
34
34
|
:class="dashboardView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">Dashboards</a>
|
|
35
|
-
<span v-else class="inline-flex items-center
|
|
35
|
+
<span v-else class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 cursor-not-allowed">
|
|
36
36
|
Dashboards
|
|
37
37
|
<svg class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
38
38
|
<path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
|
|
@@ -48,6 +48,12 @@
|
|
|
48
48
|
<path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
|
|
49
49
|
</svg>
|
|
50
50
|
</span>
|
|
51
|
+
<a
|
|
52
|
+
:href="hasTaskVisualizer"
|
|
53
|
+
class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
|
|
54
|
+
:class="taskView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">
|
|
55
|
+
Tasks
|
|
56
|
+
</a>
|
|
51
57
|
<a
|
|
52
58
|
href="https://studio.mongoosejs.io/docs"
|
|
53
59
|
target="_blank"
|
|
@@ -75,7 +81,14 @@
|
|
|
75
81
|
</button>
|
|
76
82
|
</div>
|
|
77
83
|
|
|
78
|
-
<div
|
|
84
|
+
<div
|
|
85
|
+
v-if="showFlyout"
|
|
86
|
+
class="absolute right-0 top-[90%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none"
|
|
87
|
+
role="menu"
|
|
88
|
+
aria-orientation="vertical"
|
|
89
|
+
aria-labelledby="user-menu-button"
|
|
90
|
+
style="z-index: 10000"
|
|
91
|
+
tabindex="-1">
|
|
79
92
|
<router-link to="/team" v-if="hasAccess(roles, 'team')" @click="showFlyout = false" class="cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200" role="menuitem" tabindex="-1" id="user-menu-item-2">Team</router-link>
|
|
80
93
|
<span v-else class="block px-4 py-2 text-sm text-gray-300 cursor-not-allowed" role="menuitem" tabindex="-1" id="user-menu-item-2">
|
|
81
94
|
Team
|
|
@@ -41,6 +41,9 @@ module.exports = app => app.component('navbar', {
|
|
|
41
41
|
chatView() {
|
|
42
42
|
return ['chat index', 'chat'].includes(this.$route.name);
|
|
43
43
|
},
|
|
44
|
+
taskView() {
|
|
45
|
+
return ['tasks'].includes(this.$route.name);
|
|
46
|
+
},
|
|
44
47
|
routeName() {
|
|
45
48
|
return this.$route.name;
|
|
46
49
|
},
|
|
@@ -55,6 +58,14 @@ module.exports = app => app.component('navbar', {
|
|
|
55
58
|
},
|
|
56
59
|
defaultRoute() {
|
|
57
60
|
return this.roles && this.roles[0] === 'dashboards' ? 'dashboards' : 'root';
|
|
61
|
+
},
|
|
62
|
+
hasTaskVisualizer() {
|
|
63
|
+
if (window.MONGOOSE_STUDIO_CONFIG.enableTaskVisualizer) {
|
|
64
|
+
return '#/tasks';
|
|
65
|
+
} else {
|
|
66
|
+
return 'https://www.npmjs.com/package/@mongoosejs/task';
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
}
|
|
59
70
|
},
|
|
60
71
|
methods: {
|
package/frontend/src/routes.js
CHANGED
|
@@ -9,7 +9,7 @@ const roleAccess = {
|
|
|
9
9
|
dashboards: ['dashboards', 'dashboard']
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
const allowedRoutesForLocalDev = ['document', 'root', 'chat'];
|
|
12
|
+
const allowedRoutesForLocalDev = ['document', 'root', 'chat', 'model'];
|
|
13
13
|
|
|
14
14
|
// Helper function to check if a role has access to a route
|
|
15
15
|
function hasAccess(roles, routeName) {
|
|
@@ -33,7 +33,7 @@ module.exports = {
|
|
|
33
33
|
name: 'model',
|
|
34
34
|
component: 'models',
|
|
35
35
|
meta: {
|
|
36
|
-
authorized:
|
|
36
|
+
authorized: false
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -41,7 +41,7 @@ module.exports = {
|
|
|
41
41
|
name: 'document',
|
|
42
42
|
component: 'document',
|
|
43
43
|
meta: {
|
|
44
|
-
authorized:
|
|
44
|
+
authorized: false
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
{
|
|
@@ -49,7 +49,7 @@ module.exports = {
|
|
|
49
49
|
name: 'dashboards',
|
|
50
50
|
component: 'dashboards',
|
|
51
51
|
meta: {
|
|
52
|
-
authorized:
|
|
52
|
+
authorized: false
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
{
|
|
@@ -57,13 +57,21 @@ module.exports = {
|
|
|
57
57
|
name: 'dashboard',
|
|
58
58
|
component: 'dashboard',
|
|
59
59
|
meta: {
|
|
60
|
-
authorized:
|
|
60
|
+
authorized: false
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
path: '/team',
|
|
65
65
|
name: 'team',
|
|
66
66
|
component: 'team',
|
|
67
|
+
meta: {
|
|
68
|
+
authorized: false
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: '/tasks',
|
|
73
|
+
name: 'tasks',
|
|
74
|
+
component: 'tasks',
|
|
67
75
|
meta: {
|
|
68
76
|
authorized: true
|
|
69
77
|
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<div class="p-4 space-y-6">
|
|
2
|
+
<div class="flex items-center justify-between">
|
|
3
|
+
<div>
|
|
4
|
+
<button @click="$emit('back')" class="text-gray-500 hover:text-gray-700 mb-2">
|
|
5
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
6
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
|
7
|
+
</svg>
|
|
8
|
+
Back to Task Groups
|
|
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>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
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>
|
|
61
|
+
<button
|
|
62
|
+
v-if="currentFilter"
|
|
63
|
+
@click="clearFilter"
|
|
64
|
+
class="text-sm text-ultramarine-600 hover:text-ultramarine-700 font-medium"
|
|
65
|
+
>
|
|
66
|
+
Show All
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="divide-y divide-gray-200">
|
|
70
|
+
<div v-for="task in sortedTasks" :key="task.id" class="p-6">
|
|
71
|
+
<div class="flex items-start justify-between">
|
|
72
|
+
<div class="flex-1">
|
|
73
|
+
<div class="flex items-center gap-3 mb-2">
|
|
74
|
+
<span class="text-sm font-medium text-gray-900">Task ID: {{ task.id }}</span>
|
|
75
|
+
<span
|
|
76
|
+
class="text-xs px-2 py-1 rounded-full font-medium"
|
|
77
|
+
:class="getStatusColor(task.status)"
|
|
78
|
+
>
|
|
79
|
+
{{ task.status }}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
84
|
+
<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>
|
|
87
|
+
</div>
|
|
88
|
+
<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>
|
|
91
|
+
</div>
|
|
92
|
+
<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>
|
|
95
|
+
</div>
|
|
96
|
+
<div v-if="task.error">
|
|
97
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Error</label>
|
|
98
|
+
<div class="text-sm text-red-600">{{ task.error }}</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Task Parameters -->
|
|
103
|
+
<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">
|
|
106
|
+
<pre class="text-sm text-gray-800 whitespace-pre-wrap">{{ JSON.stringify(task.parameters, null, 2) }}</pre>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="flex flex-col gap-3 ml-6">
|
|
112
|
+
<button
|
|
113
|
+
@click="showRescheduleConfirmation(task)"
|
|
114
|
+
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 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
|
115
|
+
:disabled="task.status === 'in_progress'"
|
|
116
|
+
>
|
|
117
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
118
|
+
<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>
|
|
119
|
+
</svg>
|
|
120
|
+
Reschedule
|
|
121
|
+
</button>
|
|
122
|
+
<button
|
|
123
|
+
@click="showRunConfirmation(task)"
|
|
124
|
+
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 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
|
125
|
+
:disabled="task.status === 'in_progress'"
|
|
126
|
+
>
|
|
127
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
128
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3l14 9-14 9V3z"></path>
|
|
129
|
+
</svg>
|
|
130
|
+
Run Now
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
v-if="task.status === 'pending'"
|
|
134
|
+
@click="showCancelConfirmation(task)"
|
|
135
|
+
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 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg"
|
|
136
|
+
>
|
|
137
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
138
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
139
|
+
</svg>
|
|
140
|
+
Cancel
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Reschedule Confirmation Modal -->
|
|
149
|
+
<modal v-if="showRescheduleModal" containerClass="!max-w-md">
|
|
150
|
+
<template #body>
|
|
151
|
+
<div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showRescheduleModal = false;" role="button" aria-label="Close modal">×</div>
|
|
152
|
+
<div class="p-6">
|
|
153
|
+
<div class="flex items-center mb-4">
|
|
154
|
+
<div class="flex-shrink-0">
|
|
155
|
+
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
156
|
+
<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>
|
|
157
|
+
</svg>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="ml-3">
|
|
160
|
+
<h3 class="text-lg font-medium text-gray-900">Reschedule Task</h3>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="mb-4">
|
|
164
|
+
<p class="text-sm text-gray-600">
|
|
165
|
+
Are you sure you want to reschedule task <strong>{{ selectedTask?.id }}</strong>?
|
|
166
|
+
</p>
|
|
167
|
+
<p class="text-sm text-gray-500 mt-2">
|
|
168
|
+
This will reset the task's status and schedule it to run again.
|
|
169
|
+
</p>
|
|
170
|
+
|
|
171
|
+
<div class="mt-4">
|
|
172
|
+
<label for="newScheduledTime" class="block text-sm font-medium text-gray-700 mb-2">
|
|
173
|
+
New Scheduled Time
|
|
174
|
+
</label>
|
|
175
|
+
<input
|
|
176
|
+
id="newScheduledTime"
|
|
177
|
+
v-model="newScheduledTime"
|
|
178
|
+
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"
|
|
180
|
+
required
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="flex gap-3">
|
|
185
|
+
<button
|
|
186
|
+
@click="confirmRescheduleTask"
|
|
187
|
+
class="flex-1 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 font-medium"
|
|
188
|
+
>
|
|
189
|
+
Reschedule
|
|
190
|
+
</button>
|
|
191
|
+
<button
|
|
192
|
+
@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"
|
|
194
|
+
>
|
|
195
|
+
Cancel
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</template>
|
|
200
|
+
</modal>
|
|
201
|
+
|
|
202
|
+
<!-- Run Task Confirmation Modal -->
|
|
203
|
+
<modal v-if="showRunModal" containerClass="!max-w-md">
|
|
204
|
+
<template #body>
|
|
205
|
+
<div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showRunModal = false;" role="button" aria-label="Close modal">×</div>
|
|
206
|
+
<div class="p-6">
|
|
207
|
+
<div class="flex items-center mb-4">
|
|
208
|
+
<div class="flex-shrink-0">
|
|
209
|
+
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
210
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
211
|
+
</svg>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="ml-3">
|
|
214
|
+
<h3 class="text-lg font-medium text-gray-900">Run Task Now</h3>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="mb-4">
|
|
218
|
+
<p class="text-sm text-gray-600">
|
|
219
|
+
Are you sure you want to run task <strong>{{ selectedTask?.id }}</strong> immediately?
|
|
220
|
+
</p>
|
|
221
|
+
<p class="text-sm text-gray-500 mt-2">
|
|
222
|
+
This will execute the task right away, bypassing its scheduled time.
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="flex gap-3">
|
|
226
|
+
<button
|
|
227
|
+
@click="confirmRunTask"
|
|
228
|
+
class="flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 font-medium"
|
|
229
|
+
>
|
|
230
|
+
Run Now
|
|
231
|
+
</button>
|
|
232
|
+
<button
|
|
233
|
+
@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"
|
|
235
|
+
>
|
|
236
|
+
Cancel
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</template>
|
|
241
|
+
</modal>
|
|
242
|
+
|
|
243
|
+
<!-- Cancel Task Confirmation Modal -->
|
|
244
|
+
<modal v-if="showCancelModal" containerClass="!max-w-md">
|
|
245
|
+
<template #body>
|
|
246
|
+
<div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showCancelModal = false;" role="button" aria-label="Close modal">×</div>
|
|
247
|
+
<div class="p-6">
|
|
248
|
+
<div class="flex items-center mb-4">
|
|
249
|
+
<div class="flex-shrink-0">
|
|
250
|
+
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
251
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
252
|
+
</svg>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="ml-3">
|
|
255
|
+
<h3 class="text-lg font-medium text-gray-900">Cancel Task</h3>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="mb-4">
|
|
259
|
+
<p class="text-sm text-gray-600">
|
|
260
|
+
Are you sure you want to cancel task <strong>{{ selectedTask?.id }}</strong>?
|
|
261
|
+
</p>
|
|
262
|
+
<p class="text-sm text-gray-500 mt-2">
|
|
263
|
+
This will permanently cancel the task and it cannot be undone.
|
|
264
|
+
</p>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="flex gap-3">
|
|
267
|
+
<button
|
|
268
|
+
@click="confirmCancelTask"
|
|
269
|
+
class="flex-1 bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 font-medium"
|
|
270
|
+
>
|
|
271
|
+
Cancel Task
|
|
272
|
+
</button>
|
|
273
|
+
<button
|
|
274
|
+
@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"
|
|
276
|
+
>
|
|
277
|
+
Keep Task
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</template>
|
|
282
|
+
</modal>
|
|
283
|
+
</div>
|
|
284
|
+
|