@mongoosejs/studio 0.5.1 → 0.5.3
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/ChatThread/streamChatMessage.js +12 -12
- package/backend/actions/Dashboard/getDashboard.js +29 -2
- package/backend/actions/Model/dropCollection.js +29 -0
- package/backend/actions/Model/index.js +1 -0
- package/backend/authorize.js +1 -0
- package/backend/chatAgent/agentSystemPrompt.js +22 -10
- package/backend/chatAgent/getAgentTools.js +19 -8
- package/backend/chatAgent/runChatAgent.js +17 -0
- package/backend/db/dashboardSchema.js +0 -32
- package/backend/index.js +4 -1
- package/backend/integrations/callLLM.js +5 -20
- package/backend/integrations/streamLLM.js +5 -24
- package/backend/sandbox/createSandbox.js +6 -0
- package/backend/vercel.js +3 -0
- package/debugging.js +11 -0
- package/eval/agent/cases.js +168 -0
- package/eval/agent/fixtures/carsharing.js +147 -0
- package/eval/agent/fixtures/ecommerce.js +152 -0
- package/eval/agent/run.js +323 -0
- package/frontend/index.js +2 -1
- package/frontend/public/app.js +581 -203
- package/frontend/public/tw.css +81 -0
- package/frontend/src/_util/recent-pages-history.js +116 -0
- package/frontend/src/api.js +96 -164
- package/frontend/src/chat/chat-message-script/chat-message-script.html +1 -6
- package/frontend/src/chat/chat.html +1 -1
- package/frontend/src/document/confirm-changes/confirm-changes.js +20 -0
- package/frontend/src/document/document.html +2 -2
- package/frontend/src/index.js +148 -1
- package/frontend/src/json-node/json-node.html +41 -39
- package/frontend/src/json-node/json-node.js +38 -0
- package/frontend/src/list-json/list-json.html +3 -2
- package/frontend/src/list-json/list-json.js +5 -0
- package/frontend/src/modal/modal.css +26 -1
- package/frontend/src/modal/modal.html +2 -2
- package/frontend/src/modal/modal.js +5 -1
- package/frontend/src/models/models.html +45 -5
- package/frontend/src/models/models.js +26 -0
- package/frontend/src/navbar/navbar.html +92 -5
- package/frontend/src/navbar/navbar.js +69 -1
- package/index.js +2 -1
- package/package.json +3 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<button
|
|
7
7
|
@click="viewMode = 'fields'"
|
|
8
8
|
:class="viewMode === 'fields'
|
|
9
|
-
? 'bg-
|
|
9
|
+
? 'bg-primary text-white z-10'
|
|
10
10
|
: 'bg-gray-200 text-content-secondary hover:bg-gray-300'"
|
|
11
11
|
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-edge-strong border-r-0 rounded-l-lg rounded-r-none"
|
|
12
12
|
>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<button
|
|
19
19
|
@click="viewMode = 'json'"
|
|
20
20
|
:class="viewMode === 'json'
|
|
21
|
-
? 'bg-
|
|
21
|
+
? 'bg-primary text-white z-10'
|
|
22
22
|
: 'bg-gray-200 text-content-secondary hover:bg-gray-300'"
|
|
23
23
|
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-edge-strong rounded-r-lg rounded-l-none"
|
|
24
24
|
>
|
package/frontend/src/index.js
CHANGED
|
@@ -15,12 +15,143 @@ const { routes, hasAccess } = require('./routes');
|
|
|
15
15
|
const Toast = require('vue-toastification').default;
|
|
16
16
|
const { useToast } = require('vue-toastification');
|
|
17
17
|
const appendCSS = require('./appendCSS');
|
|
18
|
+
const {
|
|
19
|
+
safeReadRecentPages,
|
|
20
|
+
saveRecentPages,
|
|
21
|
+
MAX_RECENT_PAGES,
|
|
22
|
+
normalizeTrackedPath,
|
|
23
|
+
withRecentPagesStorageLock
|
|
24
|
+
} = require('./_util/recent-pages-history');
|
|
18
25
|
appendCSS(require('vue-toastification/dist/index.css'));
|
|
19
26
|
|
|
27
|
+
const TRACKED_RECENT_PAGE_ROUTE_NAMES = new Set(['model', 'document', 'dashboard', 'chat']);
|
|
28
|
+
|
|
29
|
+
function formatHistoryLabel(route) {
|
|
30
|
+
if (!route || typeof route.path !== 'string') {
|
|
31
|
+
return 'Unknown page';
|
|
32
|
+
}
|
|
33
|
+
if (route.name === 'model') {
|
|
34
|
+
return route.params?.model ? `Model: ${route.params.model}` : 'Model';
|
|
35
|
+
}
|
|
36
|
+
if (route.name === 'document') {
|
|
37
|
+
const model = route.params?.model ? `${route.params.model} ` : '';
|
|
38
|
+
const documentId = route.params?.documentId ? String(route.params.documentId) : '';
|
|
39
|
+
const shortDocumentId = documentId ? documentId.slice(0, 24) : '';
|
|
40
|
+
return `Document: ${model}${shortDocumentId}`.trim();
|
|
41
|
+
}
|
|
42
|
+
if (route.name === 'dashboard') {
|
|
43
|
+
return route.params?.dashboardId ? `Dashboard: ${route.params.dashboardId}` : 'Dashboard';
|
|
44
|
+
}
|
|
45
|
+
if (route.name === 'chat') {
|
|
46
|
+
return route.params?.threadId ? `Chat: ${route.params.threadId}` : 'Chat';
|
|
47
|
+
}
|
|
48
|
+
const normalizedPath = route.path.replace(/^\//, '');
|
|
49
|
+
return normalizedPath || 'Home';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function trackRecentPage(route) {
|
|
53
|
+
if (!route || typeof route.path !== 'string') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!TRACKED_RECENT_PAGE_ROUTE_NAMES.has(route.name)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (typeof route.path === 'string' && route.path.includes('code=')) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const path = normalizeTrackedPath(route.fullPath || route.path || '/');
|
|
63
|
+
const label = formatHistoryLabel(route);
|
|
64
|
+
const visitedAt = Date.now();
|
|
65
|
+
|
|
66
|
+
void withRecentPagesStorageLock(() => {
|
|
67
|
+
const existing = safeReadRecentPages();
|
|
68
|
+
const deduped = existing.filter(entry => normalizeTrackedPath(entry.path) !== path);
|
|
69
|
+
const next = [...deduped, { path, label, visitedAt }]
|
|
70
|
+
.sort((a, b) => b.visitedAt - a.visitedAt)
|
|
71
|
+
.slice(0, MAX_RECENT_PAGES);
|
|
72
|
+
saveRecentPages(next);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
20
76
|
const app = Vue.createApp({
|
|
21
77
|
template: '<app-component />'
|
|
22
78
|
});
|
|
23
79
|
|
|
80
|
+
window.drawArrowTo = function drawArrowTo(elementOrSelector) {
|
|
81
|
+
const element = typeof elementOrSelector === 'string' ?
|
|
82
|
+
document.querySelector(elementOrSelector) :
|
|
83
|
+
elementOrSelector;
|
|
84
|
+
|
|
85
|
+
if (!element || typeof element.getBoundingClientRect !== 'function') {
|
|
86
|
+
throw new Error('window.drawArrowTo() requires a DOM element or selector');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const existingArrow = document.getElementById('mongoose-studio-screenshot-arrow');
|
|
90
|
+
if (existingArrow) {
|
|
91
|
+
existingArrow.remove();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const rect = element.getBoundingClientRect();
|
|
95
|
+
const arrowLength = 140;
|
|
96
|
+
const arrowRise = 80;
|
|
97
|
+
const centerX = rect.left + rect.width / 2;
|
|
98
|
+
const centerY = rect.top + rect.height / 2;
|
|
99
|
+
const startX = Math.max(24, Math.min(window.innerWidth - 24, centerX - arrowLength));
|
|
100
|
+
const startY = Math.max(24, Math.min(window.innerHeight - 24, centerY - arrowRise));
|
|
101
|
+
const targetX = Math.max(rect.left, Math.min(rect.right, startX));
|
|
102
|
+
const targetY = Math.max(rect.top, Math.min(rect.bottom, startY));
|
|
103
|
+
const controlX = startX + (targetX - startX) * 0.6;
|
|
104
|
+
const controlY = startY + (targetY - startY) * 0.15;
|
|
105
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
106
|
+
const svg = document.createElementNS(svgNS, 'svg');
|
|
107
|
+
const defs = document.createElementNS(svgNS, 'defs');
|
|
108
|
+
const marker = document.createElementNS(svgNS, 'marker');
|
|
109
|
+
const markerPath = document.createElementNS(svgNS, 'path');
|
|
110
|
+
const path = document.createElementNS(svgNS, 'path');
|
|
111
|
+
|
|
112
|
+
svg.setAttribute('id', 'mongoose-studio-screenshot-arrow');
|
|
113
|
+
svg.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`);
|
|
114
|
+
svg.setAttribute('width', '100vw');
|
|
115
|
+
svg.setAttribute('height', '100vh');
|
|
116
|
+
svg.style.position = 'fixed';
|
|
117
|
+
svg.style.left = '0';
|
|
118
|
+
svg.style.top = '0';
|
|
119
|
+
svg.style.zIndex = '2147483647';
|
|
120
|
+
svg.style.pointerEvents = 'none';
|
|
121
|
+
|
|
122
|
+
marker.setAttribute('id', 'mongoose-studio-screenshot-arrow-head');
|
|
123
|
+
marker.setAttribute('markerWidth', '14');
|
|
124
|
+
marker.setAttribute('markerHeight', '14');
|
|
125
|
+
marker.setAttribute('refX', '12');
|
|
126
|
+
marker.setAttribute('refY', '7');
|
|
127
|
+
marker.setAttribute('orient', 'auto');
|
|
128
|
+
marker.setAttribute('markerUnits', 'userSpaceOnUse');
|
|
129
|
+
|
|
130
|
+
markerPath.setAttribute('d', 'M 0 0 L 14 7 L 0 14 z');
|
|
131
|
+
markerPath.setAttribute('fill', '#dc2626');
|
|
132
|
+
|
|
133
|
+
path.setAttribute('d', `M ${startX} ${startY} Q ${controlX} ${controlY} ${targetX} ${targetY}`);
|
|
134
|
+
path.setAttribute('fill', 'none');
|
|
135
|
+
path.setAttribute('stroke', '#dc2626');
|
|
136
|
+
path.setAttribute('stroke-width', '5');
|
|
137
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
138
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
139
|
+
path.setAttribute('marker-end', 'url(#mongoose-studio-screenshot-arrow-head)');
|
|
140
|
+
|
|
141
|
+
marker.appendChild(markerPath);
|
|
142
|
+
defs.appendChild(marker);
|
|
143
|
+
svg.appendChild(defs);
|
|
144
|
+
svg.appendChild(path);
|
|
145
|
+
document.body.appendChild(svg);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
element: svg,
|
|
149
|
+
remove() {
|
|
150
|
+
svg.remove();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
24
155
|
// https://github.com/Maronato/vue-toastification/tree/main?tab=readme-ov-file#toast-types
|
|
25
156
|
app.use(Toast, { position: 'bottom-right', timeout: 3000 });
|
|
26
157
|
|
|
@@ -169,7 +300,14 @@ app.component('app-component', {
|
|
|
169
300
|
const authError = Vue.ref(null);
|
|
170
301
|
const modelSchemaPaths = Vue.ref(null);
|
|
171
302
|
|
|
172
|
-
const state = Vue.reactive({
|
|
303
|
+
const state = Vue.reactive({
|
|
304
|
+
user,
|
|
305
|
+
roles,
|
|
306
|
+
status,
|
|
307
|
+
nodeEnv,
|
|
308
|
+
authError,
|
|
309
|
+
modelSchemaPaths
|
|
310
|
+
});
|
|
173
311
|
Vue.provide('state', state);
|
|
174
312
|
|
|
175
313
|
return state;
|
|
@@ -221,6 +359,11 @@ router.beforeEach((to, from, next) => {
|
|
|
221
359
|
});
|
|
222
360
|
|
|
223
361
|
router.beforeEach((to, from, next) => {
|
|
362
|
+
if (['chat index', 'chat'].includes(to.name) && !window.MONGOOSE_STUDIO_CONFIG.hasLLMAPIKey) {
|
|
363
|
+
next({ name: 'root' });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
224
367
|
if (to.name === 'root' && window.state.roles && window.state.roles[0] === 'dashboards') {
|
|
225
368
|
return next({ name: 'dashboards' });
|
|
226
369
|
} else {
|
|
@@ -228,6 +371,10 @@ router.beforeEach((to, from, next) => {
|
|
|
228
371
|
}
|
|
229
372
|
});
|
|
230
373
|
|
|
374
|
+
router.afterEach((to) => {
|
|
375
|
+
trackRecentPage(to);
|
|
376
|
+
});
|
|
377
|
+
|
|
231
378
|
app.config.globalProperties = { format, arrayUtils, $toast: toast };
|
|
232
379
|
app.use(router);
|
|
233
380
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<div>
|
|
2
|
-
<div class="flex items-baseline whitespace-pre" :style="indentStyle">
|
|
1
|
+
<div class="min-w-0 max-w-full">
|
|
2
|
+
<div class="flex min-w-0 max-w-full items-baseline whitespace-pre-wrap break-words" :style="indentStyle">
|
|
3
3
|
<button
|
|
4
4
|
v-if="showToggle"
|
|
5
5
|
type="button"
|
|
@@ -23,36 +23,32 @@
|
|
|
23
23
|
</template>
|
|
24
24
|
</template>
|
|
25
25
|
<template v-else>
|
|
26
|
-
<!--
|
|
27
|
-
If value is a string and overflows its container (i.e. goes over one line), show an ellipsis.
|
|
28
|
-
This is done via CSS ellipsis strategy.
|
|
29
|
-
-->
|
|
30
26
|
<span
|
|
31
27
|
v-if="shouldShowReferenceLink"
|
|
32
|
-
class="inline-flex items-baseline
|
|
28
|
+
class="group inline-flex min-w-0 max-w-full items-baseline flex-wrap gap-x-1.5"
|
|
33
29
|
>
|
|
34
|
-
<span
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
? {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
whiteSpace: 'nowrap',
|
|
43
|
-
verticalAlign: 'bottom'
|
|
44
|
-
}
|
|
45
|
-
: {}"
|
|
46
|
-
:title="typeof value === 'string' && $el && $el.scrollWidth > $el.clientWidth ? value : undefined"
|
|
47
|
-
>
|
|
48
|
-
{{ formattedValue }}
|
|
49
|
-
</span>
|
|
50
|
-
<span>
|
|
51
|
-
{{ comma }}
|
|
30
|
+
<span class="min-w-0 max-w-full">
|
|
31
|
+
<span
|
|
32
|
+
:class="[...valueClasses, ...stringWrapperClasses, 'underline', 'decoration-dotted', 'underline-offset-2']"
|
|
33
|
+
:style="isStringValue ? { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } : {}"
|
|
34
|
+
:title="shouldTruncateString && !isStringExpanded ? value : undefined"
|
|
35
|
+
>
|
|
36
|
+
{{ displayedValue }}
|
|
37
|
+
</span><template v-if="!(shouldTruncateString && !isStringExpanded)">{{ comma }}</template>
|
|
52
38
|
</span>
|
|
39
|
+
<button
|
|
40
|
+
v-if="shouldTruncateString"
|
|
41
|
+
type="button"
|
|
42
|
+
class="inline-flex shrink-0 items-center gap-1 rounded-full border border-edge bg-surface px-2 py-0.5 text-[11px] font-medium text-content-secondary shadow-sm transition-colors hover:bg-muted hover:text-content focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
|
43
|
+
:title="stringToggleLabel"
|
|
44
|
+
:aria-expanded="isStringExpanded"
|
|
45
|
+
@click.stop="toggleStringExpansion"
|
|
46
|
+
>
|
|
47
|
+
<span class="leading-none">{{ stringToggleLabel }}</span>
|
|
48
|
+
</button>
|
|
53
49
|
<a
|
|
54
50
|
href="#"
|
|
55
|
-
class="ml-1 text-sm text-primary opacity-0 group-hover:opacity-100 focus:opacity-100
|
|
51
|
+
class="ml-1 shrink-0 text-sm text-primary opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100"
|
|
56
52
|
@click.stop.prevent="goToReference()"
|
|
57
53
|
>
|
|
58
54
|
View Document
|
|
@@ -60,20 +56,25 @@
|
|
|
60
56
|
</span>
|
|
61
57
|
<span
|
|
62
58
|
v-else
|
|
63
|
-
|
|
64
|
-
:style="typeof value === 'string'
|
|
65
|
-
? {
|
|
66
|
-
display: 'inline-block',
|
|
67
|
-
maxWidth: '100%',
|
|
68
|
-
overflow: 'hidden',
|
|
69
|
-
textOverflow: 'ellipsis',
|
|
70
|
-
whiteSpace: 'nowrap',
|
|
71
|
-
verticalAlign: 'bottom'
|
|
72
|
-
}
|
|
73
|
-
: {}"
|
|
74
|
-
:title="typeof value === 'string' && $el && $el.scrollWidth > $el.clientWidth ? value : undefined"
|
|
59
|
+
class="inline-flex min-w-0 max-w-full items-baseline flex-wrap gap-x-1.5"
|
|
75
60
|
>
|
|
76
|
-
|
|
61
|
+
<span
|
|
62
|
+
:class="[...valueClasses, ...stringWrapperClasses]"
|
|
63
|
+
:style="isStringValue ? { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } : {}"
|
|
64
|
+
:title="shouldTruncateString && !isStringExpanded ? value : undefined"
|
|
65
|
+
>
|
|
66
|
+
{{ displayedValue }}<template v-if="!(shouldTruncateString && !isStringExpanded)">{{ comma }}</template>
|
|
67
|
+
</span>
|
|
68
|
+
<button
|
|
69
|
+
v-if="shouldTruncateString"
|
|
70
|
+
type="button"
|
|
71
|
+
class="inline-flex shrink-0 items-center gap-1 rounded-full border border-edge bg-surface px-2 py-0.5 text-[11px] font-medium text-content-secondary shadow-sm transition-colors hover:bg-muted hover:text-content focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
|
72
|
+
:title="stringToggleLabel"
|
|
73
|
+
:aria-expanded="isStringExpanded"
|
|
74
|
+
@click.stop="toggleStringExpansion"
|
|
75
|
+
>
|
|
76
|
+
<span class="leading-none">{{ stringToggleLabel }}</span>
|
|
77
|
+
</button>
|
|
77
78
|
</span>
|
|
78
79
|
</template>
|
|
79
80
|
</div>
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
:top-level-expanded="topLevelExpanded"
|
|
95
96
|
:expand-top-level="expandTopLevel"
|
|
96
97
|
:references="references"
|
|
98
|
+
:max-string-length="maxStringLength"
|
|
97
99
|
></json-node>
|
|
98
100
|
<div
|
|
99
101
|
v-if="hasHiddenRootChildren"
|
|
@@ -56,8 +56,17 @@ module.exports = app => app.component('json-node', {
|
|
|
56
56
|
references: {
|
|
57
57
|
type: Object,
|
|
58
58
|
default: () => ({})
|
|
59
|
+
},
|
|
60
|
+
maxStringLength: {
|
|
61
|
+
type: Number,
|
|
62
|
+
default: 200
|
|
59
63
|
}
|
|
60
64
|
},
|
|
65
|
+
data() {
|
|
66
|
+
return {
|
|
67
|
+
isStringExpanded: false
|
|
68
|
+
};
|
|
69
|
+
},
|
|
61
70
|
computed: {
|
|
62
71
|
hasKey() {
|
|
63
72
|
return this.nodeKey !== null && this.nodeKey !== undefined;
|
|
@@ -151,6 +160,29 @@ module.exports = app => app.component('json-node', {
|
|
|
151
160
|
}
|
|
152
161
|
return stringified;
|
|
153
162
|
},
|
|
163
|
+
isStringValue() {
|
|
164
|
+
return typeof this.value === 'string';
|
|
165
|
+
},
|
|
166
|
+
shouldTruncateString() {
|
|
167
|
+
return this.isStringValue && this.formattedValue.length > this.maxStringLength;
|
|
168
|
+
},
|
|
169
|
+
displayedValue() {
|
|
170
|
+
if (!this.shouldTruncateString || this.isStringExpanded) {
|
|
171
|
+
return this.formattedValue;
|
|
172
|
+
}
|
|
173
|
+
const keep = Math.max(0, this.maxStringLength - 1);
|
|
174
|
+
return `${this.formattedValue.slice(0, keep)}…`;
|
|
175
|
+
},
|
|
176
|
+
stringWrapperClasses() {
|
|
177
|
+
const classes = ['min-w-0', 'max-w-full', 'break-words'];
|
|
178
|
+
if (this.shouldTruncateString && this.isStringExpanded) {
|
|
179
|
+
classes.push('rounded-md', 'border', 'border-edge', 'bg-muted/60', 'px-2', 'py-1');
|
|
180
|
+
}
|
|
181
|
+
return classes;
|
|
182
|
+
},
|
|
183
|
+
stringToggleLabel() {
|
|
184
|
+
return this.isStringExpanded ? 'Show less' : 'Show more';
|
|
185
|
+
},
|
|
154
186
|
valueClasses() {
|
|
155
187
|
const classes = ['text-slate-700'];
|
|
156
188
|
if (this.value === null) {
|
|
@@ -267,6 +299,12 @@ module.exports = app => app.component('json-node', {
|
|
|
267
299
|
return;
|
|
268
300
|
}
|
|
269
301
|
this.$router.push({ path: `/model/${this.referenceModel}/document/${id}` });
|
|
302
|
+
},
|
|
303
|
+
toggleStringExpansion() {
|
|
304
|
+
if (!this.shouldTruncateString) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.isStringExpanded = !this.isStringExpanded;
|
|
270
308
|
}
|
|
271
309
|
}
|
|
272
310
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<div class="tooltip w-full font-mono text-sm py-1 text-slate-800">
|
|
2
|
-
<div class="w-full">
|
|
1
|
+
<div class="tooltip w-full min-w-0 max-w-full font-mono text-sm py-1 text-slate-800">
|
|
2
|
+
<div class="w-full min-w-0 max-w-full">
|
|
3
3
|
<json-node
|
|
4
4
|
:node-key="null"
|
|
5
5
|
:value="value"
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
:top-level-expanded="topLevelExpanded"
|
|
15
15
|
:expand-top-level="expandTopLevel"
|
|
16
16
|
:references="references"
|
|
17
|
+
:max-string-length="maxStringLength"
|
|
17
18
|
></json-node>
|
|
18
19
|
</div>
|
|
19
20
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const template = require('./list-json.html');
|
|
4
|
+
const DEFAULT_MAX_STRING_LENGTH = 200;
|
|
4
5
|
|
|
5
6
|
module.exports = app => app.component('list-json', {
|
|
6
7
|
template: template,
|
|
@@ -19,6 +20,10 @@ module.exports = app => app.component('list-json', {
|
|
|
19
20
|
expandedFields: {
|
|
20
21
|
type: Array,
|
|
21
22
|
default: () => []
|
|
23
|
+
},
|
|
24
|
+
maxStringLength: {
|
|
25
|
+
type: Number,
|
|
26
|
+
default: DEFAULT_MAX_STRING_LENGTH
|
|
22
27
|
}
|
|
23
28
|
},
|
|
24
29
|
data() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
.modal-mask {
|
|
4
4
|
position: fixed;
|
|
5
|
-
z-index:
|
|
5
|
+
z-index: 10050;
|
|
6
6
|
top: 0;
|
|
7
7
|
left: 0;
|
|
8
8
|
width: 100%;
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
position: relative;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
.modal-container.modal-container--flush {
|
|
34
|
+
padding: 0;
|
|
35
|
+
padding-bottom: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
.modal-header {
|
|
34
39
|
margin-top: 0;
|
|
35
40
|
font-size: 18px;
|
|
@@ -50,6 +55,25 @@
|
|
|
50
55
|
overflow: auto;
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
/* Recent pages: one scroll region — inner .modal-recent-pages-scroll only (avoids double scrollbars with .modal-body overflow). */
|
|
59
|
+
.modal-body.modal-recent-pages-body {
|
|
60
|
+
box-sizing: border-box;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
margin: 0;
|
|
64
|
+
height: min(60vh, 22.5rem);
|
|
65
|
+
max-height: min(60vh, 22.5rem);
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.modal-body.modal-recent-pages-body .modal-recent-pages-scroll {
|
|
70
|
+
flex: 1 1 0;
|
|
71
|
+
min-height: 0;
|
|
72
|
+
overflow-x: hidden;
|
|
73
|
+
overflow-y: auto;
|
|
74
|
+
-webkit-overflow-scrolling: touch;
|
|
75
|
+
}
|
|
76
|
+
|
|
53
77
|
.modal__button--default {
|
|
54
78
|
float: right;
|
|
55
79
|
}
|
|
@@ -79,6 +103,7 @@
|
|
|
79
103
|
|
|
80
104
|
.modal-container .modal-exit {
|
|
81
105
|
position: absolute;
|
|
106
|
+
z-index: 2;
|
|
82
107
|
right: 0.25em;
|
|
83
108
|
top: 0.25em;
|
|
84
109
|
cursor: pointer;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<transition name="modal">
|
|
2
2
|
<div class="modal-mask">
|
|
3
3
|
<div class="modal-wrapper">
|
|
4
|
-
<div class="modal-container" :class="containerClass">
|
|
5
|
-
<div class="modal-body">
|
|
4
|
+
<div class="modal-container" :class="[containerClass, { 'modal-container--flush': flushContainer }]">
|
|
5
|
+
<div class="modal-body" :class="bodyClass">
|
|
6
6
|
<slot name="body">
|
|
7
7
|
</slot>
|
|
8
8
|
</div>
|
|
@@ -7,7 +7,11 @@ appendCSS(require('./modal.css'));
|
|
|
7
7
|
|
|
8
8
|
module.exports = app => app.component('modal', {
|
|
9
9
|
template,
|
|
10
|
-
props:
|
|
10
|
+
props: {
|
|
11
|
+
containerClass: String,
|
|
12
|
+
bodyClass: String,
|
|
13
|
+
flushContainer: Boolean
|
|
14
|
+
},
|
|
11
15
|
mounted() {
|
|
12
16
|
window.addEventListener('keydown', this.onEscape);
|
|
13
17
|
},
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
@click="stagingSelect"
|
|
118
118
|
type="button"
|
|
119
119
|
:class="{
|
|
120
|
-
'bg-gray-700 ring-inset ring-2 ring-gray-300 hover:bg-gray-
|
|
120
|
+
'bg-gray-700 ring-inset ring-2 ring-gray-300 text-white hover:bg-gray-600': selectMultiple,
|
|
121
121
|
'bg-primary hover:bg-primary-hover text-primary-text': !selectMultiple
|
|
122
122
|
}"
|
|
123
123
|
class="rounded px-2 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
@@ -190,6 +190,13 @@
|
|
|
190
190
|
>
|
|
191
191
|
Find oldest document
|
|
192
192
|
</button>
|
|
193
|
+
<button
|
|
194
|
+
@click="openDropCollectionModal"
|
|
195
|
+
type="button"
|
|
196
|
+
class="block w-full px-4 py-2 text-left text-sm text-red-700 hover:bg-red-50"
|
|
197
|
+
>
|
|
198
|
+
Drop Collection
|
|
199
|
+
</button>
|
|
193
200
|
<button
|
|
194
201
|
@click="toggleRowNumbers()"
|
|
195
202
|
type="button"
|
|
@@ -404,8 +411,10 @@
|
|
|
404
411
|
v-for="(document, docIndex) in documents"
|
|
405
412
|
:key="document._id"
|
|
406
413
|
@click="handleDocumentClick(document, $event)"
|
|
407
|
-
class="border-b border-edge cursor-pointer transition-colors
|
|
408
|
-
:class="
|
|
414
|
+
class="border-b border-edge cursor-pointer transition-colors"
|
|
415
|
+
:class="selectedDocuments.some(x => x._id.toString() === document._id.toString())
|
|
416
|
+
? 'bg-blue-200'
|
|
417
|
+
: 'hover:bg-muted'"
|
|
409
418
|
>
|
|
410
419
|
<td v-if="showRowNumbers" class="px-3 py-2 border-r border-edge align-top text-content-tertiary whitespace-nowrap">
|
|
411
420
|
{{ docIndex + 1 }}
|
|
@@ -448,13 +457,13 @@
|
|
|
448
457
|
No documents to show. Use Projection in the menu to choose columns.
|
|
449
458
|
</p>
|
|
450
459
|
</div>
|
|
451
|
-
<div v-else-if="outputType === 'json'" class="flex flex-col space-y-2 p-1 mt-1">
|
|
460
|
+
<div v-else-if="outputType === 'json'" class="flex min-w-0 flex-col space-y-2 p-1 mt-1">
|
|
452
461
|
<div
|
|
453
462
|
v-for="document in documents"
|
|
454
463
|
:key="document._id"
|
|
455
464
|
@click="handleDocumentContainerClick(document, $event)"
|
|
456
465
|
:class="[
|
|
457
|
-
'group relative transition-colors rounded-md border border-slate-100',
|
|
466
|
+
'group relative min-w-0 max-w-full transition-colors rounded-md border border-slate-100',
|
|
458
467
|
selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:shadow-sm hover:border-slate-300 bg-surface'
|
|
459
468
|
]"
|
|
460
469
|
>
|
|
@@ -609,6 +618,37 @@
|
|
|
609
618
|
</div>
|
|
610
619
|
</template>
|
|
611
620
|
</modal>
|
|
621
|
+
<modal v-if="shouldShowDropCollectionModal">
|
|
622
|
+
<template v-slot:body>
|
|
623
|
+
<div class="modal-exit" @click="shouldShowDropCollectionModal = false">×</div>
|
|
624
|
+
<h2 class="text-xl font-bold mb-3">Are you sure?</h2>
|
|
625
|
+
<p class="text-content-secondary mb-3">
|
|
626
|
+
This will permanently drop the <span class="font-semibold">{{ currentModel }}</span> collection and all its documents.
|
|
627
|
+
</p>
|
|
628
|
+
<div v-if="requiresDropCollectionNameConfirmation()" class="rounded border border-red-300 bg-red-50 px-3 py-2 mb-4">
|
|
629
|
+
<p class="text-sm font-semibold text-red-700">Danger zone</p>
|
|
630
|
+
<p class="text-sm text-red-700 mb-2">Type <span class="font-mono font-semibold">{{ currentModel }}</span> to confirm.</p>
|
|
631
|
+
<input
|
|
632
|
+
v-model="dropCollectionConfirmName"
|
|
633
|
+
type="text"
|
|
634
|
+
:placeholder="currentModel"
|
|
635
|
+
class="w-full rounded border border-red-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-red-200"
|
|
636
|
+
/>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="flex gap-4">
|
|
639
|
+
<async-button
|
|
640
|
+
@click="dropCollection"
|
|
641
|
+
:disabled="requiresDropCollectionNameConfirmation() && dropCollectionConfirmName !== currentModel"
|
|
642
|
+
class="rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
643
|
+
>
|
|
644
|
+
Drop Collection
|
|
645
|
+
</async-button>
|
|
646
|
+
<button @click="shouldShowDropCollectionModal = false" class="rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-page0">
|
|
647
|
+
Cancel
|
|
648
|
+
</button>
|
|
649
|
+
</div>
|
|
650
|
+
</template>
|
|
651
|
+
</modal>
|
|
612
652
|
<model-switcher
|
|
613
653
|
:show="showModelSwitcher"
|
|
614
654
|
:models="models"
|
|
@@ -168,6 +168,8 @@ module.exports = app => app.component('models', {
|
|
|
168
168
|
showAddFieldDropdown: false,
|
|
169
169
|
shouldShowIndexModal: false,
|
|
170
170
|
shouldShowCollectionInfoModal: false,
|
|
171
|
+
shouldShowDropCollectionModal: false,
|
|
172
|
+
dropCollectionConfirmName: '',
|
|
171
173
|
shouldShowUpdateMultipleModal: false,
|
|
172
174
|
shouldShowDeleteMultipleModal: false,
|
|
173
175
|
shouldExport: {},
|
|
@@ -923,6 +925,30 @@ module.exports = app => app.component('models', {
|
|
|
923
925
|
const { info } = await api.Model.getCollectionInfo({ model: this.currentModel });
|
|
924
926
|
this.collectionInfo = info;
|
|
925
927
|
},
|
|
928
|
+
openDropCollectionModal() {
|
|
929
|
+
this.closeActionsMenu();
|
|
930
|
+
this.dropCollectionConfirmName = '';
|
|
931
|
+
this.shouldShowDropCollectionModal = true;
|
|
932
|
+
},
|
|
933
|
+
requiresDropCollectionNameConfirmation() {
|
|
934
|
+
const nodeEnv = window?.state?.nodeEnv;
|
|
935
|
+
return nodeEnv !== 'local';
|
|
936
|
+
},
|
|
937
|
+
async dropCollection() {
|
|
938
|
+
if (this.requiresDropCollectionNameConfirmation() && this.dropCollectionConfirmName !== this.currentModel) {
|
|
939
|
+
this.$toast.error('Please type the collection name exactly to confirm.');
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
await api.Model.dropCollection({ model: this.currentModel });
|
|
944
|
+
this.shouldShowDropCollectionModal = false;
|
|
945
|
+
this.dropCollectionConfirmName = '';
|
|
946
|
+
this.documents = [];
|
|
947
|
+
this.numDocuments = 0;
|
|
948
|
+
this.selectedDocuments = [];
|
|
949
|
+
this.$toast.success(`Collection ${this.currentModel} dropped.`);
|
|
950
|
+
await this.getDocuments();
|
|
951
|
+
},
|
|
926
952
|
async findOldestDocument() {
|
|
927
953
|
this.closeActionsMenu();
|
|
928
954
|
const { docs } = await api.Model.getDocuments({
|