@mongoosejs/studio 0.5.3 → 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.
- package/backend/actions/Model/analyzeSchema.js +202 -0
- package/backend/actions/Model/index.js +2 -0
- package/backend/actions/Model/validateDocument.js +35 -0
- package/backend/authorize.js +2 -0
- package/backend/helpers/validateDocumentWithTimeout.js +39 -0
- package/frontend/index.js +1 -2
- package/frontend/public/app.js +689 -114
- package/frontend/public/tw.css +221 -28
- package/frontend/src/api.js +12 -0
- package/frontend/src/array-utils.js +60 -2
- package/frontend/src/chat/chat.js +2 -2
- package/frontend/src/detail-array/detail-array.html +98 -14
- package/frontend/src/detail-array/detail-array.js +73 -3
- package/frontend/src/document/document.html +51 -0
- package/frontend/src/document/document.js +13 -0
- package/frontend/src/document-details/document-details.html +10 -3
- package/frontend/src/document-details/document-details.js +28 -0
- package/frontend/src/document-details/document-property/document-property.html +36 -44
- package/frontend/src/document-details/document-property/document-property.js +78 -14
- package/frontend/src/edit-array/edit-array.html +143 -8
- package/frontend/src/edit-array/edit-array.js +104 -2
- package/frontend/src/index.js +45 -9
- package/frontend/src/models/analyze-schema-modal/analyze-schema-modal.html +169 -0
- package/frontend/src/models/analyze-schema-modal/analyze-schema-modal.js +149 -0
- package/frontend/src/models/models.html +25 -13
- package/frontend/src/models/models.js +5 -13
- package/frontend/src/navbar/navbar.html +4 -4
- package/frontend/src/navbar/navbar.js +2 -3
- package/package.json +2 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<div class="modal-exit" @click="close">×</div>
|
|
3
|
+
<div class="text-xl font-bold mb-2">Analyze Schema</div>
|
|
4
|
+
<div class="space-y-4">
|
|
5
|
+
<div class="grid grid-cols-2 gap-3 text-sm md:grid-cols-4">
|
|
6
|
+
<div>
|
|
7
|
+
<div class="font-semibold text-content-secondary">Documents</div>
|
|
8
|
+
<div v-if="schemaAnalysis" class="text-content">{{ formatNumber(schemaAnalysis.documentCount) }}</div>
|
|
9
|
+
<div v-else class="mt-1 h-4 w-20 animate-pulse rounded bg-muted"></div>
|
|
10
|
+
</div>
|
|
11
|
+
<div>
|
|
12
|
+
<div class="font-semibold text-content-secondary">Analyzed</div>
|
|
13
|
+
<div v-if="schemaAnalysis" class="text-content">{{ formatNumber(schemaAnalysis.sampleSize) }}{{ schemaAnalysis.sampled ? ' sampled' : '' }}</div>
|
|
14
|
+
<div v-else class="mt-1 h-4 w-24 animate-pulse rounded bg-muted"></div>
|
|
15
|
+
</div>
|
|
16
|
+
<div>
|
|
17
|
+
<div class="font-semibold text-content-secondary">Valid</div>
|
|
18
|
+
<div v-if="schemaAnalysis" class="text-content">
|
|
19
|
+
{{ formatNumber(schemaAnalysis.validDocumentCount) }} ({{ formatPercent(schemaAnalysis.validDocumentCount, schemaAnalysis.sampleSize) }})
|
|
20
|
+
<router-link
|
|
21
|
+
v-if="schemaAnalysis.firstValidDocumentId"
|
|
22
|
+
:to="documentRoute(schemaAnalysis.firstValidDocumentId)"
|
|
23
|
+
class="ml-1 text-xs text-primary underline"
|
|
24
|
+
>[Example]</router-link>
|
|
25
|
+
</div>
|
|
26
|
+
<div v-else class="mt-1 h-4 w-28 animate-pulse rounded bg-muted"></div>
|
|
27
|
+
</div>
|
|
28
|
+
<div>
|
|
29
|
+
<div class="flex items-center gap-2 font-semibold text-content-secondary">
|
|
30
|
+
<span>Invalid</span>
|
|
31
|
+
<button
|
|
32
|
+
v-if="schemaAnalysis && schemaAnalysis.invalidDocuments.length > 0"
|
|
33
|
+
type="button"
|
|
34
|
+
class="text-xs font-semibold text-primary underline"
|
|
35
|
+
@click="showInvalidDocuments = !showInvalidDocuments"
|
|
36
|
+
>{{ showInvalidDocuments ? 'Hide' : 'Show' }}</button>
|
|
37
|
+
</div>
|
|
38
|
+
<div v-if="schemaAnalysis" class="text-content">
|
|
39
|
+
{{ formatNumber(schemaAnalysis.invalidDocumentCount) }} ({{ formatPercent(schemaAnalysis.invalidDocumentCount, schemaAnalysis.sampleSize) }})
|
|
40
|
+
<router-link
|
|
41
|
+
v-if="schemaAnalysis.firstInvalidDocumentId"
|
|
42
|
+
:to="documentRoute(schemaAnalysis.firstInvalidDocumentId)"
|
|
43
|
+
class="ml-1 text-xs text-primary underline"
|
|
44
|
+
>[Example]</router-link>
|
|
45
|
+
</div>
|
|
46
|
+
<div v-else class="mt-1 h-4 w-28 animate-pulse rounded bg-muted"></div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div v-if="schemaAnalysis && showInvalidDocuments && schemaAnalysis.invalidDocuments.length > 0" class="rounded border border-edge">
|
|
50
|
+
<div class="max-h-48 overflow-auto">
|
|
51
|
+
<div
|
|
52
|
+
v-for="invalidDocument in schemaAnalysis.invalidDocuments"
|
|
53
|
+
:key="invalidDocument.documentId"
|
|
54
|
+
class="grid gap-2 border-b border-edge px-3 py-2 text-sm last:border-b-0 md:grid-cols-[minmax(180px,240px)_1fr]"
|
|
55
|
+
>
|
|
56
|
+
<router-link
|
|
57
|
+
:to="documentRoute(invalidDocument.documentId)"
|
|
58
|
+
class="font-mono text-primary underline"
|
|
59
|
+
>{{ invalidDocument.documentId }}</router-link>
|
|
60
|
+
<div class="text-content-secondary">{{ invalidDocument.error }}</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="px-px py-px">
|
|
65
|
+
<input
|
|
66
|
+
ref="fieldSearch"
|
|
67
|
+
v-model="fieldSearch"
|
|
68
|
+
type="text"
|
|
69
|
+
:disabled="!schemaAnalysis"
|
|
70
|
+
placeholder="Filter fields..."
|
|
71
|
+
class="w-full rounded border border-edge bg-surface px-3 py-2 text-sm text-content placeholder:text-content-tertiary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-wait disabled:bg-muted disabled:text-content-tertiary"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="max-h-[55vh] overflow-auto rounded border border-edge">
|
|
75
|
+
<table class="min-w-full table-fixed border-collapse text-sm">
|
|
76
|
+
<colgroup>
|
|
77
|
+
<col class="w-[40%]" />
|
|
78
|
+
<col class="w-[60%]" />
|
|
79
|
+
</colgroup>
|
|
80
|
+
<thead class="sticky top-0 bg-slate-100 border-b border-edge">
|
|
81
|
+
<tr>
|
|
82
|
+
<th class="px-3 py-2 text-left font-medium text-content border-r border-edge">Property</th>
|
|
83
|
+
<th class="px-3 py-2 text-left font-medium text-content">
|
|
84
|
+
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
85
|
+
<span>Types</span>
|
|
86
|
+
<div class="isolate inline-flex shrink-0 rounded-md shadow-sm">
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
class="relative inline-flex items-center rounded-l-md px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ring-edge-strong focus:z-10"
|
|
90
|
+
:class="typeDisplayMode === 'badges' ? 'bg-merlot-700 text-white' : 'bg-surface text-content-secondary hover:bg-muted'"
|
|
91
|
+
@click="typeDisplayMode = 'badges'"
|
|
92
|
+
>
|
|
93
|
+
Badges
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
class="relative -ml-px inline-flex items-center rounded-r-md px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ring-edge-strong focus:z-10"
|
|
98
|
+
:class="typeDisplayMode === 'bars' ? 'bg-merlot-700 text-white' : 'bg-surface text-content-secondary hover:bg-muted'"
|
|
99
|
+
@click="typeDisplayMode = 'bars'"
|
|
100
|
+
>
|
|
101
|
+
Bars
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</th>
|
|
106
|
+
</tr>
|
|
107
|
+
</thead>
|
|
108
|
+
<tbody class="bg-surface">
|
|
109
|
+
<template v-if="!schemaAnalysis">
|
|
110
|
+
<tr v-for="index in 6" :key="'skeleton-' + index" class="border-b border-edge last:border-b-0">
|
|
111
|
+
<td class="px-3 py-3 align-top border-r border-edge">
|
|
112
|
+
<div class="h-4 w-44 animate-pulse rounded bg-muted"></div>
|
|
113
|
+
</td>
|
|
114
|
+
<td class="px-3 py-3 align-top">
|
|
115
|
+
<div class="flex flex-wrap gap-1.5">
|
|
116
|
+
<div class="h-6 w-24 animate-pulse rounded bg-muted"></div>
|
|
117
|
+
<div class="h-6 w-20 animate-pulse rounded bg-muted"></div>
|
|
118
|
+
<div class="h-6 w-28 animate-pulse rounded bg-muted"></div>
|
|
119
|
+
</div>
|
|
120
|
+
</td>
|
|
121
|
+
</tr>
|
|
122
|
+
</template>
|
|
123
|
+
<tr v-for="path in filteredPaths" :key="path.path" class="border-b border-edge last:border-b-0">
|
|
124
|
+
<td class="px-3 py-2 align-top border-r border-edge text-xs text-content">
|
|
125
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
126
|
+
<span class="font-mono" v-html="highlightPath(path.path)"></span>
|
|
127
|
+
<span
|
|
128
|
+
v-if="primaryType(path)"
|
|
129
|
+
class="rounded px-1.5 py-0.5 text-[11px] font-semibold"
|
|
130
|
+
:style="typeBadgeStyle(primaryType(path).type)"
|
|
131
|
+
>{{ primaryType(path).type }}</span>
|
|
132
|
+
<span v-if="path.required" class="rounded bg-red-50 px-1.5 py-0.5 text-[11px] font-semibold text-red-700">required</span>
|
|
133
|
+
</div>
|
|
134
|
+
</td>
|
|
135
|
+
<td class="px-3 py-2 align-top">
|
|
136
|
+
<div v-if="typeDisplayMode === 'badges'" class="flex flex-wrap gap-1.5">
|
|
137
|
+
<span
|
|
138
|
+
v-for="typeInfo in path.types"
|
|
139
|
+
:key="typeInfo.type"
|
|
140
|
+
class="rounded px-2 py-1 text-xs font-semibold"
|
|
141
|
+
:style="typeBadgeStyle(typeInfo.type)"
|
|
142
|
+
>
|
|
143
|
+
<span class="font-mono">{{ typeInfo.type }}</span>:
|
|
144
|
+
{{ formatPercent(typeInfo.count, pathValueCount(path)) }}
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
<div v-else class="min-w-[260px]">
|
|
148
|
+
<div class="flex h-7 overflow-hidden rounded-lg border border-edge bg-surface">
|
|
149
|
+
<div
|
|
150
|
+
v-for="typeInfo in path.types"
|
|
151
|
+
:key="typeInfo.type"
|
|
152
|
+
:title="`${typeInfo.type}: ${formatPercent(typeInfo.count, pathValueCount(path))}`"
|
|
153
|
+
class="flex h-full items-center overflow-hidden px-2 text-xs font-semibold"
|
|
154
|
+
:style="typeSegmentStyle(typeInfo.type, typeInfo.count, pathValueCount(path))"
|
|
155
|
+
>
|
|
156
|
+
<span class="truncate">{{ typeInfo.type }}</span>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</td>
|
|
161
|
+
</tr>
|
|
162
|
+
<tr v-if="schemaAnalysis && filteredPaths.length === 0">
|
|
163
|
+
<td colspan="2" class="px-3 py-4 text-sm text-content-tertiary">No fields match "{{ fieldSearch }}".</td>
|
|
164
|
+
</tr>
|
|
165
|
+
</tbody>
|
|
166
|
+
</table>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
@@ -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="
|
|
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
|
-
|
|
184
|
+
Analyze Schema
|
|
185
185
|
</button>
|
|
186
186
|
<button
|
|
187
|
-
@click="
|
|
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
|
-
|
|
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
|
|
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
|
|
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">×</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
|
},
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
<path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
|
|
33
33
|
</svg>
|
|
34
34
|
</span>
|
|
35
|
-
<a v-if="hasAccess(roles, 'chat') &&
|
|
35
|
+
<a v-if="hasAccess(roles, 'chat') && supportsAI"
|
|
36
36
|
href="#/chat"
|
|
37
37
|
class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
|
|
38
38
|
:class="chatView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">Chat</a>
|
|
39
|
-
<span v-else-if="hasAccess(roles, 'chat') && !
|
|
39
|
+
<span v-else-if="hasAccess(roles, 'chat') && !supportsAI"
|
|
40
40
|
class="inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed"
|
|
41
41
|
:title="chatDisabledReason">
|
|
42
42
|
Chat
|
|
@@ -236,11 +236,11 @@
|
|
|
236
236
|
<path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
|
|
237
237
|
</svg>
|
|
238
238
|
</span>
|
|
239
|
-
<a v-if="hasAccess(roles, 'chat') &&
|
|
239
|
+
<a v-if="hasAccess(roles, 'chat') && supportsAI"
|
|
240
240
|
href="#/chat"
|
|
241
241
|
class="block px-3 py-2 rounded-md text-base font-medium"
|
|
242
242
|
:class="chatView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'">Chat</a>
|
|
243
|
-
<span v-else-if="hasAccess(roles, 'chat') && !
|
|
243
|
+
<span v-else-if="hasAccess(roles, 'chat') && !supportsAI"
|
|
244
244
|
class="block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed"
|
|
245
245
|
:title="chatDisabledReason">
|
|
246
246
|
Chat
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const api = require('../api');
|
|
4
3
|
const mothership = require('../mothership');
|
|
5
4
|
const template = require('./navbar.html');
|
|
6
5
|
const { routes, hasAccess } = require('../routes');
|
|
@@ -107,8 +106,8 @@ module.exports = app => app.component('navbar', {
|
|
|
107
106
|
recentPagesList() {
|
|
108
107
|
return this.localRecentPages;
|
|
109
108
|
},
|
|
110
|
-
|
|
111
|
-
return
|
|
109
|
+
supportsAI() {
|
|
110
|
+
return this.state.capabilities == null || this.state.capabilities.supportsAI !== false;
|
|
112
111
|
},
|
|
113
112
|
chatDisabledReason() {
|
|
114
113
|
return 'Chat requires an Anthropic, Gemini, or OpenAI API key.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mongoosejs/studio",
|
|
3
|
-
"version": "0.5.
|
|
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",
|