Skip to content

Commit 4c0a6e2

Browse files
committed
feat(ui): divide admin.js into modules
Signed-off-by: Gabriel Costa <gabrielcg@proton.me>
1 parent 8253c11 commit 4c0a6e2

146 files changed

Lines changed: 58448 additions & 43929 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/playwright.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,21 @@ jobs:
6060
version: "0.9.2"
6161
python-version: "3.12"
6262

63+
- name: 🟢 Set up Node.js
64+
uses: actions/setup-node@v4
65+
with:
66+
node-version: "22"
67+
cache: "npm"
68+
6369
- name: 📦 Install gateway dependencies
6470
run: |
6571
make venv install
6672
73+
- name: 🏗️ Build admin UI bundle
74+
run: |
75+
npm ci
76+
npm run vite:build
77+
6778
- name: 🎭 Run make serve + CI smoke tests
6879
shell: bash
6980
run: |

Containerfile.lite

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,26 @@ RUN if [ "$ENABLE_RUST" = "true" ]; then \
7373

7474
FROM rust-builder-base AS rust-builder
7575

76+
###########################
77+
# Frontend builder stage
78+
###########################
79+
FROM node:lts-alpine AS frontend-builder
80+
WORKDIR /app
81+
82+
# Copy package.json and package-lock.json
83+
COPY package.json package-lock.json ./
84+
85+
# Install frontend dependencies
86+
RUN npm install --frozen-lockfile
87+
88+
# Copy frontend source files
89+
COPY mcpgateway/admin_ui/ mcpgateway/admin_ui/
90+
COPY vite.config.js ./
91+
92+
# Run Vite build
93+
RUN npm run vite:build
94+
95+
7696
###########################
7797
# Builder stage
7898
###########################
@@ -125,6 +145,12 @@ COPY pyproject.toml /app/
125145
# ----------------------------------------------------------------------------
126146
COPY --from=rust-builder /build/plugins_rust/target/wheels/ /tmp/rust-wheels/
127147

148+
# ----------------------------------------------------------------------------
149+
# Copy frontend build artifacts from frontend-builder stage
150+
# ----------------------------------------------------------------------------
151+
COPY --from=frontend-builder /app/mcpgateway/static/ /app/mcpgateway/static/
152+
153+
128154
# ----------------------------------------------------------------------------
129155
# Create and populate virtual environment
130156
# - Upgrade pip, setuptools, wheel, pdm, uv

docker-compose.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ services:
221221
# JWT Configuration - Choose ONE approach:
222222
# Option 1: HMAC (Default - Simple deployments)
223223
- JWT_ALGORITHM=HS256
224-
- JWT_SECRET_KEY=my-test-key
224+
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-my-test-key}
225225
# Option 2: RSA (Production - Asymmetric, uncomment and generate certs)
226226
# - JWT_ALGORITHM=RS256
227227
# - JWT_PUBLIC_KEY_PATH=/app/certs/jwt/public.pem
@@ -1243,7 +1243,7 @@ services:
12431243
fast_time_server:
12441244
condition: service_started
12451245
environment:
1246-
- JWT_SECRET_KEY=my-test-key
1246+
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-my-test-key}
12471247
# This is a one-shot container that exits after registration
12481248
restart: "no"
12491249
entrypoint: ["/bin/sh", "-c"]
@@ -1628,7 +1628,7 @@ services:
16281628
fast_test_server:
16291629
condition: service_healthy
16301630
environment:
1631-
- JWT_SECRET_KEY=my-test-key
1631+
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-my-test-key}
16321632
restart: "no"
16331633
entrypoint: ["/bin/sh", "-c"]
16341634
command:
@@ -1728,7 +1728,7 @@ services:
17281728
a2a_echo_agent:
17291729
condition: service_healthy
17301730
environment:
1731-
- JWT_SECRET_KEY=my-test-key
1731+
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-my-test-key}
17321732
restart: "no"
17331733
entrypoint: ["/bin/sh", "-c"]
17341734
command:
@@ -1963,7 +1963,7 @@ services:
19631963
benchmark_server:
19641964
condition: service_started
19651965
environment:
1966-
- JWT_SECRET_KEY=my-test-key
1966+
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-my-test-key}
19671967
- BENCHMARK_SERVER_COUNT=${BENCHMARK_SERVER_COUNT:-10}
19681968
- BENCHMARK_START_PORT=${BENCHMARK_START_PORT:-9000}
19691969
restart: "no"

