@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.
Files changed (35) hide show
  1. package/backend/actions/Model/executeDocumentScript.js +61 -0
  2. package/backend/actions/Model/index.js +2 -0
  3. package/backend/actions/Model/listModels.js +36 -1
  4. package/backend/actions/Model/streamDocumentChanges.js +123 -0
  5. package/backend/actions/Task/cancelTask.js +24 -0
  6. package/backend/actions/Task/createTask.js +33 -0
  7. package/backend/actions/Task/getTasks.js +62 -0
  8. package/backend/actions/Task/index.js +7 -0
  9. package/backend/actions/Task/rescheduleTask.js +39 -0
  10. package/backend/actions/Task/runTask.js +25 -0
  11. package/backend/actions/index.js +1 -0
  12. package/backend/authorize.js +2 -0
  13. package/backend/index.js +7 -1
  14. package/eslint.config.js +4 -1
  15. package/express.js +4 -2
  16. package/frontend/public/app.js +14590 -13420
  17. package/frontend/public/tw.css +357 -4
  18. package/frontend/src/api.js +100 -0
  19. package/frontend/src/dashboard-result/dashboard-document/dashboard-document.html +4 -5
  20. package/frontend/src/dashboard-result/dashboard-document/dashboard-document.js +13 -14
  21. package/frontend/src/document/document.html +80 -0
  22. package/frontend/src/document/document.js +206 -19
  23. package/frontend/src/document/execute-script/execute-script.css +35 -0
  24. package/frontend/src/document/execute-script/execute-script.html +67 -0
  25. package/frontend/src/document/execute-script/execute-script.js +142 -0
  26. package/frontend/src/index.js +48 -4
  27. package/frontend/src/navbar/navbar.html +15 -2
  28. package/frontend/src/navbar/navbar.js +11 -0
  29. package/frontend/src/routes.js +13 -5
  30. package/frontend/src/tasks/task-details/task-details.html +284 -0
  31. package/frontend/src/tasks/task-details/task-details.js +182 -0
  32. package/frontend/src/tasks/tasks.css +0 -0
  33. package/frontend/src/tasks/tasks.html +220 -0
  34. package/frontend/src/tasks/tasks.js +372 -0
  35. 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
+ });
@@ -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 api.status();
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 api.status();
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 border-b-2 px-1 pt-1 text-sm font-medium text-gray-300 cursor-not-allowed">
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 v-if="showFlyout" class="absolute right-0 z-[100] top-[90%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
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: {
@@ -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: true
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: true
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: true
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: true
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">&times;</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">&times;</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">&times;</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
+