@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.
@@ -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 getDocuments(params, req, res) {
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
@@ -63,6 +63,7 @@ module.exports = async function(apiUrl, conn, options) {
63
63
  if (!user || !roles) {
64
64
  throw new Error('Not authorized');
65
65
  }
66
+ req.params.roles = roles;
66
67
 
67
68
  next();
68
69
  })
@@ -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.roles?.includes('owner') || this.roles?.includes('admin');
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
- module.exports = [
2066
- {
2067
- path: '/',
2068
- name: 'root',
2069
- component: 'models'
2070
- },
2071
- {
2072
- path: '/model/:model',
2073
- name: 'model',
2074
- component: 'models'
2075
- },
2076
- {
2077
- path: '/model/:model/document/:documentId',
2078
- name: 'document',
2079
- component: 'document'
2080
- },
2081
- {
2082
- path: '/dashboards',
2083
- name: 'dashboards',
2084
- component: 'dashboards'
2085
- },
2086
- {
2087
- path: '/dashboard/:dashboardId',
2088
- name: 'dashboard',
2089
- component: 'dashboard'
2090
- },
2091
- {
2092
- path: '/team',
2093
- name: 'team',
2094
- component: 'team'
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 &lsaquo; 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 &times; 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;\">&times;</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;\">&times;</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 &lsaquo; 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 &times; 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;\">&times;</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;\">&times;</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\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</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=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">&times;</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;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\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\">&times;</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\">&times;</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];\">&times;</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;\">&times;</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=\"/\">\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\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\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=\"canViewTeam\" @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";
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 => ({
@@ -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));
@@ -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
- return client.post('', { action: 'Model.exportQueryResults', ...params }).then(res => res.data);
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 anchor = document.createElement('a');
108
- anchor.href = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/exportQueryResults?' + (new URLSearchParams(params)).toString();
109
- anchor.target = '_blank';
110
- anchor.download = 'export.csv';
111
- anchor.click();
112
- return;
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
- <span>{{schemaPath.path}}</span>
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>
@@ -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">&times;</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">&times;</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];">&times;</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="canViewTeam" @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>
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.roles?.includes('owner') || this.roles?.includes('admin');
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;
@@ -1,34 +1,72 @@
1
1
  'use strict';
2
2
 
3
- module.exports = [
4
- {
5
- path: '/',
6
- name: 'root',
7
- component: 'models'
8
- },
9
- {
10
- path: '/model/:model',
11
- name: 'model',
12
- component: 'models'
13
- },
14
- {
15
- path: '/model/:model/document/:documentId',
16
- name: 'document',
17
- component: 'document'
18
- },
19
- {
20
- path: '/dashboards',
21
- name: 'dashboards',
22
- component: 'dashboards'
23
- },
24
- {
25
- path: '/dashboard/:dashboardId',
26
- name: 'dashboard',
27
- component: 'dashboard'
28
- },
29
- {
30
- path: '/team',
31
- name: 'team',
32
- component: 'team'
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.70",
3
+ "version": "0.0.71",
4
4
  "dependencies": {
5
5
  "archetype": "0.13.1",
6
6
  "csv-stringify": "6.3.0",