@mongoosejs/studio 0.5.4 → 0.5.5

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.
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ const api = require('../../api');
4
+ const template = require('./analyze-schema-modal.html');
5
+ const xss = require('xss');
6
+
7
+ const TYPE_COLORS = {
8
+ string: '#315f5b',
9
+ number: '#465b86',
10
+ objectid: '#2f6381',
11
+ boolean: '#5b6540',
12
+ date: '#5a5278',
13
+ array: '#6b573d',
14
+ object: '#3f6648',
15
+ mixed: '#62546a',
16
+ buffer: '#5a5f50',
17
+ decimal128: '#4a6474',
18
+ map: '#4e665d',
19
+ null: '#3f4650',
20
+ undefined: '#5b626b'
21
+ };
22
+
23
+ const FALLBACK_TYPE_COLORS = [
24
+ '#315f5b',
25
+ '#465b86',
26
+ '#2f6381',
27
+ '#5b6540',
28
+ '#5a5278',
29
+ '#6b573d',
30
+ '#3f6648',
31
+ '#62546a'
32
+ ];
33
+
34
+ module.exports = app => app.component('analyze-schema-modal', {
35
+ template,
36
+ props: ['currentModel'],
37
+ emits: ['close'],
38
+ data: () => ({
39
+ schemaAnalysis: null,
40
+ fieldSearch: '',
41
+ showInvalidDocuments: false,
42
+ typeDisplayMode: 'bars'
43
+ }),
44
+ computed: {
45
+ filteredPaths() {
46
+ if (!this.schemaAnalysis) {
47
+ return [];
48
+ }
49
+ const search = this.fieldSearch.trim().toLowerCase();
50
+ if (!search) {
51
+ return this.schemaAnalysis.paths;
52
+ }
53
+ return this.schemaAnalysis.paths.filter(path => path.path.toLowerCase().includes(search));
54
+ }
55
+ },
56
+ async mounted() {
57
+ window.analyzeSchemaModal = this;
58
+ const { analysis } = await api.Model.analyzeSchema({ model: this.currentModel });
59
+ this.schemaAnalysis = analysis;
60
+ this.$nextTick(() => {
61
+ this.$refs.fieldSearch?.focus();
62
+ });
63
+ },
64
+ unmounted() {
65
+ window.analyzeSchemaModal = null;
66
+ },
67
+ methods: {
68
+ close() {
69
+ this.$emit('close');
70
+ },
71
+ formatNumber(value) {
72
+ if (typeof value !== 'number') {
73
+ return 'Unknown';
74
+ }
75
+
76
+ return value.toLocaleString();
77
+ },
78
+ formatPercent(value, total) {
79
+ if (!total) {
80
+ return '0%';
81
+ }
82
+ return `${Math.round((value / total) * 1000) / 10}%`;
83
+ },
84
+ typePercentValue(value, total) {
85
+ if (!total) {
86
+ return 0;
87
+ }
88
+ return Math.round((value / total) * 1000) / 10;
89
+ },
90
+ pathValueCount(path) {
91
+ return typeof path?.valueCount === 'number' && path.valueCount > 0 ?
92
+ path.valueCount :
93
+ this.schemaAnalysis.sampleSize;
94
+ },
95
+ primaryType(path) {
96
+ if (!path || !Array.isArray(path.types) || path.types.length === 0) {
97
+ return null;
98
+ }
99
+
100
+ return path.types.find(typeInfo => typeInfo.type !== 'null' && typeInfo.type !== 'undefined') || path.types[0];
101
+ },
102
+ typeColor(type) {
103
+ const normalizedType = String(type || '').toLowerCase();
104
+ if (TYPE_COLORS[normalizedType]) {
105
+ return TYPE_COLORS[normalizedType];
106
+ }
107
+
108
+ let hash = 0;
109
+ for (let i = 0; i < normalizedType.length; ++i) {
110
+ hash = ((hash << 5) - hash) + normalizedType.charCodeAt(i);
111
+ hash |= 0;
112
+ }
113
+ const index = Math.abs(hash) % FALLBACK_TYPE_COLORS.length;
114
+ return FALLBACK_TYPE_COLORS[index % FALLBACK_TYPE_COLORS.length];
115
+ },
116
+ typeBadgeStyle(type) {
117
+ return {
118
+ backgroundColor: this.typeColor(type),
119
+ color: '#fff'
120
+ };
121
+ },
122
+ typeSegmentStyle(type, count, total) {
123
+ return {
124
+ width: `${this.typePercentValue(count, total)}%`,
125
+ backgroundColor: this.typeColor(type),
126
+ color: '#fff'
127
+ };
128
+ },
129
+ documentRoute(documentId) {
130
+ return `/model/${this.currentModel}/document/${documentId}`;
131
+ },
132
+ highlightPath(path) {
133
+ const search = this.fieldSearch.trim();
134
+ if (!search) {
135
+ return xss(path);
136
+ }
137
+
138
+ const index = path.toLowerCase().indexOf(search.toLowerCase());
139
+ if (index === -1) {
140
+ return xss(path);
141
+ }
142
+
143
+ const before = path.slice(0, index);
144
+ const match = path.slice(index, index + search.length);
145
+ const after = path.slice(index + search.length);
146
+ return `${xss(before)}<strong>${xss(match)}</strong>${xss(after)}`;
147
+ }
148
+ }
149
+ });
@@ -177,37 +177,34 @@
177
177
  Indexes
