@mongoosejs/studio 0.1.16 → 0.1.18

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.
Files changed (36) hide show
  1. package/backend/actions/Model/getCollectionInfo.js +49 -0
  2. package/backend/actions/Model/index.js +1 -0
  3. package/backend/actions/Model/updateDocument.js +3 -0
  4. package/backend/actions/Model/updateDocuments.js +3 -0
  5. package/frontend/public/app.js +954 -524
  6. package/frontend/public/tw.css +105 -19
  7. package/frontend/src/api.js +7 -1
  8. package/frontend/src/array-utils.js +66 -0
  9. package/frontend/src/chat/chat-message/chat-message.js +7 -0
  10. package/frontend/src/chat/chat-message-script/chat-message-script.js +21 -0
  11. package/frontend/src/chat/chat.js +22 -0
  12. package/frontend/src/clone-document/clone-document.js +14 -5
  13. package/frontend/src/create-dashboard/create-dashboard.js +8 -0
  14. package/frontend/src/create-document/create-document.js +14 -5
  15. package/frontend/src/dashboard/dashboard.js +9 -1
  16. package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +8 -0
  17. package/frontend/src/dashboards/dashboards.js +8 -0
  18. package/frontend/src/detail-array/detail-array.html +25 -3
  19. package/frontend/src/detail-array/detail-array.js +22 -6
  20. package/frontend/src/document/document.js +32 -20
  21. package/frontend/src/document-details/document-details.html +61 -12
  22. package/frontend/src/document-details/document-details.js +29 -13
  23. package/frontend/src/document-details/document-property/document-property.html +41 -3
  24. package/frontend/src/document-details/document-property/document-property.js +47 -2
  25. package/frontend/src/edit-array/edit-array.html +5 -2
  26. package/frontend/src/edit-array/edit-array.js +79 -23
  27. package/frontend/src/export-query-results/export-query-results.js +8 -1
  28. package/frontend/src/index.js +2 -1
  29. package/frontend/src/list-array/list-array.html +1 -1
  30. package/frontend/src/list-array/list-array.js +0 -2
  31. package/frontend/src/models/models.html +76 -8
  32. package/frontend/src/models/models.js +112 -1
  33. package/frontend/src/update-document/update-document.js +28 -27
  34. package/package.json +1 -1
  35. package/frontend/src/edit-array/edit-array.css +0 -3
  36. package/frontend/src/list-array/list-array.css +0 -8
@@ -79,13 +79,6 @@
79
79
  >
80
80
  Delete
81
81
  </button>
82
- <button
83
- @click="openIndexModal"
84
- type="button"
85
- v-show="!selectMultiple"
86
- 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">
87
- Indexes
88
- </button>
89
82
  <button
90
83
  @click="shouldShowCreateModal = true;"
91
84
  type="button"
@@ -100,6 +93,38 @@
100
93
  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">
101
94
  Fields
102
95
  </button>
96
+ <div class="relative" v-show="!selectMultiple" ref="actionsMenuContainer" @keyup.esc.prevent="closeActionsMenu">
97
+ <button
98
+ @click="toggleActionsMenu"
99
+ type="button"
100
+ aria-label="More actions"
101
+ class="rounded bg-white px-2 py-2 text-sm font-semibold text-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
102
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
103
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Zm0 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Zm0 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
104
+ </svg>
105
+ </button>
106
+ <div
107
+ v-if="showActionsMenu"
108
+ class="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-20"
109
+ >
110
+ <div class="py-1">
111
+ <button
112
+ @click="openIndexModal"
113
+ type="button"
114
+ class="block w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100"
115
+ >
116
+ Indexes
117
+ </button>
118
+ <button
119
+ @click="openCollectionInfo"
120
+ type="button"
121
+ class="block w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100"
122
+ >
123
+ Collection Info
124
+ </button>
125
+ </div>
126
+ </div>
127
+ </div>
103
128
  <span class="isolate inline-flex rounded-md shadow-sm">
104
129
  <button
105
130
  @click="setOutputType('table')"
@@ -193,7 +218,12 @@
193
218
  <div v-for="index in mongoDBIndexes" class="w-full flex items-center">
194
219
  <div class="grow shrink text-left flex justify-between items-center" v-if="index.name != '_id_'">
195
220
  <div>
196
- <div class="font-bold">{{ index.name }}</div>
221
+ <div class="font-bold flex items-center gap-2">
222
+ <div>{{ index.name }}</div>
223
+ <div v-if="isTTLIndex(index)" class="rounded-full bg-ultramarine-100 px-2 py-0.5 text-xs font-semibold text-ultramarine-700">
224
+ TTL: {{ formatTTL(index.expireAfterSeconds) }}
225
+ </div>
226
+ </div>
197
227
  <div class="text-sm font-mono">{{ JSON.stringify(index.key) }}</div>
