@mongoosejs/studio 0.2.10 → 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 +1 -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 +1 -0
- package/eslint.config.js +1 -0
- package/express.js +1 -0
- package/frontend/public/app.js +14306 -13379
- package/frontend/public/tw.css +311 -4
- package/frontend/src/api.js +40 -0
- package/frontend/src/document/document.html +22 -0
- package/frontend/src/document/document.js +13 -1
- 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 +36 -1
- 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,35 @@
|
|
|
1
|
+
.execute-script-backdrop {
|
|
2
|
+
animation: execute-script-fade-in 200ms ease forwards;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.execute-script-panel {
|
|
6
|
+
animation: execute-script-slide-in 200ms ease forwards;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.execute-script-drawer.is-closing .execute-script-backdrop {
|
|
10
|
+
animation: execute-script-fade-out 200ms ease forwards;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.execute-script-drawer.is-closing .execute-script-panel {
|
|
14
|
+
animation: execute-script-slide-out 200ms ease forwards;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@keyframes execute-script-fade-in {
|
|
18
|
+
from { opacity: 0; }
|
|
19
|
+
to { opacity: 1; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@keyframes execute-script-fade-out {
|
|
23
|
+
from { opacity: 1; }
|
|
24
|
+
to { opacity: 0; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@keyframes execute-script-slide-in {
|
|
28
|
+
from { transform: translateX(100%); }
|
|
29
|
+
to { transform: translateX(0); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@keyframes execute-script-slide-out {
|
|
33
|
+
from { transform: translateX(0); }
|
|
34
|
+
to { transform: translateX(100%); }
|
|
35
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div v-if="isOpen" class="fixed inset-0 z-50 execute-script-drawer" :class="{ 'is-closing': isClosing }">
|
|
2
|
+
<div class="execute-script-backdrop absolute inset-0 bg-black/40" @click="close"></div>
|
|
3
|
+
<div class="execute-script-panel absolute right-0 top-0 h-full w-full max-w-xl bg-white shadow-xl flex flex-col">
|
|
4
|
+
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-200">
|
|
5
|
+
<div>
|
|
6
|
+
<h2 class="text-lg font-semibold text-gray-900">Run Script</h2>
|
|
7
|
+
<p class="text-xs text-gray-500">Use <code class="bg-gray-100 px-1 rounded">doc</code> for this document.</p>
|
|
8
|
+
</div>
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
@click="close"
|
|
12
|
+
class="text-gray-400 hover:text-gray-600 text-2xl leading-none"
|
|
13
|
+
aria-label="Close drawer"
|
|
14
|
+
>
|
|
15
|
+
×
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="flex-1 overflow-auto p-5 space-y-4">
|
|
19
|
+
<div class="space-y-2">
|
|
20
|
+
<label class="text-sm font-medium text-gray-700">Script</label>
|
|
21
|
+
<textarea
|
|
22
|
+
ref="scriptEditor"
|
|
23
|
+
:value="scriptText"
|
|
24
|
+
@input="updateScriptText"
|
|
25
|
+
rows="8"
|
|
26
|
+
placeholder="await doc.updateName('new name') return doc"
|
|
27
|
+
class="w-full rounded-md border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
|
28
|
+
></textarea>
|
|
29
|
+
<p class="text-xs text-gray-500">Return a value to see it in the output panel.</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="flex items-center gap-3">
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
@click="runDocumentScript"
|
|
35
|
+
:disabled="scriptRunning"
|
|
36
|
+
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
37
|
+
>
|
|
38
|
+
<span v-if="scriptRunning">Running...</span>
|
|
39
|
+
<span v-else>Run script</span>
|
|
40
|
+
</button>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
@click="clearScript"
|
|
44
|
+
class="text-sm text-gray-500 hover:text-gray-700"
|
|
45
|
+
>
|
|
46
|
+
Clear
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="space-y-3">
|
|
50
|
+
<div v-if="scriptError" class="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
|
|
51
|
+
{{scriptError}}
|
|
52
|
+
</div>
|
|
53
|
+
<div v-if="scriptHasRun" class="rounded-md border border-gray-200 bg-gray-50 p-3">
|
|
54
|
+
<div class="text-xs uppercase tracking-wide text-gray-500 mb-2">Output</div>
|
|
55
|
+
<pre class="whitespace-pre-wrap text-sm text-gray-800">{{formatScriptOutput(scriptResult)}}</pre>
|
|
56
|
+
</div>
|
|
57
|
+
<div v-if="scriptLogs" class="rounded-md border border-gray-200 bg-white p-3">
|
|
58
|
+
<div class="text-xs uppercase tracking-wide text-gray-500 mb-2">Logs</div>
|
|
59
|
+
<pre class="whitespace-pre-wrap text-xs text-gray-700">{{scriptLogs}}</pre>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="border-t border-gray-200 px-5 py-3 text-xs text-gray-500">
|
|
64
|
+
Scripts run on the server with access to the Mongoose document instance.
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
@@ -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');
|
|
@@ -185,6 +185,41 @@ const router = VueRouter.createRouter({
|
|
|
185
185
|
}))
|
|
186
186
|
});
|
|
187
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
|
+
|
|
188
223
|
router.beforeEach((to, from, next) => {
|
|
189
224
|
if (to.name === 'root' && window.state.roles && window.state.roles[0] === 'dashboards') {
|
|
190
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
|
}
|