@intranefr/superbackend 1.4.4 → 1.5.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/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Admin Database Browser</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
|
+
<style>[v-cloak]{display:none}</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body class="bg-gray-50">
|
|
13
|
+
<div id="app" class="max-w-7xl mx-auto px-6 py-6" v-cloak>
|
|
14
|
+
<div class="flex items-center justify-between mb-6">
|
|
15
|
+
<div>
|
|
16
|
+
<h1 class="text-2xl font-semibold text-gray-900">Database Browser</h1>
|
|
17
|
+
<div class="text-sm text-gray-500">Manage external DB connections (Mongo/MySQL) and browse records (read-only).</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="flex items-center gap-2">
|
|
20
|
+
<button @click="refreshAll" class="px-3 py-2 rounded bg-gray-700 text-white text-sm hover:bg-gray-800">
|
|
21
|
+
<i class="ti ti-refresh mr-1"></i> Refresh
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div v-if="error" class="mb-4 p-3 rounded border border-red-200 bg-red-50 text-red-700 text-sm">
|
|
27
|
+
{{ error }}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="grid grid-cols-12 gap-6">
|
|
31
|
+
<!-- Left: Connections -->
|
|
32
|
+
<div class="col-span-5 space-y-6">
|
|
33
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
34
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
35
|
+
<div class="text-sm font-semibold text-gray-800">Connections</div>
|
|
36
|
+
<div class="text-xs text-gray-400">URIs stored encrypted; only masked version is shown.</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="p-4 space-y-3">
|
|
39
|
+
<div v-if="connections.length === 0" class="text-sm text-gray-500">No connections yet.</div>
|
|
40
|
+
<div v-for="c in connections" :key="c.id" class="p-3 border rounded flex items-start justify-between" :class="selectedConnectionId===c.id ? 'border-gray-800 bg-gray-50' : 'border-gray-200'">
|
|
41
|
+
<div class="min-w-0">
|
|
42
|
+
<div class="flex items-center gap-2">
|
|
43
|
+
<div class="font-semibold text-gray-900 truncate">{{ c.name }}</div>
|
|
44
|
+
<span class="text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-700">{{ c.type }}</span>
|
|
45
|
+
<span v-if="c.enabled" class="text-xs px-2 py-0.5 rounded bg-green-100 text-green-700">enabled</span>
|
|
46
|
+
<span v-else class="text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-500">disabled</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="text-xs text-gray-500 mt-1 break-all">{{ c.uriMasked || '—' }}</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="flex flex-col gap-2 pl-3">
|
|
51
|
+
<button @click="selectConnection(c)" class="px-2 py-1 rounded text-xs bg-gray-800 text-white hover:bg-gray-900">
|
|
52
|
+
Browse
|
|
53
|
+
</button>
|
|
54
|
+
<button @click="testConnection(c.id)" class="px-2 py-1 rounded text-xs bg-blue-600 text-white hover:bg-blue-700">
|
|
55
|
+
Test
|
|
56
|
+
</button>
|
|
57
|
+
<button @click="startEdit(c)" class="px-2 py-1 rounded text-xs bg-gray-200 hover:bg-gray-300">
|
|
58
|
+
Edit
|
|
59
|
+
</button>
|
|
60
|
+
<button @click="toggleEnabled(c)" class="px-2 py-1 rounded text-xs bg-gray-200 hover:bg-gray-300">
|
|
61
|
+
{{ c.enabled ? 'Disable' : 'Enable' }}
|
|
62
|
+
</button>
|
|
63
|
+
<button @click="deleteConnection(c.id)" class="px-2 py-1 rounded text-xs bg-red-600 text-white hover:bg-red-700">
|
|
64
|
+
Delete
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
72
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
73
|
+
<div class="text-sm font-semibold text-gray-800">{{ editing ? 'Edit connection' : 'Create connection' }}</div>
|
|
74
|
+
<button v-if="editing" @click="cancelEdit" class="text-xs text-gray-600 hover:text-gray-900">Cancel</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="p-4 space-y-3">
|
|
77
|
+
<div class="grid grid-cols-2 gap-3">
|
|
78
|
+
<div>
|
|
79
|
+
<label class="text-xs font-semibold text-gray-600">Name</label>
|
|
80
|
+
<input v-model="form.name" class="mt-1 w-full border rounded px-3 py-2" placeholder="prod-mongo" />
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<label class="text-xs font-semibold text-gray-600">Type</label>
|
|
84
|
+
<select v-model="form.type" class="mt-1 w-full border rounded px-3 py-2">
|
|
85
|
+
<option value="mongo">mongo</option>
|
|
86
|
+
<option value="mysql">mysql</option>
|
|
87
|
+
</select>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div>
|
|
92
|
+
<label class="text-xs font-semibold text-gray-600">URI {{ editing ? '(leave empty to keep existing)' : '' }}</label>
|
|
93
|
+
<input v-model="form.uri" class="mt-1 w-full border rounded px-3 py-2 font-mono text-sm" placeholder="mongodb://user:pass@host:27017/db" />
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="flex items-center gap-2">
|
|
97
|
+
<input id="enabled" type="checkbox" v-model="form.enabled" class="h-4 w-4" />
|
|
98
|
+
<label for="enabled" class="text-sm text-gray-700">Enabled</label>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="flex items-center gap-2">
|
|
102
|
+
<button v-if="!editing" @click="createConnection" class="px-3 py-2 rounded bg-green-600 text-white text-sm hover:bg-green-700">
|
|
103
|
+
<i class="ti ti-plus mr-1"></i> Create
|
|
104
|
+
</button>
|
|
105
|
+
<button v-else @click="saveEdit" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">
|
|
106
|
+
<i class="ti ti-device-floppy mr-1"></i> Save
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Right: Browser -->
|
|
114
|
+
<div class="col-span-7 space-y-6">
|
|
115
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
116
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
117
|
+
<div class="text-sm font-semibold text-gray-800">Browse</div>
|
|
118
|
+
<div class="text-xs text-gray-400">Connection → Database → Namespace</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="p-4 space-y-4">
|
|
122
|
+
<div v-if="!selectedConnection" class="text-sm text-gray-500">Select a connection and click “Browse”.</div>
|
|
123
|
+
<div v-else class="space-y-4">
|
|
124
|
+
<div class="grid grid-cols-3 gap-3">
|
|
125
|
+
<div>
|
|
126
|
+
<label class="text-xs font-semibold text-gray-600">Database</label>
|
|
127
|
+
<select v-model="selectedDatabase" @change="onDatabaseChange" class="mt-1 w-full border rounded px-3 py-2">
|
|
128
|
+
<option value="">—</option>
|
|
129
|
+
<option v-for="d in databases" :key="d" :value="d">{{ d }}</option>
|
|
130
|
+
</select>
|
|
131
|
+
</div>
|
|
132
|
+
<div>
|
|
133
|
+
<label class="text-xs font-semibold text-gray-600">Namespace</label>
|
|
134
|
+
<select v-model="selectedNamespace" @change="onNamespaceChange" class="mt-1 w-full border rounded px-3 py-2">
|
|
135
|
+
<option value="">—</option>
|
|
136
|
+
<option v-for="n in namespaces" :key="n" :value="n">{{ n }}</option>
|
|
137
|
+
</select>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="flex items-end">
|
|
140
|
+
<button @click="loadRecords" :disabled="!canBrowse" class="w-full px-3 py-2 rounded bg-gray-800 text-white text-sm hover:bg-gray-900 disabled:opacity-50">
|
|
141
|
+
Load records
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="grid grid-cols-5 gap-3">
|
|
147
|
+
<div class="col-span-2">
|
|
148
|
+
<label class="text-xs font-semibold text-gray-600">Filter field</label>
|
|
149
|
+
<select v-if="schemaFields.length" v-model="query.filterField" class="mt-1 w-full border rounded px-3 py-2">
|
|
150
|
+
<option value="">—</option>
|
|
151
|
+
<option v-for="f in schemaFields" :key="f" :value="f">{{ f }}</option>
|
|
152
|
+
</select>
|
|
153
|
+
<input v-else v-model="query.filterField" class="mt-1 w-full border rounded px-3 py-2" placeholder="field" />
|
|
154
|
+
</div>
|
|
155
|
+
<div class="col-span-3">
|
|
156
|
+
<label class="text-xs font-semibold text-gray-600">Filter value (contains)</label>
|
|
157
|
+
<input v-model="query.filterValue" class="mt-1 w-full border rounded px-3 py-2" placeholder="search" />
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="grid grid-cols-5 gap-3">
|
|
162
|
+
<div class="col-span-2">
|
|
163
|
+
<label class="text-xs font-semibold text-gray-600">Sort field</label>
|
|
164
|
+
<select v-if="schemaFields.length" v-model="query.sortField" class="mt-1 w-full border rounded px-3 py-2">
|
|
165
|
+
<option value="">—</option>
|
|
166
|
+
<option v-for="f in schemaFields" :key="f" :value="f">{{ f }}</option>
|
|
167
|
+
</select>
|
|
168
|
+
<input v-else v-model="query.sortField" class="mt-1 w-full border rounded px-3 py-2" placeholder="field" />
|
|
169
|
+
</div>
|
|
170
|
+
<div>
|
|
171
|
+
<label class="text-xs font-semibold text-gray-600">Order</label>
|
|
172
|
+
<select v-model="query.sortOrder" class="mt-1 w-full border rounded px-3 py-2">
|
|
173
|
+
<option value="desc">desc</option>
|
|
174
|
+
<option value="asc">asc</option>
|
|
175
|
+
</select>
|
|
176
|
+
</div>
|
|
177
|
+
<div>
|
|
178
|
+
<label class="text-xs font-semibold text-gray-600">Page</label>
|
|
179
|
+
<input v-model.number="query.page" type="number" min="1" class="mt-1 w-full border rounded px-3 py-2" />
|
|
180
|
+
</div>
|
|
181
|
+
<div>
|
|
182
|
+
<label class="text-xs font-semibold text-gray-600">Page size</label>
|
|
183
|
+
<input v-model.number="query.pageSize" type="number" min="1" max="100" class="mt-1 w-full border rounded px-3 py-2" />
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div v-if="records.items.length" class="border rounded overflow-hidden">
|
|
188
|
+
<div class="p-3 border-b bg-gray-50 flex items-center justify-between">
|
|
189
|
+
<div class="text-sm text-gray-700">Total: <span class="font-semibold">{{ records.total }}</span> • Page {{ records.page }} / {{ records.totalPages || 1 }}</div>
|
|
190
|
+
<div class="flex items-center gap-2">
|
|
191
|
+
<button @click="prevPage" :disabled="records.page<=1" class="px-2 py-1 rounded text-xs bg-gray-200 hover:bg-gray-300 disabled:opacity-50">Prev</button>
|
|
192
|
+
<button @click="nextPage" :disabled="records.page>=records.totalPages" class="px-2 py-1 rounded text-xs bg-gray-200 hover:bg-gray-300 disabled:opacity-50">Next</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="overflow-auto" style="max-height: 520px;">
|
|
196
|
+
<table class="min-w-full text-sm">
|
|
197
|
+
<thead class="bg-white sticky top-0">
|
|
198
|
+
<tr>
|
|
199
|
+
<th v-for="h in tableHeaders" :key="h" class="text-left px-3 py-2 border-b text-xs font-semibold text-gray-600">{{ h }}</th>
|
|
200
|
+
</tr>
|
|
201
|
+
</thead>
|
|
202
|
+
<tbody>
|
|
203
|
+
<tr v-for="(row, idx) in records.items" :key="idx" @click="openRow(row)" class="cursor-pointer hover:bg-gray-50">
|
|
204
|
+
<td v-for="h in tableHeaders" :key="h" class="px-3 py-2 border-b whitespace-nowrap">
|
|
205
|
+
{{ formatCell(row[h]) }}
|
|
206
|
+
</td>
|
|
207
|
+
</tr>
|
|
208
|
+
</tbody>
|
|
209
|
+
</table>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div v-else class="text-sm text-gray-500">No records loaded yet (or empty result).</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<!-- Row modal -->
|
|
219
|
+
<div v-if="rowModal.open" class="fixed inset-0 bg-black/40 flex items-center justify-center p-6" @click.self="rowModal.open=false">
|
|
220
|
+
<div class="bg-white w-full max-w-3xl rounded-lg shadow">
|
|
221
|
+
<div class="p-4 border-b flex items-center justify-between">
|
|
222
|
+
<div class="font-semibold text-gray-900">Row / Document</div>
|
|
223
|
+
<button @click="rowModal.open=false" class="text-gray-500 hover:text-gray-900"><i class="ti ti-x"></i></button>
|
|
224
|
+
</div>
|
|
225
|
+
<pre class="p-4 text-xs overflow-auto" style="max-height: 70vh;">{{ JSON.stringify(rowModal.row, null, 2) }}</pre>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<script>
|
|
233
|
+
const { createApp, computed, reactive, ref } = Vue;
|
|
234
|
+
|
|
235
|
+
createApp({
|
|
236
|
+
setup() {
|
|
237
|
+
const baseUrl = '<%= baseUrl %>';
|
|
238
|
+
const apiBase = baseUrl + '/api/admin/db-browser';
|
|
239
|
+
|
|
240
|
+
const error = ref('');
|
|
241
|
+
const connections = ref([]);
|
|
242
|
+
const selectedConnectionId = ref('');
|
|
243
|
+
const selectedConnection = computed(() => connections.value.find(c => c.id === selectedConnectionId.value) || null);
|
|
244
|
+
|
|
245
|
+
const editing = ref(false);
|
|
246
|
+
const editingId = ref('');
|
|
247
|
+
const form = reactive({ name: '', type: 'mongo', uri: '', enabled: true });
|
|
248
|
+
|
|
249
|
+
const databases = ref([]);
|
|
250
|
+
const namespaces = ref([]);
|
|
251
|
+
const selectedDatabase = ref('');
|
|
252
|
+
const selectedNamespace = ref('');
|
|
253
|
+
const schema = ref(null);
|
|
254
|
+
const query = reactive({ page: 1, pageSize: 20, filterField: '', filterValue: '', sortField: '', sortOrder: 'desc' });
|
|
255
|
+
const records = reactive({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 1 });
|
|
256
|
+
|
|
257
|
+
const rowModal = reactive({ open: false, row: null });
|
|
258
|
+
|
|
259
|
+
const schemaFields = computed(() => (Array.isArray(schema.value) ? schema.value.map(c => c.field).filter(Boolean) : []));
|
|
260
|
+
const canBrowse = computed(() => Boolean(selectedConnection.value && selectedDatabase.value && selectedNamespace.value));
|
|
261
|
+
|
|
262
|
+
const tableHeaders = computed(() => {
|
|
263
|
+
const first = records.items?.[0];
|
|
264
|
+
if (!first || typeof first !== 'object') return [];
|
|
265
|
+
return Object.keys(first);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
function clearError() { error.value = ''; }
|
|
269
|
+
|
|
270
|
+
async function api(path, opts = {}) {
|
|
271
|
+
clearError();
|
|
272
|
+
const res = await fetch(apiBase + path, {
|
|
273
|
+
credentials: 'same-origin',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
...opts,
|
|
276
|
+
});
|
|
277
|
+
const json = await res.json().catch(() => ({}));
|
|
278
|
+
if (!res.ok) throw new Error(json.error || 'Request failed');
|
|
279
|
+
return json;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function loadConnections() {
|
|
283
|
+
const data = await api('/connections', { method: 'GET' });
|
|
284
|
+
connections.value = data.items || [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function createConnection() {
|
|
288
|
+
const payload = { name: form.name, type: form.type, enabled: !!form.enabled, uri: form.uri };
|
|
289
|
+
const data = await api('/connections', { method: 'POST', body: JSON.stringify(payload) });
|
|
290
|
+
connections.value.unshift(data.item);
|
|
291
|
+
form.name = ''; form.uri = ''; form.type = 'mongo'; form.enabled = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function startEdit(c) {
|
|
295
|
+
editing.value = true;
|
|
296
|
+
editingId.value = c.id;
|
|
297
|
+
form.name = c.name;
|
|
298
|
+
form.type = c.type;
|
|
299
|
+
form.enabled = !!c.enabled;
|
|
300
|
+
form.uri = '';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function cancelEdit() {
|
|
304
|
+
editing.value = false;
|
|
305
|
+
editingId.value = '';
|
|
306
|
+
form.name = ''; form.uri = ''; form.type = 'mongo'; form.enabled = true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function saveEdit() {
|
|
310
|
+
const payload = { name: form.name, type: form.type, enabled: !!form.enabled };
|
|
311
|
+
if (String(form.uri || '').trim()) payload.uri = form.uri;
|
|
312
|
+
const data = await api('/connections/' + editingId.value, { method: 'PATCH', body: JSON.stringify(payload) });
|
|
313
|
+
const idx = connections.value.findIndex(x => x.id === editingId.value);
|
|
314
|
+
if (idx >= 0) connections.value[idx] = data.item;
|
|
315
|
+
cancelEdit();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function deleteConnection(id) {
|
|
319
|
+
if (!confirm('Delete this connection?')) return;
|
|
320
|
+
await api('/connections/' + id, { method: 'DELETE' });
|
|
321
|
+
connections.value = connections.value.filter(c => c.id !== id);
|
|
322
|
+
if (selectedConnectionId.value === id) {
|
|
323
|
+
selectedConnectionId.value = '';
|
|
324
|
+
databases.value = []; namespaces.value = []; selectedDatabase.value = ''; selectedNamespace.value = ''; schema.value = null;
|
|
325
|
+
records.items = []; records.total = 0; records.page = 1; records.pageSize = 20; records.totalPages = 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function testConnection(id) {
|
|
330
|
+
const out = await api('/connections/' + id + '/test', { method: 'POST' });
|
|
331
|
+
alert(out.ok ? 'OK' : 'Failed');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function toggleEnabled(c) {
|
|
335
|
+
const data = await api('/connections/' + c.id, { method: 'PATCH', body: JSON.stringify({ enabled: !c.enabled }) });
|
|
336
|
+
const idx = connections.value.findIndex(x => x.id === c.id);
|
|
337
|
+
if (idx >= 0) connections.value[idx] = data.item;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function selectConnection(c) {
|
|
341
|
+
selectedConnectionId.value = c.id;
|
|
342
|
+
databases.value = []; namespaces.value = []; selectedDatabase.value = ''; selectedNamespace.value = ''; schema.value = null;
|
|
343
|
+
records.items = []; records.total = 0; records.page = 1; records.pageSize = query.pageSize; records.totalPages = 1;
|
|
344
|
+
const data = await api('/connections/' + c.id + '/databases', { method: 'GET' });
|
|
345
|
+
databases.value = (data.items || []);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function onDatabaseChange() {
|
|
349
|
+
if (!selectedConnection.value || !selectedDatabase.value) return;
|
|
350
|
+
namespaces.value = []; selectedNamespace.value = ''; schema.value = null;
|
|
351
|
+
const data = await api(`/connections/${selectedConnectionId.value}/databases/${encodeURIComponent(selectedDatabase.value)}/namespaces`, { method: 'GET' });
|
|
352
|
+
namespaces.value = (data.items || []);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function onNamespaceChange() {
|
|
356
|
+
schema.value = null;
|
|
357
|
+
records.items = []; records.total = 0; records.totalPages = 1;
|
|
358
|
+
if (!canBrowse.value) return;
|
|
359
|
+
const data = await api(`/connections/${selectedConnectionId.value}/databases/${encodeURIComponent(selectedDatabase.value)}/namespaces/${encodeURIComponent(selectedNamespace.value)}/schema`, { method: 'GET' });
|
|
360
|
+
schema.value = data.schema;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function loadRecords() {
|
|
364
|
+
if (!canBrowse.value) return;
|
|
365
|
+
const qs = new URLSearchParams();
|
|
366
|
+
qs.set('page', String(query.page || 1));
|
|
367
|
+
qs.set('pageSize', String(query.pageSize || 20));
|
|
368
|
+
if (String(query.filterField || '').trim()) qs.set('filterField', query.filterField);
|
|
369
|
+
if (String(query.filterValue || '').trim()) qs.set('filterValue', query.filterValue);
|
|
370
|
+
if (String(query.sortField || '').trim()) qs.set('sortField', query.sortField);
|
|
371
|
+
if (String(query.sortOrder || '').trim()) qs.set('sortOrder', query.sortOrder);
|
|
372
|
+
|
|
373
|
+
const data = await api(`/connections/${selectedConnectionId.value}/databases/${encodeURIComponent(selectedDatabase.value)}/namespaces/${encodeURIComponent(selectedNamespace.value)}/records?` + qs.toString(), { method: 'GET' });
|
|
374
|
+
records.items = data.items || [];
|
|
375
|
+
records.total = data.total || 0;
|
|
376
|
+
records.page = data.page || 1;
|
|
377
|
+
records.pageSize = data.pageSize || 20;
|
|
378
|
+
records.totalPages = data.totalPages || 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function prevPage() { if (records.page > 1) { query.page = records.page - 1; loadRecords(); } }
|
|
382
|
+
function nextPage() { if (records.page < records.totalPages) { query.page = records.page + 1; loadRecords(); } }
|
|
383
|
+
|
|
384
|
+
function openRow(row) { rowModal.row = row; rowModal.open = true; }
|
|
385
|
+
|
|
386
|
+
function formatCell(v) {
|
|
387
|
+
if (v === null || v === undefined) return '';
|
|
388
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
389
|
+
return String(v);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function refreshAll() {
|
|
393
|
+
try {
|
|
394
|
+
await loadConnections();
|
|
395
|
+
if (selectedConnection.value) {
|
|
396
|
+
await selectConnection(selectedConnection.value);
|
|
397
|
+
}
|
|
398
|
+
} catch (e) {
|
|
399
|
+
error.value = e.message || String(e);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// bootstrap
|
|
404
|
+
loadConnections().catch((e) => (error.value = e.message || String(e)));
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
error,
|
|
408
|
+
connections,
|
|
409
|
+
selectedConnectionId,
|
|
410
|
+
selectedConnection,
|
|
411
|
+
databases,
|
|
412
|
+
namespaces,
|
|
413
|
+
selectedDatabase,
|
|
414
|
+
selectedNamespace,
|
|
415
|
+
schemaFields,
|
|
416
|
+
schema,
|
|
417
|
+
canBrowse,
|
|
418
|
+
query,
|
|
419
|
+
records,
|
|
420
|
+
tableHeaders,
|
|
421
|
+
rowModal,
|
|
422
|
+
editing,
|
|
423
|
+
form,
|
|
424
|
+
refreshAll,
|
|
425
|
+
selectConnection,
|
|
426
|
+
createConnection,
|
|
427
|
+
deleteConnection,
|
|
428
|
+
testConnection,
|
|
429
|
+
toggleEnabled,
|
|
430
|
+
startEdit,
|
|
431
|
+
cancelEdit,
|
|
432
|
+
saveEdit,
|
|
433
|
+
onDatabaseChange,
|
|
434
|
+
onNamespaceChange,
|
|
435
|
+
loadRecords,
|
|
436
|
+
prevPage,
|
|
437
|
+
nextPage,
|
|
438
|
+
openRow,
|
|
439
|
+
formatCell,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}).mount('#app');
|
|
443
|
+
</script>
|
|
444
|
+
</body>
|
|
445
|
+
</html>
|
|
@@ -29,15 +29,13 @@
|
|
|
29
29
|
<select id="fileSelect" class="w-full border rounded px-2 py-2 text-sm"></select>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<input id="model" class="w-full border rounded px-2 py-2 text-sm" placeholder="e.g. x-ai/grok-code-fast-1" />
|
|
40
|
-
</div>
|
|
32
|
+
<%- include('partials/llm-provider-model-picker', {
|
|
33
|
+
providerInputId: 'providerKey',
|
|
34
|
+
modelInputId: 'model',
|
|
35
|
+
providerLabel: 'LLM Provider Key',
|
|
36
|
+
modelLabel: 'Model',
|
|
37
|
+
showOpenRouterFetch: true,
|
|
38
|
+
}) %>
|
|
41
39
|
|
|
42
40
|
<div class="flex items-center gap-2">
|
|
43
41
|
<button id="btnReload" class="px-3 py-2 bg-gray-200 text-gray-900 rounded text-sm">Reload</button>
|
|
@@ -78,7 +76,7 @@
|
|
|
78
76
|
</div>
|
|
79
77
|
|
|
80
78
|
<script>
|
|
81
|
-
const API_BASE = window.location.origin + "<%= baseUrl %>"
|
|
79
|
+
const API_BASE = window.location.origin + "<%= baseUrl || '' %>";
|
|
82
80
|
|
|
83
81
|
const STORAGE_KEYS = {
|
|
84
82
|
providerKey: 'ejsVirtual.providerKey',
|
|
@@ -106,6 +104,14 @@
|
|
|
106
104
|
const model = localStorage.getItem(STORAGE_KEYS.model) || '';
|
|
107
105
|
document.getElementById('providerKey').value = pk;
|
|
108
106
|
document.getElementById('model').value = model;
|
|
107
|
+
|
|
108
|
+
if (window.__llmProviderModelPicker && window.__llmProviderModelPicker.init) {
|
|
109
|
+
window.__llmProviderModelPicker.init({
|
|
110
|
+
apiBase: API_BASE,
|
|
111
|
+
providerInputId: 'providerKey',
|
|
112
|
+
modelInputId: 'model',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
function saveSettings() {
|