@mongoosejs/studio 0.0.118 → 0.0.120
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/exportQueryResults.js +7 -8
- package/backend/actions/Model/getDocuments.js +8 -13
- package/backend/actions/Model/getDocumentsStream.js +117 -0
- package/backend/actions/Model/index.js +1 -0
- package/backend/authorize.js +1 -0
- package/backend/helpers/evaluateFilter.js +51 -0
- package/eslint.config.js +2 -1
- package/frontend/public/app.js +191 -56
- package/frontend/src/api.js +52 -0
- package/frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html +9 -0
- package/frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js +20 -0
- package/frontend/src/dashboard-result/dashboard-result.js +3 -0
- package/frontend/src/export-query-results/export-query-results.js +3 -3
- package/frontend/src/models/models.html +3 -3
- package/frontend/src/models/models.js +86 -51
- package/package.json +2 -2
|
@@ -4,14 +4,15 @@ const Archetype = require('archetype');
|
|
|
4
4
|
const mongoose = require('mongoose');
|
|
5
5
|
const { stringify } = require('csv-stringify/sync');
|
|
6
6
|
const authorize = require('../../authorize');
|
|
7
|
+
const evaluateFilter = require('../../helpers/evaluateFilter');
|
|
7
8
|
|
|
8
9
|
const GetDocumentsParams = new Archetype({
|
|
9
10
|
model: {
|
|
10
11
|
$type: 'string',
|
|
11
12
|
$required: true
|
|
12
13
|
},
|
|
13
|
-
|
|
14
|
-
$type:
|
|
14
|
+
searchText: {
|
|
15
|
+
$type: 'string'
|
|
15
16
|
},
|
|
16
17
|
propertiesToInclude: {
|
|
17
18
|
$type: ['string'],
|
|
@@ -30,8 +31,7 @@ const GetDocumentsParams = new Archetype({
|
|
|
30
31
|
|
|
31
32
|
module.exports = ({ db }) => async function exportQueryResults(params, req, res) {
|
|
32
33
|
params = new GetDocumentsParams(params);
|
|
33
|
-
|
|
34
|
-
const { model, propertiesToInclude, roles } = params;
|
|
34
|
+
const { model, propertiesToInclude, roles, searchText } = params;
|
|
35
35
|
|
|
36
36
|
await authorize('Model.exportQueryResults', roles);
|
|
37
37
|
|
|
@@ -40,12 +40,11 @@ module.exports = ({ db }) => async function exportQueryResults(params, req, res)
|
|
|
40
40
|
throw new Error(`Model ${model} not found`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
43
|
+
const parsedFilter = evaluateFilter(searchText);
|
|
44
|
+
const filter = parsedFilter == null ? {} : parsedFilter;
|
|
46
45
|
|
|
47
46
|
const docs = await Model.
|
|
48
|
-
find(filter
|
|
47
|
+
find(filter).
|
|
49
48
|
setOptions({ sanitizeFilter: true }).
|
|
50
49
|
sort({ _id: -1 });
|
|
51
50
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const Archetype = require('archetype');
|
|
4
4
|
const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
|
|
5
|
-
const
|
|
5
|
+
const evaluateFilter = require('../../helpers/evaluateFilter');
|
|
6
6
|
const authorize = require('../../authorize');
|
|
7
7
|
|
|
8
8
|
const GetDocumentsParams = new Archetype({
|
|
@@ -20,8 +20,8 @@ const GetDocumentsParams = new Archetype({
|
|
|
20
20
|
$required: true,
|
|
21
21
|
$default: 0
|
|
22
22
|
},
|
|
23
|
-
|
|
24
|
-
$type:
|
|
23
|
+
searchText: {
|
|
24
|
+
$type: 'string'
|
|
25
25
|
},
|
|
26
26
|
sort: {
|
|
27
27
|
$type: Archetype.Any
|
|
@@ -36,20 +36,15 @@ module.exports = ({ db }) => async function getDocuments(params) {
|
|
|
36
36
|
const { roles } = params;
|
|
37
37
|
await authorize('Model.getDocuments', roles);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
if (filter != null && Object.keys(filter).length > 0) {
|
|
41
|
-
filter = EJSON.parse(filter);
|
|
42
|
-
}
|
|
43
|
-
const { model, limit, skip, sort } = params;
|
|
39
|
+
const { model, limit, skip, sort, searchText } = params;
|
|
44
40
|
|
|
45
41
|
const Model = db.models[model];
|
|
46
42
|
if (Model == null) {
|
|
47
43
|
throw new Error(`Model ${model} not found`);
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
46
|
+
const parsedFilter = evaluateFilter(searchText);
|
|
47
|
+
const filter = parsedFilter == null ? {} : parsedFilter;
|
|
53
48
|
|
|
54
49
|
const hasSort = typeof sort === 'object' && sort != null && Object.keys(sort).length > 0;
|
|
55
50
|
const sortObj = hasSort ? { ...sort } : {};
|
|
@@ -57,7 +52,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
|
|
|
57
52
|
sortObj._id = -1;
|
|
58
53
|
}
|
|
59
54
|
const cursor = await Model.
|
|
60
|
-
find(filter
|
|
55
|
+
find(filter).
|
|
61
56
|
limit(limit).
|
|
62
57
|
skip(skip).
|
|
63
58
|
sort(sortObj).
|
|
@@ -78,7 +73,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
|
|
|
78
73
|
}
|
|
79
74
|
removeSpecifiedPaths(schemaPaths, '.$*');
|
|
80
75
|
|
|
81
|
-
const numDocuments =
|
|
76
|
+
const numDocuments = parsedFilter == null ?
|
|
82
77
|
await Model.estimatedDocumentCount() :
|
|
83
78
|
await Model.countDocuments(filter);
|
|
84
79
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Archetype = require('archetype');
|
|
4
|
+
const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
|
|
5
|
+
const evaluateFilter = require('../../helpers/evaluateFilter');
|
|
6
|
+
const authorize = require('../../authorize');
|
|
7
|
+
|
|
8
|
+
const GetDocumentsParams = new Archetype({
|
|
9
|
+
model: {
|
|
10
|
+
$type: 'string',
|
|
11
|
+
$required: true
|
|
12
|
+
},
|
|
13
|
+
limit: {
|
|
14
|
+
$type: 'number',
|
|
15
|
+
$required: true,
|
|
16
|
+
$default: 20
|
|
17
|
+
},
|
|
18
|
+
skip: {
|
|
19
|
+
$type: 'number',
|
|
20
|
+
$required: true,
|
|
21
|
+
$default: 0
|
|
22
|
+
},
|
|
23
|
+
searchText: {
|
|
24
|
+
$type: 'string'
|
|
25
|
+
},
|
|
26
|
+
sort: {
|
|
27
|
+
$type: Archetype.Any
|
|
28
|
+
},
|
|
29
|
+
roles: {
|
|
30
|
+
$type: ['string']
|
|
31
|
+
}
|
|
32
|
+
}).compile('GetDocumentsParams');
|
|
33
|
+
|
|
34
|
+
module.exports = ({ db }) => async function* getDocumentsStream(params) {
|
|
35
|
+
params = new GetDocumentsParams(params);
|
|
36
|
+
const { roles } = params;
|
|
37
|
+
await authorize('Model.getDocumentsStream', roles);
|
|
38
|
+
|
|
39
|
+
const { model, limit, skip, sort, searchText } = params;
|
|
40
|
+
|
|
41
|
+
const Model = db.models[model];
|
|
42
|
+
if (Model == null) {
|
|
43
|
+
throw new Error(`Model ${model} not found`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parsedFilter = evaluateFilter(searchText);
|
|
47
|
+
const filter = parsedFilter == null ? {} : parsedFilter;
|
|
48
|
+
|
|
49
|
+
const hasSort = typeof sort === 'object' && sort != null && Object.keys(sort).length > 0;
|
|
50
|
+
const sortObj = hasSort ? { ...sort } : {};
|
|
51
|
+
if (!sortObj.hasOwnProperty('_id')) {
|
|
52
|
+
sortObj._id = -1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const schemaPaths = {};
|
|
56
|
+
for (const path of Object.keys(Model.schema.paths)) {
|
|
57
|
+
schemaPaths[path] = {
|
|
58
|
+
instance: Model.schema.paths[path].instance,
|
|
59
|
+
path,
|
|
60
|
+
ref: Model.schema.paths[path].options?.ref,
|
|
61
|
+
required: Model.schema.paths[path].options?.required
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
removeSpecifiedPaths(schemaPaths, '.$*');
|
|
65
|
+
|
|
66
|
+
yield { schemaPaths };
|
|
67
|
+
|
|
68
|
+
// Start counting documents in parallel with streaming documents
|
|
69
|
+
const numDocsPromise = (parsedFilter == null)
|
|
70
|
+
? Model.estimatedDocumentCount().exec()
|
|
71
|
+
: Model.countDocuments(filter).exec();
|
|
72
|
+
|
|
73
|
+
const cursor = await Model.
|
|
74
|
+
find(filter).
|
|
75
|
+
limit(limit).
|
|
76
|
+
skip(skip).
|
|
77
|
+
sort(sortObj).
|
|
78
|
+
batchSize(1).
|
|
79
|
+
cursor();
|
|
80
|
+
|
|
81
|
+
let numDocsYielded = false;
|
|
82
|
+
let numDocumentsPromiseResolved = false;
|
|
83
|
+
let numDocumentsValue;
|
|
84
|
+
let numDocumentsError;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Start listening for numDocsPromise resolution
|
|
88
|
+
numDocsPromise.then(num => {
|
|
89
|
+
numDocumentsPromiseResolved = true;
|
|
90
|
+
numDocumentsValue = num;
|
|
91
|
+
}).catch(err => {
|
|
92
|
+
numDocumentsPromiseResolved = true;
|
|
93
|
+
numDocumentsError = err;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
|
|
97
|
+
// If numDocsPromise has resolved and not yet yielded, yield it first
|
|
98
|
+
if (numDocumentsPromiseResolved && !numDocsYielded) {
|
|
99
|
+
if (numDocumentsError) {
|
|
100
|
+
yield { error: numDocumentsError };
|
|
101
|
+
} else {
|
|
102
|
+
yield { numDocs: numDocumentsValue };
|
|
103
|
+
}
|
|
104
|
+
numDocsYielded = true;
|
|
105
|
+
}
|
|
106
|
+
yield { document: doc.toJSON({ virtuals: false, getters: false, transform: false }) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If numDocsPromise hasn't resolved yet, wait for it and yield
|
|
110
|
+
if (!numDocsYielded) {
|
|
111
|
+
const numDocuments = await numDocsPromise;
|
|
112
|
+
yield { numDocs: numDocuments };
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
await cursor.close();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -7,6 +7,7 @@ exports.dropIndex = require('./dropIndex');
|
|
|
7
7
|
exports.exportQueryResults = require('./exportQueryResults');
|
|
8
8
|
exports.getDocument = require('./getDocument');
|
|
9
9
|
exports.getDocuments = require('./getDocuments');
|
|
10
|
+
exports.getDocumentsStream = require('./getDocumentsStream');
|
|
10
11
|
exports.getIndexes = require('./getIndexes');
|
|
11
12
|
exports.listModels = require('./listModels');
|
|
12
13
|
exports.updateDocument = require('./updateDocument');
|
package/backend/authorize.js
CHANGED
|
@@ -20,6 +20,7 @@ const actionsToRequiredRoles = {
|
|
|
20
20
|
'Model.exportQueryResults': ['owner', 'admin', 'member', 'readonly'],
|
|
21
21
|
'Model.getDocument': ['owner', 'admin', 'member', 'readonly'],
|
|
22
22
|
'Model.getDocuments': ['owner', 'admin', 'member', 'readonly'],
|
|
23
|
+
'Model.getDocumentsStream': ['owner', 'admin', 'member', 'readonly'],
|
|
23
24
|
'Model.getIndexes': ['owner', 'admin', 'member', 'readonly'],
|
|
24
25
|
'Model.listModels': ['owner', 'admin', 'member', 'readonly'],
|
|
25
26
|
'Model.updateDocuments': ['owner', 'admin', 'member']
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const mongoose = require('mongoose');
|
|
4
|
+
const vm = require('vm');
|
|
5
|
+
|
|
6
|
+
const evaluate = typeof vm.evaluate === 'function' ?
|
|
7
|
+
vm.evaluate.bind(vm) :
|
|
8
|
+
(code, context) => {
|
|
9
|
+
const script = new vm.Script(code, { displayErrors: true });
|
|
10
|
+
return script.runInContext(context, { timeout: 1000 });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ObjectId = new Proxy(mongoose.Types.ObjectId, {
|
|
14
|
+
apply(target, thisArg, argumentsList) {
|
|
15
|
+
return new target(...argumentsList);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
module.exports = function evaluateFilter(searchText) {
|
|
20
|
+
if (searchText == null) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const normalized = String(searchText);
|
|
25
|
+
if (normalized.trim().length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const context = vm.createContext({
|
|
30
|
+
ObjectId,
|
|
31
|
+
Date,
|
|
32
|
+
Math
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let result;
|
|
36
|
+
try {
|
|
37
|
+
result = evaluate(`(${normalized})`, context);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new Error(`Invalid search filter: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (result == null) {
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof result === 'object') {
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error('Invalid search filter: must evaluate to an object');
|
|
51
|
+
};
|
package/eslint.config.js
CHANGED
package/frontend/public/app.js
CHANGED
|
@@ -124,6 +124,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
124
124
|
getDocuments: function getDocuments(params) {
|
|
125
125
|
return client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
|
|
126
126
|
},
|
|
127
|
+
getDocumentsStream: function getDocumentsStream(params) {
|
|
128
|
+
return client.post('', { action: 'Model.getDocumentsStream', ...params }).then(res => res.data);
|
|
129
|
+
},
|
|
127
130
|
getIndexes: function getIndexes(params) {
|
|
128
131
|
return client.post('', { action: 'Model.getIndexes', ...params }).then(res => res.data);
|
|
129
132
|
},
|
|
@@ -229,6 +232,55 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
229
232
|
getDocuments: function getDocuments(params) {
|
|
230
233
|
return client.post('/Model/getDocuments', params).then(res => res.data);
|
|
231
234
|
},
|
|
235
|
+
getDocumentsStream: async function* getDocumentsStream(params) {
|
|
236
|
+
const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
|
|
237
|
+
const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/getDocumentsStream?' + new URLSearchParams(params).toString();
|
|
238
|
+
|
|
239
|
+
const response = await fetch(url, {
|
|
240
|
+
method: 'GET',
|
|
241
|
+
headers: {
|
|
242
|
+
Authorization: `${accessToken}`,
|
|
243
|
+
Accept: 'text/event-stream'
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const reader = response.body.getReader();
|
|
252
|
+
const decoder = new TextDecoder('utf-8');
|
|
253
|
+
let buffer = '';
|
|
254
|
+
|
|
255
|
+
while (true) {
|
|
256
|
+
const { done, value } = await reader.read();
|
|
257
|
+
if (done) break;
|
|
258
|
+
buffer += decoder.decode(value, { stream: true });
|
|
259
|
+
|
|
260
|
+
let eventEnd;
|
|
261
|
+
while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
|
|
262
|
+
const eventStr = buffer.slice(0, eventEnd);
|
|
263
|
+
buffer = buffer.slice(eventEnd + 2);
|
|
264
|
+
|
|
265
|
+
// Parse SSE event
|
|
266
|
+
const lines = eventStr.split('\n');
|
|
267
|
+
let data = '';
|
|
268
|
+
for (const line of lines) {
|
|
269
|
+
if (line.startsWith('data:')) {
|
|
270
|
+
data += line.slice(5).trim();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (data) {
|
|
274
|
+
try {
|
|
275
|
+
yield JSON.parse(data);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
// If not JSON, yield as string
|
|
278
|
+
yield data;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
},
|
|
232
284
|
getIndexes: function getIndexes(params) {
|
|
233
285
|
return client.post('/Model/getIndexes', params).then(res => res.data);
|
|
234
286
|
},
|
|
@@ -1018,6 +1070,37 @@ module.exports = app => app.component('dashboard-document', {
|
|
|
1018
1070
|
});
|
|
1019
1071
|
|
|
1020
1072
|
|
|
1073
|
+
/***/ }),
|
|
1074
|
+
|
|
1075
|
+
/***/ "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js":
|
|
1076
|
+
/*!************************************************************************!*\
|
|
1077
|
+
!*** ./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js ***!
|
|
1078
|
+
\************************************************************************/
|
|
1079
|
+
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
|
|
1080
|
+
|
|
1081
|
+
"use strict";
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
const template = __webpack_require__(/*! ./dashboard-grid.html */ "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html");
|
|
1085
|
+
|
|
1086
|
+
module.exports = app => app.component('dashboard-grid', {
|
|
1087
|
+
template: template,
|
|
1088
|
+
props: ['value'],
|
|
1089
|
+
computed: {
|
|
1090
|
+
columns() {
|
|
1091
|
+
const grid = this.value && this.value.$grid;
|
|
1092
|
+
if (!Array.isArray(grid) || grid.length === 0) {
|
|
1093
|
+
return 1;
|
|
1094
|
+
}
|
|
1095
|
+
return Math.max(1, ...grid.map(row => Array.isArray(row) ? row.length : 0));
|
|
1096
|
+
},
|
|
1097
|
+
gridTemplateColumns() {
|
|
1098
|
+
return `repeat(${this.columns}, minmax(0, 1fr))`;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
|
|
1021
1104
|
/***/ }),
|
|
1022
1105
|
|
|
1023
1106
|
/***/ "./frontend/src/dashboard-result/dashboard-map/dashboard-map.js":
|
|
@@ -1139,6 +1222,9 @@ module.exports = app => app.component('dashboard-result', {
|
|
|
1139
1222
|
if (value.$text) {
|
|
1140
1223
|
return 'dashboard-text';
|
|
1141
1224
|
}
|
|
1225
|
+
if (value.$grid) {
|
|
1226
|
+
return 'dashboard-grid';
|
|
1227
|
+
}
|
|
1142
1228
|
}
|
|
1143
1229
|
}
|
|
1144
1230
|
});
|
|
@@ -1973,7 +2059,7 @@ appendCSS(__webpack_require__(/*! ./export-query-results.css */ "./frontend/src/
|
|
|
1973
2059
|
|
|
1974
2060
|
module.exports = app => app.component('export-query-results', {
|
|
1975
2061
|
template: template,
|
|
1976
|
-
props: ['schemaPaths', '
|
|
2062
|
+
props: ['schemaPaths', 'searchText', 'currentModel'],
|
|
1977
2063
|
emits: ['done'],
|
|
1978
2064
|
data: () => ({
|
|
1979
2065
|
shouldExport: {}
|
|
@@ -1990,8 +2076,8 @@ module.exports = app => app.component('export-query-results', {
|
|
|
1990
2076
|
model: this.currentModel,
|
|
1991
2077
|
propertiesToInclude: Object.keys(this.shouldExport).filter(key => this.shouldExport[key])
|
|
1992
2078
|
};
|
|
1993
|
-
if (this.
|
|
1994
|
-
params.
|
|
2079
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
2080
|
+
params.searchText = this.searchText;
|
|
1995
2081
|
}
|
|
1996
2082
|
await api.Model.exportQueryResults(params);
|
|
1997
2083
|
|
|
@@ -2514,15 +2600,6 @@ module.exports = app => app.component('modal', {
|
|
|
2514
2600
|
const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
|
|
2515
2601
|
const template = __webpack_require__(/*! ./models.html */ "./frontend/src/models/models.html");
|
|
2516
2602
|
const mpath = __webpack_require__(/*! mpath */ "./node_modules/mpath/index.js");
|
|
2517
|
-
const { BSON, EJSON } = __webpack_require__(/*! bson */ "./node_modules/bson/lib/bson.mjs");
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
const ObjectId = new Proxy(BSON.ObjectId, {
|
|
2522
|
-
apply(target, thisArg, argumentsList) {
|
|
2523
|
-
return new target(...argumentsList);
|
|
2524
|
-
}
|
|
2525
|
-
});
|
|
2526
2603
|
|
|
2527
2604
|
const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
|
|
2528
2605
|
|
|
@@ -2541,14 +2618,13 @@ module.exports = app => app.component('models', {
|
|
|
2541
2618
|
schemaPaths: [],
|
|
2542
2619
|
filteredPaths: [],
|
|
2543
2620
|
selectedPaths: [],
|
|
2544
|
-
numDocuments:
|
|
2621
|
+
numDocuments: null,
|
|
2545
2622
|
mongoDBIndexes: [],
|
|
2546
2623
|
schemaIndexes: [],
|
|
2547
2624
|
status: 'loading',
|
|
2548
2625
|
loadedAllDocs: false,
|
|
2549
2626
|
edittingDoc: null,
|
|
2550
2627
|
docEdits: null,
|
|
2551
|
-
filter: null,
|
|
2552
2628
|
selectMultiple: false,
|
|
2553
2629
|
selectedDocuments: [],
|
|
2554
2630
|
searchText: '',
|
|
@@ -2591,8 +2667,8 @@ module.exports = app => app.component('models', {
|
|
|
2591
2667
|
this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
|
|
2592
2668
|
if (this.$route.query?.search) {
|
|
2593
2669
|
this.searchText = this.$route.query.search;
|
|
2594
|
-
|
|
2595
|
-
this.
|
|
2670
|
+
} else {
|
|
2671
|
+
this.searchText = '';
|
|
2596
2672
|
}
|
|
2597
2673
|
if (this.$route.query?.sort) {
|
|
2598
2674
|
const sort = eval(`(${this.$route.query.sort})`);
|
|
@@ -2667,13 +2743,16 @@ module.exports = app => app.component('models', {
|
|
|
2667
2743
|
const container = this.$refs.documentsList;
|
|
2668
2744
|
if (container.scrollHeight - container.clientHeight - 100 < container.scrollTop) {
|
|
2669
2745
|
this.status = 'loading';
|
|
2670
|
-
const
|
|
2746
|
+
const params = {
|
|
2671
2747
|
model: this.currentModel,
|
|
2672
|
-
filter: this.filter,
|
|
2673
2748
|
sort: this.sortBy,
|
|
2674
2749
|
skip: this.documents.length,
|
|
2675
2750
|
limit
|
|
2676
|
-
}
|
|
2751
|
+
};
|
|
2752
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
2753
|
+
params.searchText = this.searchText;
|
|
2754
|
+
}
|
|
2755
|
+
const { docs } = await api.Model.getDocuments(params);
|
|
2677
2756
|
if (docs.length < limit) {
|
|
2678
2757
|
this.loadedAllDocs = true;
|
|
2679
2758
|
}
|
|
@@ -2699,21 +2778,16 @@ module.exports = app => app.component('models', {
|
|
|
2699
2778
|
await this.loadMoreDocuments();
|
|
2700
2779
|
},
|
|
2701
2780
|
async search() {
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
this.filter = EJSON.stringify(this.filter);
|
|
2781
|
+
const hasSearch = typeof this.searchText === 'string' && this.searchText.trim().length > 0;
|
|
2782
|
+
if (hasSearch) {
|
|
2705
2783
|
this.query.search = this.searchText;
|
|
2706
|
-
const query = this.query;
|
|
2707
|
-
const newUrl = this.$router.resolve({ query }).href;
|
|
2708
|
-
this.$router.push({ query });
|
|
2709
2784
|
} else {
|
|
2710
|
-
this.filter = {};
|
|
2711
2785
|
delete this.query.search;
|
|
2712
|
-
const query = this.query;
|
|
2713
|
-
const newUrl = this.$router.resolve({ query }).href;
|
|
2714
|
-
this.$router.push({ query });
|
|
2715
2786
|
}
|
|
2787
|
+
const query = this.query;
|
|
2788
|
+
this.$router.push({ query });
|
|
2716
2789
|
this.documents = [];
|
|
2790
|
+
this.loadedAllDocs = false;
|
|
2717
2791
|
this.status = 'loading';
|
|
2718
2792
|
await this.loadMoreDocuments();
|
|
2719
2793
|
this.status = 'loaded';
|
|
@@ -2734,44 +2808,91 @@ module.exports = app => app.component('models', {
|
|
|
2734
2808
|
}
|
|
2735
2809
|
},
|
|
2736
2810
|
async getDocuments() {
|
|
2737
|
-
|
|
2811
|
+
// Clear previous data
|
|
2812
|
+
this.documents = [];
|
|
2813
|
+
this.schemaPaths = [];
|
|
2814
|
+
this.numDocuments = null;
|
|
2815
|
+
this.loadedAllDocs = false;
|
|
2816
|
+
|
|
2817
|
+
let docsCount = 0;
|
|
2818
|
+
let schemaPathsReceived = false;
|
|
2819
|
+
|
|
2820
|
+
// Use async generator to stream SSEs
|
|
2821
|
+
const params = {
|
|
2738
2822
|
model: this.currentModel,
|
|
2739
|
-
filter: this.filter,
|
|
2740
2823
|
sort: this.sortBy,
|
|
2741
2824
|
limit
|
|
2742
|
-
}
|
|
2743
|
-
this.
|
|
2744
|
-
|
|
2745
|
-
this.loadedAllDocs = true;
|
|
2825
|
+
};
|
|
2826
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
2827
|
+
params.searchText = this.searchText;
|
|
2746
2828
|
}
|
|
2747
|
-
|
|
2748
|
-
if (
|
|
2749
|
-
|
|
2829
|
+
for await (const event of api.Model.getDocumentsStream(params)) {
|
|
2830
|
+
if (event.schemaPaths && !schemaPathsReceived) {
|
|
2831
|
+
// Sort schemaPaths with _id first
|
|
2832
|
+
this.schemaPaths = Object.keys(event.schemaPaths).sort((k1, k2) => {
|
|
2833
|
+
if (k1 === '_id' && k2 !== '_id') {
|
|
2834
|
+
return -1;
|
|
2835
|
+
}
|
|
2836
|
+
if (k1 !== '_id' && k2 === '_id') {
|
|
2837
|
+
return 1;
|
|
2838
|
+
}
|
|
2839
|
+
return 0;
|
|
2840
|
+
}).map(key => event.schemaPaths[key]);
|
|
2841
|
+
this.shouldExport = {};
|
|
2842
|
+
for (const { path } of this.schemaPaths) {
|
|
2843
|
+
this.shouldExport[path] = true;
|
|
2844
|
+
}
|
|
2845
|
+
this.filteredPaths = [...this.schemaPaths];
|
|
2846
|
+
this.selectedPaths = [...this.schemaPaths];
|
|
2847
|
+
schemaPathsReceived = true;
|
|
2750
2848
|
}
|
|
2751
|
-
if (
|
|
2752
|
-
|
|
2849
|
+
if (event.numDocs !== undefined) {
|
|
2850
|
+
this.numDocuments = event.numDocs;
|
|
2753
2851
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2852
|
+
if (event.document) {
|
|
2853
|
+
this.documents.push(event.document);
|
|
2854
|
+
docsCount++;
|
|
2855
|
+
}
|
|
2856
|
+
if (event.message) {
|
|
2857
|
+
this.status = 'loaded';
|
|
2858
|
+
throw new Error(event.message);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2757
2861
|
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
this.shouldExport[path] = true;
|
|
2862
|
+
if (docsCount < limit) {
|
|
2863
|
+
this.loadedAllDocs = true;
|
|
2761
2864
|
}
|
|
2762
|
-
this.filteredPaths = [...this.schemaPaths];
|
|
2763
|
-
this.selectedPaths = [...this.schemaPaths];
|
|
2764
2865
|
},
|
|
2765
2866
|
async loadMoreDocuments() {
|
|
2766
|
-
|
|
2867
|
+
let docsCount = 0;
|
|
2868
|
+
let numDocsReceived = false;
|
|
2869
|
+
|
|
2870
|
+
// Use async generator to stream SSEs
|
|
2871
|
+
const params = {
|
|
2767
2872
|
model: this.currentModel,
|
|
2768
|
-
filter: this.filter,
|
|
2769
2873
|
sort: this.sortBy,
|
|
2874
|
+
skip: this.documents.length,
|
|
2770
2875
|
limit
|
|
2771
|
-
}
|
|
2772
|
-
this.
|
|
2773
|
-
|
|
2774
|
-
|
|
2876
|
+
};
|
|
2877
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
2878
|
+
params.searchText = this.searchText;
|
|
2879
|
+
}
|
|
2880
|
+
for await (const event of api.Model.getDocumentsStream(params)) {
|
|
2881
|
+
if (event.numDocs !== undefined && !numDocsReceived) {
|
|
2882
|
+
this.numDocuments = event.numDocs;
|
|
2883
|
+
numDocsReceived = true;
|
|
2884
|
+
}
|
|
2885
|
+
if (event.document) {
|
|
2886
|
+
this.documents.push(event.document);
|
|
2887
|
+
docsCount++;
|
|
2888
|
+
}
|
|
2889
|
+
if (event.message) {
|
|
2890
|
+
this.status = 'loaded';
|
|
2891
|
+
throw new Error(event.message);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
if (docsCount < limit) {
|
|
2775
2896
|
this.loadedAllDocs = true;
|
|
2776
2897
|
}
|
|
2777
2898
|
},
|
|
@@ -3439,6 +3560,9 @@ var map = {
|
|
|
3439
3560
|
"./dashboard-result/dashboard-document/dashboard-document": "./frontend/src/dashboard-result/dashboard-document/dashboard-document.js",
|
|
3440
3561
|
"./dashboard-result/dashboard-document/dashboard-document.html": "./frontend/src/dashboard-result/dashboard-document/dashboard-document.html",
|
|
3441
3562
|
"./dashboard-result/dashboard-document/dashboard-document.js": "./frontend/src/dashboard-result/dashboard-document/dashboard-document.js",
|
|
3563
|
+
"./dashboard-result/dashboard-grid/dashboard-grid": "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js",
|
|
3564
|
+
"./dashboard-result/dashboard-grid/dashboard-grid.html": "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html",
|
|
3565
|
+
"./dashboard-result/dashboard-grid/dashboard-grid.js": "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.js",
|
|
3442
3566
|
"./dashboard-result/dashboard-map/dashboard-map": "./frontend/src/dashboard-result/dashboard-map/dashboard-map.js",
|
|
3443
3567
|
"./dashboard-result/dashboard-map/dashboard-map.html": "./frontend/src/dashboard-result/dashboard-map/dashboard-map.html",
|
|
3444
3568
|
"./dashboard-result/dashboard-map/dashboard-map.js": "./frontend/src/dashboard-result/dashboard-map/dashboard-map.js",
|
|
@@ -4306,6 +4430,17 @@ module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b
|
|
|
4306
4430
|
|
|
4307
4431
|
/***/ }),
|
|
4308
4432
|
|
|
4433
|
+
/***/ "./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html":
|
|
4434
|
+
/*!**************************************************************************!*\
|
|
4435
|
+
!*** ./frontend/src/dashboard-result/dashboard-grid/dashboard-grid.html ***!
|
|
4436
|
+
\**************************************************************************/
|
|
4437
|
+
/***/ ((module) => {
|
|
4438
|
+
|
|
4439
|
+
"use strict";
|
|
4440
|
+
module.exports = "<div class=\"grid gap-2\" :style=\"{ gridTemplateColumns: gridTemplateColumns }\">\n <template v-for=\"(row, rowIndex) in value.$grid\" :key=\"rowIndex\">\n <dashboard-result\n v-for=\"(cell, colIndex) in row\"\n :key=\"rowIndex + '-' + colIndex\"\n :result=\"cell\">\n </dashboard-result>\n </template>\n</div>\n";
|
|
4441
|
+
|
|
4442
|
+
/***/ }),
|
|
4443
|
+
|
|
4309
4444
|
/***/ "./frontend/src/dashboard-result/dashboard-map/dashboard-map.html":
|
|
4310
4445
|
/*!************************************************************************!*\
|
|
4311
4446
|
!*** ./frontend/src/dashboard-result/dashboard-map/dashboard-map.html ***!
|
|
@@ -4753,7 +4888,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
|
|
|
4753
4888
|
/***/ ((module) => {
|
|
4754
4889
|
|
|
4755
4890
|
"use strict";
|
|
4756
|
-
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=\"status === 'loading'\">Loading ...</span>\n <span v-if=\"status === 'loaded'\">{{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 :filter=\"filter\"\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";
|
|
4891
|
+
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";
|
|
4757
4892
|
|
|
4758
4893
|
/***/ }),
|
|
4759
4894
|
|
|
@@ -14871,7 +15006,7 @@ var bson = /*#__PURE__*/Object.freeze({
|
|
|
14871
15006
|
/***/ ((module) => {
|
|
14872
15007
|
|
|
14873
15008
|
"use strict";
|
|
14874
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.
|
|
15009
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.120","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"}}');
|
|
14875
15010
|
|
|
14876
15011
|
/***/ })
|
|
14877
15012
|
|
package/frontend/src/api.js
CHANGED
|
@@ -114,6 +114,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
114
114
|
getDocuments: function getDocuments(params) {
|
|
115
115
|
return client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
|
|
116
116
|
},
|
|
117
|
+
getDocumentsStream: function getDocumentsStream(params) {
|
|
118
|
+
return client.post('', { action: 'Model.getDocumentsStream', ...params }).then(res => res.data);
|
|
119
|
+
},
|
|
117
120
|
getIndexes: function getIndexes(params) {
|
|
118
121
|
return client.post('', { action: 'Model.getIndexes', ...params }).then(res => res.data);
|
|
119
122
|
},
|
|
@@ -219,6 +222,55 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
219
222
|
getDocuments: function getDocuments(params) {
|
|
220
223
|
return client.post('/Model/getDocuments', params).then(res => res.data);
|
|
221
224
|
},
|
|
225
|
+
getDocumentsStream: async function* getDocumentsStream(params) {
|
|
226
|
+
const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
|
|
227
|
+
const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/getDocumentsStream?' + new URLSearchParams(params).toString();
|
|
228
|
+
|
|
229
|
+
const response = await fetch(url, {
|
|
230
|
+
method: 'GET',
|
|
231
|
+
headers: {
|
|
232
|
+
Authorization: `${accessToken}`,
|
|
233
|
+
Accept: 'text/event-stream'
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const reader = response.body.getReader();
|
|
242
|
+
const decoder = new TextDecoder('utf-8');
|
|
243
|
+
let buffer = '';
|
|
244
|
+
|
|
245
|
+
while (true) {
|
|
246
|
+
const { done, value } = await reader.read();
|
|
247
|
+
if (done) break;
|
|
248
|
+
buffer += decoder.decode(value, { stream: true });
|
|
249
|
+
|
|
250
|
+
let eventEnd;
|
|
251
|
+
while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
|
|
252
|
+
const eventStr = buffer.slice(0, eventEnd);
|
|
253
|
+
buffer = buffer.slice(eventEnd + 2);
|
|
254
|
+
|
|
255
|
+
// Parse SSE event
|
|
256
|
+
const lines = eventStr.split('\n');
|
|
257
|
+
let data = '';
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
if (line.startsWith('data:')) {
|
|
260
|
+
data += line.slice(5).trim();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (data) {
|
|
264
|
+
try {
|
|
265
|
+
yield JSON.parse(data);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
// If not JSON, yield as string
|
|
268
|
+
yield data;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
222
274
|
getIndexes: function getIndexes(params) {
|
|
223
275
|
return client.post('/Model/getIndexes', params).then(res => res.data);
|
|
224
276
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div class="grid gap-2" :style="{ gridTemplateColumns: gridTemplateColumns }">
|
|
2
|
+
<template v-for="(row, rowIndex) in value.$grid" :key="rowIndex">
|
|
3
|
+
<dashboard-result
|
|
4
|
+
v-for="(cell, colIndex) in row"
|
|
5
|
+
:key="rowIndex + '-' + colIndex"
|
|
6
|
+
:result="cell">
|
|
7
|
+
</dashboard-result>
|
|
8
|
+
</template>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const template = require('./dashboard-grid.html');
|
|
4
|
+
|
|
5
|
+
module.exports = app => app.component('dashboard-grid', {
|
|
6
|
+
template: template,
|
|
7
|
+
props: ['value'],
|
|
8
|
+
computed: {
|
|
9
|
+
columns() {
|
|
10
|
+
const grid = this.value && this.value.$grid;
|
|
11
|
+
if (!Array.isArray(grid) || grid.length === 0) {
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
return Math.max(1, ...grid.map(row => Array.isArray(row) ? row.length : 0));
|
|
15
|
+
},
|
|
16
|
+
gridTemplateColumns() {
|
|
17
|
+
return `repeat(${this.columns}, minmax(0, 1fr))`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -9,7 +9,7 @@ appendCSS(require('./export-query-results.css'));
|
|
|
9
9
|
|
|
10
10
|
module.exports = app => app.component('export-query-results', {
|
|
11
11
|
template: template,
|
|
12
|
-
props: ['schemaPaths', '
|
|
12
|
+
props: ['schemaPaths', 'searchText', 'currentModel'],
|
|
13
13
|
emits: ['done'],
|
|
14
14
|
data: () => ({
|
|
15
15
|
shouldExport: {}
|
|
@@ -26,8 +26,8 @@ module.exports = app => app.component('export-query-results', {
|
|
|
26
26
|
model: this.currentModel,
|
|
27
27
|
propertiesToInclude: Object.keys(this.shouldExport).filter(key => this.shouldExport[key])
|
|
28
28
|
};
|
|
29
|
-
if (this.
|
|
30
|
-
params.
|
|
29
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
30
|
+
params.searchText = this.searchText;
|
|
31
31
|
}
|
|
32
32
|
await api.Model.exportQueryResults(params);
|
|
33
33
|
|
|
@@ -38,8 +38,8 @@
|
|
|
38
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" />
|
|
39
39
|
</form>
|
|
40
40
|
<div>
|
|
41
|
-
<span v-if="
|
|
42
|
-
<span v-if="
|
|
41
|
+
<span v-if="numDocuments == null">Loading ...</span>
|
|
42
|
+
<span v-else-if="typeof numDocuments === 'number'">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>
|
|
43
43
|
</div>
|
|
44
44
|
<button
|
|
45
45
|
@click="shouldShowExportModal = true"
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
<div class="modal-exit" @click="shouldShowExportModal = false">×</div>
|
|
153
153
|
<export-query-results
|
|
154
154
|
:schemaPaths="schemaPaths"
|
|
155
|
-
:
|
|
155
|
+
:search-text="searchText"
|
|
156
156
|
:currentModel="currentModel"
|
|
157
157
|
@done="shouldShowExportModal = false">
|
|
158
158
|
</export-query-results>
|
|
@@ -3,15 +3,6 @@
|
|
|
3
3
|
const api = require('../api');
|
|
4
4
|
const template = require('./models.html');
|
|
5
5
|
const mpath = require('mpath');
|
|
6
|
-
const { BSON, EJSON } = require('bson');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const ObjectId = new Proxy(BSON.ObjectId, {
|
|
11
|
-
apply(target, thisArg, argumentsList) {
|
|
12
|
-
return new target(...argumentsList);
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
6
|
|
|
16
7
|
const appendCSS = require('../appendCSS');
|
|
17
8
|
|
|
@@ -30,14 +21,13 @@ module.exports = app => app.component('models', {
|
|
|
30
21
|
schemaPaths: [],
|
|
31
22
|
filteredPaths: [],
|
|
32
23
|
selectedPaths: [],
|
|
33
|
-
numDocuments:
|
|
24
|
+
numDocuments: null,
|
|
34
25
|
mongoDBIndexes: [],
|
|
35
26
|
schemaIndexes: [],
|
|
36
27
|
status: 'loading',
|
|
37
28
|
loadedAllDocs: false,
|
|
38
29
|
edittingDoc: null,
|
|
39
30
|
docEdits: null,
|
|
40
|
-
filter: null,
|
|
41
31
|
selectMultiple: false,
|
|
42
32
|
selectedDocuments: [],
|
|
43
33
|
searchText: '',
|
|
@@ -80,8 +70,8 @@ module.exports = app => app.component('models', {
|
|
|
80
70
|
this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
|
|
81
71
|
if (this.$route.query?.search) {
|
|
82
72
|
this.searchText = this.$route.query.search;
|
|
83
|
-
|
|
84
|
-
this.
|
|
73
|
+
} else {
|
|
74
|
+
this.searchText = '';
|
|
85
75
|
}
|
|
86
76
|
if (this.$route.query?.sort) {
|
|
87
77
|
const sort = eval(`(${this.$route.query.sort})`);
|
|
@@ -156,13 +146,16 @@ module.exports = app => app.component('models', {
|
|
|
156
146
|
const container = this.$refs.documentsList;
|
|
157
147
|
if (container.scrollHeight - container.clientHeight - 100 < container.scrollTop) {
|
|
158
148
|
this.status = 'loading';
|
|
159
|
-
const
|
|
149
|
+
const params = {
|
|
160
150
|
model: this.currentModel,
|
|
161
|
-
filter: this.filter,
|
|
162
151
|
sort: this.sortBy,
|
|
163
152
|
skip: this.documents.length,
|
|
164
153
|
limit
|
|
165
|
-
}
|
|
154
|
+
};
|
|
155
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
156
|
+
params.searchText = this.searchText;
|
|
157
|
+
}
|
|
158
|
+
const { docs } = await api.Model.getDocuments(params);
|
|
166
159
|
if (docs.length < limit) {
|
|
167
160
|
this.loadedAllDocs = true;
|
|
168
161
|
}
|
|
@@ -188,21 +181,16 @@ module.exports = app => app.component('models', {
|
|
|
188
181
|
await this.loadMoreDocuments();
|
|
189
182
|
},
|
|
190
183
|
async search() {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.filter = EJSON.stringify(this.filter);
|
|
184
|
+
const hasSearch = typeof this.searchText === 'string' && this.searchText.trim().length > 0;
|
|
185
|
+
if (hasSearch) {
|
|
194
186
|
this.query.search = this.searchText;
|
|
195
|
-
const query = this.query;
|
|
196
|
-
const newUrl = this.$router.resolve({ query }).href;
|
|
197
|
-
this.$router.push({ query });
|
|
198
187
|
} else {
|
|
199
|
-
this.filter = {};
|
|
200
188
|
delete this.query.search;
|
|
201
|
-
const query = this.query;
|
|
202
|
-
const newUrl = this.$router.resolve({ query }).href;
|
|
203
|
-
this.$router.push({ query });
|
|
204
189
|
}
|
|
190
|
+
const query = this.query;
|
|
191
|
+
this.$router.push({ query });
|
|
205
192
|
this.documents = [];
|
|
193
|
+
this.loadedAllDocs = false;
|
|
206
194
|
this.status = 'loading';
|
|
207
195
|
await this.loadMoreDocuments();
|
|
208
196
|
this.status = 'loaded';
|
|
@@ -223,44 +211,91 @@ module.exports = app => app.component('models', {
|
|
|
223
211
|
}
|
|
224
212
|
},
|
|
225
213
|
async getDocuments() {
|
|
226
|
-
|
|
214
|
+
// Clear previous data
|
|
215
|
+
this.documents = [];
|
|
216
|
+
this.schemaPaths = [];
|
|
217
|
+
this.numDocuments = null;
|
|
218
|
+
this.loadedAllDocs = false;
|
|
219
|
+
|
|
220
|
+
let docsCount = 0;
|
|
221
|
+
let schemaPathsReceived = false;
|
|
222
|
+
|
|
223
|
+
// Use async generator to stream SSEs
|
|
224
|
+
const params = {
|
|
227
225
|
model: this.currentModel,
|
|
228
|
-
filter: this.filter,
|
|
229
226
|
sort: this.sortBy,
|
|
230
227
|
limit
|
|
231
|
-
}
|
|
232
|
-
this.
|
|
233
|
-
|
|
234
|
-
this.loadedAllDocs = true;
|
|
228
|
+
};
|
|
229
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
230
|
+
params.searchText = this.searchText;
|
|
235
231
|
}
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
232
|
+
for await (const event of api.Model.getDocumentsStream(params)) {
|
|
233
|
+
if (event.schemaPaths && !schemaPathsReceived) {
|
|
234
|
+
// Sort schemaPaths with _id first
|
|
235
|
+
this.schemaPaths = Object.keys(event.schemaPaths).sort((k1, k2) => {
|
|
236
|
+
if (k1 === '_id' && k2 !== '_id') {
|
|
237
|
+
return -1;
|
|
238
|
+
}
|
|
239
|
+
if (k1 !== '_id' && k2 === '_id') {
|
|
240
|
+
return 1;
|
|
241
|
+
}
|
|
242
|
+
return 0;
|
|
243
|
+
}).map(key => event.schemaPaths[key]);
|
|
244
|
+
this.shouldExport = {};
|
|
245
|
+
for (const { path } of this.schemaPaths) {
|
|
246
|
+
this.shouldExport[path] = true;
|
|
247
|
+
}
|
|
248
|
+
this.filteredPaths = [...this.schemaPaths];
|
|
249
|
+
this.selectedPaths = [...this.schemaPaths];
|
|
250
|
+
schemaPathsReceived = true;
|
|
239
251
|
}
|
|
240
|
-
if (
|
|
241
|
-
|
|
252
|
+
if (event.numDocs !== undefined) {
|
|
253
|
+
this.numDocuments = event.numDocs;
|
|
242
254
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
255
|
+
if (event.document) {
|
|
256
|
+
this.documents.push(event.document);
|
|
257
|
+
docsCount++;
|
|
258
|
+
}
|
|
259
|
+
if (event.message) {
|
|
260
|
+
this.status = 'loaded';
|
|
261
|
+
throw new Error(event.message);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
246
264
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.shouldExport[path] = true;
|
|
265
|
+
if (docsCount < limit) {
|
|
266
|
+
this.loadedAllDocs = true;
|
|
250
267
|
}
|
|
251
|
-
this.filteredPaths = [...this.schemaPaths];
|
|
252
|
-
this.selectedPaths = [...this.schemaPaths];
|
|
253
268
|
},
|
|
254
269
|
async loadMoreDocuments() {
|
|
255
|
-
|
|
270
|
+
let docsCount = 0;
|
|
271
|
+
let numDocsReceived = false;
|
|
272
|
+
|
|
273
|
+
// Use async generator to stream SSEs
|
|
274
|
+
const params = {
|
|
256
275
|
model: this.currentModel,
|
|
257
|
-
filter: this.filter,
|
|
258
276
|
sort: this.sortBy,
|
|
277
|
+
skip: this.documents.length,
|
|
259
278
|
limit
|
|
260
|
-
}
|
|
261
|
-
this.
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
};
|
|
280
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
281
|
+
params.searchText = this.searchText;
|
|
282
|
+
}
|
|
283
|
+
for await (const event of api.Model.getDocumentsStream(params)) {
|
|
284
|
+
if (event.numDocs !== undefined && !numDocsReceived) {
|
|
285
|
+
this.numDocuments = event.numDocs;
|
|
286
|
+
numDocsReceived = true;
|
|
287
|
+
}
|
|
288
|
+
if (event.document) {
|
|
289
|
+
this.documents.push(event.document);
|
|
290
|
+
docsCount++;
|
|
291
|
+
}
|
|
292
|
+
if (event.message) {
|
|
293
|
+
this.status = 'loaded';
|
|
294
|
+
throw new Error(event.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (docsCount < limit) {
|
|
264
299
|
this.loadedAllDocs = true;
|
|
265
300
|
}
|
|
266
301
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mongoosejs/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.120",
|
|
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": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"archetype": "0.13.1",
|
|
12
12
|
"csv-stringify": "6.3.0",
|
|
13
13
|
"ejson": "^2.2.3",
|
|
14
|
-
"extrovert": "0.0
|
|
14
|
+
"extrovert": "0.1.0",
|
|
15
15
|
"marked": "15.0.12",
|
|
16
16
|
"node-inspect-extracted": "3.x",
|
|
17
17
|
"tailwindcss": "3.4.0",
|