@mongoosejs/studio 0.2.8 → 0.2.10
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/Dashboard/getDashboard.js +1 -1
- package/backend/actions/Model/index.js +1 -0
- package/backend/actions/Model/listModels.js +36 -1
- package/backend/actions/Model/streamDocumentChanges.js +123 -0
- package/backend/authorize.js +1 -0
- package/backend/index.js +7 -1
- package/backend/next.js +63 -3
- package/eslint.config.js +3 -1
- package/express.js +3 -2
- package/frontend/public/app.js +282 -39
- package/frontend/public/tw.css +46 -0
- package/frontend/src/api.js +60 -0
- package/frontend/src/dashboard-result/dashboard-document/dashboard-document.html +4 -5
- package/frontend/src/dashboard-result/dashboard-document/dashboard-document.js +13 -14
- package/frontend/src/document/document.html +58 -0
- package/frontend/src/document/document.js +194 -19
- package/frontend/src/index.js +12 -3
- package/package.json +1 -1
package/frontend/public/app.js
CHANGED
|
@@ -689,6 +689,16 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
689
689
|
yield { document: doc };
|
|
690
690
|
}
|
|
691
691
|
},
|
|
692
|
+
streamDocumentChanges: async function* streamDocumentChanges(params, options = {}) {
|
|
693
|
+
const pollIntervalMs = 5000;
|
|
694
|
+
while (!options.signal?.aborted) {
|
|
695
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
696
|
+
if (options.signal?.aborted) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
yield { type: 'poll', model: params.model, documentId: params.documentId };
|
|
700
|
+
}
|
|
701
|
+
},
|
|
692
702
|
getCollectionInfo: function getCollectionInfo(params) {
|
|
693
703
|
return client.post('', { action: 'Model.getCollectionInfo', ...params }).then(res => res.data);
|
|
694
704
|
},
|
|
@@ -909,6 +919,56 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
909
919
|
}
|
|
910
920
|
}
|
|
911
921
|
},
|
|
922
|
+
streamDocumentChanges: async function* streamDocumentChanges(params, options = {}) {
|
|
923
|
+
const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
|
|
924
|
+
const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/streamDocumentChanges?' + new URLSearchParams(params).toString();
|
|
925
|
+
|
|
926
|
+
const response = await fetch(url, {
|
|
927
|
+
method: 'GET',
|
|
928
|
+
headers: {
|
|
929
|
+
Authorization: `${accessToken}`,
|
|
930
|
+
Accept: 'text/event-stream'
|
|
931
|
+
},
|
|
932
|
+
signal: options.signal
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
if (!response.ok) {
|
|
936
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const reader = response.body.getReader();
|
|
940
|
+
const decoder = new TextDecoder('utf-8');
|
|
941
|
+
let buffer = '';
|
|
942
|
+
|
|
943
|
+
while (true) {
|
|
944
|
+
const { done, value } = await reader.read();
|
|
945
|
+
if (done) break;
|
|
946
|
+
buffer += decoder.decode(value, { stream: true });
|
|
947
|
+
|
|
948
|
+
let eventEnd;
|
|
949
|
+
while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
|
|
950
|
+
const eventStr = buffer.slice(0, eventEnd);
|
|
951
|
+
buffer = buffer.slice(eventEnd + 2);
|
|
952
|
+
|
|
953
|
+
// Parse SSE event
|
|
954
|
+
const lines = eventStr.split('\n');
|
|
955
|
+
let data = '';
|
|
956
|
+
for (const line of lines) {
|
|
957
|
+
if (line.startsWith('data:')) {
|
|
958
|
+
data += line.slice(5).trim();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (data) {
|
|
962
|
+
try {
|
|
963
|
+
yield JSON.parse(data);
|
|
964
|
+
} catch (err) {
|
|
965
|
+
// If not JSON, yield as string
|
|
966
|
+
yield data;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
},
|
|
912
972
|
getCollectionInfo: function getCollectionInfo(params) {
|
|
913
973
|
return client.post('/Model/getCollectionInfo', params).then(res => res.data);
|
|
914
974
|
},
|
|
@@ -2126,7 +2186,7 @@ module.exports = app => app.component('dashboard-chart', {
|
|
|
2126
2186
|
(module) {
|
|
2127
2187
|
|
|
2128
2188
|
"use strict";
|
|
2129
|
-
module.exports = "<div class=\"py-2\">\n <div
|
|
2189
|
+
module.exports = "<div class=\"py-2\">\n <div class=\"text-xl pb-2\">\n <list-json\n :value=\"value.$document.document\"\n :references=\"references\" />\n </div>\n</div>\n";
|
|
2130
2190
|
|
|
2131
2191
|
/***/ },
|
|
2132
2192
|
|
|
@@ -2146,23 +2206,22 @@ const template = __webpack_require__(/*! ./dashboard-document.html */ "./fronten
|
|
|
2146
2206
|
module.exports = app => app.component('dashboard-document', {
|
|
2147
2207
|
template: template,
|
|
2148
2208
|
props: ['value'],
|
|
2209
|
+
inject: ['state'],
|
|
2149
2210
|
computed: {
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2211
|
+
references() {
|
|
2212
|
+
const model = this.value.$document?.model;
|
|
2213
|
+
if (typeof model === 'string' && this.state.modelSchemaPaths?.[model]) {
|
|
2214
|
+
const map = {};
|
|
2215
|
+
for (const path of Object.keys(this.state.modelSchemaPaths[model])) {
|
|
2216
|
+
const definition = this.state.modelSchemaPaths[model][path];
|
|
2217
|
+
if (definition?.ref) {
|
|
2218
|
+
map[path] = definition.ref;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return map;
|
|
2153
2222
|
}
|
|
2223
|
+
|
|
2154
2224
|
return null;
|
|
2155
|
-
},
|
|
2156
|
-
schemaPaths() {
|
|
2157
|
-
return Object.keys(this.value?.$document?.schemaPaths || {}).sort((k1, k2) => {
|
|
2158
|
-
if (k1 === '_id' && k2 !== '_id') {
|
|
2159
|
-
return -1;
|
|
2160
|
-
}
|
|
2161
|
-
if (k1 !== '_id' && k2 === '_id') {
|
|
2162
|
-
return 1;
|
|
2163
|
-
}
|
|
2164
|
-
return 0;
|
|
2165
|
-
}).map(key => this.value?.$document.schemaPaths[key]);
|
|
2166
2225
|
}
|
|
2167
2226
|
}
|
|
2168
2227
|
});
|
|
@@ -4893,7 +4952,7 @@ module.exports = ".document .document-menu {\n display: flex;\n position: stic
|
|
|
4893
4952
|
(module) {
|
|
4894
4953
|
|
|
4895
4954
|
"use strict";
|
|
4896
|
-
module.exports = "<div class=\"document px-1 pt-4 pb-16 md:px-0 bg-slate-50 w-full\">\n <div class=\"max-w-7xl mx-auto\">\n <div class=\"flex gap-4 items-center sticky top-0 z-50 bg-white p-4 border-b border-gray-200 shadow-sm\">\n <div class=\"font-bold overflow-hidden text-ellipsis\">{{model}}: {{documentId}}</div>\n <div class=\"flex grow\">\n <button\n @click=\"viewMode = 'fields'\"\n :class=\"viewMode === 'fields'\n ? 'bg-blue-600 text-white z-10'\n : 'bg-gray-200 text-gray-700 hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-gray-300 border-r-0 rounded-l-lg rounded-r-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 10h16M4 14h16M4 18h16\"></path>\n </svg>\n Fields\n </button>\n <button\n @click=\"viewMode = 'json'\"\n :class=\"viewMode === 'json'\n ? 'bg-blue-600 text-white z-10'\n : 'bg-gray-200 text-gray-700 hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-gray-300 rounded-r-lg rounded-l-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"></path>\n </svg>\n JSON\n </button>\n </div>\n\n <div class=\"gap-2 hidden md:flex items-center\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true\"\n :disabled=\"!canEdit\"\n :class=\"{'cursor-not-allowed opacity-50': !canEdit}\"\n type=\"button\"\n 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\">\n <img src=\"images/edit.svg\" class=\"inline\" /> Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false\"\n type=\"button\"\n class=\"rounded-md bg-slate-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n × Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n @click=\"shouldShowConfirmModal=true;\"\n type=\"button\"\n 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\">\n <img src=\"images/save.svg\" class=\"inline\" /> Save\n </button>\n\n <!-- 3-dot menu -->\n <div class=\"relative\">\n <button\n @click=\"desktopMenuOpen = !desktopMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-2.5 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"desktopMenuOpen\"\n aria-label=\"More options\"\n >\n <svg class=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"5\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"12\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"19\" r=\"2\"></circle>\n </svg>\n </button>\n <div\n v-show=\"desktopMenuOpen\"\n @click.away=\"desktopMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n @click=\"addField(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"md:hidden flex items-center\">\n <div class=\"relative\">\n <button\n @click=\"mobileMenuOpen = !mobileMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"mobileMenuOpen\"\n aria-label=\"Open menu\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n d=\"M4 6h16M4 12h16M4 18h16\"></path>\n </svg>\n </button>\n <div\n v-show=\"mobileMenuOpen\"\n @click.away=\"mobileMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-52 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true; mobileMenuOpen = false\"\n :disabled=\"!canEdit\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canEdit ? 'cursor-not-allowed opacity-50' : 'hover:bg-ultramarine-100']\"\n type=\"button\"\n >\n Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false; mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-slate-100\"\n >\n Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-green-100']\"\n @click=\"shouldShowConfirmModal=true; mobileMenuOpen = false\"\n type=\"button\"\n >\n Save\n </button>\n <button\n @click=\"addField(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div v-if=\"status === 'loaded'\">\n <document-details\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :virtualPaths=\"virtualPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"\n :viewMode=\"viewMode\"\n :model=\"model\"\n @add-field=\"addField\"\n @view-mode-change=\"updateViewMode\"></document-details>\n <modal v-if=\"shouldShowConfirmModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">×</div>\n <confirm-changes @close=\"shouldShowConfirmModal = false;\" @save=\"save\" :value=\"changes\"></confirm-changes>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteModal = false;\">×</div>\n <confirm-delete @close=\"shouldShowDeleteModal = false;\" @remove=\"remove\" :value=\"document\"></confirm-delete>\n </template>\n </modal>\n <modal v-if=\"shouldShowCloneModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCloneModal = false;\">×</div>\n <clone-document :currentModel=\"model\" :doc=\"document\" :schemaPaths=\"schemaPaths\" @close=\"showClonedDocument\"></clone-document>\n </template>\n </modal>\n </div>\n </div>\n</div>\n";
|
|
4955
|
+
module.exports = "<div class=\"document px-1 pt-4 pb-16 md:px-0 bg-slate-50 w-full\">\n <div class=\"max-w-7xl mx-auto\">\n <div class=\"flex gap-4 items-center sticky top-0 z-50 bg-white p-4 border-b border-gray-200 shadow-sm\">\n <div class=\"font-bold overflow-hidden text-ellipsis\">{{model}}: {{documentId}}</div>\n <div class=\"flex grow\">\n <button\n @click=\"viewMode = 'fields'\"\n :class=\"viewMode === 'fields'\n ? 'bg-blue-600 text-white z-10'\n : 'bg-gray-200 text-gray-700 hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-gray-300 border-r-0 rounded-l-lg rounded-r-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 10h16M4 14h16M4 18h16\"></path>\n </svg>\n Fields\n </button>\n <button\n @click=\"viewMode = 'json'\"\n :class=\"viewMode === 'json'\n ? 'bg-blue-600 text-white z-10'\n : 'bg-gray-200 text-gray-700 hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-gray-300 rounded-r-lg rounded-l-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"></path>\n </svg>\n JSON\n </button>\n </div>\n\n <div class=\"hidden md:flex items-center gap-3 text-sm text-slate-600\">\n <div class=\"text-right leading-tight\">\n <div class=\"text-xs uppercase tracking-wide text-slate-400 flex items-center gap-2 justify-end\">\n <span>Loaded at</span>\n <span class=\"inline-flex items-center gap-1 text-[10px] font-semibold\" :class=\"autoRefreshEnabled ? 'text-forest-green-600' : 'text-slate-400'\">\n <span class=\"inline-block h-1.5 w-1.5 rounded-full\" :class=\"autoRefreshEnabled ? 'bg-forest-green-500' : 'bg-slate-300'\"></span>\n </span>\n </div>\n <div class=\"font-medium text-slate-700\">{{lastUpdatedLabel}}</div>\n </div>\n </div>\n\n <div class=\"gap-2 hidden md:flex items-center\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true\"\n :disabled=\"!canEdit\"\n :class=\"{'cursor-not-allowed opacity-50': !canEdit}\"\n type=\"button\"\n 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\">\n <img src=\"images/edit.svg\" class=\"inline\" /> Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false\"\n type=\"button\"\n class=\"rounded-md bg-slate-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n × Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n @click=\"shouldShowConfirmModal=true;\"\n type=\"button\"\n 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\">\n <img src=\"images/save.svg\" class=\"inline\" /> Save\n </button>\n\n <!-- 3-dot menu -->\n <div class=\"relative\">\n <button\n @click=\"desktopMenuOpen = !desktopMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-2.5 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"desktopMenuOpen\"\n aria-label=\"More options\"\n >\n <svg class=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"5\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"12\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"19\" r=\"2\"></circle>\n </svg>\n </button>\n <div\n v-show=\"desktopMenuOpen\"\n @click.away=\"desktopMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n @click=\"refreshDocument({ force: true, source: 'manual' }); desktopMenuOpen = false\"\n :disabled=\"isRefreshing\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', isRefreshing ? 'cursor-not-allowed opacity-50' : 'hover:bg-slate-100']\"\n type=\"button\"\n >\n {{isRefreshing ? 'Refreshing...' : 'Refresh'}}\n </button>\n <button\n @click=\"toggleAutoRefresh(); desktopMenuOpen = false\"\n :disabled=\"isLambda\"\n type=\"button\"\n :class=\"['flex items-center px-4 py-2 text-sm', isLambda ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-slate-100']\"\n :title=\"isLambda ? 'Auto-refresh only available on Express deployments' : ''\"\n >\n {{autoRefreshEnabled ? 'Disable Auto-Refresh' : 'Enable Auto-Refresh'}}\n </button>\n <button\n @click=\"addField(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"copyDocument(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-blue-100\"\n >\n Copy Document\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"md:hidden flex items-center\">\n <div class=\"relative\">\n <button\n @click=\"mobileMenuOpen = !mobileMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"mobileMenuOpen\"\n aria-label=\"Open menu\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n d=\"M4 6h16M4 12h16M4 18h16\"></path>\n </svg>\n </button>\n <div\n v-show=\"mobileMenuOpen\"\n @click.away=\"mobileMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-52 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true; mobileMenuOpen = false\"\n :disabled=\"!canEdit\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canEdit ? 'cursor-not-allowed opacity-50' : 'hover:bg-ultramarine-100']\"\n type=\"button\"\n >\n Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false; mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-slate-100\"\n >\n Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-green-100']\"\n @click=\"shouldShowConfirmModal=true; mobileMenuOpen = false\"\n type=\"button\"\n >\n Save\n </button>\n <button\n @click=\"refreshDocument({ force: true, source: 'manual' }); mobileMenuOpen = false\"\n :disabled=\"isRefreshing\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', isRefreshing ? 'cursor-not-allowed opacity-50' : 'hover:bg-slate-100']\"\n type=\"button\"\n >\n {{isRefreshing ? 'Refreshing...' : 'Refresh'}}\n </button>\n <button\n @click=\"toggleAutoRefresh(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-slate-100\"\n >\n Auto-Refresh {{autoRefreshEnabled ? 'ON' : 'OFF'}}\n </button>\n <button\n @click=\"addField(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"copyDocument(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-blue-100\"\n >\n Copy Document\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div v-if=\"status === 'loaded'\">\n <document-details\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :virtualPaths=\"virtualPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"\n :viewMode=\"viewMode\"\n :model=\"model\"\n @add-field=\"addField\"\n @view-mode-change=\"updateViewMode\"></document-details>\n <modal v-if=\"shouldShowConfirmModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">×</div>\n <confirm-changes @close=\"shouldShowConfirmModal = false;\" @save=\"save\" :value=\"changes\"></confirm-changes>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteModal = false;\">×</div>\n <confirm-delete @close=\"shouldShowDeleteModal = false;\" @remove=\"remove\" :value=\"document\"></confirm-delete>\n </template>\n </modal>\n <modal v-if=\"shouldShowCloneModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCloneModal = false;\">×</div>\n <clone-document :currentModel=\"model\" :doc=\"document\" :schemaPaths=\"schemaPaths\" @close=\"showClonedDocument\"></clone-document>\n </template>\n </modal>\n </div>\n </div>\n</div>\n";
|
|
4897
4956
|
|
|
4898
4957
|
/***/ },
|
|
4899
4958
|
|
|
@@ -4932,30 +4991,25 @@ module.exports = app => app.component('document', {
|
|
|
4932
4991
|
shouldShowConfirmModal: false,
|
|
4933
4992
|
shouldShowDeleteModal: false,
|
|
4934
4993
|
shouldShowCloneModal: false,
|
|
4935
|
-
previousQuery: null
|
|
4994
|
+
previousQuery: null,
|
|
4995
|
+
lastUpdatedAt: null,
|
|
4996
|
+
isRefreshing: false,
|
|
4997
|
+
autoRefreshEnabled: false,
|
|
4998
|
+
autoRefreshConnecting: false,
|
|
4999
|
+
autoRefreshLoopRunning: false,
|
|
5000
|
+
autoRefreshError: null,
|
|
5001
|
+
autoRefreshAbortController: null,
|
|
5002
|
+
autoRefreshRetryTimer: null,
|
|
5003
|
+
pendingRefresh: false
|
|
4936
5004
|
}),
|
|
4937
5005
|
async mounted() {
|
|
4938
5006
|
window.pageState = this;
|
|
4939
5007
|
// Store query parameters from the route (preserved from models page)
|
|
4940
5008
|
this.previousQuery = Object.assign({}, this.$route.query);
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
this.schemaPaths = Object.keys(schemaPaths).sort((k1, k2) => {
|
|
4946
|
-
if (k1 === '_id' && k2 !== '_id') {
|
|
4947
|
-
return -1;
|
|
4948
|
-
}
|
|
4949
|
-
if (k1 !== '_id' && k2 === '_id') {
|
|
4950
|
-
return 1;
|
|
4951
|
-
}
|
|
4952
|
-
return 0;
|
|
4953
|
-
}).map(key => schemaPaths[key]);
|
|
4954
|
-
this.virtualPaths = virtualPaths || [];
|
|
4955
|
-
this.status = 'loaded';
|
|
4956
|
-
} finally {
|
|
4957
|
-
this.status = 'loaded';
|
|
4958
|
-
}
|
|
5009
|
+
await this.refreshDocument({ force: true, source: 'initial' });
|
|
5010
|
+
},
|
|
5011
|
+
beforeDestroy() {
|
|
5012
|
+
this.stopAutoRefresh();
|
|
4959
5013
|
},
|
|
4960
5014
|
computed: {
|
|
4961
5015
|
canManipulate() {
|
|
@@ -4967,8 +5021,32 @@ module.exports = app => app.component('document', {
|
|
|
4967
5021
|
}
|
|
4968
5022
|
return !this.roles.includes('readonly');
|
|
4969
5023
|
},
|
|
5024
|
+
lastUpdatedLabel() {
|
|
5025
|
+
if (!this.lastUpdatedAt) {
|
|
5026
|
+
return '—';
|
|
5027
|
+
}
|
|
5028
|
+
try {
|
|
5029
|
+
return new Date(this.lastUpdatedAt).toLocaleTimeString([], {
|
|
5030
|
+
hour: '2-digit',
|
|
5031
|
+
minute: '2-digit',
|
|
5032
|
+
second: '2-digit'
|
|
5033
|
+
});
|
|
5034
|
+
} catch (err) {
|
|
5035
|
+
return '—';
|
|
5036
|
+
}
|
|
5037
|
+
},
|
|
4970
5038
|
canEdit() {
|
|
4971
5039
|
return this.canManipulate && this.viewMode === 'fields';
|
|
5040
|
+
},
|
|
5041
|
+
isLambda() {
|
|
5042
|
+
return !!window?.MONGOOSE_STUDIO_CONFIG?.isLambda;
|
|
5043
|
+
}
|
|
5044
|
+
},
|
|
5045
|
+
watch: {
|
|
5046
|
+
editting(nextValue) {
|
|
5047
|
+
if (!nextValue && this.pendingRefresh) {
|
|
5048
|
+
this.refreshDocument({ source: 'pending' });
|
|
5049
|
+
}
|
|
4972
5050
|
}
|
|
4973
5051
|
},
|
|
4974
5052
|
methods: {
|
|
@@ -4976,6 +5054,125 @@ module.exports = app => app.component('document', {
|
|
|
4976
5054
|
this.changes = {};
|
|
4977
5055
|
this.editting = false;
|
|
4978
5056
|
},
|
|
5057
|
+
async refreshDocument(options = {}) {
|
|
5058
|
+
const { force = false, source = 'manual' } = options;
|
|
5059
|
+
if (this.editting && !force) {
|
|
5060
|
+
this.pendingRefresh = true;
|
|
5061
|
+
return;
|
|
5062
|
+
}
|
|
5063
|
+
if (this.isRefreshing) {
|
|
5064
|
+
this.pendingRefresh = true;
|
|
5065
|
+
return;
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
const isInitial = this.status === 'init';
|
|
5069
|
+
if (isInitial) {
|
|
5070
|
+
this.status = 'loading';
|
|
5071
|
+
}
|
|
5072
|
+
this.isRefreshing = true;
|
|
5073
|
+
this.autoRefreshError = null;
|
|
5074
|
+
try {
|
|
5075
|
+
const { doc, schemaPaths, virtualPaths } = await api.Model.getDocument({
|
|
5076
|
+
model: this.model,
|
|
5077
|
+
documentId: this.documentId
|
|
5078
|
+
});
|
|
5079
|
+
window.doc = doc;
|
|
5080
|
+
this.document = doc;
|
|
5081
|
+
this.schemaPaths = Object.keys(schemaPaths).sort((k1, k2) => {
|
|
5082
|
+
if (k1 === '_id' && k2 !== '_id') {
|
|
5083
|
+
return -1;
|
|
5084
|
+
}
|
|
5085
|
+
if (k1 !== '_id' && k2 === '_id') {
|
|
5086
|
+
return 1;
|
|
5087
|
+
}
|
|
5088
|
+
return 0;
|
|
5089
|
+
}).map(key => schemaPaths[key]);
|
|
5090
|
+
this.virtualPaths = virtualPaths || [];
|
|
5091
|
+
this.lastUpdatedAt = new Date();
|
|
5092
|
+
this.pendingRefresh = false;
|
|
5093
|
+
} catch (err) {
|
|
5094
|
+
console.error('Error refreshing document:', err);
|
|
5095
|
+
if (this.$toast && source !== 'initial') {
|
|
5096
|
+
this.$toast.error('Failed to refresh document');
|
|
5097
|
+
}
|
|
5098
|
+
} finally {
|
|
5099
|
+
this.status = 'loaded';
|
|
5100
|
+
this.isRefreshing = false;
|
|
5101
|
+
if (this.pendingRefresh && !this.editting) {
|
|
5102
|
+
this.pendingRefresh = false;
|
|
5103
|
+
this.refreshDocument({ source: 'pending' });
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
},
|
|
5107
|
+
toggleAutoRefresh() {
|
|
5108
|
+
if (this.autoRefreshEnabled) {
|
|
5109
|
+
this.stopAutoRefresh();
|
|
5110
|
+
} else {
|
|
5111
|
+
this.startAutoRefresh();
|
|
5112
|
+
}
|
|
5113
|
+
},
|
|
5114
|
+
startAutoRefresh() {
|
|
5115
|
+
if (this.autoRefreshEnabled) {
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
5118
|
+
this.autoRefreshEnabled = true;
|
|
5119
|
+
this.autoRefreshError = null;
|
|
5120
|
+
this.runAutoRefreshLoop();
|
|
5121
|
+
},
|
|
5122
|
+
stopAutoRefresh() {
|
|
5123
|
+
this.autoRefreshEnabled = false;
|
|
5124
|
+
this.autoRefreshConnecting = false;
|
|
5125
|
+
if (this.autoRefreshAbortController) {
|
|
5126
|
+
this.autoRefreshAbortController.abort();
|
|
5127
|
+
this.autoRefreshAbortController = null;
|
|
5128
|
+
}
|
|
5129
|
+
if (this.autoRefreshRetryTimer) {
|
|
5130
|
+
clearTimeout(this.autoRefreshRetryTimer);
|
|
5131
|
+
this.autoRefreshRetryTimer = null;
|
|
5132
|
+
}
|
|
5133
|
+
},
|
|
5134
|
+
async runAutoRefreshLoop() {
|
|
5135
|
+
if (this.autoRefreshLoopRunning) {
|
|
5136
|
+
return;
|
|
5137
|
+
}
|
|
5138
|
+
this.autoRefreshLoopRunning = true;
|
|
5139
|
+
this.autoRefreshAbortController = new AbortController();
|
|
5140
|
+
let retryDelay = 1500;
|
|
5141
|
+
|
|
5142
|
+
while (this.autoRefreshEnabled && !this.autoRefreshAbortController.signal.aborted) {
|
|
5143
|
+
try {
|
|
5144
|
+
this.autoRefreshConnecting = true;
|
|
5145
|
+
for await (const event of api.Model.streamDocumentChanges({
|
|
5146
|
+
model: this.model,
|
|
5147
|
+
documentId: this.documentId
|
|
5148
|
+
}, { signal: this.autoRefreshAbortController.signal })) {
|
|
5149
|
+
this.autoRefreshConnecting = false;
|
|
5150
|
+
if (!event || event.type === 'heartbeat') {
|
|
5151
|
+
continue;
|
|
5152
|
+
}
|
|
5153
|
+
await this.refreshDocument({ source: 'auto' });
|
|
5154
|
+
}
|
|
5155
|
+
} catch (err) {
|
|
5156
|
+
if (this.autoRefreshAbortController.signal.aborted) {
|
|
5157
|
+
break;
|
|
5158
|
+
}
|
|
5159
|
+
this.autoRefreshError = err?.message || String(err);
|
|
5160
|
+
} finally {
|
|
5161
|
+
this.autoRefreshConnecting = false;
|
|
5162
|
+
}
|
|
5163
|
+
|
|
5164
|
+
if (!this.autoRefreshEnabled || this.autoRefreshAbortController.signal.aborted) {
|
|
5165
|
+
break;
|
|
5166
|
+
}
|
|
5167
|
+
|
|
5168
|
+
await new Promise(resolve => {
|
|
5169
|
+
this.autoRefreshRetryTimer = setTimeout(resolve, retryDelay);
|
|
5170
|
+
});
|
|
5171
|
+
retryDelay = Math.min(retryDelay * 2, 15000);
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5174
|
+
this.autoRefreshLoopRunning = false;
|
|
5175
|
+
},
|
|
4979
5176
|
async save() {
|
|
4980
5177
|
if (Object.keys(this.invalid).length > 0) {
|
|
4981
5178
|
throw new Error('Invalid paths: ' + Object.keys(this.invalid).join(', '));
|
|
@@ -5043,6 +5240,43 @@ module.exports = app => app.component('document', {
|
|
|
5043
5240
|
this.changes = {};
|
|
5044
5241
|
}
|
|
5045
5242
|
},
|
|
5243
|
+
copyDocument() {
|
|
5244
|
+
if (!this.document) {
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
5247
|
+
|
|
5248
|
+
const textToCopy = JSON.stringify(this.document, null, 2);
|
|
5249
|
+
const fallbackCopy = () => {
|
|
5250
|
+
if (typeof document === 'undefined') {
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
const textArea = document.createElement('textarea');
|
|
5254
|
+
textArea.value = textToCopy;
|
|
5255
|
+
textArea.setAttribute('readonly', '');
|
|
5256
|
+
textArea.style.position = 'absolute';
|
|
5257
|
+
textArea.style.left = '-9999px';
|
|
5258
|
+
document.body.appendChild(textArea);
|
|
5259
|
+
textArea.select();
|
|
5260
|
+
try {
|
|
5261
|
+
document.execCommand('copy');
|
|
5262
|
+
} finally {
|
|
5263
|
+
document.body.removeChild(textArea);
|
|
5264
|
+
}
|
|
5265
|
+
this.$toast.success('Document copied!');
|
|
5266
|
+
};
|
|
5267
|
+
|
|
5268
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
|
|
5269
|
+
navigator.clipboard.writeText(textToCopy)
|
|
5270
|
+
.then(() => {
|
|
5271
|
+
this.$toast.success('Document copied!');
|
|
5272
|
+
})
|
|
5273
|
+
.catch(() => {
|
|
5274
|
+
fallbackCopy();
|
|
5275
|
+
});
|
|
5276
|
+
} else {
|
|
5277
|
+
fallbackCopy();
|
|
5278
|
+
}
|
|
5279
|
+
},
|
|
5046
5280
|
goBack() {
|
|
5047
5281
|
// Preserve query parameters when going back to models page
|
|
5048
5282
|
this.$router.push({
|
|
@@ -5869,8 +6103,12 @@ app.component('app-component', {
|
|
|
5869
6103
|
const { user, roles } = await mothership.me();
|
|
5870
6104
|
|
|
5871
6105
|
try {
|
|
5872
|
-
const { nodeEnv } = await
|
|
6106
|
+
const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
|
|
6107
|
+
api.status(),
|
|
6108
|
+
api.Model.listModels()
|
|
6109
|
+
]);
|
|
5873
6110
|
this.nodeEnv = nodeEnv;
|
|
6111
|
+
this.modelSchemaPaths = modelSchemaPaths;
|
|
5874
6112
|
} catch (err) {
|
|
5875
6113
|
this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
|
|
5876
6114
|
this.status = 'loaded';
|
|
@@ -5883,8 +6121,12 @@ app.component('app-component', {
|
|
|
5883
6121
|
}
|
|
5884
6122
|
} else {
|
|
5885
6123
|
try {
|
|
5886
|
-
const { nodeEnv } = await
|
|
6124
|
+
const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
|
|
6125
|
+
api.status(),
|
|
6126
|
+
api.Model.listModels()
|
|
6127
|
+
]);
|
|
5887
6128
|
this.nodeEnv = nodeEnv;
|
|
6129
|
+
this.modelSchemaPaths = modelSchemaPaths;
|
|
5888
6130
|
} catch (err) {
|
|
5889
6131
|
this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
|
|
5890
6132
|
}
|
|
@@ -5898,8 +6140,9 @@ app.component('app-component', {
|
|
|
5898
6140
|
const status = Vue.ref('init');
|
|
5899
6141
|
const nodeEnv = Vue.ref(null);
|
|
5900
6142
|
const authError = Vue.ref(null);
|
|
6143
|
+
const modelSchemaPaths = Vue.ref(null);
|
|
5901
6144
|
|
|
5902
|
-
const state = Vue.reactive({ user, roles, status, nodeEnv, authError });
|
|
6145
|
+
const state = Vue.reactive({ user, roles, status, nodeEnv, authError, modelSchemaPaths });
|
|
5903
6146
|
Vue.provide('state', state);
|
|
5904
6147
|
|
|
5905
6148
|
return state;
|
|
@@ -35732,7 +35975,7 @@ module.exports = FilterXSS;
|
|
|
35732
35975
|
(module) {
|
|
35733
35976
|
|
|
35734
35977
|
"use strict";
|
|
35735
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.2.
|
|
35978
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.2.10","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"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":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js"}}');
|
|
35736
35979
|
|
|
35737
35980
|
/***/ }
|
|
35738
35981
|
|
package/frontend/public/tw.css
CHANGED
|
@@ -950,6 +950,14 @@ video {
|
|
|
950
950
|
height: 0px;
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
.h-1 {
|
|
954
|
+
height: 0.25rem;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.h-1\.5 {
|
|
958
|
+
height: 0.375rem;
|
|
959
|
+
}
|
|
960
|
+
|
|
953
961
|
.h-10 {
|
|
954
962
|
height: 2.5rem;
|
|
955
963
|
}
|
|
@@ -1062,6 +1070,14 @@ video {
|
|
|
1062
1070
|
width: 0px;
|
|
1063
1071
|
}
|
|
1064
1072
|
|
|
1073
|
+
.w-1 {
|
|
1074
|
+
width: 0.25rem;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.w-1\.5 {
|
|
1078
|
+
width: 0.375rem;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1065
1081
|
.w-12 {
|
|
1066
1082
|
width: 3rem;
|
|
1067
1083
|
}
|
|
@@ -1661,6 +1677,11 @@ video {
|
|
|
1661
1677
|
background-color: rgb(5 150 105 / var(--tw-bg-opacity));
|
|
1662
1678
|
}
|
|
1663
1679
|
|
|
1680
|
+
.bg-forest-green-500 {
|
|
1681
|
+
--tw-bg-opacity: 1;
|
|
1682
|
+
background-color: rgb(0 242 58 / var(--tw-bg-opacity));
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1664
1685
|
.bg-forest-green-600 {
|
|
1665
1686
|
--tw-bg-opacity: 1;
|
|
1666
1687
|
background-color: rgb(0 202 44 / var(--tw-bg-opacity));
|
|
@@ -1751,6 +1772,11 @@ video {
|
|
|
1751
1772
|
background-color: rgb(241 245 249 / var(--tw-bg-opacity));
|
|
1752
1773
|
}
|
|
1753
1774
|
|
|
1775
|
+
.bg-slate-300 {
|
|
1776
|
+
--tw-bg-opacity: 1;
|
|
1777
|
+
background-color: rgb(203 213 225 / var(--tw-bg-opacity));
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1754
1780
|
.bg-slate-50 {
|
|
1755
1781
|
--tw-bg-opacity: 1;
|
|
1756
1782
|
background-color: rgb(248 250 252 / var(--tw-bg-opacity));
|
|
@@ -2124,6 +2150,11 @@ video {
|
|
|
2124
2150
|
color: rgb(0 242 58 / var(--tw-text-opacity));
|
|
2125
2151
|
}
|
|
2126
2152
|
|
|
2153
|
+
.text-forest-green-600 {
|
|
2154
|
+
--tw-text-opacity: 1;
|
|
2155
|
+
color: rgb(0 202 44 / var(--tw-text-opacity));
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2127
2158
|
.text-gray-300 {
|
|
2128
2159
|
--tw-text-opacity: 1;
|
|
2129
2160
|
color: rgb(209 213 219 / var(--tw-text-opacity));
|
|
@@ -2204,11 +2235,21 @@ video {
|
|
|
2204
2235
|
color: rgb(7 89 133 / var(--tw-text-opacity));
|
|
2205
2236
|
}
|
|
2206
2237
|
|
|
2238
|
+
.text-slate-400 {
|
|
2239
|
+
--tw-text-opacity: 1;
|
|
2240
|
+
color: rgb(148 163 184 / var(--tw-text-opacity));
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2207
2243
|
.text-slate-500 {
|
|
2208
2244
|
--tw-text-opacity: 1;
|
|
2209
2245
|
color: rgb(100 116 139 / var(--tw-text-opacity));
|
|
2210
2246
|
}
|
|
2211
2247
|
|
|
2248
|
+
.text-slate-600 {
|
|
2249
|
+
--tw-text-opacity: 1;
|
|
2250
|
+
color: rgb(71 85 105 / var(--tw-text-opacity));
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2212
2253
|
.text-slate-700 {
|
|
2213
2254
|
--tw-text-opacity: 1;
|
|
2214
2255
|
color: rgb(51 65 85 / var(--tw-text-opacity));
|
|
@@ -2502,6 +2543,11 @@ video {
|
|
|
2502
2543
|
background-color: rgb(253 230 138 / var(--tw-bg-opacity));
|
|
2503
2544
|
}
|
|
2504
2545
|
|
|
2546
|
+
.hover\:bg-blue-100:hover {
|
|
2547
|
+
--tw-bg-opacity: 1;
|
|
2548
|
+
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2505
2551
|
.hover\:bg-blue-600:hover {
|
|
2506
2552
|
--tw-bg-opacity: 1;
|
|
2507
2553
|
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
package/frontend/src/api.js
CHANGED
|
@@ -132,6 +132,16 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
132
132
|
yield { document: doc };
|
|
133
133
|
}
|
|
134
134
|
},
|
|
135
|
+
streamDocumentChanges: async function* streamDocumentChanges(params, options = {}) {
|
|
136
|
+
const pollIntervalMs = 5000;
|
|
137
|
+
while (!options.signal?.aborted) {
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
139
|
+
if (options.signal?.aborted) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
yield { type: 'poll', model: params.model, documentId: params.documentId };
|
|
143
|
+
}
|
|
144
|
+
},
|
|
135
145
|
getCollectionInfo: function getCollectionInfo(params) {
|
|
136
146
|
return client.post('', { action: 'Model.getCollectionInfo', ...params }).then(res => res.data);
|
|
137
147
|
},
|
|
@@ -352,6 +362,56 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
352
362
|
}
|
|
353
363
|
}
|
|
354
364
|
},
|
|
365
|
+
streamDocumentChanges: async function* streamDocumentChanges(params, options = {}) {
|
|
366
|
+
const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
|
|
367
|
+
const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/streamDocumentChanges?' + new URLSearchParams(params).toString();
|
|
368
|
+
|
|
369
|
+
const response = await fetch(url, {
|
|
370
|
+
method: 'GET',
|
|
371
|
+
headers: {
|
|
372
|
+
Authorization: `${accessToken}`,
|
|
373
|
+
Accept: 'text/event-stream'
|
|
374
|
+
},
|
|
375
|
+
signal: options.signal
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (!response.ok) {
|
|
379
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const reader = response.body.getReader();
|
|
383
|
+
const decoder = new TextDecoder('utf-8');
|
|
384
|
+
let buffer = '';
|
|
385
|
+
|
|
386
|
+
while (true) {
|
|
387
|
+
const { done, value } = await reader.read();
|
|
388
|
+
if (done) break;
|
|
389
|
+
buffer += decoder.decode(value, { stream: true });
|
|
390
|
+
|
|
391
|
+
let eventEnd;
|
|
392
|
+
while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
|
|
393
|
+
const eventStr = buffer.slice(0, eventEnd);
|
|
394
|
+
buffer = buffer.slice(eventEnd + 2);
|
|
395
|
+
|
|
396
|
+
// Parse SSE event
|
|
397
|
+
const lines = eventStr.split('\n');
|
|
398
|
+
let data = '';
|
|
399
|
+
for (const line of lines) {
|
|
400
|
+
if (line.startsWith('data:')) {
|
|
401
|
+
data += line.slice(5).trim();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (data) {
|
|
405
|
+
try {
|
|
406
|
+
yield JSON.parse(data);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
// If not JSON, yield as string
|
|
409
|
+
yield data;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
355
415
|
getCollectionInfo: function getCollectionInfo(params) {
|
|
356
416
|
return client.post('/Model/getCollectionInfo', params).then(res => res.data);
|
|
357
417
|
},
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<div class="py-2">
|
|
2
|
-
<div v-if="header" class="border-b border-gray-100 px-2 pb-2 text-xl font-bold">
|
|
3
|
-
{{header}}
|
|
4
|
-
</div>
|
|
5
2
|
<div class="text-xl pb-2">
|
|
6
|
-
<
|
|
3
|
+
<list-json
|
|
4
|
+
:value="value.$document.document"
|
|
5
|
+
:references="references" />
|
|
7
6
|
</div>
|
|
8
|
-
</div>
|
|
7
|
+
</div>
|