198
228
  </div>
199
229
  <div>
@@ -208,6 +238,44 @@
208
238
  </div>
209
239
  </template>
210
240
  </modal>
241
+ <modal v-if="shouldShowCollectionInfoModal">
242
+ <template v-slot:body>
243
+ <div class="modal-exit" @click="shouldShowCollectionInfoModal = false">&times;</div>
244
+ <div class="text-xl font-bold mb-2">Collection Info</div>
245
+ <div v-if="!collectionInfo" class="text-gray-600">Loading collection details...</div>
246
+ <div v-else class="space-y-3">
247
+ <div class="flex justify-between gap-4">
248
+ <div class="font-semibold text-gray-700">Documents</div>
249
+ <div class="text-gray-900">{{ formatNumber(collectionInfo.documentCount) }}</div>
250
+ </div>
251
+ <div class="flex justify-between gap-4">
252
+ <div class="font-semibold text-gray-700">Indexes</div>
253
+ <div class="text-gray-900">{{ formatNumber(collectionInfo.indexCount) }}</div>
254
+ </div>
255
+ <div class="flex justify-between gap-4">
256
+ <div class="font-semibold text-gray-700">Total Index Size</div>
257
+ <div class="text-gray-900">{{ formatCollectionSize(collectionInfo.totalIndexSize) }}</div>
258
+ </div>
259
+ <div class="flex justify-between gap-4">
260
+ <div class="font-semibold text-gray-700">Total Storage Size</div>
261
+ <div class="text-gray-900">{{ formatCollectionSize(collectionInfo.size) }}</div>
262
+ </div>
263
+ <div class="flex flex-col gap-1">
264
+ <div class="flex justify-between gap-4">
265
+ <div class="font-semibold text-gray-700">Collation</div>
266
+ <div class="text-gray-900">{{ collectionInfo.hasCollation ? 'Yes' : 'No' }}</div>
267
+ </div>
268
+ <div v-if="collectionInfo.hasCollation" class="rounded bg-gray-100 p-3 text-sm text-gray-800 overflow-x-auto">
269
+ <pre class="whitespace-pre-wrap">{{ JSON.stringify(collectionInfo.collation, null, 2) }}</pre>
270
+ </div>
271
+ </div>
272
+ <div class="flex justify-between gap-4">
273
+ <div class="font-semibold text-gray-700">Capped</div>
274
+ <div class="text-gray-900">{{ collectionInfo.capped ? 'Yes' : 'No' }}</div>
275
+ </div>
276
+ </div>
277
+ </template>
278
+ </modal>
211
279
  <modal v-if="shouldShowFieldModal">
212
280
  <template v-slot:body>
213
281
  <div class="modal-exit" @click="shouldShowFieldModal = false; selectedPaths = [...filteredPaths];">&times;</div>
@@ -3,6 +3,7 @@
3
3
  const api = require('../api');
4
4
  const template = require('./models.html');
5
5
  const mpath = require('mpath');
6
+ const vanillatoasts = require('vanillatoasts');
6
7
 
7
8
  const appendCSS = require('../appendCSS');
8
9
  appendCSS(require('./models.css'));
