@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
|
@@ -13,23 +13,29 @@ module.exports = app => app.component('navbar', {
|
|
|
13
13
|
template: template,
|
|
14
14
|
props: ['user', 'roles'],
|
|
15
15
|
inject: ['state'],
|
|
16
|
-
data: () => ({
|
|
16
|
+
data: () => ({
|
|
17
|
+
showFlyout: false,
|
|
18
|
+
darkMode: typeof localStorage !== 'undefined' && localStorage.getItem('studio-theme') === 'dark'
|
|
19
|
+
}),
|
|
17
20
|
mounted: function() {
|
|
18
21
|
const mobileMenuMask = document.querySelector('#mobile-menu-mask');
|
|
19
22
|
const mobileMenu = document.querySelector('#mobile-menu');
|
|
23
|
+
const openBtn = document.querySelector('#open-mobile-menu');
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
if (openBtn && mobileMenuMask && mobileMenu) {
|
|
26
|
+
openBtn.addEventListener('click', (event) => {
|
|
27
|
+
event.stopPropagation();
|
|
28
|
+
mobileMenuMask.style.display = 'block';
|
|
29
|
+
mobileMenu.classList.remove('translate-x-full');
|
|
30
|
+
mobileMenu.classList.add('translate-x-0');
|
|
31
|
+
});
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
document.querySelector('body').addEventListener('click', () => {
|
|
34
|
+
mobileMenuMask.style.display = 'none';
|
|
35
|
+
mobileMenu.classList.remove('translate-x-0');
|
|
36
|
+
mobileMenu.classList.add('translate-x-full');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
33
39
|
},
|
|
34
40
|
computed: {
|
|
35
41
|
dashboardView() {
|
|
@@ -42,7 +48,7 @@ module.exports = app => app.component('navbar', {
|
|
|
42
48
|
return ['chat index', 'chat'].includes(this.$route.name);
|
|
43
49
|
},
|
|
44
50
|
taskView() {
|
|
45
|
-
return ['tasks'].includes(this.$route.name);
|
|
51
|
+
return ['tasks', 'taskByName', 'taskSingle'].includes(this.$route.name);
|
|
46
52
|
},
|
|
47
53
|
routeName() {
|
|
48
54
|
return this.$route.name;
|
|
@@ -82,6 +88,19 @@ module.exports = app => app.component('navbar', {
|
|
|
82
88
|
logout() {
|
|
83
89
|
window.localStorage.setItem('_mongooseStudioAccessToken', '');
|
|
84
90
|
window.location.reload();
|
|
91
|
+
},
|
|
92
|
+
toggleDarkMode() {
|
|
93
|
+
this.darkMode = !this.darkMode;
|
|
94
|
+
const theme = this.darkMode ? 'dark' : 'light';
|
|
95
|
+
window.localStorage.setItem('studio-theme', theme);
|
|
96
|
+
if (this.darkMode) {
|
|
97
|
+
document.documentElement.classList.add('dark');
|
|
98
|
+
} else {
|
|
99
|
+
document.documentElement.classList.remove('dark');
|
|
100
|
+
}
|
|
101
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
102
|
+
if (meta) meta.setAttribute('content', this.darkMode ? '#0f0f0f' : '#ffffff');
|
|
103
|
+
document.documentElement.dispatchEvent(new CustomEvent('studio-theme-changed', { detail: { dark: this.darkMode } }));
|
|
85
104
|
}
|
|
86
105
|
},
|
|
87
106
|
directives: {
|
package/frontend/src/routes.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// Role-based access control configuration
|
|
4
4
|
const roleAccess = {
|
|
5
|
-
owner: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team', 'chat'],
|
|
6
|
-
admin: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team', 'chat'],
|
|
7
|
-
member: ['root', 'model', 'document', 'dashboards', 'dashboard', 'chat'],
|
|
5
|
+
owner: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team', 'chat', 'tasks', 'taskByName', 'taskSingle'],
|
|
6
|
+
admin: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team', 'chat', 'tasks', 'taskByName', 'taskSingle'],
|
|
7
|
+
member: ['root', 'model', 'document', 'dashboards', 'dashboard', 'chat', 'tasks', 'taskByName', 'taskSingle'],
|
|
8
8
|
readonly: ['root', 'model', 'document', 'chat'],
|
|
9
9
|
dashboards: ['dashboards', 'dashboard']
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
const allowedRoutesForLocalDev = ['document', 'root', 'chat', 'model'];
|
|
12
|
+
const allowedRoutesForLocalDev = ['document', 'dashboards', 'dashboard', 'root', 'chat', 'model', 'tasks', 'taskByName', 'taskSingle'];
|
|
13
13
|
|
|
14
14
|
// Helper function to check if a role has access to a route
|
|
15
15
|
function hasAccess(roles, routeName) {
|
|
@@ -76,6 +76,22 @@ module.exports = {
|
|
|
76
76
|
authorized: true
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
path: '/tasks/:name',
|
|
81
|
+
name: 'taskByName',
|
|
82
|
+
component: 'task-by-name',
|
|
83
|
+
meta: {
|
|
84
|
+
authorized: true
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: '/tasks/:name/:id',
|
|
89
|
+
name: 'taskSingle',
|
|
90
|
+
component: 'task-single',
|
|
91
|
+
meta: {
|
|
92
|
+
authorized: true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
79
95
|
{
|
|
80
96
|
path: '/chat',
|
|
81
97
|
name: 'chat index',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div class="w-full h-full flex items-center justify-center">
|
|
2
2
|
<div class="text-center">
|
|
3
|
-
<div class="rounded-full bg-
|
|
4
|
-
<img src="images/
|
|
3
|
+
<div class="rounded-full bg-muted p-6 inline-block">
|
|
4
|
+
<img src="images/mongoose-studio.svg" class="w-48 h-48">
|
|
5
5
|
</div>
|
|
6
6
|
<div class="text-lg mt-2 font-bold">
|
|
7
7
|
Mongoose Studio
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<div v-if="loading" class="mt-2">
|
|
10
10
|
<img src="images/loader.gif" class="inline w-16 h-16">
|
|
11
11
|
</div>
|
|
12
|
-
<div class="mt-2 text-
|
|
12
|
+
<div class="mt-2 text-content-secondary" v-if="!loading">
|
|
13
13
|
{{workspaceName}}
|
|
14
14
|
</div>
|
|
15
15
|
<div class="mt-4 flex gap-4 justify-center" v-if="!loading">
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<async-button
|
|
18
18
|
type="button"
|
|
19
19
|
@click="loginWithGithub"
|
|
20
|
-
class="rounded bg-
|
|
20
|
+
class="rounded bg-primary px-2 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:bg-page0">
|
|
21
21
|
<svg viewBox="0 0 98 98" class="inline mr-1" height="1.5em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
|
22
22
|
Login With GitHub
|
|
23
23
|
</async-button>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<async-button
|
|
27
27
|
type="button"
|
|
28
28
|
@click="loginWithGoogle"
|
|
29
|
-
class="rounded bg-
|
|
29
|
+
class="rounded bg-primary px-2 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:bg-page0">
|
|
30
30
|
<svg class="inline" xmlns="http://www.w3.org/2000/svg" height="1.5em" viewBox="0 0 512 512"><path fill="#fff" d="M386 400c45-42 65-112 53-179H260v74h102c-4 24-18 44-38 57z"/><path fill="#fff" d="M90 341a192 192 0 0 0 296 59l-62-48c-53 35-141 22-171-60z"/><path fill="#fff" d="M153 292c-8-25-8-48 0-73l-63-49c-23 46-30 111 0 171z"/><path fill="#fff" d="M153 219c22-69 116-109 179-50l55-54c-78-75-230-72-297 55z"/></svg>
|
|
31
31
|
Login With Google
|
|
32
32
|
</async-button>
|
|
@@ -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-content-tertiary hover:text-content-secondary 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-content-secondary mb-1">{{ task.name }}</h1>
|
|
19
|
+
<p class="text-content-tertiary mb-6">Task details</p>
|
|
20
|
+
|
|
21
|
+
<div class="bg-surface 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-content">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-content-secondary mb-1">Scheduled At</label>
|
|
35
|
+
<div class="text-sm text-content">{{ formatDate(task.scheduledAt) }}</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div v-if="task?.startedAt">
|
|
38
|
+
<label class="block text-sm font-medium text-content-secondary mb-1">Started At</label>
|
|
39
|
+
<div class="text-sm text-content">{{ formatDate(task.startedAt) }}</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div v-if="task?.completedAt">
|
|
42
|
+
<label class="block text-sm font-medium text-content-secondary mb-1">Completed At</label>
|
|
43
|
+
<div class="text-sm text-content">{{ 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-content-secondary mb-2">Params</label>
|
|
49
|
+
<div class="bg-page 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-content-secondary mb-2">Result</label>
|
|
56
|
+
<div class="bg-page 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-content-secondary mb-2">Error</label>
|
|
63
|
+
<div class="bg-page 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-edge">
|
|
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-content 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-content-tertiary 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-content-secondary 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-edge-strong 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-content-secondary 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-content 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-content-tertiary 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-content-secondary 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-content 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-content-tertiary 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-content-secondary 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
|
+
});
|