178
178
  </button>
179
179
  <button
180
- @click="openCollectionInfo"
180
+ @click="openAnalyzeSchemaModal"
181
181
  type="button"
182
182
  class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
183
183
  >
184
- Collection Info
184
+ Analyze Schema
185
185
  </button>
186
186
  <button
187
- @click="findOldestDocument"
187
+ @click="openCollectionInfo"
188
188
  type="button"
189
189
  class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
190
190
  >
191
- Find oldest document
192
- </button>
193
- <button
194
- @click="openDropCollectionModal"
195
- type="button"
196
- class="block w-full px-4 py-2 text-left text-sm text-red-700 hover:bg-red-50"
197
- >
198
- Drop Collection
191
+ Collection Info
199
192
  </button>
200
193
  <button
201
194
  @click="toggleRowNumbers()"
195
+ :disabled="outputType !== 'table'"
202
196
  type="button"
203
- class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
197
+ class="block w-full px-4 py-2 text-left text-sm"
198
+ :class="outputType === 'table' ? 'text-content-secondary hover:bg-muted' : 'text-content-tertiary cursor-not-allowed opacity-50'"
204
199
  >
205
200
  {{ showRowNumbers ? 'Hide row numbers' : 'Show row numbers' }}
206
201
  </button>
207
202
  <button
208
203
  @click="resetFilter()"
204
+ :disabled="searchText.trim().length === 0"
209
205
  type="button"
210
- class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
206
+ class="block w-full px-4 py-2 text-left text-sm"
207
+ :class="searchText.trim().length > 0 ? 'text-content-secondary hover:bg-muted' : 'text-content-tertiary cursor-not-allowed opacity-50'"
211
208
  >
212
209
  Reset Filter
213
210
  </button>
@@ -235,6 +232,13 @@
235
232
  >
236
233
  Default Projection
237
234
  </async-button>
235
+ <button
236
+ @click="openDropCollectionModal"
237
+ type="button"
238
+ class="block w-full px-4 py-2 text-left text-sm text-red-700 hover:bg-red-50"
239
+ >
240
+ Drop Collection
241
+ </button>
238
242
  </div>
239
243
  </div>
240
244
  </div>
@@ -551,6 +555,14 @@
551
555
  </div>
552
556
  </template>
553
557
  </modal>
558
+ <modal v-if="shouldShowAnalyzeSchemaModal" containerClass="md:!w-[70vw]">
559
+ <template v-slot:body>
560
+ <analyze-schema-modal
561
+ :current-model="currentModel"
562
+ @close="shouldShowAnalyzeSchemaModal = false"
563
+ ></analyze-schema-modal>
564
+ </template>
565
+ </modal>
554
566
  <modal v-if="shouldShowCollectionInfoModal">
555
567
  <template v-slot:body>
556
568
  <div class="modal-exit" @click="shouldShowCollectionInfoModal = false">&times;</div>
@@ -167,6 +167,7 @@ module.exports = app => app.component('models', {
167
167
  addFieldFilterText: '',
168
168
  showAddFieldDropdown: false,
169
169
  shouldShowIndexModal: false,
170
+ shouldShowAnalyzeSchemaModal: false,
170
171
  shouldShowCollectionInfoModal: false,
171
172
  shouldShowDropCollectionModal: false,
172
173
  dropCollectionConfirmName: '',
@@ -889,6 +890,10 @@ module.exports = app => app.component('models', {
889
890
  this.mongoDBIndexes = mongoDBIndexes;
890
891
  this.schemaIndexes = schemaIndexes;
891
892
  },
893
+ openAnalyzeSchemaModal() {
894
+ this.closeActionsMenu();
895
+ this.shouldShowAnalyzeSchemaModal = true;
896
+ },
892
897
  toggleActionsMenu() {
893
898
  this.showActionsMenu = !this.showActionsMenu;
894
899
  },
@@ -949,19 +954,6 @@ module.exports = app => app.component('models', {
949
954
  this.$toast.success(`Collection ${this.currentModel} dropped.`);
950
955
  await this.getDocuments();
951
956
  },
952
- async findOldestDocument() {
953
- this.closeActionsMenu();
954
- const { docs } = await api.Model.getDocuments({
955
- model: this.currentModel,
956
- limit: 1,
957
- sortKey: '_id',
958
- sortDirection: 1
959
- });
960
- if (!Array.isArray(docs) || docs.length === 0) {
961
- throw new Error('No documents found');
962
- }
963
- this.openDocument(docs[0]);
964
- },
965
957
  isTTLIndex(index) {
966
958
  return index != null && index.expireAfterSeconds != null;
967
959
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  "ejson": "^2.2.3",
20
20
  "extrovert": "^0.2.0",
21
21
  "marked": "15.0.12",
22
+ "mpath": "^0.9.0",
22
23
  "node-inspect-extracted": "3.x",
23
24
  "regexp.escape": "^2.0.1",
24
25
  "tailwindcss": "3.4.0",