@mongoosejs/studio 0.2.13 → 0.3.1
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/ChatMessage/executeScript.js +5 -1
- package/backend/actions/ChatThread/createChatMessage.js +4 -1
- package/backend/actions/ChatThread/streamChatMessage.js +4 -2
- package/backend/actions/Dashboard/updateDashboard.js +2 -2
- package/backend/actions/Task/getTaskOverview.js +102 -0
- package/backend/actions/Task/getTasks.js +85 -45
- package/backend/actions/Task/index.js +1 -0
- package/eslint.config.js +4 -1
- package/frontend/public/app.js +25025 -762
- package/frontend/public/dark-theme.css +365 -0
- package/frontend/public/images/mongoose-studio.svg +4 -0
- package/frontend/public/index.html +21 -1
- package/frontend/public/style.css +5 -7
- package/frontend/public/theme-variables.css +294 -0
- package/frontend/public/tw.css +348 -213
- package/frontend/src/_util/dateRange.js +82 -0
- package/frontend/src/ace-editor/ace-editor.html +4 -0
- package/frontend/src/ace-editor/ace-editor.js +95 -0
- package/frontend/src/aceEditor.js +69 -0
- package/frontend/src/api.js +6 -0
- package/frontend/src/chat/chat-message/chat-message.html +1 -1
- package/frontend/src/chat/chat-message/chat-message.js +1 -1
- package/frontend/src/chat/chat-message-script/chat-message-script.html +54 -42
- package/frontend/src/chat/chat-message-script/chat-message-script.js +6 -55
- package/frontend/src/chat/chat.html +68 -39
- package/frontend/src/chat/chat.js +26 -2
- package/frontend/src/clone-document/clone-document.html +7 -2
- package/frontend/src/clone-document/clone-document.js +1 -8
- package/frontend/src/create-dashboard/create-dashboard.html +11 -6
- package/frontend/src/create-dashboard/create-dashboard.js +0 -7
- package/frontend/src/create-document/create-document.html +15 -9
- package/frontend/src/create-document/create-document.js +5 -12
- package/frontend/src/dashboard/dashboard.html +14 -12
- package/frontend/src/dashboard/dashboard.js +21 -4
- package/frontend/src/dashboard/edit-dashboard/edit-dashboard.html +13 -7
- package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +16 -23
- package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html +19 -17
- package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js +97 -2
- package/frontend/src/dashboard-result/dashboard-map/dashboard-map.js +27 -3
- package/frontend/src/dashboard-result/dashboard-result.html +3 -3
- package/frontend/src/dashboard-result/dashboard-result.js +3 -0
- package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +34 -0
- package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +37 -0
- package/frontend/src/dashboards/dashboards.html +101 -109
- package/frontend/src/dashboards/dashboards.js +25 -1
- package/frontend/src/detail-default/detail-default.html +2 -2
- package/frontend/src/detail-default/detail-default.js +24 -3
- package/frontend/src/document/confirm-changes/confirm-changes.html +1 -1
- package/frontend/src/document/confirm-delete/confirm-delete.html +1 -1
- package/frontend/src/document/document.css +1 -1
- package/frontend/src/document/document.html +28 -28
- package/frontend/src/document/execute-script/execute-script.html +20 -21
- package/frontend/src/document/execute-script/execute-script.js +1 -43
- package/frontend/src/document-details/document-details.css +4 -9
- package/frontend/src/document-details/document-details.html +34 -33
- package/frontend/src/document-details/document-details.js +2 -53
- package/frontend/src/document-details/document-property/document-property.html +12 -12
- package/frontend/src/edit-array/edit-array.html +7 -6
- package/frontend/src/edit-array/edit-array.js +10 -50
- package/frontend/src/edit-boolean/edit-boolean.html +12 -12
- package/frontend/src/edit-date/edit-date.html +2 -2
- package/frontend/src/edit-default/edit-default.html +1 -1
- package/frontend/src/edit-string/edit-string.html +3 -3
- package/frontend/src/edit-subdocument/edit-subdocument.html +5 -3
- package/frontend/src/edit-subdocument/edit-subdocument.js +1 -15
- package/frontend/src/export-query-results/export-query-results.html +3 -3
- package/frontend/src/json-node/json-node.html +3 -3
- package/frontend/src/list-json/json-node.html +1 -1
- package/frontend/src/models/document-search/document-search.html +3 -3
- package/frontend/src/models/model-switcher/model-switcher.html +53 -0
- package/frontend/src/models/model-switcher/model-switcher.js +123 -0
- package/frontend/src/models/models.css +3 -10
- package/frontend/src/models/models.html +146 -80
- package/frontend/src/models/models.js +116 -7
- package/frontend/src/navbar/navbar.html +157 -97
- package/frontend/src/navbar/navbar.js +31 -12
- package/frontend/src/routes.js +1 -1
- package/frontend/src/splash/splash.html +5 -5
- package/frontend/src/task-by-name/task-by-name.html +77 -7
- package/frontend/src/task-by-name/task-by-name.js +84 -9
- package/frontend/src/task-single/task-single.html +29 -29
- package/frontend/src/task-single/task-single.js +10 -10
- package/frontend/src/tasks/task-details/task-details.html +43 -43
- package/frontend/src/tasks/task-details/task-details.js +9 -3
- package/frontend/src/tasks/tasks.html +36 -35
- package/frontend/src/tasks/tasks.js +27 -143
- package/frontend/src/team/new-invitation/new-invitation.html +8 -8
- package/frontend/src/team/team.html +27 -27
- package/frontend/src/update-document/update-document.html +7 -2
- package/frontend/src/update-document/update-document.js +2 -11
- package/package.json +3 -1
- package/tailwind.config.js +75 -11
|
@@ -1,48 +1,107 @@
|
|
|
1
1
|
<div class="models flex" style="height: calc(100vh - 55px); height: calc(100dvh - 55px)">
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
<aside class="bg-page border-r overflow-hidden transition-all duration-300 ease-in-out z-20 w-0 lg:w-64 fixed lg:relative shrink-0 flex flex-col top-[55px] bottom-0 lg:top-auto lg:bottom-auto lg:h-full" :class="hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-64' : ''">
|
|
3
|
+
<!-- Search -->
|
|
4
|
+
<div class="p-3 shrink-0">
|
|
5
|
+
<div class="relative">
|
|
6
|
+
<svg class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
7
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
|
8
|
+
</svg>
|
|
9
|
+
<input
|
|
10
|
+
v-model="modelSearch"
|
|
11
|
+
type="text"
|
|
12
|
+
placeholder="Find model..."
|
|
13
|
+
@keydown.esc="modelSearch = ''"
|
|
14
|
+
class="w-full rounded-md border border-edge bg-surface py-1.5 pl-8 pr-3 text-sm text-content placeholder:text-gray-400 focus:border-edge-strong focus:outline-none focus:ring-1 focus:ring-gray-300"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
15
17
|
</div>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
<!-- Model list (scrollable) -->
|
|
19
|
+
<nav class="flex-1 overflow-y-auto px-2 pb-2">
|
|
20
|
+
<!-- Recently Viewed -->
|
|
21
|
+
<div v-if="filteredRecentModels.length > 0 && !modelSearch.trim()">
|
|
22
|
+
<div class="px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider">Recently Viewed</div>
|
|
23
|
+
<ul role="list">
|
|
24
|
+
<li v-for="model in filteredRecentModels" :key="'recent-' + model">
|
|
25
|
+
<router-link
|
|
26
|
+
:to="'/model/' + model"
|
|
27
|
+
class="flex items-center rounded-md py-1.5 px-2 text-sm text-content-secondary"
|
|
28
|
+
:class="model === currentModel ? 'bg-gray-200 font-semibold text-content' : 'hover:bg-muted'">
|
|
29
|
+
<span class="truncate" v-html="highlightMatch(model)"></span>
|
|
30
|
+
<span
|
|
31
|
+
v-if="modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel"
|
|
32
|
+
class="ml-auto text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]"
|
|
33
|
+
>
|
|
34
|
+
{{formatCompactCount(modelDocumentCounts[model])}}
|
|
35
|
+
</span>
|
|
36
|
+
</router-link>
|
|
37
|
+
</li>
|
|
38
|
+
</ul>
|
|
39
|
+
<div class="border-b border-edge my-2"></div>
|
|
40
|
+
</div>
|
|
41
|
+
<!-- All Models / Search Results -->
|
|
42
|
+
<div class="px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ modelSearch.trim() ? 'Search Results' : 'All Models' }}</div>
|
|
43
|
+
<ul role="list">
|
|
44
|
+
<li v-for="model in filteredModels" :key="'all-' + model">
|
|
45
|
+
<router-link
|
|
46
|
+
:to="'/model/' + model"
|
|
47
|
+
class="flex items-center rounded-md py-1.5 px-2 text-sm text-content-secondary"
|
|
48
|
+
:class="model === currentModel ? 'bg-gray-200 font-semibold text-content' : 'hover:bg-muted'">
|
|
49
|
+
<span class="truncate" v-html="highlightMatch(model)"></span>
|
|
50
|
+
<span
|
|
51
|
+
v-if="modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel"
|
|
52
|
+
class="ml-auto text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]"
|
|
53
|
+
>
|
|
54
|
+
{{formatCompactCount(modelDocumentCounts[model])}}
|
|
55
|
+
</span>
|
|
56
|
+
</router-link>
|
|
35
57
|
</li>
|
|
36
58
|
</ul>
|
|
37
|
-
<div v-if="
|
|
59
|
+
<div v-if="filteredModels.length === 0 && modelSearch.trim()" class="px-2 py-2 text-sm text-content-tertiary">
|
|
60
|
+
No models match "{{modelSearch}}"
|
|
61
|
+
</div>
|
|
62
|
+
<div v-if="models.length === 0 && status === 'loaded'" class="p-2 bg-red-100 rounded-md">
|
|
38
63
|
No models found
|
|
39
64
|
</div>
|
|
40
65
|
</nav>
|
|
66
|
+
<!-- Bottom toolbar -->
|
|
67
|
+
<div class="shrink-0 border-t border-edge bg-page px-2 py-1.5 flex items-center gap-1">
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
@click="hideSidebar = true"
|
|
71
|
+
class="rounded p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted"
|
|
72
|
+
title="Hide sidebar"
|
|
73
|
+
>
|
|
74
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
75
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5" />
|
|
76
|
+
</svg>
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
@click="openModelSwitcher"
|
|
81
|
+
class="rounded p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted"
|
|
82
|
+
title="Quick switch (Ctrl+P)"
|
|
83
|
+
>
|
|
84
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
85
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
41
89
|
</aside>
|
|
42
|
-
<div class="documents bg-slate-50" ref="documentsList">
|
|
43
|
-
<div class="relative h-[42px]" style="z-index: 1000">
|
|
90
|
+
<div class="documents bg-slate-50 min-w-0" ref="documentsList">
|
|
44
91
|
<div class="documents-menu bg-slate-50">
|
|
45
92
|
<div class="flex flex-row items-center w-full gap-2">
|
|
93
|
+
<button
|
|
94
|
+
v-if="hideSidebar === true || hideSidebar === null"
|
|
95
|
+
type="button"
|
|
96
|
+
@click="hideSidebar = false"
|
|
97
|
+
class="shrink-0 rounded-md p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted"
|
|
98
|
+
:class="hideSidebar === null ? 'lg:hidden' : ''"
|
|
99
|
+
title="Show sidebar"
|
|
100
|
+
>
|
|
101
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
|
102
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5" />
|
|
103
|
+
</svg>
|
|
104
|
+
</button>
|
|
46
105
|
<document-search
|
|
47
106
|
ref="documentSearch"
|
|
48
107
|
:value="searchText"
|
|
@@ -57,8 +116,8 @@
|
|
|
57
116
|
<button
|
|
58
117
|
@click="stagingSelect"
|
|
59
118
|
type="button"
|
|
60
|
-
:class="{ 'bg-
|
|
61
|
-
class="rounded px-2 py-2 text-sm font-semibold text-
|
|
119
|
+
:class="{ 'bg-page0 ring-inset ring-2 ring-gray-300 hover:bg-gray-600': selectMultiple, 'bg-primary hover:bg-primary-hover' : !selectMultiple }"
|
|
120
|
+
class="rounded px-2 py-2 text-sm font-semibold text-primary-text shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
62
121
|
>
|
|
63
122
|
{{ selectMultiple ? 'Cancel' : 'Select' }}
|
|
64
123
|
</button>
|
|
@@ -83,55 +142,55 @@
|
|
|
83
142
|
@click="toggleActionsMenu"
|
|
84
143
|
type="button"
|
|
85
144
|
aria-label="More actions"
|
|
86
|
-
class="rounded bg-
|
|
145
|
+
class="rounded bg-surface px-2 py-2 text-sm font-semibold text-content-secondary shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-page focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
|
|
87
146
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
|
88
147
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Zm0 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Zm0 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
|
|
89
148
|
</svg>
|
|
90
149
|
</button>
|
|
91
150
|
<div
|
|
92
151
|
v-if="showActionsMenu"
|
|
93
|
-
class="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-
|
|
152
|
+
class="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-20"
|
|
94
153
|
>
|
|
95
154
|
<div class="py-1">
|
|
96
155
|
<button
|
|
97
156
|
@click="shouldShowExportModal = true; showActionsMenu = false"
|
|
98
157
|
type="button"
|
|
99
|
-
class="block w-full px-4 py-2 text-left text-sm text-
|
|
158
|
+
class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
|
|
100
159
|
>
|
|
101
160
|
Export
|
|
102
161
|
</button>
|
|
103
162
|
<button
|
|
104
163
|
@click="shouldShowCreateModal = true; showActionsMenu = false"
|
|
105
164
|
type="button"
|
|
106
|
-
class="block w-full px-4 py-2 text-left text-sm text-
|
|
165
|
+
class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
|
|
107
166
|
>
|
|
108
167
|
Create
|
|
109
168
|
</button>
|
|
110
169
|
<button
|
|
111
170
|
@click="openFieldSelection(); showActionsMenu = false"
|
|
112
171
|
type="button"
|
|
113
|
-
class="block w-full px-4 py-2 text-left text-sm text-
|
|
172
|
+
class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
|
|
114
173
|
>
|
|
115
174
|
Projection
|
|
116
175
|
</button>
|
|
117
176
|
<button
|
|
118
177
|
@click="openIndexModal"
|
|
119
178
|
type="button"
|
|
120
|
-
class="block w-full px-4 py-2 text-left text-sm text-
|
|
179
|
+
class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
|
|
121
180
|
>
|
|
122
181
|
Indexes
|
|
123
182
|
</button>
|
|
124
183
|
<button
|
|
125
184
|
@click="openCollectionInfo"
|
|
126
185
|
type="button"
|
|
127
|
-
class="block w-full px-4 py-2 text-left text-sm text-
|
|
186
|
+
class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
|
|
128
187
|
>
|
|
129
188
|
Collection Info
|
|
130
189
|
</button>
|
|
131
190
|
<button
|
|
132
191
|
@click="findOldestDocument"
|
|
133
192
|
type="button"
|
|
134
|
-
class="block w-full px-4 py-2 text-left text-sm text-
|
|
193
|
+
class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
|
|
135
194
|
>
|
|
136
195
|
Find oldest document
|
|
137
196
|
</button>
|
|
@@ -142,15 +201,15 @@
|
|
|
142
201
|
<button
|
|
143
202
|
@click="setOutputType('table')"
|
|
144
203
|
type="button"
|
|
145
|
-
class="relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-
|
|
146
|
-
:class="outputType === 'table' ? 'bg-gray-200' : 'bg-
|
|
204
|
+
class="relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-page focus:z-10"
|
|
205
|
+
:class="outputType === 'table' ? 'bg-gray-200' : 'bg-surface'">
|
|
147
206
|
<img class="h-5 w-5" src="images/table.svg">
|
|
148
207
|
</button>
|
|
149
208
|
<button
|
|
150
209
|
@click="setOutputType('json')"
|
|
151
210
|
type="button"
|
|
152
|
-
class="relative -ml-px inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-
|
|
153
|
-
:class="outputType === 'json' ? 'bg-gray-200' : 'bg-
|
|
211
|
+
class="relative -ml-px inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-page focus:z-10"
|
|
212
|
+
:class="outputType === 'json' ? 'bg-gray-200' : 'bg-surface'">
|
|
154
213
|
<img class="h-5 w-5" src="images/json.svg">
|
|
155
214
|
</button>
|
|
156
215
|
<button
|
|
@@ -160,8 +219,8 @@
|
|
|
160
219
|
:title="geoJsonFields.length > 0 ? 'Map view' : 'No GeoJSON fields detected'"
|
|
161
220
|
class="relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 ring-1 ring-inset ring-gray-300 focus:z-10"
|
|
162
221
|
:class="[
|
|
163
|
-
geoJsonFields.length === 0 ? 'text-gray-300 cursor-not-allowed bg-
|
|
164
|
-
outputType === 'map' ? 'bg-gray-200' : (geoJsonFields.length > 0 ? 'bg-
|
|
222
|
+
geoJsonFields.length === 0 ? 'text-gray-300 cursor-not-allowed bg-muted' : 'text-gray-400 hover:bg-page',
|
|
223
|
+
outputType === 'map' ? 'bg-gray-200' : (geoJsonFields.length > 0 ? 'bg-surface' : '')
|
|
165
224
|
]">
|
|
166
225
|
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
167
226
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z" />
|
|
@@ -170,7 +229,6 @@
|
|
|
170
229
|
</span>
|
|
171
230
|
</div>
|
|
172
231
|
</div>
|
|
173
|
-
</div>
|
|
174
232
|
<div class="documents-container relative">
|
|
175
233
|
<div v-if="error">
|
|
176
234
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 relative m-4 rounded-md" role="alert">
|
|
@@ -180,7 +238,7 @@
|
|
|
180
238
|
</div>
|
|
181
239
|
<table v-else-if="outputType === 'table'">
|
|
182
240
|
<thead class="bg-slate-50">
|
|
183
|
-
<th v-for="path in filteredPaths" @click="addPathFilter(path.path)" class="cursor-pointer p-3">
|
|
241
|
+
<th v-for="path in filteredPaths" @click="addPathFilter(path.path)" class="bg-slate-50 cursor-pointer p-3">
|
|
184
242
|
{{path.path}}
|
|
185
243
|
<span class="path-type">
|
|
186
244
|
({{(path.instance || 'unknown')}})
|
|
@@ -190,7 +248,7 @@
|
|
|
190
248
|
</th>
|
|
191
249
|
</thead>
|
|
192
250
|
<tbody>
|
|
193
|
-
<tr v-for="document in documents" @click="handleDocumentClick(document, $event)" :key="document._id" class="bg-
|
|
251
|
+
<tr v-for="document in documents" @click="handleDocumentClick(document, $event)" :key="document._id" class="bg-surface hover:bg-slate-50">
|
|
194
252
|
<td v-for="schemaPath in filteredPaths" class="p-3 cursor-pointer" :class="{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }">
|
|
195
253
|
<component
|
|
196
254
|
:is="getComponentForPath(schemaPath)"
|
|
@@ -208,12 +266,12 @@
|
|
|
208
266
|
@click="handleDocumentContainerClick(document, $event)"
|
|
209
267
|
:class="[
|
|
210
268
|
'group relative transition-colors rounded-md border border-slate-100',
|
|
211
|
-
selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:shadow-sm hover:border-slate-300 bg-
|
|
269
|
+
selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:shadow-sm hover:border-slate-300 bg-surface'
|
|
212
270
|
]"
|
|
213
271
|
>
|
|
214
272
|
<button
|
|
215
273
|
type="button"
|
|
216
|
-
class="absolute top-2 right-2 z-10 inline-flex items-center rounded bg-
|
|
274
|
+
class="absolute top-2 right-2 z-10 inline-flex items-center rounded bg-primary px-2 py-1 text-xs font-semibold text-primary-text shadow-sm transition-opacity duration-150 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
217
275
|
@click.stop="openDocument(document)"
|
|
218
276
|
>
|
|
219
277
|
Open this Document
|
|
@@ -223,12 +281,12 @@
|
|
|
223
281
|
</div>
|
|
224
282
|
</div>
|
|
225
283
|
<div v-else-if="outputType === 'map'" class="flex flex-col h-full">
|
|
226
|
-
<div class="p-2 bg-
|
|
227
|
-
<label class="text-sm font-medium text-
|
|
284
|
+
<div class="p-2 bg-surface border-b flex items-center gap-2">
|
|
285
|
+
<label class="text-sm font-medium text-content-secondary">GeoJSON Field:</label>
|
|
228
286
|
<select
|
|
229
287
|
:value="selectedGeoField"
|
|
230
288
|
@change="setSelectedGeoField($event.target.value)"
|
|
231
|
-
class="rounded-md border border-
|
|
289
|
+
class="rounded-md border border-edge-strong py-1 px-2 text-sm focus:border-primary focus:ring-primary"
|
|
232
290
|
>
|
|
233
291
|
<option v-for="field in geoJsonFields" :key="field.path" :value="field.path">
|
|
234
292
|
{{ field.label }}
|
|
@@ -238,8 +296,8 @@
|
|
|
238
296
|
@click="loadMoreDocuments"
|
|
239
297
|
:disabled="loadedAllDocs"
|
|
240
298
|
type="button"
|
|
241
|
-
class="rounded px-2 py-1 text-xs font-semibold text-
|
|
242
|
-
:class="loadedAllDocs ? 'bg-gray-400 cursor-not-allowed' : 'bg-
|
|
299
|
+
class="rounded px-2 py-1 text-xs font-semibold text-primary-text shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
300
|
+
:class="loadedAllDocs ? 'bg-gray-400 cursor-not-allowed' : 'bg-primary hover:bg-primary-hover'"
|
|
243
301
|
>
|
|
244
302
|
Load more
|
|
245
303
|
</async-button>
|
|
@@ -271,7 +329,7 @@
|
|
|
271
329
|
<div>
|
|
272
330
|
<div class="font-bold flex items-center gap-2">
|
|
273
331
|
<div>{{ index.name }}</div>
|
|
274
|
-
<div v-if="isTTLIndex(index)" class="rounded-full bg-
|
|
332
|
+
<div v-if="isTTLIndex(index)" class="rounded-full bg-primary-subtle px-2 py-0.5 text-xs font-semibold text-primary">
|
|
275
333
|
TTL: {{ formatTTL(index.expireAfterSeconds) }}
|
|
276
334
|
</div>
|
|
277
335
|
</div>
|
|
@@ -296,33 +354,33 @@
|
|
|
296
354
|
<div v-if="!collectionInfo" class="text-gray-600">Loading collection details...</div>
|
|
297
355
|
<div v-else class="space-y-3">
|
|
298
356
|
<div class="flex justify-between gap-4">
|
|
299
|
-
<div class="font-semibold text-
|
|
300
|
-
<div class="text-
|
|
357
|
+
<div class="font-semibold text-content-secondary">Documents</div>
|
|
358
|
+
<div class="text-content">{{ formatNumber(collectionInfo.documentCount) }}</div>
|
|
301
359
|
</div>
|
|
302
360
|
<div class="flex justify-between gap-4">
|
|
303
|
-
<div class="font-semibold text-
|
|
304
|
-
<div class="text-
|
|
361
|
+
<div class="font-semibold text-content-secondary">Indexes</div>
|
|
362
|
+
<div class="text-content">{{ formatNumber(collectionInfo.indexCount) }}</div>
|
|
305
363
|
</div>
|
|
306
364
|
<div class="flex justify-between gap-4">
|
|
307
|
-
<div class="font-semibold text-
|
|
308
|
-
<div class="text-
|
|
365
|
+
<div class="font-semibold text-content-secondary">Total Index Size</div>
|
|
366
|
+
<div class="text-content">{{ formatCollectionSize(collectionInfo.totalIndexSize) }}</div>
|
|
309
367
|
</div>
|
|
310
368
|
<div class="flex justify-between gap-4">
|
|
311
|
-
<div class="font-semibold text-
|
|
312
|
-
<div class="text-
|
|
369
|
+
<div class="font-semibold text-content-secondary">Total Storage Size</div>
|
|
370
|
+
<div class="text-content">{{ formatCollectionSize(collectionInfo.size) }}</div>
|
|
313
371
|
</div>
|
|
314
372
|
<div class="flex flex-col gap-1">
|
|
315
373
|
<div class="flex justify-between gap-4">
|
|
316
|
-
<div class="font-semibold text-
|
|
317
|
-
<div class="text-
|
|
374
|
+
<div class="font-semibold text-content-secondary">Collation</div>
|
|
375
|
+
<div class="text-content">{{ collectionInfo.hasCollation ? 'Yes' : 'No' }}</div>
|
|
318
376
|
</div>
|
|
319
|
-
<div v-if="collectionInfo.hasCollation" class="rounded bg-
|
|
377
|
+
<div v-if="collectionInfo.hasCollation" class="rounded bg-muted p-3 text-sm text-gray-800 overflow-x-auto">
|
|
320
378
|
<pre class="whitespace-pre-wrap">{{ JSON.stringify(collectionInfo.collation, null, 2) }}</pre>
|
|
321
379
|
</div>
|
|
322
380
|
</div>
|
|
323
381
|
<div class="flex justify-between gap-4">
|
|
324
|
-
<div class="font-semibold text-
|
|
325
|
-
<div class="text-
|
|
382
|
+
<div class="font-semibold text-content-secondary">Capped</div>
|
|
383
|
+
<div class="text-content">{{ collectionInfo.capped ? 'Yes' : 'No' }}</div>
|
|
326
384
|
</div>
|
|
327
385
|
</div>
|
|
328
386
|
</template>
|
|
@@ -331,16 +389,16 @@
|
|
|
331
389
|
<template v-slot:body>
|
|
332
390
|
<div class="modal-exit" @click="shouldShowFieldModal = false; selectedPaths = [...filteredPaths];">×</div>
|
|
333
391
|
<div v-for="(path, index) in schemaPaths" :key="index" class="w-5 flex items-center">
|
|
334
|
-
<input class="mt-0 h-4 w-4 rounded border-
|
|
335
|
-
<div class="ml-2 text-
|
|
392
|
+
<input class="mt-0 h-4 w-4 rounded border-edge-strong text-sky-600 focus:ring-sky-600 accent-sky-600" type="checkbox" :id="'path.path'+index" @change="addOrRemove(path)" :value="path.path" :checked="isSelected(path.path)" />
|
|
393
|
+
<div class="ml-2 text-content-secondary grow shrink text-left">
|
|
336
394
|
<label :for="'path.path' + index">{{path.path}}</label>
|
|
337
395
|
</div>
|
|
338
396
|
</div>
|
|
339
397
|
<div class="mt-4 flex gap-2">
|
|
340
|
-
<button type="button" @click="filterDocuments()" class="rounded-md bg-
|
|
398
|
+
<button type="button" @click="filterDocuments()" class="rounded-md bg-primary px-2.5 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600">Filter Selection</button>
|
|
341
399
|
<button type="button" @click="selectAll()" class="rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600">Select All</button>
|
|
342
400
|
<button type="button" @click="deselectAll()" class="rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">Deselect All</button>
|
|
343
|
-
<button type="button" @click="resetDocuments()" class="rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-
|
|
401
|
+
<button type="button" @click="resetDocuments()" class="rounded-md bg-gray-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-page0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600" >Cancel</button>
|
|
344
402
|
</div>
|
|
345
403
|
</template>
|
|
346
404
|
</modal>
|
|
@@ -367,10 +425,18 @@
|
|
|
367
425
|
<async-button @click="deleteDocuments" class="rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600">
|
|
368
426
|
Confirm
|
|
369
427
|
</async-button>
|
|
370
|
-
<button @click="shouldShowDeleteMultipleModal = false;" class="rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-
|
|
428
|
+
<button @click="shouldShowDeleteMultipleModal = false;" class="rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-page0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
|
371
429
|
Cancel
|
|
372
430
|
</button>
|
|
373
431
|
</div>
|
|
374
432
|
</template>
|
|
375
433
|
</modal>
|
|
434
|
+
<model-switcher
|
|
435
|
+
:show="showModelSwitcher"
|
|
436
|
+
:models="models"
|
|
437
|
+
:recently-viewed-models="recentlyViewedModels"
|
|
438
|
+
:model-document-counts="modelDocumentCounts"
|
|
439
|
+
@close="showModelSwitcher = false"
|
|
440
|
+
@select="selectSwitcherModel"
|
|
441
|
+
></model-switcher>
|
|
376
442
|
</div>
|
|
@@ -13,6 +13,8 @@ appendCSS(require('./models.css'));
|
|
|
13
13
|
const limit = 20;
|
|
14
14
|
const OUTPUT_TYPE_STORAGE_KEY = 'studio:model-output-type';
|
|
15
15
|
const SELECTED_GEO_FIELD_STORAGE_KEY = 'studio:model-selected-geo-field';
|
|
16
|
+
const RECENTLY_VIEWED_MODELS_KEY = 'studio:recently-viewed-models';
|
|
17
|
+
const MAX_RECENT_MODELS = 4;
|
|
16
18
|
|
|
17
19
|
module.exports = app => app.component('models', {
|
|
18
20
|
template: template,
|
|
@@ -51,21 +53,29 @@ module.exports = app => app.component('models', {
|
|
|
51
53
|
selectedGeoField: null,
|
|
52
54
|
mapInstance: null,
|
|
53
55
|
mapLayer: null,
|
|
56
|
+
mapTileLayer: null,
|
|
54
57
|
hideSidebar: null,
|
|
55
58
|
lastSelectedIndex: null,
|
|
56
59
|
error: null,
|
|
57
60
|
showActionsMenu: false,
|
|
58
|
-
collectionInfo: null
|
|
61
|
+
collectionInfo: null,
|
|
62
|
+
modelSearch: '',
|
|
63
|
+
recentlyViewedModels: [],
|
|
64
|
+
showModelSwitcher: false
|
|
59
65
|
}),
|
|
60
66
|
created() {
|
|
61
67
|
this.currentModel = this.model;
|
|
68
|
+
this.setSearchTextFromRoute();
|
|
62
69
|
this.loadOutputPreference();
|
|
63
70
|
this.loadSelectedGeoField();
|
|
71
|
+
this.loadRecentlyViewedModels();
|
|
64
72
|
},
|
|
65
73
|
beforeDestroy() {
|
|
66
74
|
document.removeEventListener('scroll', this.onScroll, true);
|
|
67
75
|
window.removeEventListener('popstate', this.onPopState, true);
|
|
68
76
|
document.removeEventListener('click', this.onOutsideActionsMenuClick, true);
|
|
77
|
+
document.documentElement.removeEventListener('studio-theme-changed', this.onStudioThemeChanged);
|
|
78
|
+
document.removeEventListener('keydown', this.onCtrlP, true);
|
|
69
79
|
this.destroyMap();
|
|
70
80
|
},
|
|
71
81
|
async mounted() {
|
|
@@ -83,6 +93,16 @@ module.exports = app => app.component('models', {
|
|
|
83
93
|
}
|
|
84
94
|
};
|
|
85
95
|
document.addEventListener('click', this.onOutsideActionsMenuClick, true);
|
|
96
|
+
this.onStudioThemeChanged = () => this.updateMapTileLayer();
|
|
97
|
+
document.documentElement.addEventListener('studio-theme-changed', this.onStudioThemeChanged);
|
|
98
|
+
this.onCtrlP = (event) => {
|
|
99
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'p') {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
this.openModelSwitcher();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
document.addEventListener('keydown', this.onCtrlP, true);
|
|
105
|
+
this.query = Object.assign({}, this.$route.query);
|
|
86
106
|
const { models, readyState } = await api.Model.listModels();
|
|
87
107
|
this.models = models;
|
|
88
108
|
await this.loadModelCounts();
|
|
@@ -140,6 +160,21 @@ module.exports = app => app.component('models', {
|
|
|
140
160
|
}
|
|
141
161
|
return map;
|
|
142
162
|
},
|
|
163
|
+
filteredModels() {
|
|
164
|
+
if (!this.modelSearch.trim()) {
|
|
165
|
+
return this.models;
|
|
166
|
+
}
|
|
167
|
+
const search = this.modelSearch.trim().toLowerCase();
|
|
168
|
+
return this.models.filter(m => m.toLowerCase().includes(search));
|
|
169
|
+
},
|
|
170
|
+
filteredRecentModels() {
|
|
171
|
+
const recent = this.recentlyViewedModels.filter(m => this.models.includes(m));
|
|
172
|
+
if (!this.modelSearch.trim()) {
|
|
173
|
+
return recent;
|
|
174
|
+
}
|
|
175
|
+
const search = this.modelSearch.trim().toLowerCase();
|
|
176
|
+
return recent.filter(m => m.toLowerCase().includes(search));
|
|
177
|
+
},
|
|
143
178
|
geoJsonFields() {
|
|
144
179
|
// Find schema paths that look like GeoJSON fields
|
|
145
180
|
// GeoJSON fields have nested 'type' and 'coordinates' properties
|
|
@@ -192,6 +227,49 @@ module.exports = app => app.component('models', {
|
|
|
192
227
|
}
|
|
193
228
|
},
|
|
194
229
|
methods: {
|
|
230
|
+
highlightMatch(model) {
|
|
231
|
+
const search = this.modelSearch.trim();
|
|
232
|
+
if (!search) {
|
|
233
|
+
return model;
|
|
234
|
+
}
|
|
235
|
+
const idx = model.toLowerCase().indexOf(search.toLowerCase());
|
|
236
|
+
if (idx === -1) {
|
|
237
|
+
return model;
|
|
238
|
+
}
|
|
239
|
+
const before = model.slice(0, idx);
|
|
240
|
+
const match = model.slice(idx, idx + search.length);
|
|
241
|
+
const after = model.slice(idx + search.length);
|
|
242
|
+
return `${xss(before)}<strong>${xss(match)}</strong>${xss(after)}`;
|
|
243
|
+
},
|
|
244
|
+
loadRecentlyViewedModels() {
|
|
245
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const stored = window.localStorage.getItem(RECENTLY_VIEWED_MODELS_KEY);
|
|
250
|
+
if (stored) {
|
|
251
|
+
const parsed = JSON.parse(stored);
|
|
252
|
+
if (Array.isArray(parsed)) {
|
|
253
|
+
this.recentlyViewedModels = parsed.map(item => String(item)).slice(0, MAX_RECENT_MODELS);
|
|
254
|
+
} else {
|
|
255
|
+
this.recentlyViewedModels = [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
this.recentlyViewedModels = [];
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
trackRecentModel(model) {
|
|
263
|
+
if (!model) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const filtered = this.recentlyViewedModels.filter(m => m !== model);
|
|
267
|
+
filtered.unshift(model);
|
|
268
|
+
this.recentlyViewedModels = filtered.slice(0, MAX_RECENT_MODELS);
|
|
269
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
270
|
+
window.localStorage.setItem(RECENTLY_VIEWED_MODELS_KEY, JSON.stringify(this.recentlyViewedModels));
|
|
271
|
+
}
|
|
272
|
+
},
|
|
195
273
|
loadOutputPreference() {
|
|
196
274
|
if (typeof window === 'undefined' || !window.localStorage) {
|
|
197
275
|
return;
|
|
@@ -265,9 +343,7 @@ module.exports = app => app.component('models', {
|
|
|
265
343
|
mapElement.style.setProperty('width', '100%', 'important');
|
|
266
344
|
|
|
267
345
|
this.mapInstance = L.map(this.$refs.modelsMap).setView([0, 0], 2);
|
|
268
|
-
|
|
269
|
-
attribution: '© OpenStreetMap contributors'
|
|
270
|
-
}).addTo(this.mapInstance);
|
|
346
|
+
this.updateMapTileLayer();
|
|
271
347
|
|
|
272
348
|
this.$nextTick(() => {
|
|
273
349
|
if (this.mapInstance) {
|
|
@@ -276,11 +352,30 @@ module.exports = app => app.component('models', {
|
|
|
276
352
|
}
|
|
277
353
|
});
|
|
278
354
|
},
|
|
355
|
+
getMapTileLayerOptions() {
|
|
356
|
+
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
|
|
357
|
+
return isDark
|
|
358
|
+
? { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', subdomains: 'abcd', maxZoom: 20 }
|
|
359
|
+
: { url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap contributors' };
|
|
360
|
+
},
|
|
361
|
+
updateMapTileLayer() {
|
|
362
|
+
if (!this.mapInstance || typeof L === 'undefined') return;
|
|
363
|
+
if (this.mapTileLayer) {
|
|
364
|
+
this.mapTileLayer.remove();
|
|
365
|
+
this.mapTileLayer = null;
|
|
366
|
+
}
|
|
367
|
+
const opts = this.getMapTileLayerOptions();
|
|
368
|
+
this.mapTileLayer = L.tileLayer(opts.url, opts).addTo(this.mapInstance);
|
|
369
|
+
},
|
|
279
370
|
destroyMap() {
|
|
280
371
|
if (this.mapLayer) {
|
|
281
372
|
this.mapLayer.remove();
|
|
282
373
|
this.mapLayer = null;
|
|
283
374
|
}
|
|
375
|
+
if (this.mapTileLayer) {
|
|
376
|
+
this.mapTileLayer.remove();
|
|
377
|
+
this.mapTileLayer = null;
|
|
378
|
+
}
|
|
284
379
|
if (this.mapInstance) {
|
|
285
380
|
this.mapInstance.remove();
|
|
286
381
|
this.mapInstance = null;
|
|
@@ -390,14 +485,17 @@ module.exports = app => app.component('models', {
|
|
|
390
485
|
|
|
391
486
|
return params;
|
|
392
487
|
},
|
|
393
|
-
|
|
394
|
-
this.status = 'loading';
|
|
395
|
-
this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
|
|
488
|
+
setSearchTextFromRoute() {
|
|
396
489
|
if (this.$route.query?.search) {
|
|
397
490
|
this.searchText = this.$route.query.search;
|
|
398
491
|
} else {
|
|
399
492
|
this.searchText = '';
|
|
400
493
|
}
|
|
494
|
+
},
|
|
495
|
+
async initSearchFromUrl() {
|
|
496
|
+
this.status = 'loading';
|
|
497
|
+
this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
|
|
498
|
+
this.setSearchTextFromRoute();
|
|
401
499
|
if (this.$route.query?.sort) {
|
|
402
500
|
const sort = eval(`(${this.$route.query.sort})`);
|
|
403
501
|
const path = Object.keys(sort)[0];
|
|
@@ -620,6 +718,9 @@ module.exports = app => app.component('models', {
|
|
|
620
718
|
}
|
|
621
719
|
},
|
|
622
720
|
async getDocuments() {
|
|
721
|
+
// Track recently viewed model
|
|
722
|
+
this.trackRecentModel(this.currentModel);
|
|
723
|
+
|
|
623
724
|
// Clear previous data
|
|
624
725
|
this.documents = [];
|
|
625
726
|
this.schemaPaths = [];
|
|
@@ -864,6 +965,14 @@ module.exports = app => app.component('models', {
|
|
|
864
965
|
this.selectMultiple = true;
|
|
865
966
|
}
|
|
866
967
|
},
|
|
968
|
+
openModelSwitcher() {
|
|
969
|
+
this.showModelSwitcher = true;
|
|
970
|
+
},
|
|
971
|
+
selectSwitcherModel(model) {
|
|
972
|
+
this.showModelSwitcher = false;
|
|
973
|
+
this.trackRecentModel(model);
|
|
974
|
+
this.$router.push('/model/' + model);
|
|
975
|
+
},
|
|
867
976
|
async loadModelCounts() {
|
|
868
977
|
if (!Array.isArray(this.models) || this.models.length === 0) {
|
|
869
978
|
return;
|