@mongoosejs/studio 0.0.123 → 0.0.125
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/README.md +7 -2
- package/backend/actions/ChatThread/createChatMessage.js +70 -4
- package/frontend/public/app.js +271 -56
- package/frontend/public/tw.css +8 -10
- package/frontend/src/api.js +5 -46
- package/frontend/src/models/models.html +11 -8
- package/frontend/src/models/models.js +134 -8
- package/frontend/src/models/trie.js +117 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ If you have a Mongoose Studio Pro API key, you can set it as follows:
|
|
|
34
34
|
const opts = process.env.MONGOOSE_STUDIO_API_KEY ? { apiKey: process.env.MONGOOSE_STUDIO_API_KEY } : {};
|
|
35
35
|
// Optionally specify which ChatGPT model to use for chat messages
|
|
36
36
|
opts.model = 'gpt-4o-mini';
|
|
37
|
+
// Provide your own OpenAI API key to run chat completions locally
|
|
38
|
+
opts.openAIAPIKey = process.env.OPENAI_API_KEY;
|
|
37
39
|
|
|
38
40
|
// Mount Mongoose Studio on '/studio'
|
|
39
41
|
app.use('/studio', await studio('/studio/api', mongoose, opts));
|
|
@@ -52,7 +54,9 @@ const { execSync } = require('child_process');
|
|
|
52
54
|
const opts = {
|
|
53
55
|
apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
|
|
54
56
|
// Optionally specify which ChatGPT model to use for chat messages
|
|
55
|
-
model: 'gpt-4o-mini'
|
|
57
|
+
model: 'gpt-4o-mini',
|
|
58
|
+
// Provide your own OpenAI API key to run chat completions locally
|
|
59
|
+
openAIAPIKey: process.env.OPENAI_API_KEY
|
|
56
60
|
};
|
|
57
61
|
console.log('Creating Mongoose studio', opts);
|
|
58
62
|
require('@mongoosejs/studio/frontend')(`/.netlify/functions/studio`, true, opts).then(() => {
|
|
@@ -70,7 +74,8 @@ const mongoose = require('mongoose');
|
|
|
70
74
|
|
|
71
75
|
const handler = require('@mongoosejs/studio/backend/netlify')({
|
|
72
76
|
apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
|
|
73
|
-
model: 'gpt-4o-mini'
|
|
77
|
+
model: 'gpt-4o-mini',
|
|
78
|
+
openAIAPIKey: process.env.OPENAI_API_KEY
|
|
74
79
|
}).handler;
|
|
75
80
|
|
|
76
81
|
let conn = null;
|
|
@@ -48,7 +48,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
|
|
|
48
48
|
|
|
49
49
|
let summarizePromise = Promise.resolve();
|
|
50
50
|
if (chatThread.title == null) {
|
|
51
|
-
summarizePromise = summarizeChatThread(llmMessages, authorization).then(res => {
|
|
51
|
+
summarizePromise = summarizeChatThread(llmMessages, authorization, options).then(res => {
|
|
52
52
|
const title = res.response;
|
|
53
53
|
chatThread.title = title;
|
|
54
54
|
return chatThread.save();
|
|
@@ -73,7 +73,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
|
|
|
73
73
|
script,
|
|
74
74
|
executionResult: null
|
|
75
75
|
}),
|
|
76
|
-
createChatMessageCore(llmMessages, modelDescriptions, options?.model, authorization).then(res => {
|
|
76
|
+
createChatMessageCore(llmMessages, modelDescriptions, options?.model, authorization, options).then(res => {
|
|
77
77
|
const content = res.response;
|
|
78
78
|
return ChatMessage.create({
|
|
79
79
|
chatThreadId,
|
|
@@ -87,7 +87,23 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
|
|
|
87
87
|
return { chatMessages, chatThread };
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
async function summarizeChatThread(messages, authorization) {
|
|
90
|
+
async function summarizeChatThread(messages, authorization, options) {
|
|
91
|
+
if (options?.openAIAPIKey) {
|
|
92
|
+
const response = await callOpenAI({
|
|
93
|
+
apiKey: options.openAIAPIKey,
|
|
94
|
+
model: options.model,
|
|
95
|
+
messages: [
|
|
96
|
+
{
|
|
97
|
+
role: 'system',
|
|
98
|
+
content: 'Summarize the following conversation into a concise title of at most 7 words. Respond with the title only.'
|
|
99
|
+
},
|
|
100
|
+
...messages
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return { response };
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
const headers = { 'Content-Type': 'application/json' };
|
|
92
108
|
if (authorization) {
|
|
93
109
|
headers.Authorization = authorization;
|
|
@@ -110,7 +126,26 @@ async function summarizeChatThread(messages, authorization) {
|
|
|
110
126
|
return await response.json();
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
async function createChatMessageCore(messages, modelDescriptions, model, authorization) {
|
|
129
|
+
async function createChatMessageCore(messages, modelDescriptions, model, authorization, options) {
|
|
130
|
+
if (options?.openAIAPIKey) {
|
|
131
|
+
const openAIMessages = [];
|
|
132
|
+
if (modelDescriptions) {
|
|
133
|
+
openAIMessages.push({
|
|
134
|
+
role: 'system',
|
|
135
|
+
content: `Here are the Mongoose model descriptions you can refer to:\n\n${modelDescriptions}`
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
openAIMessages.push(...messages);
|
|
139
|
+
|
|
140
|
+
const response = await callOpenAI({
|
|
141
|
+
apiKey: options.openAIAPIKey,
|
|
142
|
+
model,
|
|
143
|
+
messages: openAIMessages
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return { response };
|
|
147
|
+
}
|
|
148
|
+
|
|
114
149
|
const headers = { 'Content-Type': 'application/json' };
|
|
115
150
|
if (authorization) {
|
|
116
151
|
headers.Authorization = authorization;
|
|
@@ -134,3 +169,34 @@ async function createChatMessageCore(messages, modelDescriptions, model, authori
|
|
|
134
169
|
|
|
135
170
|
return await response.json();
|
|
136
171
|
}
|
|
172
|
+
|
|
173
|
+
async function callOpenAI({ apiKey, model, messages }) {
|
|
174
|
+
if (!apiKey) {
|
|
175
|
+
throw new Error('OpenAI API key required');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
Authorization: `Bearer ${apiKey}`
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
model: model || 'gpt-4o-mini',
|
|
186
|
+
messages
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const data = await response.json();
|
|
191
|
+
|
|
192
|
+
if (response.status < 200 || response.status >= 400) {
|
|
193
|
+
throw new Error(`OpenAI chat completion error ${response.status}: ${data.error?.message || data.message || 'Unknown error'}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
197
|
+
if (!content) {
|
|
198
|
+
throw new Error('OpenAI chat completion error: missing response content');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return content.trim();
|
|
202
|
+
}
|
package/frontend/public/app.js
CHANGED
|
@@ -125,52 +125,11 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
125
125
|
return client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
|
|
126
126
|
},
|
|
127
127
|
getDocumentsStream: async function* getDocumentsStream(params) {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
headers: {
|
|
134
|
-
Authorization: `${accessToken}`,
|
|
135
|
-
Accept: 'text/event-stream'
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (!response.ok) {
|
|
140
|
-
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const reader = response.body.getReader();
|
|
144
|
-
const decoder = new TextDecoder('utf-8');
|
|
145
|
-
let buffer = '';
|
|
146
|
-
|
|
147
|
-
while (true) {
|
|
148
|
-
const { done, value } = await reader.read();
|
|
149
|
-
if (done) break;
|
|
150
|
-
buffer += decoder.decode(value, { stream: true });
|
|
151
|
-
|
|
152
|
-
let eventEnd;
|
|
153
|
-
while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
|
|
154
|
-
const eventStr = buffer.slice(0, eventEnd);
|
|
155
|
-
buffer = buffer.slice(eventEnd + 2);
|
|
156
|
-
|
|
157
|
-
// Parse SSE event
|
|
158
|
-
const lines = eventStr.split('\n');
|
|
159
|
-
let data = '';
|
|
160
|
-
for (const line of lines) {
|
|
161
|
-
if (line.startsWith('data:')) {
|
|
162
|
-
data += line.slice(5).trim();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (data) {
|
|
166
|
-
try {
|
|
167
|
-
yield JSON.parse(data);
|
|
168
|
-
} catch (err) {
|
|
169
|
-
// If not JSON, yield as string
|
|
170
|
-
yield data;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
128
|
+
const data = await client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
|
|
129
|
+
yield { schemaPaths: data.schemaPaths };
|
|
130
|
+
yield { numDocs: data.numDocs };
|
|
131
|
+
for (const doc of data.docs) {
|
|
132
|
+
yield { document: doc };
|
|
174
133
|
}
|
|
175
134
|
},
|
|
176
135
|
getIndexes: function getIndexes(params) {
|
|
@@ -2648,6 +2607,34 @@ const template = __webpack_require__(/*! ./models.html */ "./frontend/src/models
|
|
|
2648
2607
|
const mpath = __webpack_require__(/*! mpath */ "./node_modules/mpath/index.js");
|
|
2649
2608
|
|
|
2650
2609
|
const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
|
|
2610
|
+
const { Trie } = __webpack_require__(/*! ./trie */ "./frontend/src/models/trie.js");
|
|
2611
|
+
|
|
2612
|
+
const QUERY_SELECTORS = [
|
|
2613
|
+
'$eq',
|
|
2614
|
+
'$ne',
|
|
2615
|
+
'$gt',
|
|
2616
|
+
'$gte',
|
|
2617
|
+
'$lt',
|
|
2618
|
+
'$lte',
|
|
2619
|
+
'$in',
|
|
2620
|
+
'$nin',
|
|
2621
|
+
'$exists',
|
|
2622
|
+
'$regex',
|
|
2623
|
+
'$options',
|
|
2624
|
+
'$text',
|
|
2625
|
+
'$search',
|
|
2626
|
+
'$and',
|
|
2627
|
+
'$or',
|
|
2628
|
+
'$nor',
|
|
2629
|
+
'$not',
|
|
2630
|
+
'$elemMatch',
|
|
2631
|
+
'$size',
|
|
2632
|
+
'$all',
|
|
2633
|
+
'$type',
|
|
2634
|
+
'$expr',
|
|
2635
|
+
'$jsonSchema',
|
|
2636
|
+
'$mod'
|
|
2637
|
+
];
|
|
2651
2638
|
|
|
2652
2639
|
|
|
2653
2640
|
appendCSS(__webpack_require__(/*! ./models.css */ "./frontend/src/models/models.css"));
|
|
@@ -2674,6 +2661,9 @@ module.exports = app => app.component('models', {
|
|
|
2674
2661
|
selectMultiple: false,
|
|
2675
2662
|
selectedDocuments: [],
|
|
2676
2663
|
searchText: '',
|
|
2664
|
+
autocompleteSuggestions: [],
|
|
2665
|
+
autocompleteIndex: 0,
|
|
2666
|
+
autocompleteTrie: null,
|
|
2677
2667
|
shouldShowExportModal: false,
|
|
2678
2668
|
shouldShowCreateModal: false,
|
|
2679
2669
|
shouldShowFieldModal: false,
|
|
@@ -2686,10 +2676,12 @@ module.exports = app => app.component('models', {
|
|
|
2686
2676
|
scrollHeight: 0,
|
|
2687
2677
|
interval: null,
|
|
2688
2678
|
outputType: 'table', // json, table
|
|
2689
|
-
hideSidebar: null
|
|
2679
|
+
hideSidebar: null,
|
|
2680
|
+
lastSelectedIndex: null
|
|
2690
2681
|
}),
|
|
2691
2682
|
created() {
|
|
2692
2683
|
this.currentModel = this.model;
|
|
2684
|
+
this.buildAutocompleteTrie();
|
|
2693
2685
|
},
|
|
2694
2686
|
beforeDestroy() {
|
|
2695
2687
|
document.removeEventListener('scroll', this.onScroll, true);
|
|
@@ -2708,6 +2700,16 @@ module.exports = app => app.component('models', {
|
|
|
2708
2700
|
await this.initSearchFromUrl();
|
|
2709
2701
|
},
|
|
2710
2702
|
methods: {
|
|
2703
|
+
buildAutocompleteTrie() {
|
|
2704
|
+
this.autocompleteTrie = new Trie();
|
|
2705
|
+
this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5);
|
|
2706
|
+
if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
|
|
2707
|
+
const paths = this.schemaPaths
|
|
2708
|
+
.map(path => path?.path)
|
|
2709
|
+
.filter(path => typeof path === 'string' && path.length > 0);
|
|
2710
|
+
this.autocompleteTrie.bulkInsert(paths, 10);
|
|
2711
|
+
}
|
|
2712
|
+
},
|
|
2711
2713
|
async initSearchFromUrl() {
|
|
2712
2714
|
this.status = 'loading';
|
|
2713
2715
|
this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
|
|
@@ -2745,6 +2747,62 @@ module.exports = app => app.component('models', {
|
|
|
2745
2747
|
});
|
|
2746
2748
|
}
|
|
2747
2749
|
},
|
|
2750
|
+
updateAutocomplete() {
|
|
2751
|
+
const input = this.$refs.searchInput;
|
|
2752
|
+
const cursorPos = input ? input.selectionStart : 0;
|
|
2753
|
+
const before = this.searchText.slice(0, cursorPos);
|
|
2754
|
+
const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
|
|
2755
|
+
if (match && match[1]) {
|
|
2756
|
+
const term = match[1].replace(/["']/g, '');
|
|
2757
|
+
if (!term) {
|
|
2758
|
+
this.autocompleteSuggestions = [];
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
if (this.autocompleteTrie) {
|
|
2762
|
+
this.autocompleteSuggestions = this.autocompleteTrie.getSuggestions(term, 10);
|
|
2763
|
+
this.autocompleteIndex = 0;
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
this.autocompleteSuggestions = [];
|
|
2768
|
+
},
|
|
2769
|
+
handleKeyDown(ev) {
|
|
2770
|
+
if (this.autocompleteSuggestions.length === 0) {
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
if (ev.key === 'Tab' || ev.key === 'Enter') {
|
|
2774
|
+
ev.preventDefault();
|
|
2775
|
+
this.applySuggestion(this.autocompleteIndex);
|
|
2776
|
+
} else if (ev.key === 'ArrowDown') {
|
|
2777
|
+
ev.preventDefault();
|
|
2778
|
+
this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
|
|
2779
|
+
} else if (ev.key === 'ArrowUp') {
|
|
2780
|
+
ev.preventDefault();
|
|
2781
|
+
this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
|
|
2782
|
+
}
|
|
2783
|
+
},
|
|
2784
|
+
applySuggestion(index) {
|
|
2785
|
+
const suggestion = this.autocompleteSuggestions[index];
|
|
2786
|
+
if (!suggestion) {
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
const input = this.$refs.searchInput;
|
|
2790
|
+
const cursorPos = input.selectionStart;
|
|
2791
|
+
const before = this.searchText.slice(0, cursorPos);
|
|
2792
|
+
const after = this.searchText.slice(cursorPos);
|
|
2793
|
+
const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
|
|
2794
|
+
if (!match) {
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
const token = match[1];
|
|
2798
|
+
const start = cursorPos - token.length;
|
|
2799
|
+
this.searchText = this.searchText.slice(0, start) + suggestion + after;
|
|
2800
|
+
this.$nextTick(() => {
|
|
2801
|
+
const pos = start + suggestion.length;
|
|
2802
|
+
input.setSelectionRange(pos, pos);
|
|
2803
|
+
});
|
|
2804
|
+
this.autocompleteSuggestions = [];
|
|
2805
|
+
},
|
|
2748
2806
|
clickFilter(path) {
|
|
2749
2807
|
if (this.searchText) {
|
|
2750
2808
|
if (this.searchText.endsWith('}')) {
|
|
@@ -2857,8 +2915,10 @@ module.exports = app => app.component('models', {
|
|
|
2857
2915
|
// Clear previous data
|
|
2858
2916
|
this.documents = [];
|
|
2859
2917
|
this.schemaPaths = [];
|
|
2918
|
+
this.buildAutocompleteTrie();
|
|
2860
2919
|
this.numDocuments = null;
|
|
2861
2920
|
this.loadedAllDocs = false;
|
|
2921
|
+
this.lastSelectedIndex = null;
|
|
2862
2922
|
|
|
2863
2923
|
let docsCount = 0;
|
|
2864
2924
|
let schemaPathsReceived = false;
|
|
@@ -2890,6 +2950,7 @@ module.exports = app => app.component('models', {
|
|
|
2890
2950
|
}
|
|
2891
2951
|
this.filteredPaths = [...this.schemaPaths];
|
|
2892
2952
|
this.selectedPaths = [...this.schemaPaths];
|
|
2953
|
+
this.buildAutocompleteTrie();
|
|
2893
2954
|
schemaPathsReceived = true;
|
|
2894
2955
|
}
|
|
2895
2956
|
if (event.numDocs !== undefined) {
|
|
@@ -3032,17 +3093,38 @@ module.exports = app => app.component('models', {
|
|
|
3032
3093
|
}
|
|
3033
3094
|
this.edittingDoc = null;
|
|
3034
3095
|
},
|
|
3035
|
-
handleDocumentClick(document) {
|
|
3036
|
-
console.log(this.selectedDocuments);
|
|
3096
|
+
handleDocumentClick(document, event) {
|
|
3037
3097
|
if (this.selectMultiple) {
|
|
3038
|
-
const
|
|
3039
|
-
if (
|
|
3040
|
-
const
|
|
3041
|
-
if (
|
|
3042
|
-
|
|
3098
|
+
const documentIndex = this.documents.findIndex(doc => doc._id.toString() == document._id.toString());
|
|
3099
|
+
if (event?.shiftKey && this.selectedDocuments.length > 0) {
|
|
3100
|
+
const anchorIndex = this.lastSelectedIndex;
|
|
3101
|
+
if (anchorIndex != null && anchorIndex !== -1 && documentIndex !== -1) {
|
|
3102
|
+
const start = Math.min(anchorIndex, documentIndex);
|
|
3103
|
+
const end = Math.max(anchorIndex, documentIndex);
|
|
3104
|
+
const selectedDocumentIds = new Set(this.selectedDocuments.map(doc => doc._id.toString()));
|
|
3105
|
+
for (let i = start; i <= end; i++) {
|
|
3106
|
+
const docInRange = this.documents[i];
|
|
3107
|
+
const existsInRange = selectedDocumentIds.has(docInRange._id.toString());
|
|
3108
|
+
if (!existsInRange) {
|
|
3109
|
+
this.selectedDocuments.push(docInRange);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
this.lastSelectedIndex = documentIndex;
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
const index = this.selectedDocuments.findIndex(x => x._id.toString() == document._id.toString());
|
|
3117
|
+
if (index !== -1) {
|
|
3118
|
+
this.selectedDocuments.splice(index, 1);
|
|
3119
|
+
if (this.selectedDocuments.length === 0) {
|
|
3120
|
+
this.lastSelectedIndex = null;
|
|
3121
|
+
} else {
|
|
3122
|
+
const lastDoc = this.selectedDocuments[this.selectedDocuments.length - 1];
|
|
3123
|
+
this.lastSelectedIndex = this.documents.findIndex(doc => doc._id.toString() == lastDoc._id.toString());
|
|
3043
3124
|
}
|
|
3044
3125
|
} else {
|
|
3045
3126
|
this.selectedDocuments.push(document);
|
|
3127
|
+
this.lastSelectedIndex = documentIndex;
|
|
3046
3128
|
}
|
|
3047
3129
|
} else {
|
|
3048
3130
|
this.$router.push('/model/' + this.currentModel + '/document/' + document._id);
|
|
@@ -3056,18 +3138,21 @@ module.exports = app => app.component('models', {
|
|
|
3056
3138
|
});
|
|
3057
3139
|
await this.getDocuments();
|
|
3058
3140
|
this.selectedDocuments.length = 0;
|
|
3141
|
+
this.lastSelectedIndex = null;
|
|
3059
3142
|
this.shouldShowDeleteMultipleModal = false;
|
|
3060
3143
|
this.selectMultiple = false;
|
|
3061
3144
|
},
|
|
3062
3145
|
async updateDocuments() {
|
|
3063
3146
|
await this.getDocuments();
|
|
3064
3147
|
this.selectedDocuments.length = 0;
|
|
3148
|
+
this.lastSelectedIndex = null;
|
|
3065
3149
|
this.selectMultiple = false;
|
|
3066
3150
|
},
|
|
3067
3151
|
stagingSelect() {
|
|
3068
3152
|
if (this.selectMultiple) {
|
|
3069
3153
|
this.selectMultiple = false;
|
|
3070
3154
|
this.selectedDocuments.length = 0;
|
|
3155
|
+
this.lastSelectedIndex = null;
|
|
3071
3156
|
} else {
|
|
3072
3157
|
this.selectMultiple = true;
|
|
3073
3158
|
}
|
|
@@ -3076,6 +3161,134 @@ module.exports = app => app.component('models', {
|
|
|
3076
3161
|
});
|
|
3077
3162
|
|
|
3078
3163
|
|
|
3164
|
+
/***/ }),
|
|
3165
|
+
|
|
3166
|
+
/***/ "./frontend/src/models/trie.js":
|
|
3167
|
+
/*!*************************************!*\
|
|
3168
|
+
!*** ./frontend/src/models/trie.js ***!
|
|
3169
|
+
\*************************************/
|
|
3170
|
+
/***/ ((module) => {
|
|
3171
|
+
|
|
3172
|
+
"use strict";
|
|
3173
|
+
|
|
3174
|
+
|
|
3175
|
+
class TrieNode {
|
|
3176
|
+
constructor() {
|
|
3177
|
+
this.children = Object.create(null);
|
|
3178
|
+
this.isEnd = false;
|
|
3179
|
+
this.freq = 0;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
class Trie {
|
|
3184
|
+
constructor() {
|
|
3185
|
+
this.root = new TrieNode();
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
insert(word, freq = 1) {
|
|
3189
|
+
if (!word) {
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
let node = this.root;
|
|
3193
|
+
for (const ch of word) {
|
|
3194
|
+
if (!node.children[ch]) {
|
|
3195
|
+
node.children[ch] = new TrieNode();
|
|
3196
|
+
}
|
|
3197
|
+
node = node.children[ch];
|
|
3198
|
+
}
|
|
3199
|
+
node.isEnd = true;
|
|
3200
|
+
node.freq += freq;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
bulkInsert(words, freq = 1) {
|
|
3204
|
+
if (!Array.isArray(words)) {
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
for (const word of words) {
|
|
3208
|
+
this.insert(word, freq);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
collect(node, prefix, out) {
|
|
3213
|
+
if (node.isEnd) {
|
|
3214
|
+
out.push([prefix, node.freq]);
|
|
3215
|
+
}
|
|
3216
|
+
for (const [ch, child] of Object.entries(node.children)) {
|
|
3217
|
+
this.collect(child, prefix + ch, out);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
suggest(prefix, limit = 10) {
|
|
3222
|
+
let node = this.root;
|
|
3223
|
+
for (const ch of prefix) {
|
|
3224
|
+
if (!node.children[ch]) {
|
|
3225
|
+
return [];
|
|
3226
|
+
}
|
|
3227
|
+
node = node.children[ch];
|
|
3228
|
+
}
|
|
3229
|
+
const results = [];
|
|
3230
|
+
this.collect(node, prefix, results);
|
|
3231
|
+
results.sort((a, b) => b[1] - a[1]);
|
|
3232
|
+
return results.slice(0, limit).map(([word]) => word);
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
fuzzySuggest(prefix, limit = 10) {
|
|
3236
|
+
const results = new Set();
|
|
3237
|
+
|
|
3238
|
+
const dfs = (node, path, edits) => {
|
|
3239
|
+
if (edits > 1) {
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
if (node.isEnd && Math.abs(path.length - prefix.length) <= 1) {
|
|
3243
|
+
const dist = levenshtein(prefix, path);
|
|
3244
|
+
if (dist <= 1) {
|
|
3245
|
+
results.add(path);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
for (const [ch, child] of Object.entries(node.children)) {
|
|
3249
|
+
const nextEdits = ch === prefix[path.length] ? edits : edits + 1;
|
|
3250
|
+
dfs(child, path + ch, nextEdits);
|
|
3251
|
+
}
|
|
3252
|
+
};
|
|
3253
|
+
|
|
3254
|
+
dfs(this.root, '', 0);
|
|
3255
|
+
return Array.from(results).slice(0, limit);
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
getSuggestions(prefix, limit = 10) {
|
|
3259
|
+
if (!prefix) {
|
|
3260
|
+
return [];
|
|
3261
|
+
}
|
|
3262
|
+
const exact = this.suggest(prefix, limit);
|
|
3263
|
+
if (exact.length >= limit) {
|
|
3264
|
+
return exact;
|
|
3265
|
+
}
|
|
3266
|
+
const fuzzy = this.fuzzySuggest(prefix, limit - exact.length);
|
|
3267
|
+
return [...exact, ...fuzzy];
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
function levenshtein(a, b) {
|
|
3272
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
3273
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
3274
|
+
);
|
|
3275
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3276
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3277
|
+
dp[i][j] =
|
|
3278
|
+
a[i - 1] === b[j - 1]
|
|
3279
|
+
? dp[i - 1][j - 1]
|
|
3280
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
return dp[a.length][b.length];
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
module.exports = {
|
|
3287
|
+
Trie,
|
|
3288
|
+
TrieNode
|
|
3289
|
+
};
|
|
3290
|
+
|
|
3291
|
+
|
|
3079
3292
|
/***/ }),
|
|
3080
3293
|
|
|
3081
3294
|
/***/ "./frontend/src/mothership.js":
|
|
@@ -3710,6 +3923,8 @@ var map = {
|
|
|
3710
3923
|
"./models/models.css": "./frontend/src/models/models.css",
|
|
3711
3924
|
"./models/models.html": "./frontend/src/models/models.html",
|
|
3712
3925
|
"./models/models.js": "./frontend/src/models/models.js",
|
|
3926
|
+
"./models/trie": "./frontend/src/models/trie.js",
|
|
3927
|
+
"./models/trie.js": "./frontend/src/models/trie.js",
|
|
3713
3928
|
"./mothership": "./frontend/src/mothership.js",
|
|
3714
3929
|
"./mothership.js": "./frontend/src/mothership.js",
|
|
3715
3930
|
"./navbar/navbar": "./frontend/src/navbar/navbar.js",
|
|
@@ -4934,7 +5149,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
|
|
|
4934
5149
|
/***/ ((module) => {
|
|
4935
5150
|
|
|
4936
5151
|
"use strict";
|
|
4937
|
-
module.exports = "<div class=\"models flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <div class=\"fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10\" @click=\"hideSidebar = false\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"#5f6368\"><path d=\"M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z\"/></svg>\n </div>\n <aside class=\"bg-white border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-48 fixed lg:relative shrink-0\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-48' : ''\">\n <div class=\"flex items-center border-b border-gray-100 w-48 overflow-x-hidden\">\n <div class=\"p-4 font-bold text-lg\">Models</div>\n <button\n @click=\"hideSidebar = true\"\n class=\"ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none\"\n aria-label=\"Close sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"currentColor\"><path d=\"M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z\"/></svg>\n </button>\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 </aside>\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\" v-model=\"searchText\" @click=\"initFilter\" />\n </form>\n <div>\n <span v-if=\"numDocuments == null\">Loading ...</span>\n <span v-else-if=\"typeof numDocuments === 'number'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n v-show=\"!selectMultiple\"\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=\"stagingSelect\"\n type=\"button\"\n :class=\"{ 'bg-ultramarine-500 ring-inset ring-2 ring-gray-300 hover:bg-ultramarine-600': selectMultiple }\"\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 >\n Select\n </button>\n <button\n v-show=\"selectMultiple\"\n @click=\"shouldShowUpdateMultipleModal=true;\"\n type=\"button\"\n class=\"rounded bg-green-600 px-2 py-2 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 >\n Update\n </button>\n <button\n @click=\"shouldShowDeleteMultipleModal=true;\"\n type=\"button\"\n v-show=\"selectMultiple\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500\"\n >\n Delete\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n v-show=\"!selectMultiple\"\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 v-show=\"!selectMultiple\"\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 v-show=\"!selectMultiple\"\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=\"handleDocumentClick(document)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\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=\"handleDocumentClick(document)\" :key=\"document._id\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\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\">×</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :search-text=\"searchText\"\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\">×</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 <async-button\n type=\"button\"\n @click=\"dropIndex(index.name)\"\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 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n Drop\n </async-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];\">×</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.path' + index\">{{path.path}}</label>\n </div>\n </div>\n <div class=\"mt-4 flex gap-2\">\n <button type=\"button\" @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=\"button\" @click=\"selectAll()\" 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\">Select All</button>\n <button type=\"button\" @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=\"button\" @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;\">×</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowUpdateMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowUpdateMultipleModal = false;\">×</div>\n <update-document :currentModel=\"currentModel\" :document=\"selectedDocuments\" :multiple=\"true\" @update=\"updateDocuments\" @close=\"shouldShowUpdateMultipleModal=false;\"></update-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteMultipleModal = false;\">×</div>\n <h2>Are you sure you want to delete {{selectedDocuments.length}} documents?</h2>\n <div>\n <list-json :value=\"selectedDocuments\"></list-json>\n </div>\n <div class=\"flex gap-4\">\n <async-button @click=\"deleteDocuments\" class=\"rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">\n Confirm\n </async-button>\n <button @click=\"shouldShowDeleteMultipleModal = false;\" class=\"rounded bg-gray-400 px-2 py-2 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-gray-500\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n</div>\n";
|
|
5152
|
+
module.exports = "<div class=\"models flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <div class=\"fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10\" @click=\"hideSidebar = false\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"#5f6368\"><path d=\"M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z\"/></svg>\n </div>\n <aside class=\"bg-white border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-48 fixed lg:relative shrink-0\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-48' : ''\">\n <div class=\"flex items-center border-b border-gray-100 w-48 overflow-x-hidden\">\n <div class=\"p-4 font-bold text-lg\">Models</div>\n <button\n @click=\"hideSidebar = true\"\n class=\"ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none\"\n aria-label=\"Close sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"currentColor\"><path d=\"M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z\"/></svg>\n </button>\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 </aside>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px] z-10\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"relative 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\" v-model=\"searchText\" @click=\"initFilter\" @input=\"updateAutocomplete\" @keydown=\"handleKeyDown\" />\n <ul v-if=\"autocompleteSuggestions.length\" class=\"absolute z-[9999] bg-white border border-gray-300 rounded mt-1 w-full max-h-40 overflow-y-auto shadow\">\n <li v-for=\"(suggestion, index) in autocompleteSuggestions\" :key=\"suggestion\" class=\"px-2 py-1 cursor-pointer\" :class=\"{ 'bg-ultramarine-100': index === autocompleteIndex }\" @mousedown.prevent=\"applySuggestion(index)\">{{ suggestion }}</li>\n </ul>\n </form>\n <div>\n <span v-if=\"numDocuments == null\">Loading ...</span>\n <span v-else-if=\"typeof numDocuments === 'number'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n v-show=\"!selectMultiple\"\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=\"stagingSelect\"\n type=\"button\"\n :class=\"{ 'bg-gray-500 ring-inset ring-2 ring-gray-300 hover:bg-gray-600': selectMultiple, 'bg-ultramarine-600 hover:bg-ultramarine-500' : !selectMultiple }\"\n class=\"rounded px-2 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\"\n >\n {{ selectMultiple ? 'Cancel' : 'Select' }}\n </button>\n <button\n v-show=\"selectMultiple\"\n @click=\"shouldShowUpdateMultipleModal=true;\"\n type=\"button\"\n class=\"rounded bg-green-600 px-2 py-2 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 >\n Update\n </button>\n <button\n @click=\"shouldShowDeleteMultipleModal=true;\"\n type=\"button\"\n v-show=\"selectMultiple\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500\"\n >\n Delete\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n v-show=\"!selectMultiple\"\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 v-show=\"!selectMultiple\"\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 v-show=\"!selectMultiple\"\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=\"handleDocumentClick(document, $event)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\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=\"handleDocumentClick(document, $event)\" :key=\"document._id\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\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\">×</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :search-text=\"searchText\"\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\">×</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 <async-button\n type=\"button\"\n @click=\"dropIndex(index.name)\"\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 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n Drop\n </async-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];\">×</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.path' + index\">{{path.path}}</label>\n </div>\n </div>\n <div class=\"mt-4 flex gap-2\">\n <button type=\"button\" @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=\"button\" @click=\"selectAll()\" 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\">Select All</button>\n <button type=\"button\" @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=\"button\" @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;\">×</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowUpdateMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowUpdateMultipleModal = false;\">×</div>\n <update-document :currentModel=\"currentModel\" :document=\"selectedDocuments\" :multiple=\"true\" @update=\"updateDocuments\" @close=\"shouldShowUpdateMultipleModal=false;\"></update-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteMultipleModal = false;\">×</div>\n <h2>Are you sure you want to delete {{selectedDocuments.length}} documents?</h2>\n <div>\n <list-json :value=\"selectedDocuments\"></list-json>\n </div>\n <div class=\"flex gap-4\">\n <async-button @click=\"deleteDocuments\" class=\"rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">\n Confirm\n </async-button>\n <button @click=\"shouldShowDeleteMultipleModal = false;\" class=\"rounded bg-gray-400 px-2 py-2 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-gray-500\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n</div>\n";
|
|
4938
5153
|
|
|
4939
5154
|
/***/ }),
|
|
4940
5155
|
|
|
@@ -15052,7 +15267,7 @@ var bson = /*#__PURE__*/Object.freeze({
|
|
|
15052
15267
|
/***/ ((module) => {
|
|
15053
15268
|
|
|
15054
15269
|
"use strict";
|
|
15055
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.
|
|
15270
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.125","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.1.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"8.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
|
|
15056
15271
|
|
|
15057
15272
|
/***/ })
|
|
15058
15273
|
|
package/frontend/public/tw.css
CHANGED
|
@@ -662,6 +662,10 @@ video {
|
|
|
662
662
|
z-index: 50;
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
+
.z-\[9999\] {
|
|
666
|
+
z-index: 9999;
|
|
667
|
+
}
|
|
668
|
+
|
|
665
669
|
.col-start-1 {
|
|
666
670
|
grid-column-start: 1;
|
|
667
671
|
}
|
|
@@ -902,6 +906,10 @@ video {
|
|
|
902
906
|
height: 1px;
|
|
903
907
|
}
|
|
904
908
|
|
|
909
|
+
.max-h-40 {
|
|
910
|
+
max-height: 10rem;
|
|
911
|
+
}
|
|
912
|
+
|
|
905
913
|
.max-h-\[50vh\] {
|
|
906
914
|
max-height: 50vh;
|
|
907
915
|
}
|
|
@@ -1474,11 +1482,6 @@ video {
|
|
|
1474
1482
|
background-color: rgb(229 234 255 / var(--tw-bg-opacity));
|
|
1475
1483
|
}
|
|
1476
1484
|
|
|
1477
|
-
.bg-ultramarine-500 {
|
|
1478
|
-
--tw-bg-opacity: 1;
|
|
1479
|
-
background-color: rgb(63 83 255 / var(--tw-bg-opacity));
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
1485
|
.bg-ultramarine-600 {
|
|
1483
1486
|
--tw-bg-opacity: 1;
|
|
1484
1487
|
background-color: rgb(24 35 255 / var(--tw-bg-opacity));
|
|
@@ -2086,11 +2089,6 @@ video {
|
|
|
2086
2089
|
background-color: rgb(63 83 255 / var(--tw-bg-opacity));
|
|
2087
2090
|
}
|
|
2088
2091
|
|
|
2089
|
-
.hover\:bg-ultramarine-600:hover {
|
|
2090
|
-
--tw-bg-opacity: 1;
|
|
2091
|
-
background-color: rgb(24 35 255 / var(--tw-bg-opacity));
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
2092
|
.hover\:bg-valencia-400:hover {
|
|
2095
2093
|
--tw-bg-opacity: 1;
|
|
2096
2094
|
background-color: rgb(235 126 126 / var(--tw-bg-opacity));
|
package/frontend/src/api.js
CHANGED
|
@@ -115,52 +115,11 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
115
115
|
return client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
|
|
116
116
|
},
|
|
117
117
|
getDocumentsStream: async function* getDocumentsStream(params) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
headers: {
|
|
124
|
-
Authorization: `${accessToken}`,
|
|
125
|
-
Accept: 'text/event-stream'
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
if (!response.ok) {
|
|
130
|
-
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const reader = response.body.getReader();
|
|
134
|
-
const decoder = new TextDecoder('utf-8');
|
|
135
|
-
let buffer = '';
|
|
136
|
-
|
|
137
|
-
while (true) {
|
|
138
|
-
const { done, value } = await reader.read();
|
|
139
|
-
if (done) break;
|
|
140
|
-
buffer += decoder.decode(value, { stream: true });
|
|
141
|
-
|
|
142
|
-
let eventEnd;
|
|
143
|
-
while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
|
|
144
|
-
const eventStr = buffer.slice(0, eventEnd);
|
|
145
|
-
buffer = buffer.slice(eventEnd + 2);
|
|
146
|
-
|
|
147
|
-
// Parse SSE event
|
|
148
|
-
const lines = eventStr.split('\n');
|
|
149
|
-
let data = '';
|
|
150
|
-
for (const line of lines) {
|
|
151
|
-
if (line.startsWith('data:')) {
|
|
152
|
-
data += line.slice(5).trim();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (data) {
|
|
156
|
-
try {
|
|
157
|
-
yield JSON.parse(data);
|
|
158
|
-
} catch (err) {
|
|
159
|
-
// If not JSON, yield as string
|
|
160
|
-
yield data;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
118
|
+
const data = await client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
|
|
119
|
+
yield { schemaPaths: data.schemaPaths };
|
|
120
|
+
yield { numDocs: data.numDocs };
|
|
121
|
+
for (const doc of data.docs) {
|
|
122
|
+
yield { document: doc };
|
|
164
123
|
}
|
|
165
124
|
},
|
|
166
125
|
getIndexes: function getIndexes(params) {
|
|
@@ -31,11 +31,14 @@
|
|
|
31
31
|
</nav>
|
|
32
32
|
</aside>
|
|
33
33
|
<div class="documents" ref="documentsList">
|
|
34
|
-
<div class="relative h-[42px]">
|
|
34
|
+
<div class="relative h-[42px] z-10">
|
|
35
35
|
<div class="documents-menu">
|
|
36
36
|
<div class="flex flex-row items-center w-full gap-2">
|
|
37
|
-
<form @submit.prevent="search" class="flex-grow m-0">
|
|
38
|
-
<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" v-model="searchText" @click="initFilter" />
|
|
37
|
+
<form @submit.prevent="search" class="relative flex-grow m-0">
|
|
38
|
+
<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" v-model="searchText" @click="initFilter" @input="updateAutocomplete" @keydown="handleKeyDown" />
|
|
39
|
+
<ul v-if="autocompleteSuggestions.length" class="absolute z-[9999] bg-white border border-gray-300 rounded mt-1 w-full max-h-40 overflow-y-auto shadow">
|
|
40
|
+
<li v-for="(suggestion, index) in autocompleteSuggestions" :key="suggestion" class="px-2 py-1 cursor-pointer" :class="{ 'bg-ultramarine-100': index === autocompleteIndex }" @mousedown.prevent="applySuggestion(index)">{{ suggestion }}</li>
|
|
41
|
+
</ul>
|
|
39
42
|
</form>
|
|
40
43
|
<div>
|
|
41
44
|
<span v-if="numDocuments == null">Loading ...</span>
|
|
@@ -51,10 +54,10 @@
|
|
|
51
54
|
<button
|
|
52
55
|
@click="stagingSelect"
|
|
53
56
|
type="button"
|
|
54
|
-
:class="{ 'bg-
|
|
55
|
-
class="rounded
|
|
57
|
+
:class="{ 'bg-gray-500 ring-inset ring-2 ring-gray-300 hover:bg-gray-600': selectMultiple, 'bg-ultramarine-600 hover:bg-ultramarine-500' : !selectMultiple }"
|
|
58
|
+
class="rounded px-2 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600"
|
|
56
59
|
>
|
|
57
|
-
Select
|
|
60
|
+
{{ selectMultiple ? 'Cancel' : 'Select' }}
|
|
58
61
|
</button>
|
|
59
62
|
<button
|
|
60
63
|
v-show="selectMultiple"
|
|
@@ -125,7 +128,7 @@
|
|
|
125
128
|
</th>
|
|
126
129
|
</thead>
|
|
127
130
|
<tbody>
|
|
128
|
-
<tr v-for="document in documents" @click="handleDocumentClick(document)" :key="document._id">
|
|
131
|
+
<tr v-for="document in documents" @click="handleDocumentClick(document, $event)" :key="document._id">
|
|
129
132
|
<td v-for="schemaPath in filteredPaths" :class="{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }">
|
|
130
133
|
<component
|
|
131
134
|
:is="getComponentForPath(schemaPath)"
|
|
@@ -137,7 +140,7 @@
|
|
|
137
140
|
</tbody>
|
|
138
141
|
</table>
|
|
139
142
|
<div v-if="outputType === 'json'">
|
|
140
|
-
<div v-for="document in documents" @click="handleDocumentClick(document)" :key="document._id" :class="{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }">
|
|
143
|
+
<div v-for="document in documents" @click="handleDocumentClick(document, $event)" :key="document._id" :class="{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }">
|
|
141
144
|
<list-json :value="filterDocument(document)">
|
|
142
145
|
</list-json>
|
|
143
146
|
</div>
|
|
@@ -5,6 +5,34 @@ const template = require('./models.html');
|
|
|
5
5
|
const mpath = require('mpath');
|
|
6
6
|
|
|
7
7
|
const appendCSS = require('../appendCSS');
|
|
8
|
+
const { Trie } = require('./trie');
|
|
9
|
+
|
|
10
|
+
const QUERY_SELECTORS = [
|
|
11
|
+
'$eq',
|
|
12
|
+
'$ne',
|
|
13
|
+
'$gt',
|
|
14
|
+
'$gte',
|
|
15
|
+
'$lt',
|
|
16
|
+
'$lte',
|
|
17
|
+
'$in',
|
|
18
|
+
'$nin',
|
|
19
|
+
'$exists',
|
|
20
|
+
'$regex',
|
|
21
|
+
'$options',
|
|
22
|
+
'$text',
|
|
23
|
+
'$search',
|
|
24
|
+
'$and',
|
|
25
|
+
'$or',
|
|
26
|
+
'$nor',
|
|
27
|
+
'$not',
|
|
28
|
+
'$elemMatch',
|
|
29
|
+
'$size',
|
|
30
|
+
'$all',
|
|
31
|
+
'$type',
|
|
32
|
+
'$expr',
|
|
33
|
+
'$jsonSchema',
|
|
34
|
+
'$mod'
|
|
35
|
+
];
|
|
8
36
|
|
|
9
37
|
|
|
10
38
|
appendCSS(require('./models.css'));
|
|
@@ -31,6 +59,9 @@ module.exports = app => app.component('models', {
|
|
|
31
59
|
selectMultiple: false,
|
|
32
60
|
selectedDocuments: [],
|
|
33
61
|
searchText: '',
|
|
62
|
+
autocompleteSuggestions: [],
|
|
63
|
+
autocompleteIndex: 0,
|
|
64
|
+
autocompleteTrie: null,
|
|
34
65
|
shouldShowExportModal: false,
|
|
35
66
|
shouldShowCreateModal: false,
|
|
36
67
|
shouldShowFieldModal: false,
|
|
@@ -43,10 +74,12 @@ module.exports = app => app.component('models', {
|
|
|
43
74
|
scrollHeight: 0,
|
|
44
75
|
interval: null,
|
|
45
76
|
outputType: 'table', // json, table
|
|
46
|
-
hideSidebar: null
|
|
77
|
+
hideSidebar: null,
|
|
78
|
+
lastSelectedIndex: null
|
|
47
79
|
}),
|
|
48
80
|
created() {
|
|
49
81
|
this.currentModel = this.model;
|
|
82
|
+
this.buildAutocompleteTrie();
|
|
50
83
|
},
|
|
51
84
|
beforeDestroy() {
|
|
52
85
|
document.removeEventListener('scroll', this.onScroll, true);
|
|
@@ -65,6 +98,16 @@ module.exports = app => app.component('models', {
|
|
|
65
98
|
await this.initSearchFromUrl();
|
|
66
99
|
},
|
|
67
100
|
methods: {
|
|
101
|
+
buildAutocompleteTrie() {
|
|
102
|
+
this.autocompleteTrie = new Trie();
|
|
103
|
+
this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5);
|
|
104
|
+
if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
|
|
105
|
+
const paths = this.schemaPaths
|
|
106
|
+
.map(path => path?.path)
|
|
107
|
+
.filter(path => typeof path === 'string' && path.length > 0);
|
|
108
|
+
this.autocompleteTrie.bulkInsert(paths, 10);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
68
111
|
async initSearchFromUrl() {
|
|
69
112
|
this.status = 'loading';
|
|
70
113
|
this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
|
|
@@ -102,6 +145,62 @@ module.exports = app => app.component('models', {
|
|
|
102
145
|
});
|
|
103
146
|
}
|
|
104
147
|
},
|
|
148
|
+
updateAutocomplete() {
|
|
149
|
+
const input = this.$refs.searchInput;
|
|
150
|
+
const cursorPos = input ? input.selectionStart : 0;
|
|
151
|
+
const before = this.searchText.slice(0, cursorPos);
|
|
152
|
+
const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
|
|
153
|
+
if (match && match[1]) {
|
|
154
|
+
const term = match[1].replace(/["']/g, '');
|
|
155
|
+
if (!term) {
|
|
156
|
+
this.autocompleteSuggestions = [];
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (this.autocompleteTrie) {
|
|
160
|
+
this.autocompleteSuggestions = this.autocompleteTrie.getSuggestions(term, 10);
|
|
161
|
+
this.autocompleteIndex = 0;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.autocompleteSuggestions = [];
|
|
166
|
+
},
|
|
167
|
+
handleKeyDown(ev) {
|
|
168
|
+
if (this.autocompleteSuggestions.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (ev.key === 'Tab' || ev.key === 'Enter') {
|
|
172
|
+
ev.preventDefault();
|
|
173
|
+
this.applySuggestion(this.autocompleteIndex);
|
|
174
|
+
} else if (ev.key === 'ArrowDown') {
|
|
175
|
+
ev.preventDefault();
|
|
176
|
+
this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
|
|
177
|
+
} else if (ev.key === 'ArrowUp') {
|
|
178
|
+
ev.preventDefault();
|
|
179
|
+
this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
applySuggestion(index) {
|
|
183
|
+
const suggestion = this.autocompleteSuggestions[index];
|
|
184
|
+
if (!suggestion) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const input = this.$refs.searchInput;
|
|
188
|
+
const cursorPos = input.selectionStart;
|
|
189
|
+
const before = this.searchText.slice(0, cursorPos);
|
|
190
|
+
const after = this.searchText.slice(cursorPos);
|
|
191
|
+
const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
|
|
192
|
+
if (!match) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const token = match[1];
|
|
196
|
+
const start = cursorPos - token.length;
|
|
197
|
+
this.searchText = this.searchText.slice(0, start) + suggestion + after;
|
|
198
|
+
this.$nextTick(() => {
|
|
199
|
+
const pos = start + suggestion.length;
|
|
200
|
+
input.setSelectionRange(pos, pos);
|
|
201
|
+
});
|
|
202
|
+
this.autocompleteSuggestions = [];
|
|
203
|
+
},
|
|
105
204
|
clickFilter(path) {
|
|
106
205
|
if (this.searchText) {
|
|
107
206
|
if (this.searchText.endsWith('}')) {
|
|
@@ -214,8 +313,10 @@ module.exports = app => app.component('models', {
|
|
|
214
313
|
// Clear previous data
|
|
215
314
|
this.documents = [];
|
|
216
315
|
this.schemaPaths = [];
|
|
316
|
+
this.buildAutocompleteTrie();
|
|
217
317
|
this.numDocuments = null;
|
|
218
318
|
this.loadedAllDocs = false;
|
|
319
|
+
this.lastSelectedIndex = null;
|
|
219
320
|
|
|
220
321
|
let docsCount = 0;
|
|
221
322
|
let schemaPathsReceived = false;
|
|
@@ -247,6 +348,7 @@ module.exports = app => app.component('models', {
|
|
|
247
348
|
}
|
|
248
349
|
this.filteredPaths = [...this.schemaPaths];
|
|
249
350
|
this.selectedPaths = [...this.schemaPaths];
|
|
351
|
+
this.buildAutocompleteTrie();
|
|
250
352
|
schemaPathsReceived = true;
|
|
251
353
|
}
|
|
252
354
|
if (event.numDocs !== undefined) {
|
|
@@ -389,17 +491,38 @@ module.exports = app => app.component('models', {
|
|
|
389
491
|
}
|
|
390
492
|
this.edittingDoc = null;
|
|
391
493
|
},
|
|
392
|
-
handleDocumentClick(document) {
|
|
393
|
-
console.log(this.selectedDocuments);
|
|
494
|
+
handleDocumentClick(document, event) {
|
|
394
495
|
if (this.selectMultiple) {
|
|
395
|
-
const
|
|
396
|
-
if (
|
|
397
|
-
const
|
|
398
|
-
if (
|
|
399
|
-
|
|
496
|
+
const documentIndex = this.documents.findIndex(doc => doc._id.toString() == document._id.toString());
|
|
497
|
+
if (event?.shiftKey && this.selectedDocuments.length > 0) {
|
|
498
|
+
const anchorIndex = this.lastSelectedIndex;
|
|
499
|
+
if (anchorIndex != null && anchorIndex !== -1 && documentIndex !== -1) {
|
|
500
|
+
const start = Math.min(anchorIndex, documentIndex);
|
|
501
|
+
const end = Math.max(anchorIndex, documentIndex);
|
|
502
|
+
const selectedDocumentIds = new Set(this.selectedDocuments.map(doc => doc._id.toString()));
|
|
503
|
+
for (let i = start; i <= end; i++) {
|
|
504
|
+
const docInRange = this.documents[i];
|
|
505
|
+
const existsInRange = selectedDocumentIds.has(docInRange._id.toString());
|
|
506
|
+
if (!existsInRange) {
|
|
507
|
+
this.selectedDocuments.push(docInRange);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
this.lastSelectedIndex = documentIndex;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const index = this.selectedDocuments.findIndex(x => x._id.toString() == document._id.toString());
|
|
515
|
+
if (index !== -1) {
|
|
516
|
+
this.selectedDocuments.splice(index, 1);
|
|
517
|
+
if (this.selectedDocuments.length === 0) {
|
|
518
|
+
this.lastSelectedIndex = null;
|
|
519
|
+
} else {
|
|
520
|
+
const lastDoc = this.selectedDocuments[this.selectedDocuments.length - 1];
|
|
521
|
+
this.lastSelectedIndex = this.documents.findIndex(doc => doc._id.toString() == lastDoc._id.toString());
|
|
400
522
|
}
|
|
401
523
|
} else {
|
|
402
524
|
this.selectedDocuments.push(document);
|
|
525
|
+
this.lastSelectedIndex = documentIndex;
|
|
403
526
|
}
|
|
404
527
|
} else {
|
|
405
528
|
this.$router.push('/model/' + this.currentModel + '/document/' + document._id);
|
|
@@ -413,18 +536,21 @@ module.exports = app => app.component('models', {
|
|
|
413
536
|
});
|
|
414
537
|
await this.getDocuments();
|
|
415
538
|
this.selectedDocuments.length = 0;
|
|
539
|
+
this.lastSelectedIndex = null;
|
|
416
540
|
this.shouldShowDeleteMultipleModal = false;
|
|
417
541
|
this.selectMultiple = false;
|
|
418
542
|
},
|
|
419
543
|
async updateDocuments() {
|
|
420
544
|
await this.getDocuments();
|
|
421
545
|
this.selectedDocuments.length = 0;
|
|
546
|
+
this.lastSelectedIndex = null;
|
|
422
547
|
this.selectMultiple = false;
|
|
423
548
|
},
|
|
424
549
|
stagingSelect() {
|
|
425
550
|
if (this.selectMultiple) {
|
|
426
551
|
this.selectMultiple = false;
|
|
427
552
|
this.selectedDocuments.length = 0;
|
|
553
|
+
this.lastSelectedIndex = null;
|
|
428
554
|
} else {
|
|
429
555
|
this.selectMultiple = true;
|
|
430
556
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class TrieNode {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.children = Object.create(null);
|
|
6
|
+
this.isEnd = false;
|
|
7
|
+
this.freq = 0;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class Trie {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.root = new TrieNode();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
insert(word, freq = 1) {
|
|
17
|
+
if (!word) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let node = this.root;
|
|
21
|
+
for (const ch of word) {
|
|
22
|
+
if (!node.children[ch]) {
|
|
23
|
+
node.children[ch] = new TrieNode();
|
|
24
|
+
}
|
|
25
|
+
node = node.children[ch];
|
|
26
|
+
}
|
|
27
|
+
node.isEnd = true;
|
|
28
|
+
node.freq += freq;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
bulkInsert(words, freq = 1) {
|
|
32
|
+
if (!Array.isArray(words)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const word of words) {
|
|
36
|
+
this.insert(word, freq);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
collect(node, prefix, out) {
|
|
41
|
+
if (node.isEnd) {
|
|
42
|
+
out.push([prefix, node.freq]);
|
|
43
|
+
}
|
|
44
|
+
for (const [ch, child] of Object.entries(node.children)) {
|
|
45
|
+
this.collect(child, prefix + ch, out);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
suggest(prefix, limit = 10) {
|
|
50
|
+
let node = this.root;
|
|
51
|
+
for (const ch of prefix) {
|
|
52
|
+
if (!node.children[ch]) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
node = node.children[ch];
|
|
56
|
+
}
|
|
57
|
+
const results = [];
|
|
58
|
+
this.collect(node, prefix, results);
|
|
59
|
+
results.sort((a, b) => b[1] - a[1]);
|
|
60
|
+
return results.slice(0, limit).map(([word]) => word);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fuzzySuggest(prefix, limit = 10) {
|
|
64
|
+
const results = new Set();
|
|
65
|
+
|
|
66
|
+
const dfs = (node, path, edits) => {
|
|
67
|
+
if (edits > 1) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (node.isEnd && Math.abs(path.length - prefix.length) <= 1) {
|
|
71
|
+
const dist = levenshtein(prefix, path);
|
|
72
|
+
if (dist <= 1) {
|
|
73
|
+
results.add(path);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const [ch, child] of Object.entries(node.children)) {
|
|
77
|
+
const nextEdits = ch === prefix[path.length] ? edits : edits + 1;
|
|
78
|
+
dfs(child, path + ch, nextEdits);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
dfs(this.root, '', 0);
|
|
83
|
+
return Array.from(results).slice(0, limit);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getSuggestions(prefix, limit = 10) {
|
|
87
|
+
if (!prefix) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const exact = this.suggest(prefix, limit);
|
|
91
|
+
if (exact.length >= limit) {
|
|
92
|
+
return exact;
|
|
93
|
+
}
|
|
94
|
+
const fuzzy = this.fuzzySuggest(prefix, limit - exact.length);
|
|
95
|
+
return [...exact, ...fuzzy];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function levenshtein(a, b) {
|
|
100
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
101
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
102
|
+
);
|
|
103
|
+
for (let i = 1; i <= a.length; i++) {
|
|
104
|
+
for (let j = 1; j <= b.length; j++) {
|
|
105
|
+
dp[i][j] =
|
|
106
|
+
a[i - 1] === b[j - 1]
|
|
107
|
+
? dp[i - 1][j - 1]
|
|
108
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return dp[a.length][b.length];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
Trie,
|
|
116
|
+
TrieNode
|
|
117
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mongoosejs/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.125",
|
|
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": {
|