@mongoosejs/studio 0.5.1 → 0.5.2

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.
@@ -1583,6 +1583,16 @@ video {
1583
1583
  gap: 2rem;
1584
1584
  }
1585
1585
 
1586
+ .gap-x-1 {
1587
+ -moz-column-gap: 0.25rem;
1588
+ column-gap: 0.25rem;
1589
+ }
1590
+
1591
+ .gap-x-1\.5 {
1592
+ -moz-column-gap: 0.375rem;
1593
+ column-gap: 0.375rem;
1594
+ }
1595
+
1586
1596
  .gap-x-4 {
1587
1597
  -moz-column-gap: 1rem;
1588
1598
  column-gap: 1rem;
@@ -1867,6 +1877,11 @@ video {
1867
1877
  border-color: rgb(254 202 202 / var(--tw-border-opacity));
1868
1878
  }
1869
1879
 
1880
+ .border-red-300 {
1881
+ --tw-border-opacity: 1;
1882
+ border-color: rgb(252 165 165 / var(--tw-border-opacity));
1883
+ }
1884
+
1870
1885
  .border-red-400 {
1871
1886
  --tw-border-opacity: 1;
1872
1887
  border-color: rgb(248 113 113 / var(--tw-border-opacity));
@@ -2446,6 +2461,10 @@ video {
2446
2461
  font-size: 10px;
2447
2462
  }
2448
2463
 
2464
+ .text-\[11px\] {
2465
+ font-size: 11px;
2466
+ }
2467
+
2449
2468
  .text-base {
2450
2469
  font-size: 1rem;
2451
2470
  line-height: 1.5rem;
@@ -3227,6 +3246,11 @@ video {
3227
3246
  background-color: rgb(254 226 226 / var(--tw-bg-opacity));
3228
3247
  }
3229
3248
 
3249
+ .hover\:bg-red-50:hover {
3250
+ --tw-bg-opacity: 1;
3251
+ background-color: rgb(254 242 242 / var(--tw-bg-opacity));
3252
+ }
3253
+
3230
3254
  .hover\:bg-red-500:hover {
3231
3255
  --tw-bg-opacity: 1;
3232
3256
  background-color: rgb(239 68 68 / var(--tw-bg-opacity));
@@ -3487,6 +3511,11 @@ video {
3487
3511
  --tw-ring-color: var(--color-primary-subtle);
3488
3512
  }
3489
3513
 
3514
+ .focus\:ring-red-200:focus {
3515
+ --tw-ring-opacity: 1;
3516
+ --tw-ring-color: rgb(254 202 202 / var(--tw-ring-opacity));
3517
+ }
3518
+
3490
3519
  .focus\:ring-sky-600:focus {
3491
3520
  --tw-ring-opacity: 1;
3492
3521
  --tw-ring-color: rgb(2 132 199 / var(--tw-ring-opacity));
@@ -9,6 +9,7 @@ const client = axios.create({
9
9
  console.log('API baseURL:', window.MONGOOSE_STUDIO_CONFIG.baseURL);
10
10
 
11
11
  window.apiClient = client;
12
+ exports.fetch = fetch;
12
13
  client.interceptors.request.use(req => {
13
14
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
14
15
  if (accessToken) {
@@ -24,10 +25,92 @@ client.interceptors.response.use(
24
25
  if (typeof err?.response?.data === 'string') {
25
26
  throw new Error(`Error in ${err.config?.method} ${err.config?.url}: ${err.response.data}`);
26
27
  }
28
+ if (typeof err?.response?.data?.message === 'string') {
29
+ throw new Error(`Error in ${err.config?.method} ${err.config?.url}: ${err.response.data.message}`);
30
+ }
27
31
  throw err;
28
32
  }
29
33
  );
30
34
 
35
+ async function* streamSSE(url, options = {}) {
36
+ const fetch = exports.fetch;
37
+ const response = await fetch(url, {
38
+ method: 'GET',
39
+ ...options,
40
+ headers: {
41
+ ...(options.headers || {}),
42
+ Accept: 'text/event-stream'
43
+ }
44
+ });
45
+
46
+ if (!response.ok) {
47
+ throw new Error(`HTTP error! Status: ${response.status}`);
48
+ }
49
+
50
+ const reader = response.body.getReader();
51
+ const decoder = new TextDecoder('utf-8');
52
+ let buffer = '';
53
+
54
+ while (true) {
55
+ const { done, value } = await reader.read();
56
+ if (done) {
57
+ break;
58
+ }
59
+ buffer += decoder.decode(value, { stream: true });
60
+
61
+ let eventEnd;
62
+ while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
63
+ const eventStr = buffer.slice(0, eventEnd);
64
+ buffer = buffer.slice(eventEnd + 2);
65
+
66
+ const event = parseSSEEvent(eventStr);
67
+ if (event == null) {
68
+ continue;
69
+ }
70
+ if (event.type === 'error') {
71
+ const message = getSSEErrorMessage(event.data);
72
+ throw new Error(message);
73
+ }
74
+ yield event.data;
75
+ }
76
+ }
77
+ }
78
+
79
+ function parseSSEEvent(eventStr) {
80
+ const lines = eventStr.split('\n');
81
+ let type = 'message';
82
+ let data = '';
83
+ for (const line of lines) {
84
+ if (line.startsWith('event:')) {
85
+ type = line.slice(6).trim() || 'message';
86
+ } else if (line.startsWith('data:')) {
87
+ data += line.slice(5).trim();
88
+ }
89
+ }
90
+ if (!data) {
91
+ return null;
92
+ }
93
+
94
+ try {
95
+ return { type, data: JSON.parse(data) };
96
+ } catch (err) {
97
+ return { type, data };
98
+ }
99
+ }
100
+
101
+ function getSSEErrorMessage(data) {
102
+ if (typeof data === 'string') {
103
+ return data || 'Streaming request failed.';
104
+ }
105
+ if (typeof data?.message === 'string' && data.message.length > 0) {
106
+ return data.message;
107
+ }
108
+ if (typeof data?.error === 'string' && data.error.length > 0) {
109
+ return data.error;
110
+ }
111
+ return 'Streaming request failed.';
112
+ }
113
+
31
114
  if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
32
115
  exports.getCapabilities = function getCapabilities() {
33
116
  return client.post('', { action: 'getCapabilities' }).then(res => res.data);
@@ -103,6 +186,7 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
103
186
  exportQueryResults(params) {
104
187
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
105
188
 
189
+ const fetch = exports.fetch;
106
190
  return fetch(window.MONGOOSE_STUDIO_CONFIG.baseURL + '?' + new URLSearchParams({ ...params, action: 'Model.exportQueryResults' }).toString(), {
107
191
  method: 'GET',
108
192
  headers: {
@@ -166,6 +250,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
166
250
  dropIndex: function dropIndex(params) {
167
251
  return client.post('', { action: 'Model.dropIndex', ...params }).then(res => res.data);
168
252
  },
253
+ dropCollection: function dropCollection(params) {
254
+ return client.post('', { action: 'Model.dropCollection', ...params }).then(res => res.data);
255
+ },
169
256
  listModels: function listModels() {
170
257
  return client.post('', { action: 'Model.listModels' }).then(res => res.data);
171
258
  },
@@ -254,50 +341,11 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
254
341
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
255
342
  const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/ChatThread/streamChatMessage?' + new URLSearchParams(params).toString();
256
343
 
257
- const response = await fetch(url, {
258
- method: 'GET',
344
+ yield* streamSSE(url, {
259
345
  headers: {
260
- Authorization: `${accessToken}`,
261
- Accept: 'text/event-stream'
346
+ Authorization: `${accessToken}`
262
347
  }
263
348
  });
264
-
265
- if (!response.ok) {
266
- throw new Error(`HTTP error! Status: ${response.status}`);
267
- }
268
-
269
- const reader = response.body.getReader();
270
- const decoder = new TextDecoder('utf-8');
271
- let buffer = '';
272
-
273
- while (true) {
274
- const { done, value } = await reader.read();
275
- if (done) break;
276
- buffer += decoder.decode(value, { stream: true });
277
-
278
- let eventEnd;
279
- while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
280
- const eventStr = buffer.slice(0, eventEnd);
281
- buffer = buffer.slice(eventEnd + 2);
282
-
283
- // Parse SSE event
284
- const lines = eventStr.split('\n');
285
- let data = '';
286
- for (const line of lines) {
287
- if (line.startsWith('data:')) {
288
- data += line.slice(5).trim();
289
- }
290
- }
291
- if (data) {
292
- try {
293
- const res = JSON.parse(data);
294
- yield res;
295
- } catch (err) {
296
- yield data;
297
- }
298
- }
299
- }
300
- }
301
349
  }
302
350
  };
303
351
  exports.ChatMessage = {
@@ -330,6 +378,7 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
330
378
  exportQueryResults(params) {
331
379
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
332
380
 
381
+ const fetch = exports.fetch;
333
382
  return fetch(window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/exportQueryResults?' + new URLSearchParams(params).toString(), {
334
383
  method: 'GET',
335
384
  headers: {
@@ -367,50 +416,11 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
367
416
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
368
417
  const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/getDocumentsStream?' + new URLSearchParams(params).toString();
369
418
 
370
- const response = await fetch(url, {
371
- method: 'GET',
419
+ yield* streamSSE(url, {
372
420
  headers: {
373
- Authorization: `${accessToken}`,
374
- Accept: 'text/event-stream'
421
+ Authorization: `${accessToken}`
375
422
  }
376
423
  });
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
424
  },
415
425
  getEstimatedDocumentCounts: function getEstimatedDocumentCounts() {
416
426
  return client.post('/Model/getEstimatedDocumentCounts', {}).then(res => res.data);
@@ -419,51 +429,12 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
419
429
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
420
430
  const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/streamDocumentChanges?' + new URLSearchParams(params).toString();
421
431
 
422
- const response = await fetch(url, {
423
- method: 'GET',
432
+ yield* streamSSE(url, {
424
433
  headers: {
425
- Authorization: `${accessToken}`,
426
- Accept: 'text/event-stream'
434
+ Authorization: `${accessToken}`
427
435
  },
428
436
  signal: options.signal
429
437
  });
430
-
431
- if (!response.ok) {
432
- throw new Error(`HTTP error! Status: ${response.status}`);
433
- }
434
-
435
- const reader = response.body.getReader();
436
- const decoder = new TextDecoder('utf-8');
437
- let buffer = '';
438
-
439
- while (true) {
440
- const { done, value } = await reader.read();
441
- if (done) break;
442
- buffer += decoder.decode(value, { stream: true });
443
-
444
- let eventEnd;
445
- while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
446
- const eventStr = buffer.slice(0, eventEnd);
447
- buffer = buffer.slice(eventEnd + 2);
448
-
449
- // Parse SSE event
450
- const lines = eventStr.split('\n');
451
- let data = '';
452
- for (const line of lines) {
453
- if (line.startsWith('data:')) {
454
- data += line.slice(5).trim();
455
- }
456
- }
457
- if (data) {
458
- try {
459
- yield JSON.parse(data);
460
- } catch (err) {
461
- // If not JSON, yield as string
462
- yield data;
463
- }
464
- }
465
- }
466
- }
467
438
  },
468
439
  getCollectionInfo: function getCollectionInfo(params) {
469
440
  return client.post('/Model/getCollectionInfo', params).then(res => res.data);
@@ -484,50 +455,11 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
484
455
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
485
456
  const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/streamChatMessage?' + new URLSearchParams(params).toString();
486
457
 
487
- const response = await fetch(url, {
488
- method: 'GET',
458
+ yield* streamSSE(url, {
489
459
  headers: {
490
- Authorization: `${accessToken}`,
491
- Accept: 'text/event-stream'
460
+ Authorization: `${accessToken}`
492
461
  }
493
462
  });
494
-
495
- if (!response.ok) {
496
- throw new Error(`HTTP error! Status: ${response.status}`);
497
- }
498
-
499
- const reader = response.body.getReader();
500
- const decoder = new TextDecoder('utf-8');
501
- let buffer = '';
502
-
503
- while (true) {
504
- const { done, value } = await reader.read();
505
- if (done) break;
506
- buffer += decoder.decode(value, { stream: true });
507
-
508
- let eventEnd;
509
- while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
510
- const eventStr = buffer.slice(0, eventEnd);
511
- buffer = buffer.slice(eventEnd + 2);
512
-
513
- // Parse SSE event
514
- const lines = eventStr.split('\n');
515
- let data = '';
516
- for (const line of lines) {
517
- if (line.startsWith('data:')) {
518
- data += line.slice(5).trim();
519
- }
520
- }
521
- if (data) {
522
- try {
523
- yield JSON.parse(data);
524
- } catch (err) {
525
- // If not JSON, yield as string
526
- yield data;
527
- }
528
- }
529
- }
530
- }
531
463
  },
532
464
  updateDocuments: function updateDocument(params) {
533
465
  return client.post('/Model/updateDocuments', params).then(res => res.data);
@@ -12,7 +12,7 @@
12
12
  </button>
13
13
  <!-- Sidebar: Chat Threads -->
14
14
  <aside
15
- class="bg-page border-r overflow-hidden h-full transition-all duration-300 ease-in-out z-20 w-64 fixed lg:relative shrink-0 flex flex-col"
15
+ class="bg-page border-r overflow-hidden transition-all duration-300 ease-in-out z-20 w-64 fixed top-[55px] bottom-0 lg:top-auto lg:bottom-auto lg:relative lg:h-full shrink-0 flex flex-col"
16
16
  :class="hideSidebar === false ? 'translate-x-0' : hideSidebar === true ? '-translate-x-full lg:translate-x-0 lg:!w-0 lg:!border-r-0' : '-translate-x-full lg:translate-x-0'"
17
17
  style="z-index: 1000">
18
18
  <!-- Search + New Thread -->
@@ -26,9 +26,29 @@ module.exports = app => app.component('confirm-changes', {
26
26
  },
27
27
  startSave() {
28
28
  this.$emit('save');
29
+ },
30
+ onEnter(event) {
31
+ if (event.key !== 'Enter') {
32
+ return;
33
+ }
34
+
35
+ const modalMasks = Array.from(document.querySelectorAll('.modal-mask'));
36
+ const currentMask = this.$el?.closest('.modal-mask');
37
+ const isTopMostModal = modalMasks.length > 0 && modalMasks[modalMasks.length - 1] === currentMask;
38
+
39
+ if (!isTopMostModal) {
40
+ return;
41
+ }
42
+
43
+ event.preventDefault();
44
+ this.startSave();
29
45
  }
30
46
  },
31
47
  mounted() {
48
+ window.addEventListener('keydown', this.onEnter);
32
49
  Prism.highlightElement(this.$refs.code);
50
+ },
51
+ beforeUnmount() {
52
+ window.removeEventListener('keydown', this.onEnter);
33
53
  }
34
54
  });
@@ -221,6 +221,11 @@ router.beforeEach((to, from, next) => {
221
221
  });
222
222
 
223
223
  router.beforeEach((to, from, next) => {
224
+ if (['chat index', 'chat'].includes(to.name) && !window.MONGOOSE_STUDIO_CONFIG.hasLLMAPIKey) {
225
+ next({ name: 'root' });
226
+ return;
227
+ }
228
+
224
229
  if (to.name === 'root' && window.state.roles && window.state.roles[0] === 'dashboards') {
225
230
  return next({ name: 'dashboards' });
226
231
  } else {
@@ -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 group"
28
+ class="group inline-flex min-w-0 max-w-full items-baseline flex-wrap gap-x-1.5"
33
29
  >
34
- <span
35
- :class="[...valueClasses, 'underline', 'decoration-dotted', 'underline-offset-2']"
36
- :style="typeof value === 'string'
37
- ? {
38
- display: 'inline-block',
39
- maxWidth: '100%',
40
- overflow: 'hidden',
41
- textOverflow: 'ellipsis',
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 transition-opacity"
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
- :class="valueClasses"
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
- {{ formattedValue }}{{ comma }}
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() {