@@ -34,6 +35,7 @@ module.exports = app => app.component('models', {
34
35
  shouldShowCreateModal: false,
35
36
  shouldShowFieldModal: false,
36
37
  shouldShowIndexModal: false,
38
+ shouldShowCollectionInfoModal: false,
37
39
  shouldShowUpdateMultipleModal: false,
38
40
  shouldShowDeleteMultipleModal: false,
39
41
  shouldExport: {},
@@ -44,7 +46,9 @@ module.exports = app => app.component('models', {
44
46
  outputType: 'table', // json, table
45
47
  hideSidebar: null,
46
48
  lastSelectedIndex: null,
47
- error: null
49
+ error: null,
50
+ showActionsMenu: false,
51
+ collectionInfo: null
48
52
  }),
49
53
  created() {
50
54
  this.currentModel = this.model;
@@ -53,12 +57,23 @@ module.exports = app => app.component('models', {
53
57
  beforeDestroy() {
54
58
  document.removeEventListener('scroll', this.onScroll, true);
55
59
  window.removeEventListener('popstate', this.onPopState, true);
60
+ document.removeEventListener('click', this.onOutsideActionsMenuClick, true);
56
61
  },
57
62
  async mounted() {
58
63
  this.onScroll = () => this.checkIfScrolledToBottom();
59
64
  document.addEventListener('scroll', this.onScroll, true);
60
65
  this.onPopState = () => this.initSearchFromUrl();
61
66
  window.addEventListener('popstate', this.onPopState, true);
67
+ this.onOutsideActionsMenuClick = event => {
68
+ if (!this.showActionsMenu) {
69
+ return;
70
+ }
71
+ const actionsMenu = this.$refs.actionsMenuContainer;
72
+ if (actionsMenu && !actionsMenu.contains(event.target)) {
73
+ this.closeActionsMenu();
74
+ }
75
+ };
76
+ document.addEventListener('click', this.onOutsideActionsMenuClick, true);
62
77
  const { models, readyState } = await api.Model.listModels();
63
78
  this.models = models;
64
79
  if (this.currentModel == null && this.models.length > 0) {
@@ -160,6 +175,13 @@ module.exports = app => app.component('models', {
160
175
  async dropIndex(name) {
161
176
  const { mongoDBIndexes } = await api.Model.dropIndex({ model: this.currentModel, name });
162
177
  this.mongoDBIndexes = mongoDBIndexes;
178
+ vanillatoasts.create({
179
+ title: 'Index dropped!',
180
+ type: 'success',
181
+ timeout: 3000,
182
+ icon: 'images/success.png',
183
+ positionClass: 'bottomRight'
184
+ });
163
185
  },
164
186
  async closeCreationModal() {
165
187
  this.shouldShowCreateModal = false;
@@ -232,11 +254,86 @@ module.exports = app => app.component('models', {
232
254
  }
233
255
  },
234
256
  async openIndexModal() {
257
+ this.closeActionsMenu();
235
258
  this.shouldShowIndexModal = true;
236
259
  const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel });
237
260
  this.mongoDBIndexes = mongoDBIndexes;
238
261
  this.schemaIndexes = schemaIndexes;
239
262
  },
263
+ toggleActionsMenu() {
264
+ this.showActionsMenu = !this.showActionsMenu;
265
+ },
266
+ closeActionsMenu() {
267
+ this.showActionsMenu = false;
268
+ },
269
+ async openCollectionInfo() {
270
+ this.closeActionsMenu();
271
+ this.shouldShowCollectionInfoModal = true;
272
+ this.collectionInfo = null;
273
+ const { info } = await api.Model.getCollectionInfo({ model: this.currentModel });
274
+ this.collectionInfo = info;
275
+ },
276
+ isTTLIndex(index) {
277
+ return index != null && index.expireAfterSeconds != null;
278
+ },
279
+ formatTTL(expireAfterSeconds) {
280
+ if (typeof expireAfterSeconds !== 'number') {
281
+ return '';
282
+ }
283
+
284
+ let remaining = expireAfterSeconds;
285
+ const days = Math.floor(remaining / (24 * 60 * 60));
286
+ remaining = remaining % (24 * 60 * 60);
287
+ const hours = Math.floor(remaining / (60 * 60));
288
+ remaining = remaining % (60 * 60);
289
+ const minutes = Math.floor(remaining / 60);
290
+ const seconds = remaining % 60;
291
+
292
+ const parts = [];
293
+ if (days > 0) {
294
+ parts.push(`${days} day${days === 1 ? '' : 's'}`);
295
+ }
296
+ if (hours > 0) {
297
+ parts.push(`${hours} hour${hours === 1 ? '' : 's'}`);
298
+ }
299
+ if (minutes > 0) {
300
+ parts.push(`${minutes} minute${minutes === 1 ? '' : 's'}`);
301
+ }
302
+ if (seconds > 0 || parts.length === 0) {
303
+ parts.push(`${seconds} second${seconds === 1 ? '' : 's'}`);
304
+ }
305
+
306
+ return parts.join(', ');
307
+ },
308
+ formatCollectionSize(size) {
309
+ if (typeof size !== 'number') {
310
+ return 'Unknown';
311
+ }
312
+
313
+ const KB = 1024;
314
+ const MB = KB * 1024;
315
+ const GB = MB * 1024;
316
+ const TB = GB * 1024;
317
+
318
+ if (size >= TB) {
319
+ return `${(size / TB).toFixed(3)} TB`;
320
+ } else if (size >= GB) {
321
+ return `${(size / GB).toFixed(3)} GB`;
322
+ } else if (size >= MB) {
323
+ return `${(size / MB).toFixed(3)} MB`;
324
+ } else if (size >= KB) {
325
+ return `${(size / KB).toFixed(3)} KB`;
326
+ } else {
327
+ return `${size.toLocaleString()} bytes`;
328
+ }
329
+ },
330
+ formatNumber(value) {
331
+ if (typeof value !== 'number') {
332
+ return 'Unknown';
333
+ }
334
+
335
+ return value.toLocaleString();
336
+ },
240
337
  checkIndexLocation(indexName) {
241
338
  if (this.schemaIndexes.find(x => x.name == indexName) && this.mongoDBIndexes.find(x => x.name == indexName)) {
242
339
  return 'text-gray-500';
@@ -410,6 +507,13 @@ module.exports = app => app.component('models', {
410
507
  this.documents[index] = res.doc;
411
508
  }
412
509
  this.edittingDoc = null;
510
+ vanillatoasts.create({
511
+ title: 'Document updated!',
512
+ type: 'success',
513
+ timeout: 3000,
514
+ icon: 'images/success.png',
515
+ positionClass: 'bottomRight'
516
+ });
413
517
  },
414
518
  handleDocumentClick(document, event) {
415
519
  if (this.selectMultiple) {
@@ -473,6 +577,13 @@ module.exports = app => app.component('models', {
473
577
  this.lastSelectedIndex = null;
474
578
  this.shouldShowDeleteMultipleModal = false;
475
579
  this.selectMultiple = false;
580
+ vanillatoasts.create({
581
+ title: 'Documents deleted!',
582
+ type: 'success',
583
+ timeout: 3000,
584
+ icon: 'images/success.png',
585
+ positionClass: 'bottomRight'
586
+ });
476
587
  },
477
588
  async updateDocuments() {
478
589
  await this.getDocuments();
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const api = require('../api');
4
+ const vanillatoasts = require('vanillatoasts');
4
5
 
5
6
  const { BSON, EJSON } = require('mongodb/lib/bson');
6
7
 
@@ -28,35 +29,35 @@ module.exports = app => app.component('update-document', {
28
29
  methods: {
29
30
  async updateDocument() {
30
31
  const data = EJSON.serialize(eval(`(${this.editor.getValue()})`));
31
- if (this.multiple) {
32
- const ids = this.document.map(x => x._id);
33
- await api.Model.updateDocuments({ model: this.currentModel, _id: ids, update: data }).catch(err => {
34
- if (err.response?.data?.message) {
35
- console.log(err.response.data);
36
- const message = err.response.data.message.split(': ').slice(1).join(': ');
37
- this.errors = message.split(',').map(error => {
38
- return error.split(': ').slice(1).join(': ').trim();
39
- });
40
- throw new Error(err.response?.data?.message);
41
- }
42
- throw err;
43
- });
44
- } else {
45
- await api.Model.updateDocument({ model: this.currentModel, _id: this.document._id, update: data }).catch(err => {
46
- if (err.response?.data?.message) {
47
- console.log(err.response.data);
48
- const message = err.response.data.message.split(': ').slice(1).join(': ');
49
- this.errors = message.split(',').map(error => {
50
- return error.split(': ').slice(1).join(': ').trim();
51
- });
52
- throw new Error(err.response?.data?.message);
53
- }
54
- throw err;
32
+ try {
33
+ if (this.multiple) {
34
+ const ids = this.document.map(x => x._id);
35
+ await api.Model.updateDocuments({ model: this.currentModel, _id: ids, update: data });
36
+ } else {
37
+ await api.Model.updateDocument({ model: this.currentModel, _id: this.document._id, update: data });
38
+ }
39
+ this.errors.length = 0;
40
+ this.$emit('update');
41
+ this.$emit('close');
42
+ this.$nextTick(() => {
43
+ vanillatoasts.create({
44
+ title: this.multiple ? 'Documents updated!' : 'Document updated!',
45
+ type: 'success',
46
+ timeout: 3000,
47
+ icon: 'images/success.png',
48
+ positionClass: 'bottomRight'
49
+ });
55
50
  });
51
+ } catch (err) {
52
+ if (err.response?.data?.message) {
53
+ console.log(err.response.data);
54
+ const message = err.response.data.message.split(': ').slice(1).join(': ');
55
+ this.errors = message.split(',').map(error => {
56
+ return error.split(': ').slice(1).join(': ').trim();
57
+ });
58
+ }
59
+ throw err;
56
60
  }
57
- this.errors.length = 0;
58
- this.$emit('update');
59
- this.$emit('close');
60
61
  }
61
62
  },
62
63
  mounted: function() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.",
5
5
  "homepage": "https://studio.mongoosejs.io/",
6
6
  "repository": {
@@ -1,3 +0,0 @@
1
- .edit-array button {
2
- margin-top: 0.5em;
3
- }
@@ -1,8 +0,0 @@
1
- .list-array pre {
2
- max-height: 6.5em;
3
- max-width: 30em;
4
- }
5
-
6
- .list-array pre.maximized {
7
- max-height: auto;
8
- }