@mongoosejs/studio 0.0.70 → 0.0.72
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/createDocument.js +8 -1
- package/backend/actions/Model/deleteDocument.js +8 -1
- package/backend/actions/Model/exportQueryResults.js +3 -3
- package/backend/actions/Model/getIndexes.js +33 -0
- package/backend/actions/Model/index.js +1 -0
- package/backend/actions/Model/updateDocument.js +7 -1
- package/express.js +1 -0
- package/frontend/public/app.js +310 -51
- package/frontend/public/images/duplicate.svg +1 -0
- package/frontend/public/tw.css +27 -0
- package/frontend/src/api.js +53 -7
- package/frontend/src/clone-document/clone-document.css +0 -0
- package/frontend/src/clone-document/clone-document.html +26 -0
- package/frontend/src/clone-document/clone-document.js +72 -0
- package/frontend/src/document/document.html +20 -0
- package/frontend/src/document/document.js +14 -2
- package/frontend/src/export-query-results/export-query-results.html +6 -4
- package/frontend/src/index.js +4 -2
- package/frontend/src/models/models.html +26 -4
- package/frontend/src/models/models.js +41 -2
- package/frontend/src/navbar/navbar.html +4 -4
- package/frontend/src/navbar/navbar.js +20 -2
- package/frontend/src/routes.js +70 -32
- package/package.json +1 -1
package/frontend/public/tw.css
CHANGED
|
@@ -909,6 +909,10 @@ video {
|
|
|
909
909
|
transform-origin: top right;
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
+
.cursor-not-allowed {
|
|
913
|
+
cursor: not-allowed;
|
|
914
|
+
}
|
|
915
|
+
|
|
912
916
|
.cursor-pointer {
|
|
913
917
|
cursor: pointer;
|
|
914
918
|
}
|
|
@@ -1195,6 +1199,11 @@ video {
|
|
|
1195
1199
|
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
|
1196
1200
|
}
|
|
1197
1201
|
|
|
1202
|
+
.bg-pink-600 {
|
|
1203
|
+
--tw-bg-opacity: 1;
|
|
1204
|
+
background-color: rgb(219 39 119 / var(--tw-bg-opacity));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1198
1207
|
.bg-red-300 {
|
|
1199
1208
|
--tw-bg-opacity: 1;
|
|
1200
1209
|
background-color: rgb(252 165 165 / var(--tw-bg-opacity));
|
|
@@ -1409,6 +1418,10 @@ video {
|
|
|
1409
1418
|
font-family: 'Lato';
|
|
1410
1419
|
}
|
|
1411
1420
|
|
|
1421
|
+
.font-mono {
|
|
1422
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1412
1425
|
.text-base {
|
|
1413
1426
|
font-size: 1rem;
|
|
1414
1427
|
line-height: 1.5rem;
|
|
@@ -1473,6 +1486,11 @@ video {
|
|
|
1473
1486
|
color: rgb(0 0 0 / var(--tw-text-opacity));
|
|
1474
1487
|
}
|
|
1475
1488
|
|
|
1489
|
+
.text-forest-green-500 {
|
|
1490
|
+
--tw-text-opacity: 1;
|
|
1491
|
+
color: rgb(0 242 58 / var(--tw-text-opacity));
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1476
1494
|
.text-gray-400 {
|
|
1477
1495
|
--tw-text-opacity: 1;
|
|
1478
1496
|
color: rgb(156 163 175 / var(--tw-text-opacity));
|
|
@@ -1542,6 +1560,10 @@ video {
|
|
|
1542
1560
|
accent-color: #0284c7;
|
|
1543
1561
|
}
|
|
1544
1562
|
|
|
1563
|
+
.opacity-50 {
|
|
1564
|
+
opacity: 0.5;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1545
1567
|
.shadow-lg {
|
|
1546
1568
|
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
1547
1569
|
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
|
@@ -1894,6 +1916,11 @@ video {
|
|
|
1894
1916
|
cursor: not-allowed;
|
|
1895
1917
|
}
|
|
1896
1918
|
|
|
1919
|
+
.disabled\:bg-gray-400:disabled {
|
|
1920
|
+
--tw-bg-opacity: 1;
|
|
1921
|
+
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1897
1924
|
.disabled\:bg-gray-500:disabled {
|
|
1898
1925
|
--tw-bg-opacity: 1;
|
|
1899
1926
|
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
package/frontend/src/api.js
CHANGED
|
@@ -57,7 +57,31 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
57
57
|
return client.post('', { action: 'Model.deleteDocument', ...params}).then(res => res.data);
|
|
58
58
|
},
|
|
59
59
|
exportQueryResults(params) {
|
|
60
|
-
|
|
60
|
+
const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
|
|
61
|
+
|
|
62
|
+
return fetch(window.MONGOOSE_STUDIO_CONFIG.baseURL + new URLSearchParams({ ...params, action: 'Model.exportQueryResults' }).toString(), {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
headers: {
|
|
65
|
+
'Authorization': `${accessToken}`, // Set your authorization token here
|
|
66
|
+
'Accept': 'text/csv'
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.then(response => {
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
return response.blob();
|
|
74
|
+
})
|
|
75
|
+
.then(blob => {
|
|
76
|
+
const blobURL = window.URL.createObjectURL(blob);
|
|
77
|
+
const anchor = document.createElement('a');
|
|
78
|
+
anchor.href = blobURL;
|
|
79
|
+
anchor.download = 'export.csv';
|
|
80
|
+
document.body.appendChild(anchor);
|
|
81
|
+
anchor.click();
|
|
82
|
+
document.body.removeChild(anchor);
|
|
83
|
+
window.URL.revokeObjectURL(blobURL);
|
|
84
|
+
});
|
|
61
85
|
},
|
|
62
86
|
getDocument: function getDocument(params) {
|
|
63
87
|
return client.post('', { action: 'Model.getDocument', ...params }).then(res => res.data);
|
|
@@ -104,12 +128,31 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
104
128
|
return client.post('/Model/deleteDocument', params).then(res => res.data);
|
|
105
129
|
},
|
|
106
130
|
exportQueryResults(params) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
|
|
132
|
+
|
|
133
|
+
return fetch(window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/exportQueryResults?' + new URLSearchParams(params).toString(), {
|
|
134
|
+
method: 'GET',
|
|
135
|
+
headers: {
|
|
136
|
+
'Authorization': `${accessToken}`, // Set your authorization token here
|
|
137
|
+
'Accept': 'text/csv'
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.then(response => {
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
143
|
+
}
|
|
144
|
+
return response.blob();
|
|
145
|
+
})
|
|
146
|
+
.then(blob => {
|
|
147
|
+
const blobURL = window.URL.createObjectURL(blob);
|
|
148
|
+
const anchor = document.createElement('a');
|
|
149
|
+
anchor.href = blobURL;
|
|
150
|
+
anchor.download = 'export.csv';
|
|
151
|
+
document.body.appendChild(anchor);
|
|
152
|
+
anchor.click();
|
|
153
|
+
document.body.removeChild(anchor);
|
|
154
|
+
window.URL.revokeObjectURL(blobURL);
|
|
155
|
+
});
|
|
113
156
|
},
|
|
114
157
|
getDocument: function getDocument(params) {
|
|
115
158
|
return client.post('/Model/getDocument', params).then(res => res.data);
|
|
@@ -117,6 +160,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
117
160
|
getDocuments: function getDocuments(params) {
|
|
118
161
|
return client.post('/Model/getDocuments', params).then(res => res.data);
|
|
119
162
|
},
|
|
163
|
+
getIndexes: function getIndexes(params) {
|
|
164
|
+
return client.post('/Model/getIndexes', params).then(res => res.data);
|
|
165
|
+
},
|
|
120
166
|
listModels: function listModels() {
|
|
121
167
|
return client.post('/Model/listModels', {}).then(res => res.data);
|
|
122
168
|
},
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<div class="mb-2">
|
|
3
|
+
<textarea class="border border-gray-200 p-2 h-[300px] w-full" ref="codeEditor"></textarea>
|
|
4
|
+
</div>
|
|
5
|
+
<button @click="cloneDocument()" class="rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600">Submit</button>
|
|
6
|
+
<div v-if="errors.length > 0" class="rounded-md bg-red-50 p-4 mt-1">
|
|
7
|
+
<div class="flex">
|
|
8
|
+
<div class="flex-shrink-0">
|
|
9
|
+
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
10
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
|
|
11
|
+
</svg>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="ml-3">
|
|
14
|
+
<h3 class="text-sm font-medium text-red-800">There were {{errors.length}} errors with your submission</h3>
|
|
15
|
+
<div class="mt-2 text-sm text-red-700">
|
|
16
|
+
<ul role="list" class="list-disc space-y-1 pl-5">
|
|
17
|
+
<li v-for="error in errors">
|
|
18
|
+
{{error}}
|
|
19
|
+
</li>
|
|
20
|
+
</ul>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
|
|
5
|
+
const { BSON, EJSON } = require('bson');
|
|
6
|
+
|
|
7
|
+
const ObjectId = new Proxy(BSON.ObjectId, {
|
|
8
|
+
apply (target, thisArg, argumentsList) {
|
|
9
|
+
return new target(...argumentsList);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const appendCSS = require('../appendCSS');
|
|
14
|
+
|
|
15
|
+
appendCSS(require('./clone-document.css'));
|
|
16
|
+
|
|
17
|
+
const template = require('./clone-document.html')
|
|
18
|
+
|
|
19
|
+
module.exports = app => app.component('clone-document', {
|
|
20
|
+
props: ['currentModel', 'doc', 'schemaPaths'],
|
|
21
|
+
template,
|
|
22
|
+
data: function() {
|
|
23
|
+
return {
|
|
24
|
+
documentData: '',
|
|
25
|
+
editor: null,
|
|
26
|
+
errors: []
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
methods: {
|
|
30
|
+
async cloneDocument() {
|
|
31
|
+
const data = EJSON.serialize(eval(`(${this.editor.getValue()})`));
|
|
32
|
+
const { doc } = await api.Model.createDocument({ model: this.currentModel, data }).catch(err => {
|
|
33
|
+
if (err.response?.data?.message) {
|
|
34
|
+
console.log(err.response.data);
|
|
35
|
+
const message = err.response.data.message.split(": ").slice(1).join(": ");
|
|
36
|
+
this.errors = message.split(',').map(error => {
|
|
37
|
+
return error.split(': ').slice(1).join(': ').trim();
|
|
38
|
+
})
|
|
39
|
+
throw new Error(err.response?.data?.message);
|
|
40
|
+
}
|
|
41
|
+
throw err;
|
|
42
|
+
});
|
|
43
|
+
this.errors.length = 0;
|
|
44
|
+
this.$emit('close', doc);
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
mounted: function() {
|
|
48
|
+
const pathsToClone = this.schemaPaths.map(x => x.path);
|
|
49
|
+
|
|
50
|
+
// Create a filtered version of the document data
|
|
51
|
+
const filteredDoc = {};
|
|
52
|
+
pathsToClone.forEach(path => {
|
|
53
|
+
const value = this.doc[path];
|
|
54
|
+
if (value !== undefined) {
|
|
55
|
+
filteredDoc[path] = value;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Replace _id with a new ObjectId
|
|
60
|
+
if (pathsToClone.includes('_id')) {
|
|
61
|
+
filteredDoc._id = new ObjectId();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.documentData = JSON.stringify(filteredDoc, null, 2);
|
|
65
|
+
this.$refs.codeEditor.value = this.documentData;
|
|
66
|
+
this.editor = CodeMirror.fromTextArea(this.$refs.codeEditor, {
|
|
67
|
+
mode: 'javascript',
|
|
68
|
+
lineNumbers: true,
|
|
69
|
+
smartIndent: false
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
})
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
<button
|
|
13
13
|
v-if="!editting"
|
|
14
14
|
@click="editting = true"
|
|
15
|
+
:disabled="!canManipulate"
|
|
16
|
+
:class="{'cursor-not-allowed opacity-50': !canManipulate}"
|
|
15
17
|
type="button"
|
|
16
18
|
class="rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600">
|
|
17
19
|
<img src="images/edit.svg" class="inline" /> Edit
|
|
@@ -25,6 +27,8 @@
|
|
|
25
27
|
</button>
|
|
26
28
|
<button
|
|
27
29
|
v-if="editting"
|
|
30
|
+
:disabled="!canManipulate"
|
|
31
|
+
:class="{'cursor-not-allowed opacity-50': !canManipulate}"
|
|
28
32
|
@click="shouldShowConfirmModal=true;"
|
|
29
33
|
type="button"
|
|
30
34
|
class="rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600">
|
|
@@ -32,10 +36,20 @@
|
|
|
32
36
|
</button>
|
|
33
37
|
<button
|
|
34
38
|
@click="shouldShowDeleteModal=true;"
|
|
39
|
+
:disabled="!canManipulate"
|
|
40
|
+
:class="{'cursor-not-allowed opacity-50': !canManipulate}"
|
|
35
41
|
type="button"
|
|
36
42
|
class="rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">
|
|
37
43
|
<img src="images/delete.svg" class="inline" /> Delete
|
|
38
44
|
</button>
|
|
45
|
+
<button
|
|
46
|
+
@click="shouldShowCloneModal=true;"
|
|
47
|
+
:disabled="!canManipulate"
|
|
48
|
+
:class="{'cursor-not-allowed opacity-50': !canManipulate}"
|
|
49
|
+
type="button"
|
|
50
|
+
class="rounded-md bg-pink-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">
|
|
51
|
+
<img src="images/duplicate.svg" class="inline" /> Clone
|
|
52
|
+
</button>
|
|
39
53
|
</div>
|
|
40
54
|
</div>
|
|
41
55
|
<div v-if="status === 'loaded'">
|
|
@@ -57,5 +71,11 @@
|
|
|
57
71
|
<confirm-delete @close="shouldShowConfirmModal = false;" @remove="remove" :value="document"></confirm-delete>
|
|
58
72
|
</template>
|
|
59
73
|
</modal>
|
|
74
|
+
<modal v-if="shouldShowCloneModal">
|
|
75
|
+
<template v-slot:body>
|
|
76
|
+
<div class="modal-exit" @click="shouldShowCloneModal = false;">×</div>
|
|
77
|
+
<clone-document :currentModel="model" :doc="document" :schemaPaths="schemaPaths" @close="showClonedDocument"></clone-document>
|
|
78
|
+
</template>
|
|
79
|
+
</modal>
|
|
60
80
|
</div>
|
|
61
81
|
</div>
|
|
@@ -11,7 +11,7 @@ appendCSS(require('./document.css'));
|
|
|
11
11
|
|
|
12
12
|
module.exports = app => app.component('document', {
|
|
13
13
|
template: template,
|
|
14
|
-
props: ['model', 'documentId'],
|
|
14
|
+
props: ['model', 'documentId', 'user', 'roles'],
|
|
15
15
|
data: () => ({
|
|
16
16
|
schemaPaths: [],
|
|
17
17
|
status: 'init',
|
|
@@ -21,7 +21,8 @@ module.exports = app => app.component('document', {
|
|
|
21
21
|
editting: false,
|
|
22
22
|
virtuals: [],
|
|
23
23
|
shouldShowConfirmModal: false,
|
|
24
|
-
shouldShowDeleteModal: false
|
|
24
|
+
shouldShowDeleteModal: false,
|
|
25
|
+
shouldShowCloneModal: false
|
|
25
26
|
}),
|
|
26
27
|
async mounted() {
|
|
27
28
|
window.pageState = this;
|
|
@@ -39,6 +40,14 @@ module.exports = app => app.component('document', {
|
|
|
39
40
|
}).map(key => schemaPaths[key]);
|
|
40
41
|
this.status = 'loaded';
|
|
41
42
|
},
|
|
43
|
+
computed: {
|
|
44
|
+
canManipulate() {
|
|
45
|
+
if (!this.roles) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return !this.roles.includes('readonly');
|
|
49
|
+
}
|
|
50
|
+
},
|
|
42
51
|
methods: {
|
|
43
52
|
cancelEdit() {
|
|
44
53
|
this.changes = {};
|
|
@@ -74,6 +83,9 @@ module.exports = app => app.component('document', {
|
|
|
74
83
|
});
|
|
75
84
|
this.$router.push({ path: `/model/${this.model}`});
|
|
76
85
|
}
|
|
86
|
+
},
|
|
87
|
+
showClonedDocument(doc) {
|
|
88
|
+
this.$router.push({ path: `/model/${this.model}/document/${doc._id}`});
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
});
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
<div>
|
|
4
4
|
Choose fields to export
|
|
5
5
|
</div>
|
|
6
|
-
<div v-for="schemaPath in schemaPaths">
|
|
7
|
-
<input type="checkbox" v-model="shouldExport[schemaPath.path]">
|
|
8
|
-
<
|
|
6
|
+
<div v-for="(schemaPath,index) in schemaPaths" class="w-5 flex items-center">
|
|
7
|
+
<input type="checkbox" class="mt-0 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-600 accent-sky-600" v-model="shouldExport[schemaPath.path]" :id="'schemaPath.path'+index">
|
|
8
|
+
<div class="ml-2 text-gray-700 grow shrink text-left">
|
|
9
|
+
<label :for="'schemaPath.path'+index">{{schemaPath.path}}</label>
|
|
10
|
+
</div>
|
|
9
11
|
</div>
|
|
10
|
-
<async-button @click="exportQueryResults">Export</async-button>
|
|
12
|
+
<async-button class="rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600" @click="exportQueryResults">Export</async-button>
|
|
11
13
|
</div>
|
package/frontend/src/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const app = Vue.createApp({
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
require('./async-button/async-button')(app);
|
|
16
|
+
require('./clone-document/clone-document')(app);
|
|
16
17
|
require('./create-dashboard/create-dashboard')(app);
|
|
17
18
|
require('./create-document/create-document')(app);
|
|
18
19
|
require('./dashboards/dashboards')(app);
|
|
@@ -58,7 +59,7 @@ app.component('app-component', {
|
|
|
58
59
|
<div v-else-if="!hasAPIKey || user">
|
|
59
60
|
<navbar :user="user" :roles="roles" />
|
|
60
61
|
<div class="view">
|
|
61
|
-
<router-view :key="$route.fullPath" />
|
|
62
|
+
<router-view :key="$route.fullPath" :user="user" :roles="roles" />
|
|
62
63
|
</div>
|
|
63
64
|
</div>
|
|
64
65
|
</div>
|
|
@@ -78,6 +79,7 @@ app.component('app-component', {
|
|
|
78
79
|
},
|
|
79
80
|
async mounted() {
|
|
80
81
|
window.$router = this.$router;
|
|
82
|
+
window.state = this;
|
|
81
83
|
|
|
82
84
|
if (mothership.hasAPIKey) {
|
|
83
85
|
const href = window.location.href;
|
|
@@ -135,7 +137,7 @@ app.component('app-component', {
|
|
|
135
137
|
}
|
|
136
138
|
});
|
|
137
139
|
|
|
138
|
-
const routes = require('./routes');
|
|
140
|
+
const { routes } = require('./routes');
|
|
139
141
|
const router = VueRouter.createRouter({
|
|
140
142
|
history: VueRouter.createWebHashHistory(),
|
|
141
143
|
routes: routes.map(route => ({
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
<div class="documents-menu">
|
|
29
29
|
<div class="flex flex-row items-center w-full gap-2">
|
|
30
30
|
<form @submit.prevent="search" class="flex-grow m-0">
|
|
31
|
-
<input class="w-full rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none" type="text" placeholder="Filter or text" v-model="searchText" />
|
|
31
|
+
<input ref="searchInput" class="w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none" type="text" placeholder="Filter or text" v-model="searchText" />
|
|
32
32
|
</form>
|
|
33
33
|
<div>
|
|
34
34
|
<span v-if="status === 'loading'">Loading ...</span>
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
class="rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
|
|
41
41
|
Export
|
|
42
42
|
</button>
|
|
43
|
+
<button
|
|
44
|
+
@click="openIndexModal"
|
|
45
|
+
type="button"
|
|
46
|
+
class="rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
|
|
47
|
+
Indexes
|
|
48
|
+
</button>
|
|
43
49
|
<button
|
|
44
50
|
@click="shouldShowCreateModal = true;"
|
|
45
51
|
type="button"
|
|
@@ -74,7 +80,7 @@
|
|
|
74
80
|
<div class="documents-container relative">
|
|
75
81
|
<table v-if="outputType === 'table'">
|
|
76
82
|
<thead>
|
|
77
|
-
<th v-for="path in filteredPaths">
|
|
83
|
+
<th v-for="path in filteredPaths" @click="clickFilter(path.path)" class="cursor-pointer">
|
|
78
84
|
{{path.path}}
|
|
79
85
|
<span class="path-type">
|
|
80
86
|
({{(path.instance || 'unknown')}})
|
|
@@ -106,7 +112,6 @@
|
|
|
106
112
|
</div>
|
|
107
113
|
</div>
|
|
108
114
|
</div>
|
|
109
|
-
|
|
110
115
|
<modal v-if="shouldShowExportModal">
|
|
111
116
|
<template v-slot:body>
|
|
112
117
|
<div class="modal-exit" @click="shouldShowExportModal = false">×</div>
|
|
@@ -118,13 +123,30 @@
|
|
|
118
123
|
</export-query-results>
|
|
119
124
|
</template>
|
|
120
125
|
</modal>
|
|
126
|
+
<modal v-if="shouldShowIndexModal">
|
|
127
|
+
<template v-slot:body>
|
|
128
|
+
<div class="modal-exit" @click="shouldShowIndexModal = false">×</div>
|
|
129
|
+
<div class="text-xl font-bold mb-2">Indexes</div>
|
|
130
|
+
<div v-for="index in mongoDBIndexes" class="w-full flex items-center">
|
|
131
|
+
<div class="grow shrink text-left flex justify-between items-center" v-if="index.name != '_id_'">
|
|
132
|
+
<div>
|
|
133
|
+
<div class="font-bold">{{ index.name }}</div>
|
|
134
|
+
<div class="text-sm font-mono">{{ JSON.stringify(index.key) }}</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<button type="submit" @click="dropIndex()" disabled="true" class="rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed">Drop</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</template>
|
|
142
|
+
</modal>
|
|
121
143
|
<modal v-if="shouldShowFieldModal">
|
|
122
144
|
<template v-slot:body>
|
|
123
145
|
<div class="modal-exit" @click="shouldShowFieldModal = false; selectedPaths = [...filteredPaths];">×</div>
|
|
124
146
|
<div v-for="(path, index) in schemaPaths" :key="index" class="w-5 flex items-center">
|
|
125
147
|
<input class="mt-0 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-600 accent-sky-600" type="checkbox" :id="'path.path'+index" @change="addOrRemove(path)" :value="path.path" :checked="isSelected(path.path)" />
|
|
126
148
|
<div class="ml-2 text-gray-700 grow shrink text-left">
|
|
127
|
-
<label :for="'path' + index">{{path.path}}</label>
|
|
149
|
+
<label :for="'path.path' + index">{{path.path}}</label>
|
|
128
150
|
</div>
|
|
129
151
|
</div>
|
|
130
152
|
<div class="mt-4 flex gap-2">
|
|
@@ -22,7 +22,7 @@ const limit = 20;
|
|
|
22
22
|
|
|
23
23
|
module.exports = app => app.component('models', {
|
|
24
24
|
template: template,
|
|
25
|
-
props: ['model'],
|
|
25
|
+
props: ['model', 'user', 'roles'],
|
|
26
26
|
data: () => ({
|
|
27
27
|
models: [],
|
|
28
28
|
currentModel: null,
|
|
@@ -31,6 +31,8 @@ module.exports = app => app.component('models', {
|
|
|
31
31
|
filteredPaths: [],
|
|
32
32
|
selectedPaths: [],
|
|
33
33
|
numDocuments: 0,
|
|
34
|
+
mongoDBIndexes: [],
|
|
35
|
+
schemaIndexes: [],
|
|
34
36
|
status: 'loading',
|
|
35
37
|
loadedAllDocs: false,
|
|
36
38
|
edittingDoc: null,
|
|
@@ -40,6 +42,7 @@ module.exports = app => app.component('models', {
|
|
|
40
42
|
shouldShowExportModal: false,
|
|
41
43
|
shouldShowCreateModal: false,
|
|
42
44
|
shouldShowFieldModal: false,
|
|
45
|
+
shouldShowIndexModal: false,
|
|
43
46
|
shouldExport: {},
|
|
44
47
|
sortBy: {},
|
|
45
48
|
query: {},
|
|
@@ -86,6 +89,28 @@ module.exports = app => app.component('models', {
|
|
|
86
89
|
this.status = 'loaded';
|
|
87
90
|
},
|
|
88
91
|
methods: {
|
|
92
|
+
clickFilter(path) {
|
|
93
|
+
if (this.searchText) {
|
|
94
|
+
if (this.searchText.endsWith('}')) {
|
|
95
|
+
this.searchText = this.searchText.slice(0, -1) + `, ${path}: }`;
|
|
96
|
+
} else {
|
|
97
|
+
this.searchText += `, ${path}: }`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
} else {
|
|
101
|
+
// If this.searchText is empty or undefined, initialize it with a new object
|
|
102
|
+
this.searchText = `{ ${path}: }`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
this.$nextTick(() => {
|
|
107
|
+
const input = this.$refs.searchInput;
|
|
108
|
+
const cursorIndex = this.searchText.lastIndexOf(":") + 2; // Move cursor after ": "
|
|
109
|
+
|
|
110
|
+
input.focus();
|
|
111
|
+
input.setSelectionRange(cursorIndex, cursorIndex);
|
|
112
|
+
});
|
|
113
|
+
},
|
|
89
114
|
async closeCreationModal() {
|
|
90
115
|
this.shouldShowCreateModal = false;
|
|
91
116
|
await this.getDocuments();
|
|
@@ -152,6 +177,21 @@ module.exports = app => app.component('models', {
|
|
|
152
177
|
}
|
|
153
178
|
await this.loadMoreDocuments();
|
|
154
179
|
},
|
|
180
|
+
async openIndexModal() {
|
|
181
|
+
this.shouldShowIndexModal = true;
|
|
182
|
+
const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel })
|
|
183
|
+
this.mongoDBIndexes = mongoDBIndexes;
|
|
184
|
+
this.schemaIndexes = schemaIndexes;
|
|
185
|
+
},
|
|
186
|
+
checkIndexLocation(indexName) {
|
|
187
|
+
if (this.schemaIndexes.find(x => x.name == indexName) && this.mongoDBIndexes.find(x => x.name == indexName)) {
|
|
188
|
+
return 'text-gray-500'
|
|
189
|
+
} else if (this.schemaIndexes.find(x => x.name == indexName)) {
|
|
190
|
+
return 'text-forest-green-500'
|
|
191
|
+
} else {
|
|
192
|
+
return 'text-valencia-500'
|
|
193
|
+
}
|
|
194
|
+
},
|
|
155
195
|
async getDocuments() {
|
|
156
196
|
const { docs, schemaPaths, numDocs } = await api.Model.getDocuments({
|
|
157
197
|
model: this.currentModel,
|
|
@@ -178,7 +218,6 @@ module.exports = app => app.component('models', {
|
|
|
178
218
|
for (const { path } of this.schemaPaths) {
|
|
179
219
|
this.shouldExport[path] = true;
|
|
180
220
|
}
|
|
181
|
-
|
|
182
221
|
this.filteredPaths = [...this.schemaPaths];
|
|
183
222
|
this.selectedPaths = [...this.schemaPaths];
|
|
184
223
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="navbar">
|
|
2
2
|
<div class="nav-left flex items-center gap-4 h-full">
|
|
3
|
-
<router-link to="
|
|
3
|
+
<router-link :to="{ name: defaultRoute }">
|
|
4
4
|
<img src="images/logo.svg" alt="Mongoose Studio Logo" />
|
|
5
5
|
</router-link>
|
|
6
6
|
<div v-if="!!state.nodeEnv" class="inline-flex items-center rounded-md px-2 py-1 text-sm font-medium text-gray-900" :class="warnEnv ? 'bg-red-300' : 'bg-yellow-300'">
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
</div>
|
|
10
10
|
<div class="nav-right h-full">
|
|
11
11
|
<div class="sm:ml-6 sm:flex sm:space-x-8 h-full">
|
|
12
|
-
<a
|
|
12
|
+
<a v-if="hasAccess(roles, 'root')"
|
|
13
13
|
href="#/"
|
|
14
14
|
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
15
15
|
:class="documentView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">Documents</a>
|
|
16
|
-
<a
|
|
16
|
+
<a v-if="hasAccess(roles, 'dashboards')"
|
|
17
17
|
href="#/dashboards"
|
|
18
18
|
class="inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
|
|
19
19
|
:class="dashboardView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'">Dashboards</a>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
</div>
|
|
37
37
|
|
|
38
38
|
<div v-if="showFlyout" class="absolute right-0 z-10 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">
|
|
39
|
-
<router-link to="/team" v-if="
|
|
39
|
+
<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>
|
|
40
40
|
<span @click="logout" 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">Sign out</span>
|
|
41
41
|
</div>
|
|
42
42
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const api = require('../api');
|
|
4
4
|
const mothership = require('../mothership');
|
|
5
5
|
const template = require('./navbar.html');
|
|
6
|
-
const routes = require('../routes');
|
|
6
|
+
const { routes, hasAccess } = require('../routes');
|
|
7
7
|
|
|
8
8
|
const appendCSS = require('../appendCSS');
|
|
9
9
|
|
|
@@ -14,6 +14,15 @@ module.exports = app => app.component('navbar', {
|
|
|
14
14
|
props: ['user', 'roles'],
|
|
15
15
|
inject: ['state'],
|
|
16
16
|
data: () => ({ showFlyout: false }),
|
|
17
|
+
mounted: function() {
|
|
18
|
+
// Redirect to first allowed route if current route is not allowed
|
|
19
|
+
if (!this.hasAccess(this.roles, this.$route.name)) {
|
|
20
|
+
const firstAllowedRoute = this.allowedRoutes[0];
|
|
21
|
+
if (firstAllowedRoute) {
|
|
22
|
+
this.$router.push({ name: firstAllowedRoute.name });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
17
26
|
computed: {
|
|
18
27
|
dashboardView() {
|
|
19
28
|
return routes.filter(x => x.name.startsWith('dashboard')).map(x => x.name).includes(this.$route.name)
|
|
@@ -31,10 +40,19 @@ module.exports = app => app.component('navbar', {
|
|
|
31
40
|
return mothership.hasAPIKey;
|
|
32
41
|
},
|
|
33
42
|
canViewTeam() {
|
|
34
|
-
return this.
|
|
43
|
+
return this.hasAccess(this.roles, 'team');
|
|
44
|
+
},
|
|
45
|
+
allowedRoutes() {
|
|
46
|
+
return routes.filter(route => this.hasAccess(this.roles, route.name));
|
|
47
|
+
},
|
|
48
|
+
defaultRoute() {
|
|
49
|
+
return this.allowedRoutes[0]?.name || 'dashboards';
|
|
35
50
|
}
|
|
36
51
|
},
|
|
37
52
|
methods: {
|
|
53
|
+
hasAccess(roles, routeName) {
|
|
54
|
+
return hasAccess(roles, routeName);
|
|
55
|
+
},
|
|
38
56
|
async loginWithGithub() {
|
|
39
57
|
const { url } = await mothership.githubLogin();
|
|
40
58
|
window.location.href = url;
|