@mongoosejs/studio 0.0.70 → 0.0.71
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 +149 -42
- package/frontend/public/tw.css +22 -0
- package/frontend/src/api.js +53 -7
- package/frontend/src/document/document.html +6 -0
- package/frontend/src/document/document.js +9 -1
- package/frontend/src/export-query-results/export-query-results.html +6 -4
- package/frontend/src/index.js +3 -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
|
@@ -11,11 +11,18 @@ const CreateDocumentParams = new Archetype({
|
|
|
11
11
|
data: {
|
|
12
12
|
$type: Archetype.Any,
|
|
13
13
|
$required: true
|
|
14
|
+
},
|
|
15
|
+
roles: {
|
|
16
|
+
$type: ['string'],
|
|
14
17
|
}
|
|
15
18
|
}).compile('CreateDocumentParams');
|
|
16
19
|
|
|
17
20
|
module.exports = ({ db }) => async function CreateDocument(params) {
|
|
18
|
-
const { model, data } = new CreateDocumentParams(params);
|
|
21
|
+
const { model, data, roles } = new CreateDocumentParams(params);
|
|
22
|
+
|
|
23
|
+
if (roles && roles.includes('readonly')) {
|
|
24
|
+
throw new Error('Not authorized');
|
|
25
|
+
}
|
|
19
26
|
|
|
20
27
|
const Model = db.models[model];
|
|
21
28
|
if (Model == null) {
|
|
@@ -10,13 +10,20 @@ const DeleteDocumentParams = new Archetype({
|
|
|
10
10
|
documentId: {
|
|
11
11
|
$type: 'string',
|
|
12
12
|
$required: true
|
|
13
|
+
},
|
|
14
|
+
roles: {
|
|
15
|
+
$type: ['string'],
|
|
13
16
|
}
|
|
14
17
|
}).compile('DeleteDocumentParams');
|
|
15
18
|
|
|
16
19
|
module.exports = ({ db }) => async function DeleteDocument(params) {
|
|
17
|
-
const { model, documentId } = new DeleteDocumentParams(params);
|
|
20
|
+
const { model, documentId, roles } = new DeleteDocumentParams(params);
|
|
18
21
|
|
|
19
22
|
const Model = db.models[model];
|
|
23
|
+
|
|
24
|
+
if (roles && roles.includes('readonly')) {
|
|
25
|
+
throw new Error('Not authorized');
|
|
26
|
+
}
|
|
20
27
|
if (Model == null) {
|
|
21
28
|
throw new Error(`Model ${model} not found`);
|
|
22
29
|
}
|
|
@@ -24,7 +24,7 @@ const GetDocumentsParams = new Archetype({
|
|
|
24
24
|
}
|
|
25
25
|
}).compile('GetDocumentsParams');
|
|
26
26
|
|
|
27
|
-
module.exports = ({ db }) => async function
|
|
27
|
+
module.exports = ({ db }) => async function exportQueryResults(params, req, res) {
|
|
28
28
|
params = new GetDocumentsParams(params);
|
|
29
29
|
let { filter } = params;
|
|
30
30
|
const { model, propertiesToInclude } = params;
|
|
@@ -57,10 +57,10 @@ module.exports = ({ db }) => async function getDocuments(params, req, res) {
|
|
|
57
57
|
}));
|
|
58
58
|
}
|
|
59
59
|
const csv = stringify(rows);
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
res.set({
|
|
62
62
|
'Content-Type': 'text/csv',
|
|
63
63
|
'Content-Disposition': `attachment; filename="${model.toLowerCase()}-export.csv"`,
|
|
64
64
|
});
|
|
65
65
|
return csv;
|
|
66
|
-
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Archetype = require('archetype');
|
|
4
|
+
|
|
5
|
+
const GetDocumentsParams = new Archetype({
|
|
6
|
+
model: {
|
|
7
|
+
$type: 'string',
|
|
8
|
+
$required: true
|
|
9
|
+
},
|
|
10
|
+
}).compile('GetDocumentsParams');
|
|
11
|
+
|
|
12
|
+
module.exports = ({ db }) => async function getIndexes(params) {
|
|
13
|
+
params = new GetDocumentsParams(params);
|
|
14
|
+
|
|
15
|
+
const { model } = params;
|
|
16
|
+
|
|
17
|
+
const Model = db.models[model];
|
|
18
|
+
if (Model == null) {
|
|
19
|
+
throw new Error(`Model ${model} not found`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const mongoDBIndexes = await Model.listIndexes();
|
|
23
|
+
const schemaIndexes = Model.schema.indexes().map(([fields, options]) => ({
|
|
24
|
+
key: fields,
|
|
25
|
+
name: Object.keys(fields).map(key => `${key}_${fields[key]}`).join("_")
|
|
26
|
+
}));
|
|
27
|
+
const diffIndexes = await Model.diffIndexes();
|
|
28
|
+
return {
|
|
29
|
+
mongoDBIndexes,
|
|
30
|
+
schemaIndexes,
|
|
31
|
+
diffIndexes
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -5,5 +5,6 @@ exports.deleteDocument = require('./deleteDocument');
|
|
|
5
5
|
exports.exportQueryResults = require('./exportQueryResults');
|
|
6
6
|
exports.getDocument = require('./getDocument');
|
|
7
7
|
exports.getDocuments = require('./getDocuments');
|
|
8
|
+
exports.getIndexes = require('./getIndexes');
|
|
8
9
|
exports.listModels = require('./listModels');
|
|
9
10
|
exports.updateDocument = require('./updateDocument');
|
|
@@ -14,12 +14,18 @@ const UpdateDocumentsParams = new Archetype({
|
|
|
14
14
|
update: {
|
|
15
15
|
$type: Object,
|
|
16
16
|
$required: true
|
|
17
|
+
},
|
|
18
|
+
roles: {
|
|
19
|
+
$type: ['string'],
|
|
17
20
|
}
|
|
18
21
|
}).compile('UpdateDocumentsParams');
|
|
19
22
|
|
|
20
23
|
module.exports = ({ db }) => async function updateDocument(params) {
|
|
21
|
-
const { model, _id, update } = new UpdateDocumentsParams(params);
|
|
24
|
+
const { model, _id, update, roles } = new UpdateDocumentsParams(params);
|
|
22
25
|
|
|
26
|
+
if (roles && roles.includes('readonly')) {
|
|
27
|
+
throw new Error('Not authorized');
|
|
28
|
+
}
|
|
23
29
|
const Model = db.models[model];
|
|
24
30
|
if (Model == null) {
|
|
25
31
|
throw new Error(`Model ${model} not found`);
|
package/express.js
CHANGED
package/frontend/public/app.js
CHANGED
|
@@ -127,6 +127,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
127
127
|
getDocuments: function getDocuments(params) {
|
|
128
128
|
return client.post('/Model/getDocuments', params).then(res => res.data);
|
|
129
129
|
},
|
|
130
|
+
getIndexes: function getIndexes(params) {
|
|
131
|
+
return client.post('/Model/getIndexes', params).then(res => res.data);
|
|
132
|
+
},
|
|
130
133
|
listModels: function listModels() {
|
|
131
134
|
return client.post('/Model/listModels', {}).then(res => res.data);
|
|
132
135
|
},
|
|
@@ -930,7 +933,7 @@ appendCSS(__webpack_require__(/*! ./document.css */ "./frontend/src/document/doc
|
|
|
930
933
|
|
|
931
934
|
module.exports = app => app.component('document', {
|
|
932
935
|
template: template,
|
|
933
|
-
props: ['model', 'documentId'],
|
|
936
|
+
props: ['model', 'documentId', 'user', 'roles'],
|
|
934
937
|
data: () => ({
|
|
935
938
|
schemaPaths: [],
|
|
936
939
|
status: 'init',
|
|
@@ -958,6 +961,14 @@ module.exports = app => app.component('document', {
|
|
|
958
961
|
}).map(key => schemaPaths[key]);
|
|
959
962
|
this.status = 'loaded';
|
|
960
963
|
},
|
|
964
|
+
computed: {
|
|
965
|
+
canManipulate() {
|
|
966
|
+
if (!this.roles) {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
return !this.roles.includes('readonly');
|
|
970
|
+
}
|
|
971
|
+
},
|
|
961
972
|
methods: {
|
|
962
973
|
cancelEdit() {
|
|
963
974
|
this.changes = {};
|
|
@@ -1648,7 +1659,7 @@ const limit = 20;
|
|
|
1648
1659
|
|
|
1649
1660
|
module.exports = app => app.component('models', {
|
|
1650
1661
|
template: template,
|
|
1651
|
-
props: ['model'],
|
|
1662
|
+
props: ['model', 'user', 'roles'],
|
|
1652
1663
|
data: () => ({
|
|
1653
1664
|
models: [],
|
|
1654
1665
|
currentModel: null,
|
|
@@ -1657,6 +1668,8 @@ module.exports = app => app.component('models', {
|
|
|
1657
1668
|
filteredPaths: [],
|
|
1658
1669
|
selectedPaths: [],
|
|
1659
1670
|
numDocuments: 0,
|
|
1671
|
+
mongoDBIndexes: [],
|
|
1672
|
+
schemaIndexes: [],
|
|
1660
1673
|
status: 'loading',
|
|
1661
1674
|
loadedAllDocs: false,
|
|
1662
1675
|
edittingDoc: null,
|
|
@@ -1666,6 +1679,7 @@ module.exports = app => app.component('models', {
|
|
|
1666
1679
|
shouldShowExportModal: false,
|
|
1667
1680
|
shouldShowCreateModal: false,
|
|
1668
1681
|
shouldShowFieldModal: false,
|
|
1682
|
+
shouldShowIndexModal: false,
|
|
1669
1683
|
shouldExport: {},
|
|
1670
1684
|
sortBy: {},
|
|
1671
1685
|
query: {},
|
|
@@ -1712,6 +1726,28 @@ module.exports = app => app.component('models', {
|
|
|
1712
1726
|
this.status = 'loaded';
|
|
1713
1727
|
},
|
|
1714
1728
|
methods: {
|
|
1729
|
+
clickFilter(path) {
|
|
1730
|
+
if (this.searchText) {
|
|
1731
|
+
if (this.searchText.endsWith('}')) {
|
|
1732
|
+
this.searchText = this.searchText.slice(0, -1) + `, ${path}: }`;
|
|
1733
|
+
} else {
|
|
1734
|
+
this.searchText += `, ${path}: }`;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
} else {
|
|
1738
|
+
// If this.searchText is empty or undefined, initialize it with a new object
|
|
1739
|
+
this.searchText = `{ ${path}: }`;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
this.$nextTick(() => {
|
|
1744
|
+
const input = this.$refs.searchInput;
|
|
1745
|
+
const cursorIndex = this.searchText.lastIndexOf(":") + 2; // Move cursor after ": "
|
|
1746
|
+
|
|
1747
|
+
input.focus();
|
|
1748
|
+
input.setSelectionRange(cursorIndex, cursorIndex);
|
|
1749
|
+
});
|
|
1750
|
+
},
|
|
1715
1751
|
async closeCreationModal() {
|
|
1716
1752
|
this.shouldShowCreateModal = false;
|
|
1717
1753
|
await this.getDocuments();
|
|
@@ -1778,6 +1814,21 @@ module.exports = app => app.component('models', {
|
|
|
1778
1814
|
}
|
|
1779
1815
|
await this.loadMoreDocuments();
|
|
1780
1816
|
},
|
|
1817
|
+
async openIndexModal() {
|
|
1818
|
+
this.shouldShowIndexModal = true;
|
|
1819
|
+
const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel })
|
|
1820
|
+
this.mongoDBIndexes = mongoDBIndexes;
|
|
1821
|
+
this.schemaIndexes = schemaIndexes;
|
|
1822
|
+
},
|
|
1823
|
+
checkIndexLocation(indexName) {
|
|
1824
|
+
if (this.schemaIndexes.find(x => x.name == indexName) && this.mongoDBIndexes.find(x => x.name == indexName)) {
|
|
1825
|
+
return 'text-gray-500'
|
|
1826
|
+
} else if (this.schemaIndexes.find(x => x.name == indexName)) {
|
|
1827
|
+
return 'text-forest-green-500'
|
|
1828
|
+
} else {
|
|
1829
|
+
return 'text-valencia-500'
|
|
1830
|
+
}
|
|
1831
|
+
},
|
|
1781
1832
|
async getDocuments() {
|
|
1782
1833
|
const { docs, schemaPaths, numDocs } = await api.Model.getDocuments({
|
|
1783
1834
|
model: this.currentModel,
|
|
@@ -1804,7 +1855,6 @@ module.exports = app => app.component('models', {
|
|
|
1804
1855
|
for (const { path } of this.schemaPaths) {
|
|
1805
1856
|
this.shouldExport[path] = true;
|
|
1806
1857
|
}
|
|
1807
|
-
|
|
1808
1858
|
this.filteredPaths = [...this.schemaPaths];
|
|
1809
1859
|
this.selectedPaths = [...this.schemaPaths];
|
|
1810
1860
|
},
|
|
@@ -1985,7 +2035,7 @@ exports.hasAPIKey = client.hasAPIKey;
|
|
|
1985
2035
|
const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
|
|
1986
2036
|
const mothership = __webpack_require__(/*! ../mothership */ "./frontend/src/mothership.js");
|
|
1987
2037
|
const template = __webpack_require__(/*! ./navbar.html */ "./frontend/src/navbar/navbar.html");
|
|
1988
|
-
const routes = __webpack_require__(/*! ../routes */ "./frontend/src/routes.js");
|
|
2038
|
+
const { routes, hasAccess } = __webpack_require__(/*! ../routes */ "./frontend/src/routes.js");
|
|
1989
2039
|
|
|
1990
2040
|
const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
|
|
1991
2041
|
|
|
@@ -1996,6 +2046,15 @@ module.exports = app => app.component('navbar', {
|
|
|
1996
2046
|
props: ['user', 'roles'],
|
|
1997
2047
|
inject: ['state'],
|
|
1998
2048
|
data: () => ({ showFlyout: false }),
|
|
2049
|
+
mounted: function() {
|
|
2050
|
+
// Redirect to first allowed route if current route is not allowed
|
|
2051
|
+
if (!this.hasAccess(this.roles, this.$route.name)) {
|
|
2052
|
+
const firstAllowedRoute = this.allowedRoutes[0];
|
|
2053
|
+
if (firstAllowedRoute) {
|
|
2054
|
+
this.$router.push({ name: firstAllowedRoute.name });
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
},
|
|
1999
2058
|
computed: {
|
|
2000
2059
|
dashboardView() {
|
|
2001
2060
|
return routes.filter(x => x.name.startsWith('dashboard')).map(x => x.name).includes(this.$route.name)
|
|
@@ -2013,10 +2072,19 @@ module.exports = app => app.component('navbar', {
|
|
|
2013
2072
|
return mothership.hasAPIKey;
|
|
2014
2073
|
},
|
|
2015
2074
|
canViewTeam() {
|
|
2016
|
-
return this.
|
|
2075
|
+
return this.hasAccess(this.roles, 'team');
|
|
2076
|
+
},
|
|
2077
|
+
allowedRoutes() {
|
|
2078
|
+
return routes.filter(route => this.hasAccess(this.roles, route.name));
|
|
2079
|
+
},
|
|
2080
|
+
defaultRoute() {
|
|
2081
|
+
return this.allowedRoutes[0]?.name || 'dashboards';
|
|
2017
2082
|
}
|
|
2018
2083
|
},
|
|
2019
2084
|
methods: {
|
|
2085
|
+
hasAccess(roles, routeName) {
|
|
2086
|
+
return hasAccess(roles, routeName);
|
|
2087
|
+
},
|
|
2020
2088
|
async loginWithGithub() {
|
|
2021
2089
|
const { url } = await mothership.githubLogin();
|
|
2022
2090
|
window.location.href = url;
|
|
@@ -2062,38 +2130,76 @@ module.exports = app => app.component('navbar', {
|
|
|
2062
2130
|
"use strict";
|
|
2063
2131
|
|
|
2064
2132
|
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2133
|
+
// Role-based access control configuration
|
|
2134
|
+
const roleAccess = {
|
|
2135
|
+
owner: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team'],
|
|
2136
|
+
admin: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team'],
|
|
2137
|
+
member: ['root', 'model', 'document', 'dashboards', 'dashboard'],
|
|
2138
|
+
readonly: ['root', 'model', 'document'],
|
|
2139
|
+
dashboards: ['dashboards', 'dashboard']
|
|
2140
|
+
};
|
|
2141
|
+
|
|
2142
|
+
// Helper function to check if a role has access to a route
|
|
2143
|
+
function hasAccess(roles, routeName) {
|
|
2144
|
+
// change to true for local development
|
|
2145
|
+
if (!roles) return false;
|
|
2146
|
+
return roles.some(role => roleAccess[role]?.includes(routeName));
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
module.exports = {
|
|
2150
|
+
routes: [
|
|
2151
|
+
{
|
|
2152
|
+
path: '/',
|
|
2153
|
+
name: 'root',
|
|
2154
|
+
component: 'models',
|
|
2155
|
+
meta: {
|
|
2156
|
+
authorized: true
|
|
2157
|
+
}
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
path: '/model/:model',
|
|
2161
|
+
name: 'model',
|
|
2162
|
+
component: 'models',
|
|
2163
|
+
meta: {
|
|
2164
|
+
authorized: true
|
|
2165
|
+
}
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
path: '/model/:model/document/:documentId',
|
|
2169
|
+
name: 'document',
|
|
2170
|
+
component: 'document',
|
|
2171
|
+
meta: {
|
|
2172
|
+
authorized: true
|
|
2173
|
+
}
|
|
2174
|
+
},
|
|
2175
|
+
{
|
|
2176
|
+
path: '/dashboards',
|
|
2177
|
+
name: 'dashboards',
|
|
2178
|
+
component: 'dashboards',
|
|
2179
|
+
meta: {
|
|
2180
|
+
authorized: true
|
|
2181
|
+
}
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
path: '/dashboard/:dashboardId',
|
|
2185
|
+
name: 'dashboard',
|
|
2186
|
+
component: 'dashboard',
|
|
2187
|
+
meta: {
|
|
2188
|
+
authorized: true
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
path: '/team',
|
|
2193
|
+
name: 'team',
|
|
2194
|
+
component: 'team',
|
|
2195
|
+
meta: {
|
|
2196
|
+
authorized: true
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
],
|
|
2200
|
+
roleAccess,
|
|
2201
|
+
hasAccess
|
|
2202
|
+
};
|
|
2097
2203
|
|
|
2098
2204
|
|
|
2099
2205
|
/***/ }),
|
|
@@ -3058,7 +3164,7 @@ module.exports = ".document {\n max-width: 1200px;\n margin-left: auto;\n mar
|
|
|
3058
3164
|
/***/ ((module) => {
|
|
3059
3165
|
|
|
3060
3166
|
"use strict";
|
|
3061
|
-
module.exports = "<div class=\"document\">\n <div class=\"document-menu\">\n <div class=\"left\">\n <button\n @click=\"$router.push('/model/' + this.model)\"\n class=\"rounded-md bg-gray-400 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n ‹ Back\n </button>\n </div>\n\n <div class=\"right\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true\"\n type=\"button\"\n 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\">\n <img src=\"images/edit.svg\" class=\"inline\" /> Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false\"\n type=\"button\"\n class=\"rounded-md bg-slate-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n × Cancel\n </button>\n <button\n v-if=\"editting\"\n @click=\"shouldShowConfirmModal=true;\"\n type=\"button\"\n 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\">\n <img src=\"images/save.svg\" class=\"inline\" /> Save\n </button>\n <button\n @click=\"shouldShowDeleteModal=true;\"\n type=\"button\"\n 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\">\n <img src=\"images/delete.svg\" class=\"inline\" /> Delete\n </button>\n </div>\n </div>\n <div v-if=\"status === 'loaded'\">\n <document-details\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"></document-details>\n <modal v-if=\"shouldShowConfirmModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">×</div>\n <confirm-changes @close=\"shouldShowConfirmModal = false;\" @save=\"save\" :value=\"changes\"></confirm-changes>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">×</div>\n <confirm-delete @close=\"shouldShowConfirmModal = false;\" @remove=\"remove\" :value=\"document\"></confirm-delete>\n </template>\n </modal>\n </div>\n</div>\n";
|
|
3167
|
+
module.exports = "<div class=\"document\">\n <div class=\"document-menu\">\n <div class=\"left\">\n <button\n @click=\"$router.push('/model/' + this.model)\"\n class=\"rounded-md bg-gray-400 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n ‹ Back\n </button>\n </div>\n\n <div class=\"right\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n type=\"button\"\n 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\">\n <img src=\"images/edit.svg\" class=\"inline\" /> Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false\"\n type=\"button\"\n class=\"rounded-md bg-slate-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n × Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n @click=\"shouldShowConfirmModal=true;\"\n type=\"button\"\n 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\">\n <img src=\"images/save.svg\" class=\"inline\" /> Save\n </button>\n <button\n @click=\"shouldShowDeleteModal=true;\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n type=\"button\"\n 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\">\n <img src=\"images/delete.svg\" class=\"inline\" /> Delete\n </button>\n </div>\n </div>\n <div v-if=\"status === 'loaded'\">\n <document-details\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"></document-details>\n <modal v-if=\"shouldShowConfirmModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">×</div>\n <confirm-changes @close=\"shouldShowConfirmModal = false;\" @save=\"save\" :value=\"changes\"></confirm-changes>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">×</div>\n <confirm-delete @close=\"shouldShowConfirmModal = false;\" @remove=\"remove\" :value=\"document\"></confirm-delete>\n </template>\n </modal>\n </div>\n</div>\n";
|
|
3062
3168
|
|
|
3063
3169
|
/***/ }),
|
|
3064
3170
|
|
|
@@ -3322,7 +3428,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
|
|
|
3322
3428
|
/***/ ((module) => {
|
|
3323
3429
|
|
|
3324
3430
|
"use strict";
|
|
3325
|
-
module.exports = "<div class=\"models\">\n <div>\n <div class=\"flex grow flex-col gap-y-5 overflow-auto border-r border-gray-200 bg-white px-2 h-[calc(100vh-55px)] w-48\">\n <div class=\"flex font-bold font-xl mt-4 pl-2\">\n Models\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </div>\n\n </div>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px]\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"flex-grow m-0\">\n <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\" />\n </form>\n <div>\n <span v-if=\"status === 'loading'\">Loading ...</span>\n <span v-if=\"status === 'loaded'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n 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\">\n Export\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n 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\">\n Create\n </button>\n <button\n @click=\"openFieldSelection\"\n type=\"button\"\n 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\">\n Fields\n </button>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"outputType = 'table'\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"outputType = 'json'\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container relative\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"$router.push('/model/' + currentModel + '/document/' + document._id)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\n <div v-if=\"outputType === 'json'\">\n <div v-for=\"document in documents\" @click=\"$router.push('/model/' + currentModel + '/document/' + document._id)\" :key=\"document._id\">\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"loader\">\n <img src=\"images/loader.gif\">\n </div>\n </div>\n </div>\n
|
|
3431
|
+
module.exports = "<div class=\"models\">\n <div>\n <div class=\"flex grow flex-col gap-y-5 overflow-auto border-r border-gray-200 bg-white px-2 h-[calc(100vh-55px)] w-48\">\n <div class=\"flex font-bold font-xl mt-4 pl-2\">\n Models\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </div>\n\n </div>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px]\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"flex-grow m-0\">\n <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\" />\n </form>\n <div>\n <span v-if=\"status === 'loading'\">Loading ...</span>\n <span v-if=\"status === 'loaded'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n 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\">\n Export\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n 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\">\n Indexes\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n 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\">\n Create\n </button>\n <button\n @click=\"openFieldSelection\"\n type=\"button\"\n 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\">\n Fields\n </button>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"outputType = 'table'\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"outputType = 'json'\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container relative\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\" @click=\"clickFilter(path.path)\" class=\"cursor-pointer\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"$router.push('/model/' + currentModel + '/document/' + document._id)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\n <div v-if=\"outputType === 'json'\">\n <div v-for=\"document in documents\" @click=\"$router.push('/model/' + currentModel + '/document/' + document._id)\" :key=\"document._id\">\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"loader\">\n <img src=\"images/loader.gif\">\n </div>\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">×</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :filter=\"filter\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowIndexModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowIndexModal = false\">×</div>\n <div class=\"text-xl font-bold mb-2\">Indexes</div>\n <div v-for=\"index in mongoDBIndexes\" class=\"w-full flex items-center\">\n <div class=\"grow shrink text-left flex justify-between items-center\" v-if=\"index.name != '_id_'\">\n <div>\n <div class=\"font-bold\">{{ index.name }}</div>\n <div class=\"text-sm font-mono\">{{ JSON.stringify(index.key) }}</div>\n </div>\n <div>\n <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>\n </div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">×</div>\n <div v-for=\"(path, index) in schemaPaths\" :key=\"index\" class=\"w-5 flex items-center\">\n <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)\" />\n <div class=\"ml-2 text-gray-700 grow shrink text-left\">\n <label :for=\"'path' + index\">{{path.path}}</label>\n </div>\n </div>\n <div class=\"mt-4 flex gap-2\">\n <button type=\"submit\" @click=\"filterDocuments()\" 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\">Filter Selection</button>\n <button type=\"submit\" @click=\"deselectAll()\" 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\">Deselect All</button>\n <button type=\"submit\" @click=\"resetDocuments()\" class=\"rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\" >Cancel</button>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">×</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n</div>\n";
|
|
3326
3432
|
|
|
3327
3433
|
/***/ }),
|
|
3328
3434
|
|
|
@@ -3344,7 +3450,7 @@ module.exports = ".navbar {\n width: 100%;\n background-color: #eee;\n}\n\n.ac
|
|
|
3344
3450
|
/***/ ((module) => {
|
|
3345
3451
|
|
|
3346
3452
|
"use strict";
|
|
3347
|
-
module.exports = "<div class=\"navbar\">\n <div class=\"nav-left flex items-center gap-4 h-full\">\n <router-link to=\"
|
|
3453
|
+
module.exports = "<div class=\"navbar\">\n <div class=\"nav-left flex items-center gap-4 h-full\">\n <router-link :to=\"{ name: defaultRoute }\">\n <img src=\"images/logo.svg\" alt=\"Mongoose Studio Logo\" />\n </router-link>\n <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'\">\n {{state.nodeEnv}}\n </div>\n </div>\n <div class=\"nav-right h-full\">\n <div class=\"sm:ml-6 sm:flex sm:space-x-8 h-full\">\n <a v-if=\"hasAccess(roles, 'root')\"\n href=\"#/\"\n class=\"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium\"\n :class=\"documentView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'\">Documents</a>\n <a v-if=\"hasAccess(roles, 'dashboards')\"\n href=\"#/dashboards\"\n class=\"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium\"\n :class=\"dashboardView ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'\">Dashboards</a>\n\n <div class=\"h-full flex items-center\" v-if=\"!user && hasAPIKey\">\n <button\n type=\"button\"\n @click=\"loginWithGithub\"\n 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\">\n Login\n </button>\n </div>\n <div v-if=\"user && hasAPIKey\" class=\"h-full flex items-center relative\" v-clickOutside=\"hideFlyout\">\n <div>\n <button type=\"button\" @click=\"showFlyout = !showFlyout\" class=\"relative flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800\" id=\"user-menu-button\" aria-expanded=\"false\" aria-haspopup=\"true\">\n <span class=\"absolute -inset-1.5\"></span>\n <span class=\"sr-only\">Open user menu</span>\n <img class=\"size-8 rounded-full\" :src=\"user.picture\" alt=\"\">\n </button>\n </div>\n\n <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\">\n <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>\n <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>\n </div>\n </div>\n\n </div>\n </div>\n <div style=\"clear: both\"></div>\n</div>\n";
|
|
3348
3454
|
|
|
3349
3455
|
/***/ }),
|
|
3350
3456
|
|
|
@@ -11252,7 +11358,7 @@ app.component('app-component', {
|
|
|
11252
11358
|
<div v-else-if="!hasAPIKey || user">
|
|
11253
11359
|
<navbar :user="user" :roles="roles" />
|
|
11254
11360
|
<div class="view">
|
|
11255
|
-
<router-view :key="$route.fullPath" />
|
|
11361
|
+
<router-view :key="$route.fullPath" :user="user" :roles="roles" />
|
|
11256
11362
|
</div>
|
|
11257
11363
|
</div>
|
|
11258
11364
|
</div>
|
|
@@ -11272,6 +11378,7 @@ app.component('app-component', {
|
|
|
11272
11378
|
},
|
|
11273
11379
|
async mounted() {
|
|
11274
11380
|
window.$router = this.$router;
|
|
11381
|
+
window.state = this;
|
|
11275
11382
|
|
|
11276
11383
|
if (mothership.hasAPIKey) {
|
|
11277
11384
|
const href = window.location.href;
|
|
@@ -11329,7 +11436,7 @@ app.component('app-component', {
|
|
|
11329
11436
|
}
|
|
11330
11437
|
});
|
|
11331
11438
|
|
|
11332
|
-
const routes = __webpack_require__(/*! ./routes */ "./frontend/src/routes.js");
|
|
11439
|
+
const { routes } = __webpack_require__(/*! ./routes */ "./frontend/src/routes.js");
|
|
11333
11440
|
const router = VueRouter.createRouter({
|
|
11334
11441
|
history: VueRouter.createWebHashHistory(),
|
|
11335
11442
|
routes: routes.map(route => ({
|
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
|
}
|
|
@@ -1409,6 +1413,10 @@ video {
|
|
|
1409
1413
|
font-family: 'Lato';
|
|
1410
1414
|
}
|
|
1411
1415
|
|
|
1416
|
+
.font-mono {
|
|
1417
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1412
1420
|
.text-base {
|
|
1413
1421
|
font-size: 1rem;
|
|
1414
1422
|
line-height: 1.5rem;
|
|
@@ -1473,6 +1481,11 @@ video {
|
|
|
1473
1481
|
color: rgb(0 0 0 / var(--tw-text-opacity));
|
|
1474
1482
|
}
|
|
1475
1483
|
|
|
1484
|
+
.text-forest-green-500 {
|
|
1485
|
+
--tw-text-opacity: 1;
|
|
1486
|
+
color: rgb(0 242 58 / var(--tw-text-opacity));
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1476
1489
|
.text-gray-400 {
|
|
1477
1490
|
--tw-text-opacity: 1;
|
|
1478
1491
|
color: rgb(156 163 175 / var(--tw-text-opacity));
|
|
@@ -1542,6 +1555,10 @@ video {
|
|
|
1542
1555
|
accent-color: #0284c7;
|
|
1543
1556
|
}
|
|
1544
1557
|
|
|
1558
|
+
.opacity-50 {
|
|
1559
|
+
opacity: 0.5;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1545
1562
|
.shadow-lg {
|
|
1546
1563
|
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
1547
1564
|
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
|
@@ -1894,6 +1911,11 @@ video {
|
|
|
1894
1911
|
cursor: not-allowed;
|
|
1895
1912
|
}
|
|
1896
1913
|
|
|
1914
|
+
.disabled\:bg-gray-400:disabled {
|
|
1915
|
+
--tw-bg-opacity: 1;
|
|
1916
|
+
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1897
1919
|
.disabled\:bg-gray-500:disabled {
|
|
1898
1920
|
--tw-bg-opacity: 1;
|
|
1899
1921
|
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
|
},
|
|
@@ -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,6 +36,8 @@
|
|
|
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
|
|
@@ -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',
|
|
@@ -39,6 +39,14 @@ module.exports = app => app.component('document', {
|
|
|
39
39
|
}).map(key => schemaPaths[key]);
|
|
40
40
|
this.status = 'loaded';
|
|
41
41
|
},
|
|
42
|
+
computed: {
|
|
43
|
+
canManipulate() {
|
|
44
|
+
if (!this.roles) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return !this.roles.includes('readonly');
|
|
48
|
+
}
|
|
49
|
+
},
|
|
42
50
|
methods: {
|
|
43
51
|
cancelEdit() {
|
|
44
52
|
this.changes = {};
|
|
@@ -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
|
@@ -58,7 +58,7 @@ app.component('app-component', {
|
|
|
58
58
|
<div v-else-if="!hasAPIKey || user">
|
|
59
59
|
<navbar :user="user" :roles="roles" />
|
|
60
60
|
<div class="view">
|
|
61
|
-
<router-view :key="$route.fullPath" />
|
|
61
|
+
<router-view :key="$route.fullPath" :user="user" :roles="roles" />
|
|
62
62
|
</div>
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
@@ -78,6 +78,7 @@ app.component('app-component', {
|
|
|
78
78
|
},
|
|
79
79
|
async mounted() {
|
|
80
80
|
window.$router = this.$router;
|
|
81
|
+
window.state = this;
|
|
81
82
|
|
|
82
83
|
if (mothership.hasAPIKey) {
|
|
83
84
|
const href = window.location.href;
|
|
@@ -135,7 +136,7 @@ app.component('app-component', {
|
|
|
135
136
|
}
|
|
136
137
|
});
|
|
137
138
|
|
|
138
|
-
const routes = require('./routes');
|
|
139
|
+
const { routes } = require('./routes');
|
|
139
140
|
const router = VueRouter.createRouter({
|
|
140
141
|
history: VueRouter.createWebHashHistory(),
|
|
141
142
|
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;
|
package/frontend/src/routes.js
CHANGED
|
@@ -1,34 +1,72 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
3
|
+
// Role-based access control configuration
|
|
4
|
+
const roleAccess = {
|
|
5
|
+
owner: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team'],
|
|
6
|
+
admin: ['root', 'model', 'document', 'dashboards', 'dashboard', 'team'],
|
|
7
|
+
member: ['root', 'model', 'document', 'dashboards', 'dashboard'],
|
|
8
|
+
readonly: ['root', 'model', 'document'],
|
|
9
|
+
dashboards: ['dashboards', 'dashboard']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Helper function to check if a role has access to a route
|
|
13
|
+
function hasAccess(roles, routeName) {
|
|
14
|
+
// change to true for local development
|
|
15
|
+
if (!roles) return false;
|
|
16
|
+
return roles.some(role => roleAccess[role]?.includes(routeName));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
routes: [
|
|
21
|
+
{
|
|
22
|
+
path: '/',
|
|
23
|
+
name: 'root',
|
|
24
|
+
component: 'models',
|
|
25
|
+
meta: {
|
|
26
|
+
authorized: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: '/model/:model',
|
|
31
|
+
name: 'model',
|
|
32
|
+
component: 'models',
|
|
33
|
+
meta: {
|
|
34
|
+
authorized: true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path: '/model/:model/document/:documentId',
|
|
39
|
+
name: 'document',
|
|
40
|
+
component: 'document',
|
|
41
|
+
meta: {
|
|
42
|
+
authorized: true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: '/dashboards',
|
|
47
|
+
name: 'dashboards',
|
|
48
|
+
component: 'dashboards',
|
|
49
|
+
meta: {
|
|
50
|
+
authorized: true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
path: '/dashboard/:dashboardId',
|
|
55
|
+
name: 'dashboard',
|
|
56
|
+
component: 'dashboard',
|
|
57
|
+
meta: {
|
|
58
|
+
authorized: true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
path: '/team',
|
|
63
|
+
name: 'team',
|
|
64
|
+
component: 'team',
|
|
65
|
+
meta: {
|
|
66
|
+
authorized: true
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
roleAccess,
|
|
71
|
+
hasAccess
|
|
72
|
+
};
|