eslint.config.js

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,18 @@
11
"use strict";
22
const neostandard = require("neostandard");
3-
const prettierPlugin = require("eslint-plugin-prettier");
4-
const prettierRecommended = require("eslint-plugin-prettier/recommended");
53

64
module.exports = [
7-
...neostandard({
8-
env: ["browser"],
9-
ignores: neostandard.resolveIgnoresFromGitignore(),
10-
noStyle: true,
11-
}),
12-
prettierRecommended,
13-
{
14-
plugins: {
15-
prettier: prettierPlugin,
16-
},
17-
rules: {
18-
// Match previous style preferences: semicolons and double quotes
19-
"prettier/prettier": [
20-
"error",
21-
{
22-
semi: true,
23-
singleQuote: false,
24-
},
25-
],
26-
// Preserve previous lint behavior for curly braces and prefer-const
27-
curly: "error",
28-
"prefer-const": "error",
29-
},
5+
...neostandard({
6+
env: ["browser"],
7+
ignores: neostandard.resolveIgnoresFromGitignore(),
8+
noStyle: true,
9+
}),
10+
{
11+
rules: {
12+
indent: ["error", 2, { SwitchCase: 1 }],
13+
// Preserve previous lint behavior for curly braces and prefer-const
14+
curly: "error",
15+
"prefer-const": "error",
3016
},
17+
},
3118
];

mcpgateway/admin.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ class GrpcServiceNameConflictError(GrpcServiceError): # type: ignore
188188
# This will be set by main.py when it imports admin_router
189189
logging_service: Optional[LoggingService] = None
190190
LOGGER: logging.Logger = logging.getLogger("mcpgateway.admin")
191-
192191
UI_SECTION_TO_TABS: Dict[str, tuple[str, ...]] = {
193192
"overview": ("overview",),
194193
"servers": ("catalog",),
@@ -214,6 +213,50 @@ class GrpcServiceNameConflictError(GrpcServiceError): # type: ignore
214213
UI_HIDE_SECTIONS_COOKIE_NAME = "mcpgateway_ui_hide_sections"
215214
UI_HIDE_SECTIONS_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 days
216215

216+
# Cache for the bundle filename to avoid reading manifest on every request
217+
# Using a mutable dict to avoid the need for a global statement in the accessor function
218+
_bundle_js_cache: dict[str, Optional[str]] = {"filename": None}
219+
220+
221+
def get_bundle_js_filename() -> str:
222+
"""Get the hashed bundle.js filename from Vite manifest.
223+
224+
Reads the Vite manifest file to get the current hashed bundle filename.
225+
Falls back to scanning for bundle-*.js on disk if the manifest is unreadable.
226+
Invalidates the cache when the cached bundle file no longer exists on disk.
227+
228+
Returns:
229+
str: The bundle filename (e.g., 'bundle-abc123.js')
230+
"""
231+
static_dir = Path(__file__).parent / "static"
232+
233+
# Use cache if the bundle file still exists on disk
234+
cached = _bundle_js_cache["filename"]
235+
if cached is not None and (static_dir / cached).exists():
236+
return cached
237+
238+
manifest_path = static_dir / ".vite" / "manifest.json"
239+
try:
240+
if manifest_path.exists():
241+
with open(manifest_path, "r", encoding="utf-8") as f:
242+
manifest = orjson.loads(f.read())
243+
# The key is the input path relative to the project root
244+
entry_key = "mcpgateway/admin_ui/index.js"
245+
if entry_key in manifest and manifest[entry_key].get("file"):
246+
_bundle_js_cache["filename"] = manifest[entry_key]["file"]
247+
return _bundle_js_cache["filename"] # type: ignore[return-value]
248+
except Exception as e:
249+
LOGGER.warning(f"Failed to read Vite manifest: {e}")
250+
251+
# Manifest unreadable or missing entry — find bundle file directly on disk
252+
bundles = sorted(static_dir.glob("bundle-*.js"), key=lambda p: p.stat().st_mtime, reverse=True)
253+
if bundles:
254+
_bundle_js_cache["filename"] = bundles[0].name
255+
return _bundle_js_cache["filename"] # type: ignore[return-value]
256+
257+
LOGGER.error("No bundle-*.js found in %s — admin UI will not load", static_dir)
258+
return ""
259+
217260

218261
def _normalize_ui_hide_values(raw: Any, valid_values: frozenset[str], aliases: Optional[Dict[str, str]] = None) -> set[str]:
219262
"""Normalize UI hide values from CSV/list input into a validated set.
@@ -3548,6 +3591,7 @@ def _to_dict_and_filter(raw_list):
35483591
"roots": roots,
35493592
"include_inactive": include_inactive,
35503593
"root_path": root_path,
3594+
"bundle_js": get_bundle_js_filename(),
35513595
"max_name_length": max_name_length,
35523596
"gateway_tool_name_separator": settings.gateway_tool_name_separator,
35533597
"bulk_import_max_tools": settings.mcpgateway_bulk_import_max_tools,
@@ -5569,7 +5613,7 @@ async def admin_get_team_edit(
55695613
</select>
55705614
</div>
55715615
<div class="flex justify-end space-x-3">
5572-
<button type="button" onclick="hideTeamEditModal()"
5616+
<button type="button" onclick="Admin.hideTeamEditModal()"
55735617
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
55745618
Cancel
55755619
</button>
@@ -7268,7 +7312,7 @@ async def admin_get_user_edit(
72687312

72697313
# Create edit form HTML
72707314
edit_form = f"""
7271-
<div class="space-y-4">
7315+
<div id="user-edit-modal-content" class="space-y-4">
72727316
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Edit User</h3>
72737317
<div id="edit-user-error"></div>
72747318
<form hx-post="{root_path}/admin/users/{user_email}/update" hx-target="#edit-user-error" hx-swap="innerHTML" class="space-y-4">
@@ -7292,13 +7336,13 @@ async def admin_get_user_edit(
72927336
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">New Password (leave empty to keep current)</label>
72937337
<input type="password" name="password" id="password-field"
72947338
class="mt-1 px-1.5 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 text-gray-900 dark:text-white"
7295-
oninput="validatePasswordRequirements(); validatePasswordMatch();">
7339+
oninput="Admin.validatePasswordRequirements(); Admin.validatePasswordMatch();">
72967340
</div>
72977341
<div>
72987342
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Confirm New Password</label>
72997343
<input type="password" name="confirm_password" id="confirm-password-field"
73007344
class="mt-1 px-1.5 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 text-gray-900 dark:text-white"
7301-
oninput="validatePasswordMatch()">
7345+
oninput="Admin.validatePasswordMatch()">
73027346
<div id="password-match-message" class="mt-1 text-sm text-red-600 hidden">Passwords do not match</div>
73037347
</div>
73047348
{password_requirements_html}
@@ -7312,7 +7356,7 @@ async def admin_get_user_edit(
73127356
data-require-special="{"true" if settings.password_require_special else "false"}"
73137357
></div>
73147358
<div class="flex justify-end space-x-3">
7315-
<button type="button" onclick="hideUserEditModal()"
7359+
<button type="button" onclick="Admin.hideUserEditModal()"
73167360
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
73177361
Cancel
73187362
</button>
@@ -7406,6 +7450,9 @@ async def admin_update_user(
74067450
success_html = """
74077451
<div class="text-green-500 text-center p-4">
74087452
<p>User updated successfully</p>
7453+
<button type="button" onclick="Admin.hideUserEditModal()" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
7454+
Close
7455+
</button>
74097456
</div>
74107457
"""
74117458
response = HTMLResponse(content=success_html)

0 commit comments

Comments
 (0)