@mongoosejs/studio 0.0.70 → 0.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -909,6 +909,10 @@ video {
909
909
  transform-origin: top right;
910
910
  }
911
911
 
912
+ .cursor-not-allowed {
913
+ cursor: not-allowed;
914
+ }
915
+
912
916
  .cursor-pointer {
913
917
  cursor: pointer;
914
918
  }
@@ -1195,6 +1199,11 @@ video {
1195
1199
  background-color: rgb(22 163 74 / var(--tw-bg-opacity));
1196
1200
  }
1197
1201
 
1202
+ .bg-pink-600 {
1203
+ --tw-bg-opacity: 1;
1204
+ background-color: rgb(219 39 119 / var(--tw-bg-opacity));
1205
+ }
1206
+
1198
1207
  .bg-red-300 {
1199
1208
  --tw-bg-opacity: 1;
1200
1209
  background-color: rgb(252 165 165 / var(--tw-bg-opacity));
@@ -1409,6 +1418,10 @@ video {
1409
1418
  font-family: 'Lato';
1410
1419
  }
1411
1420
 
1421
+ .font-mono {
1422
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1423
+ }
1424
+
1412
1425
  .text-base {
1413
1426
  font-size: 1rem;
1414
1427
  line-height: 1.5rem;
@@ -1473,6 +1486,11 @@ video {
1473
1486
  color: rgb(0 0 0 / var(--tw-text-opacity));
1474
1487
  }
1475
1488
 
1489
+ .text-forest-green-500 {
1490
+ --tw-text-opacity: 1;
1491
+ color: rgb(0 242 58 / var(--tw-text-opacity));
1492
+ }
1493
+
1476
1494
  .text-gray-400 {
1477
1495
  --tw-text-opacity: 1;
1478
1496
  color: rgb(156 163 175 / var(--tw-text-opacity));
@@ -1542,6 +1560,10 @@ video {
1542
1560
  accent-color: #0284c7;
1543
1561
  }
1544
1562
 
1563
+ .opacity-50 {
1564
+ opacity: 0.5;
1565
+ }
1566
+
1545
1567
  .shadow-lg {
1546
1568
  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1547
1569
  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
@@ -1894,6 +1916,11 @@ video {
1894
1916
  cursor: not-allowed;
1895
1917
  }
1896
1918
 
1919
+ .disabled\:bg-gray-400:disabled {
1920
+ --tw-bg-opacity: 1;
1921
+ background-color: rgb(156 163 175 / var(--tw-bg-opacity));
1922
+ }
1923
+
1897
1924
  .disabled\:bg-gray-500:disabled {
1898
1925
  --tw-bg-opacity: 1;
1899
1926
  background-color: rgb(107 114 128 / var(--tw-bg-opacity));
@@ -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
  },
File without changes
@@ -0,0 +1,26 @@
1
+ <div>
2
+ <div class="mb-2">
3
+ <textarea class="border border-gray-200 p-2 h-[300px] w-full" ref="codeEditor"></textarea>
4
+ </div>
5
+ <button @click="cloneDocument()" class="rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600">Submit</button>
6
+ <div v-if="errors.length > 0" class="rounded-md bg-red-50 p-4 mt-1">
7
+ <div class="flex">
8
+ <div class="flex-shrink-0">
9
+ <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
10
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
11
+ </svg>
12
+ </div>
13
+ <div class="ml-3">
14
+ <h3 class="text-sm font-medium text-red-800">There were {{errors.length}} errors with your submission</h3>
15
+ <div class="mt-2 text-sm text-red-700">
16
+ <ul role="list" class="list-disc space-y-1 pl-5">
17
+ <li v-for="error in errors">
18
+ {{error}}
19
+ </li>
20
+ </ul>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const api = require('../api');
4
+
5
+ const { BSON, EJSON } = require('bson');
6
+
7
+ const ObjectId = new Proxy(BSON.ObjectId, {
8
+ apply (target, thisArg, argumentsList) {
9
+ return new target(...argumentsList);
10
+ }
11
+ });
12
+
13
+ const appendCSS = require('../appendCSS');
14
+
15
+ appendCSS(require('./clone-document.css'));
16
+
17
+ const template = require('./clone-document.html')
18
+
19
+ module.exports = app => app.component('clone-document', {
20
+ props: ['currentModel', 'doc', 'schemaPaths'],
21
+ template,
22
+ data: function() {
23
+ return {
24
+ documentData: '',
25
+ editor: null,
26
+ errors: []
27
+ }
28
+ },
29
+ methods: {
30
+ async cloneDocument() {
31
+ const data = EJSON.serialize(eval(`(${this.editor.getValue()})`));
32
+ const { doc } = await api.Model.createDocument({ model: this.currentModel, data }).catch(err => {
33
+ if (err.response?.data?.message) {
34
+ console.log(err.response.data);
35
+ const message = err.response.data.message.split(": ").slice(1).join(": ");
36
+ this.errors = message.split(',').map(error => {
37
+ return error.split(': ').slice(1).join(': ').trim();
38
+ })
39
+ throw new Error(err.response?.data?.message);
40
+ }
41
+ throw err;
42
+ });
43
+ this.errors.length = 0;
44
+ this.$emit('close', doc);
45
+ },
46
+ },
47
+ mounted: function() {
48
+ const pathsToClone = this.schemaPaths.map(x => x.path);
49
+
50
+ // Create a filtered version of the document data
51
+ const filteredDoc = {};
52
+ pathsToClone.forEach(path => {
53
+ const value = this.doc[path];
54
+ if (value !== undefined) {
55
+ filteredDoc[path] = value;
56
+ }
57
+ });
58
+
59
+ // Replace _id with a new ObjectId
60
+ if (pathsToClone.includes('_id')) {
61
+ filteredDoc._id = new ObjectId();
62
+ }
63
+
64
+ this.documentData = JSON.stringify(filteredDoc, null, 2);
65
+ this.$refs.codeEditor.value = this.documentData;
66
+ this.editor = CodeMirror.fromTextArea(this.$refs.codeEditor, {
67
+ mode: 'javascript',
68
+ lineNumbers: true,
69
+ smartIndent: false
70
+ });
71
+ },
72
+ })
@@ -12,6 +12,8 @@
12
12
  <button
13
13
  v-if="!editting"
14
14
  @click="editting = true"
15
+ :disabled="!canManipulate"
16
+ :class="{'cursor-not-allowed opacity-50': !canManipulate}"
15
17
  type="button"
16
18
  class="rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600">
17
19
  <img src="images/edit.svg" class="inline" /> Edit
@@ -25,6 +27,8 @@
25
27
  </button>
26
28
  <button
27
29
  v-if="editting"
30
+ :disabled="!canManipulate"
31
+ :class="{'cursor-not-allowed opacity-50': !canManipulate}"
28
32
  @click="shouldShowConfirmModal=true;"
29
33
  type="button"
30
34
  class="rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600">
@@ -32,10 +36,20 @@
32
36
  </button>
33
37
  <button
34
38
  @click="shouldShowDeleteModal=true;"
39
+ :disabled="!canManipulate"
40
+ :class="{'cursor-not-allowed opacity-50': !canManipulate}"
35
41
  type="button"
36
42
  class="rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">
37
43
  <img src="images/delete.svg" class="inline" /> Delete
38
44
  </button>
45
+ <button
46
+ @click="shouldShowCloneModal=true;"
47
+ :disabled="!canManipulate"
48
+ :class="{'cursor-not-allowed opacity-50': !canManipulate}"
49
+ type="button"
50
+ class="rounded-md bg-pink-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">
51
+ <img src="images/duplicate.svg" class="inline" /> Clone
52
+ </button>
39
53
  </div>
40
54
  </div>
41
55
  <div v-if="status === 'loaded'">
@@ -57,5 +71,11 @@
57
71
  <confirm-delete @close="shouldShowConfirmModal = false;" @remove="remove" :value="document"></confirm-delete>
58
72
  </template>
59
73
  </modal>
74
+ <modal v-if="shouldShowCloneModal">
75
+ <template v-slot:body>
76
+ <div class="modal-exit" @click="shouldShowCloneModal = false;">&times;</div>
77
+ <clone-document :currentModel="model" :doc="document" :schemaPaths="schemaPaths" @close="showClonedDocument"></clone-document>
78
+ </template>
79
+ </modal>
60
80
  </div>
61
81
  </div>
@@ -11,7 +11,7 @@ appendCSS(require('./document.css'));
11
11
 
12
12
  module.exports = app => app.component('document', {
13
13
  template: template,
14
- props: ['model', 'documentId'],
14
+ props: ['model', 'documentId', 'user', 'roles'],
15
15
  data: () => ({
16
16
  schemaPaths: [],
17
17
  status: 'init',
@@ -21,7 +21,8 @@ module.exports = app => app.component('document', {
21
21
  editting: false,
22
22
  virtuals: [],
23
23
  shouldShowConfirmModal: false,
24
- shouldShowDeleteModal: false
24
+ shouldShowDeleteModal: false,
25
+ shouldShowCloneModal: false
25
26
  }),
26
27
  async mounted() {
27
28
  window.pageState = this;
@@ -39,6 +40,14 @@ module.exports = app => app.component('document', {
39
40
  }).map(key => schemaPaths[key]);
40
41
  this.status = 'loaded';
41
42
  },
43
+ computed: {
44
+ canManipulate() {
45
+ if (!this.roles) {
46
+ return false;
47
+ }
48
+ return !this.roles.includes('readonly');
49
+ }
50
+ },
42
51
  methods: {
43
52
  cancelEdit() {
44
53
  this.changes = {};
@@ -74,6 +83,9 @@ module.exports = app => app.component('document', {
74
83
  });
75
84
  this.$router.push({ path: `/model/${this.model}`});
76
85
  }
86
+ },
87
+ showClonedDocument(doc) {
88
+ this.$router.push({ path: `/model/${this.model}/document/${doc._id}`});
77
89
  }
78
90
  }
79
91
  });
@@ -3,9 +3,11 @@
3
3
  <div>
4
4
  Choose fields to export
5
5
  </div>
6
- <div v-for="schemaPath in schemaPaths">
7
- <input type="checkbox" v-model="shouldExport[schemaPath.path]">
8
- <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>
@@ -13,6 +13,7 @@ const app = Vue.createApp({
13
13
  });
14
14
 
15
15
  require('./async-button/async-button')(app);
16
+ require('./clone-document/clone-document')(app);
16
17
  require('./create-dashboard/create-dashboard')(app);
17
18
  require('./create-document/create-document')(app);
18
19
  require('./dashboards/dashboards')(app);
@@ -58,7 +59,7 @@ app.component('app-component', {
58
59
  <div v-else-if="!hasAPIKey || user">
59
60
  <navbar :user="user" :roles="roles" />
60
61
  <div class="view">
61
- <router-view :key="$route.fullPath" />
62
+ <router-view :key="$route.fullPath" :user="user" :roles="roles" />
62
63
  </div>
63
64
  </div>
64
65
  </div>
@@ -78,6 +79,7 @@ app.component('app-component', {
78
79
  },
79
80
  async mounted() {
80
81
  window.$router = this.$router;
82
+ window.state = this;
81
83
 
82
84
  if (mothership.hasAPIKey) {
83
85
  const href = window.location.href;
@@ -135,7 +137,7 @@ app.component('app-component', {
135
137
  }
136
138
  });
137
139
 
138
- const routes = require('./routes');
140
+ const { routes } = require('./routes');
139
141
  const router = VueRouter.createRouter({
140
142
  history: VueRouter.createWebHashHistory(),
141
143
  routes: routes.map(route => ({
@@ -28,7 +28,7 @@
28
28
  <div class="documents-menu">
29
29
  <div class="flex flex-row items-center w-full gap-2">
30
30
  <form @submit.prevent="search" class="flex-grow m-0">
31
- <input class="w-full rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none" type="text" placeholder="Filter or text" v-model="searchText" />
31
+ <input ref="searchInput" class="w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none" type="text" placeholder="Filter or text" v-model="searchText" />
32
32
  </form>
33
33
  <div>
34
34
  <span v-if="status === 'loading'">Loading ...</span>
@@ -40,6 +40,12 @@
40
40
  class="rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
41
41
  Export
42
42
  </button>
43
+ <button
44
+ @click="openIndexModal"
45
+ type="button"
46
+ class="rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
47
+ Indexes
48
+ </button>
43
49
  <button
44
50
  @click="shouldShowCreateModal = true;"
45
51
  type="button"
@@ -74,7 +80,7 @@
74
80
  <div class="documents-container relative">
75
81
  <table v-if="outputType === 'table'">
76
82
  <thead>
77
- <th v-for="path in filteredPaths">
83
+ <th v-for="path in filteredPaths" @click="clickFilter(path.path)" class="cursor-pointer">
78
84
  {{path.path}}
79
85
  <span class="path-type">
80
86
  ({{(path.instance || 'unknown')}})
@@ -106,7 +112,6 @@
106
112
  </div>
107
113
  </div>
108
114
  </div>
109
-
110
115
  <modal v-if="shouldShowExportModal">
111
116
  <template v-slot:body>
112
117
  <div class="modal-exit" @click="shouldShowExportModal = false">&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;