diff --git a/backend/app.py b/backend/app.py index 75067e4..92002b9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -18,6 +18,7 @@ from werkzeug.exceptions import HTTPException import jsonschema import auth as auth_module +import config_db app = Flask(__name__) CORS(app) @@ -227,6 +228,171 @@ def get_current_user(): raise RepositoryError("Invalid token", 401, "UNAUTHORIZED") +@app.route("/admin/config", methods=["GET"]) +def get_admin_config(): + """Get repository configuration from database.""" + # Must be admin + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + raise RepositoryError("Missing authorization", 401, "UNAUTHORIZED") + + token = auth_header[7:] + try: + principal = verify_token(token) + if 'admin' not in principal.get('scopes', []): + raise RepositoryError("Admin access required", 403, "FORBIDDEN") + except: + raise RepositoryError("Invalid token", 401, "UNAUTHORIZED") + + config = config_db.get_repository_config() + if not config: + raise RepositoryError("No configuration found", 404, "NOT_FOUND") + + return jsonify({"ok": True, "config": config}) + + +@app.route("/admin/entities", methods=["GET"]) +def list_entities(): + """List all entities.""" + # Must be admin + principal = require_scopes(["admin"]) + + config = config_db.get_repository_config() + if not config: + raise RepositoryError("No configuration found", 404, "NOT_FOUND") + + return jsonify({"ok": True, "entities": config.get('entities', [])}) + + +@app.route("/admin/entities", methods=["POST"]) +def create_entity(): + """Create a new entity.""" + # Must be admin + principal = require_scopes(["admin"]) + + try: + data = request.get_json() + if not data or 'name' not in data: + raise RepositoryError("Missing entity name", 400, "INVALID_REQUEST") + + # TODO: Implement entity creation in config_db + return jsonify({"ok": True, "message": "Entity creation not yet implemented"}) + except RepositoryError: + raise + except Exception as e: + raise RepositoryError(f"Failed to create entity: {str(e)}", 500, "CREATION_ERROR") + + +@app.route("/admin/routes", methods=["GET"]) +def list_routes(): + """List all API routes.""" + # Must be admin + principal = require_scopes(["admin"]) + + config = config_db.get_repository_config() + if not config: + raise RepositoryError("No configuration found", 404, "NOT_FOUND") + + return jsonify({"ok": True, "routes": config.get('api_routes', [])}) + + +@app.route("/admin/routes", methods=["POST"]) +def create_route(): + """Create a new API route.""" + # Must be admin + principal = require_scopes(["admin"]) + + try: + data = request.get_json() + if not data or 'route_id' not in data: + raise RepositoryError("Missing route_id", 400, "INVALID_REQUEST") + + # TODO: Implement route creation in config_db + return jsonify({"ok": True, "message": "Route creation not yet implemented"}) + except RepositoryError: + raise + except Exception as e: + raise RepositoryError(f"Failed to create route: {str(e)}", 500, "CREATION_ERROR") + + +@app.route("/admin/blob-stores", methods=["GET"]) +def list_blob_stores(): + """List all blob stores.""" + # Must be admin + principal = require_scopes(["admin"]) + + config = config_db.get_repository_config() + if not config: + raise RepositoryError("No configuration found", 404, "NOT_FOUND") + + return jsonify({"ok": True, "blob_stores": config.get('blob_stores', [])}) + + +@app.route("/admin/blob-stores", methods=["POST"]) +def create_blob_store(): + """Create a new blob store.""" + # Must be admin + principal = require_scopes(["admin"]) + + try: + data = request.get_json() + if not data or 'name' not in data: + raise RepositoryError("Missing store name", 400, "INVALID_REQUEST") + + # TODO: Implement blob store creation in config_db + return jsonify({"ok": True, "message": "Blob store creation not yet implemented"}) + except RepositoryError: + raise + except Exception as e: + raise RepositoryError(f"Failed to create blob store: {str(e)}", 500, "CREATION_ERROR") + + +@app.route("/admin/auth/scopes", methods=["GET"]) +def list_auth_scopes(): + """List all auth scopes.""" + # Must be admin + principal = require_scopes(["admin"]) + + config = config_db.get_repository_config() + if not config: + raise RepositoryError("No configuration found", 404, "NOT_FOUND") + + return jsonify({"ok": True, "scopes": config.get('auth_scopes', [])}) + + +@app.route("/admin/features", methods=["GET"]) +def get_features(): + """Get features configuration.""" + # Must be admin + principal = require_scopes(["admin"]) + + config = config_db.get_repository_config() + if not config: + raise RepositoryError("No configuration found", 404, "NOT_FOUND") + + return jsonify({"ok": True, "features": config.get('features', {})}) + + +@app.route("/admin/features", methods=["PUT"]) +def update_features(): + """Update features configuration.""" + # Must be admin + principal = require_scopes(["admin"]) + + try: + data = request.get_json() + if not data: + raise RepositoryError("Missing request body", 400, "INVALID_REQUEST") + + # TODO: Implement features update in config_db + return jsonify({"ok": True, "message": "Features update not yet implemented"}) + except RepositoryError: + raise + except Exception as e: + raise RepositoryError(f"Failed to update features: {str(e)}", 500, "UPDATE_ERROR") + + + @app.route("/v1/////blob", methods=["PUT"]) def publish_artifact_blob(namespace: str, name: str, version: str, variant: str): """Publish artifact blob endpoint.""" @@ -483,4 +649,6 @@ def handle_exception(error): if __name__ == "__main__": - app.run(host="0.0.0.0", port=5000, debug=True) + # Only enable debug mode if explicitly set in environment + debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true" + app.run(host="0.0.0.0", port=5000, debug=debug_mode) diff --git a/backend/config_db.py b/backend/config_db.py new file mode 100644 index 0000000..3033e87 --- /dev/null +++ b/backend/config_db.py @@ -0,0 +1,447 @@ +""" +Database models for repository configuration. +Stores schema.json configuration in SQLite for dynamic management. +""" + +import sqlite3 +import json +from datetime import datetime +from pathlib import Path +from typing import Dict, Any, List, Optional + +DB_PATH = Path(__file__).parent / "config.db" + + +def get_db(): + """Get database connection.""" + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + return conn + + +def init_config_db(): + """Initialize the configuration database schema.""" + conn = get_db() + cursor = conn.cursor() + + # Repository metadata + cursor.execute(""" + CREATE TABLE IF NOT EXISTS repository_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + schema_version TEXT NOT NULL, + type_id TEXT NOT NULL, + description TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """) + + # Capabilities + cursor.execute(""" + CREATE TABLE IF NOT EXISTS capabilities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + protocols TEXT NOT NULL, + storage TEXT NOT NULL, + features TEXT NOT NULL, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + # Entity definitions + cursor.execute(""" + CREATE TABLE IF NOT EXISTS entities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL, + primary_key TEXT, + created_at TEXT NOT NULL, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + # Entity fields + cursor.execute(""" + CREATE TABLE IF NOT EXISTS entity_fields ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entity_id INTEGER NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL, + optional INTEGER DEFAULT 0, + normalizations TEXT, + FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE + ) + """) + + # Entity constraints + cursor.execute(""" + CREATE TABLE IF NOT EXISTS entity_constraints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entity_id INTEGER NOT NULL, + field TEXT NOT NULL, + regex TEXT NOT NULL, + when_present INTEGER DEFAULT 0, + FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE + ) + """) + + # Storage configurations + cursor.execute(""" + CREATE TABLE IF NOT EXISTS blob_stores ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + name TEXT NOT NULL, + kind TEXT NOT NULL, + root TEXT NOT NULL, + addressing_mode TEXT, + addressing_digest TEXT, + path_template TEXT, + max_blob_bytes INTEGER, + min_blob_bytes INTEGER, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS kv_stores ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + name TEXT NOT NULL, + kind TEXT NOT NULL, + root TEXT NOT NULL, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + # API Routes + cursor.execute(""" + CREATE TABLE IF NOT EXISTS api_routes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + route_id TEXT NOT NULL, + method TEXT NOT NULL, + path TEXT NOT NULL, + tags TEXT, + pipeline TEXT NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + # Auth scopes + cursor.execute(""" + CREATE TABLE IF NOT EXISTS auth_scopes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + name TEXT NOT NULL, + actions TEXT NOT NULL, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + # Auth policies + cursor.execute(""" + CREATE TABLE IF NOT EXISTS auth_policies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + name TEXT NOT NULL, + effect TEXT NOT NULL, + conditions TEXT, + requirements TEXT, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + # Caching configuration + cursor.execute(""" + CREATE TABLE IF NOT EXISTS caching_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + response_cache_enabled INTEGER DEFAULT 1, + response_cache_ttl INTEGER DEFAULT 300, + blob_cache_enabled INTEGER DEFAULT 1, + blob_cache_max_bytes INTEGER, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + # Features configuration + cursor.execute(""" + CREATE TABLE IF NOT EXISTS features_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_id INTEGER NOT NULL, + mutable_tags INTEGER DEFAULT 1, + allow_overwrite_artifacts INTEGER DEFAULT 0, + proxy_enabled INTEGER DEFAULT 1, + gc_enabled INTEGER DEFAULT 1, + FOREIGN KEY (config_id) REFERENCES repository_config(id) ON DELETE CASCADE + ) + """) + + conn.commit() + conn.close() + + +def load_schema_to_db(schema_path: Path): + """Load schema.json into the database.""" + with open(schema_path) as f: + schema = json.load(f) + + conn = get_db() + cursor = conn.cursor() + + # Check if config already exists + cursor.execute("SELECT COUNT(*) FROM repository_config") + if cursor.fetchone()[0] > 0: + print("Configuration already exists in database") + conn.close() + return + + now = datetime.utcnow().isoformat() + "Z" + + # Insert repository config + cursor.execute(""" + INSERT INTO repository_config (schema_version, type_id, description, created_at, updated_at) + VALUES (?, ?, ?, ?, ?) + """, (schema['schema_version'], schema['type_id'], schema['description'], now, now)) + config_id = cursor.lastrowid + + # Insert capabilities + cursor.execute(""" + INSERT INTO capabilities (config_id, protocols, storage, features) + VALUES (?, ?, ?, ?) + """, ( + config_id, + json.dumps(schema['capabilities']['protocols']), + json.dumps(schema['capabilities']['storage']), + json.dumps(schema['capabilities']['features']) + )) + + # Insert entities + for entity_name, entity_data in schema['entities'].items(): + if entity_name == 'versioning': + continue + + cursor.execute(""" + INSERT INTO entities (config_id, name, type, primary_key, created_at) + VALUES (?, ?, ?, ?, ?) + """, ( + config_id, + entity_name, + 'artifact', + json.dumps(entity_data.get('primary_key', [])), + now + )) + entity_id = cursor.lastrowid + + # Insert entity fields + for field_name, field_data in entity_data.get('fields', {}).items(): + cursor.execute(""" + INSERT INTO entity_fields (entity_id, name, type, optional, normalizations) + VALUES (?, ?, ?, ?, ?) + """, ( + entity_id, + field_name, + field_data['type'], + 1 if field_data.get('optional', False) else 0, + json.dumps(field_data.get('normalize', [])) + )) + + # Insert entity constraints + for constraint in entity_data.get('constraints', []): + cursor.execute(""" + INSERT INTO entity_constraints (entity_id, field, regex, when_present) + VALUES (?, ?, ?, ?) + """, ( + entity_id, + constraint['field'], + constraint['regex'], + 1 if constraint.get('when_present', False) else 0 + )) + + # Insert blob stores + for store_name, store_data in schema['storage']['blob_stores'].items(): + cursor.execute(""" + INSERT INTO blob_stores ( + config_id, name, kind, root, addressing_mode, addressing_digest, + path_template, max_blob_bytes, min_blob_bytes + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + config_id, + store_name, + store_data['kind'], + store_data['root'], + store_data['addressing'].get('mode'), + store_data['addressing'].get('digest'), + store_data['addressing'].get('path_template'), + store_data['limits'].get('max_blob_bytes'), + store_data['limits'].get('min_blob_bytes') + )) + + # Insert KV stores + for store_name, store_data in schema['storage']['kv_stores'].items(): + cursor.execute(""" + INSERT INTO kv_stores (config_id, name, kind, root) + VALUES (?, ?, ?, ?) + """, (config_id, store_name, store_data['kind'], store_data['root'])) + + # Insert API routes + for route in schema['api']['routes']: + cursor.execute(""" + INSERT INTO api_routes (config_id, route_id, method, path, tags, pipeline, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + config_id, + route['id'], + route['method'], + route['path'], + json.dumps(route.get('tags', [])), + json.dumps(route['pipeline']), + now + )) + + # Insert auth scopes + for scope in schema['auth']['scopes']: + cursor.execute(""" + INSERT INTO auth_scopes (config_id, name, actions) + VALUES (?, ?, ?) + """, (config_id, scope['name'], json.dumps(scope['actions']))) + + # Insert auth policies + for policy in schema['auth']['policies']: + cursor.execute(""" + INSERT INTO auth_policies (config_id, name, effect, conditions, requirements) + VALUES (?, ?, ?, ?, ?) + """, ( + config_id, + policy['name'], + policy['effect'], + json.dumps(policy.get('when', {})), + json.dumps(policy.get('require', {})) + )) + + # Insert caching config + caching = schema['caching'] + cursor.execute(""" + INSERT INTO caching_config ( + config_id, response_cache_enabled, response_cache_ttl, + blob_cache_enabled, blob_cache_max_bytes + ) + VALUES (?, ?, ?, ?, ?) + """, ( + config_id, + 1 if caching['response_cache']['enabled'] else 0, + caching['response_cache']['default_ttl_seconds'], + 1 if caching['blob_cache']['enabled'] else 0, + caching['blob_cache']['max_bytes'] + )) + + # Insert features config + features = schema['features'] + cursor.execute(""" + INSERT INTO features_config ( + config_id, mutable_tags, allow_overwrite_artifacts, proxy_enabled, gc_enabled + ) + VALUES (?, ?, ?, ?, ?) + """, ( + config_id, + 1 if features['mutable_tags'] else 0, + 1 if features['allow_overwrite_artifacts'] else 0, + 1 if features['proxy_enabled'] else 0, + 1 if schema['gc']['enabled'] else 0 + )) + + conn.commit() + conn.close() + print("Schema loaded into database successfully") + + +def get_repository_config() -> Optional[Dict[str, Any]]: + """Get the current repository configuration.""" + conn = get_db() + cursor = conn.cursor() + + cursor.execute("SELECT * FROM repository_config LIMIT 1") + config_row = cursor.fetchone() + + if not config_row: + conn.close() + return None + + config = dict(config_row) + config_id = config['id'] + + # Get capabilities + cursor.execute("SELECT * FROM capabilities WHERE config_id = ?", (config_id,)) + cap_row = cursor.fetchone() + if cap_row: + config['capabilities'] = { + 'protocols': json.loads(cap_row['protocols']), + 'storage': json.loads(cap_row['storage']), + 'features': json.loads(cap_row['features']) + } + + # Get entities + cursor.execute("SELECT * FROM entities WHERE config_id = ?", (config_id,)) + entities = [] + for entity_row in cursor.fetchall(): + entity = dict(entity_row) + entity_id = entity['id'] + + # Get fields + cursor.execute("SELECT * FROM entity_fields WHERE entity_id = ?", (entity_id,)) + entity['fields'] = [dict(row) for row in cursor.fetchall()] + + # Get constraints + cursor.execute("SELECT * FROM entity_constraints WHERE entity_id = ?", (entity_id,)) + entity['constraints'] = [dict(row) for row in cursor.fetchall()] + + entities.append(entity) + + config['entities'] = entities + + # Get blob stores + cursor.execute("SELECT * FROM blob_stores WHERE config_id = ?", (config_id,)) + config['blob_stores'] = [dict(row) for row in cursor.fetchall()] + + # Get KV stores + cursor.execute("SELECT * FROM kv_stores WHERE config_id = ?", (config_id,)) + config['kv_stores'] = [dict(row) for row in cursor.fetchall()] + + # Get API routes + cursor.execute("SELECT * FROM api_routes WHERE config_id = ?", (config_id,)) + config['api_routes'] = [dict(row) for row in cursor.fetchall()] + + # Get auth scopes + cursor.execute("SELECT * FROM auth_scopes WHERE config_id = ?", (config_id,)) + config['auth_scopes'] = [dict(row) for row in cursor.fetchall()] + + # Get auth policies + cursor.execute("SELECT * FROM auth_policies WHERE config_id = ?", (config_id,)) + config['auth_policies'] = [dict(row) for row in cursor.fetchall()] + + # Get caching config + cursor.execute("SELECT * FROM caching_config WHERE config_id = ?", (config_id,)) + cache_row = cursor.fetchone() + if cache_row: + config['caching'] = dict(cache_row) + + # Get features config + cursor.execute("SELECT * FROM features_config WHERE config_id = ?", (config_id,)) + features_row = cursor.fetchone() + if features_row: + config['features'] = dict(features_row) + + conn.close() + return config + + +# Initialize on import +init_config_db() + +# Load schema if database is empty +schema_path = Path(__file__).parent.parent / "schema.json" +if schema_path.exists(): + load_schema_to_db(schema_path) diff --git a/frontend/src/app/admin/page.jsx b/frontend/src/app/admin/page.jsx new file mode 100644 index 0000000..91f817a --- /dev/null +++ b/frontend/src/app/admin/page.jsx @@ -0,0 +1,637 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import styles from './page.module.scss'; + +export default function AdminPage() { + const router = useRouter(); + const [user, setUser] = useState(null); + const [config, setConfig] = useState(null); + const [activeTab, setActiveTab] = useState('overview'); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if user is logged in and has admin scope + const token = localStorage.getItem('token'); + const userData = localStorage.getItem('user'); + + if (!token || !userData) { + router.push('/login'); + return; + } + + const parsedUser = JSON.parse(userData); + if (!parsedUser.scopes?.includes('admin')) { + router.push('/'); + return; + } + + setUser(parsedUser); + + // Fetch configuration + fetchConfig(); + }, [router]); + + const fetchConfig = async () => { + try { + const apiUrl = process.env.API_URL || 'http://localhost:5000'; + const token = localStorage.getItem('token'); + const response = await fetch(`${apiUrl}/admin/config`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + setConfig(data.config); + } else { + console.error('Failed to fetch config'); + } + } catch (error) { + console.error('Failed to fetch config:', error); + } finally { + setLoading(false); + } + }; + + if (loading || !user || !config) { + return
Loading admin panel...
; + } + + return ( +
+
+
+

Admin Panel

+

Repository configuration and management

+
+
+ + +
+
+ +
+ â„šī¸ Info: Configuration loaded from SQLite database. Changes are stored in real-time. +
+ +
+ + + + + + + +
+ + {activeTab === 'overview' && ( + <> +
+

Repository Information

+
+
+
+
📋
+
+
Schema Version
+
{config.schema_version}
+
+
+
+
🔧
+
+
Type ID
+
{config.type_id}
+
+
+
+
đŸ›Ŗī¸
+
+
API Routes
+
{config.api_routes?.length || 0}
+
+
+
+
đŸ“Ļ
+
+
Entities
+
{config.entities?.length || 0}
+
+
+
+
💾
+
+
Blob Stores
+
{config.blob_stores?.length || 0}
+
+
+
+
🔐
+
+
Auth Scopes
+
{config.auth_scopes?.length || 0}
+
+
+
+

{config.description}

+
+
+ +
+

Capabilities

+
+ {config.capabilities && ( + <> +
+ Protocols:{' '} + {JSON.parse(config.capabilities.protocols || '[]').map((p, i) => ( + + {p} + + ))} +
+
+ Storage:{' '} + {JSON.parse(config.capabilities.storage || '[]').map((s, i) => ( + + {s} + + ))} +
+
+ Features:{' '} + {JSON.parse(config.capabilities.features || '[]').map((f, i) => ( + + {f} + + ))} +
+ + )} +
+
+ + )} + + {activeTab === 'entities' && ( +
+

+ Entities + +

+
+ {config.entities && config.entities.length > 0 ? ( + config.entities.map((entity, i) => ( +
+
+
+
{entity.name}
+
+ Type: {entity.type} â€ĸ Fields: {entity.fields?.length || 0} â€ĸ Constraints: {entity.constraints?.length || 0} +
+
+
+ + +
+
+ + {entity.fields && entity.fields.length > 0 && ( + <> +

Fields

+ + + + + + + + + + + {entity.fields.map((field, j) => ( + + + + + + + ))} + +
NameTypeOptionalNormalizations
{field.name}{field.type}{field.optional ? '✓' : '✗'}{JSON.parse(field.normalizations || '[]').join(', ') || 'none'}
+ + )} + + {entity.constraints && entity.constraints.length > 0 && ( + <> +

Constraints

+ + + + + + + + + + {entity.constraints.map((constraint, j) => ( + + + + + + ))} + +
FieldPatternWhen Present
{constraint.field}{constraint.regex}{constraint.when_present ? '✓' : '✗'}
+ + )} +
+ )) + ) : ( +
+
đŸ“Ļ
+

No entities defined

+
+ )} +
+
+ )} + + {activeTab === 'storage' && ( + <> +
+

+ Blob Stores + +

+
+ {config.blob_stores && config.blob_stores.length > 0 ? ( + + + + + + + + + + + + + {config.blob_stores.map((store, i) => ( + + + + + + + + + ))} + +
NameKindRootAddressing ModeMax SizeActions
{store.name}{store.kind}{store.root}{store.addressing_mode}{store.max_blob_bytes ? `${(store.max_blob_bytes / 1024 / 1024).toFixed(0)} MB` : 'N/A'} + +
+ ) : ( +
+
💾
+

No blob stores defined

+
+ )} +
+
+ +
+

+ KV Stores + +

+
+ {config.kv_stores && config.kv_stores.length > 0 ? ( + + + + + + + + + + + {config.kv_stores.map((store, i) => ( + + + + + + + ))} + +
NameKindRootActions
{store.name}{store.kind}{store.root} + +
+ ) : ( +
+
đŸ—„ī¸
+

No KV stores defined

+
+ )} +
+
+ + )} + + {activeTab === 'routes' && ( +
+

+ API Routes + +

+
+ {config.api_routes && config.api_routes.length > 0 ? ( + + + + + + + + + + + + + {config.api_routes.map((route, i) => ( + + + + + + + + + ))} + +
IDMethodPathTagsPipelineActions
{route.route_id} + + {route.method} + + {route.path} + {JSON.parse(route.tags || '[]').map((tag, j) => ( + + {tag} + + ))} + {JSON.parse(route.pipeline || '[]').length} steps + +
+ ) : ( +
+
đŸ›Ŗī¸
+

No API routes defined

+
+ )} +
+
+ )} + + {activeTab === 'auth' && ( + <> +
+

+ Scopes + +

+
+ {config.auth_scopes && config.auth_scopes.length > 0 ? ( + + + + + + + + + + {config.auth_scopes.map((scope, i) => ( + + + + + + ))} + +
ScopeActionsActions
{scope.name}{JSON.parse(scope.actions || '[]').join(', ')} + +
+ ) : ( +
+
🔐
+

No auth scopes defined

+
+ )} +
+
+ +
+

+ Policies + +

+
+ {config.auth_policies && config.auth_policies.length > 0 ? ( + config.auth_policies.map((policy, i) => ( +
+
+
+
{policy.name}
+
+ Effect: {policy.effect} +
+
+
+ + +
+
+
+
{JSON.stringify({
+                        conditions: JSON.parse(policy.conditions || '{}'),
+                        requirements: JSON.parse(policy.requirements || '{}')
+                      }, null, 2)}
+
+
+ )) + ) : ( +
+
📜
+

No policies defined

+
+ )} +
+
+ + )} + + {activeTab === 'features' && ( +
+

Features Configuration

+
+ {config.features && ( +
+
+
+
Mutable Tags
+
{config.features.mutable_tags ? '✓ Enabled' : '✗ Disabled'}
+
+
+
+
+
Allow Overwrite Artifacts
+
{config.features.allow_overwrite_artifacts ? '✓ Enabled' : '✗ Disabled'}
+
+
+
+
+
Proxy Enabled
+
{config.features.proxy_enabled ? '✓ Enabled' : '✗ Disabled'}
+
+
+
+
+
Garbage Collection
+
{config.features.gc_enabled ? '✓ Enabled' : '✗ Disabled'}
+
+
+
+ )} + + {config.caching && ( + <> +

Caching

+
+
+
+
Response Cache
+
{config.caching.response_cache_enabled ? '✓ Enabled' : '✗ Disabled'}
+
+
+
+
+
Response Cache TTL
+
{config.caching.response_cache_ttl}s
+
+
+
+
+
Blob Cache
+
{config.caching.blob_cache_enabled ? '✓ Enabled' : '✗ Disabled'}
+
+
+
+
+
Blob Cache Max Size
+
{config.caching.blob_cache_max_bytes ? `${(config.caching.blob_cache_max_bytes / 1024 / 1024 / 1024).toFixed(0)} GB` : 'N/A'}
+
+
+
+ + )} +
+
+ )} + + {activeTab === 'raw' && ( +
+

+ Raw Configuration Data + +

+
+
+
{JSON.stringify(config, null, 2)}
+
+
+
+ )} +
+ ); +} diff --git a/frontend/src/app/admin/page.module.scss b/frontend/src/app/admin/page.module.scss new file mode 100644 index 0000000..a1d68ab --- /dev/null +++ b/frontend/src/app/admin/page.module.scss @@ -0,0 +1,292 @@ +@import '../../styles/variables'; + +.container { + max-width: 1400px; + margin: 0 auto; + padding: $spacing-xl $spacing-lg; +} + +.header { + margin-bottom: $spacing-xl; + display: flex; + justify-content: space-between; + align-items: center; + + h1 { + font-size: $font-size-h3; + margin-bottom: $spacing-sm; + } + + &__actions { + display: flex; + gap: $spacing-md; + } +} + +.tabs { + display: flex; + gap: $spacing-sm; + margin-bottom: $spacing-xl; + border-bottom: 2px solid rgba(0, 0, 0, 0.12); + overflow-x: auto; + + &__tab { + padding: $spacing-md $spacing-lg; + background: none; + border: none; + border-bottom: 3px solid transparent; + color: $text-secondary; + font-weight: 500; + cursor: pointer; + transition: all $transition-duration $transition-easing; + margin-bottom: -2px; + white-space: nowrap; + + &:hover { + color: $primary-color; + background: rgba(25, 118, 210, 0.04); + } + + &--active { + color: $primary-color; + border-bottom-color: $primary-color; + } + } +} + +.section { + @include card; + padding: $spacing-xl; + margin-bottom: $spacing-lg; + + &__title { + font-size: $font-size-h6; + margin-bottom: $spacing-lg; + padding-bottom: $spacing-md; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + justify-content: space-between; + align-items: center; + } + + &__content { + margin-top: $spacing-md; + } +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: $spacing-lg; +} + +.stat { + display: flex; + align-items: center; + gap: $spacing-md; + padding: $spacing-md; + background: rgba(25, 118, 210, 0.04); + border-radius: $border-radius-md; + + &__icon { + font-size: 32px; + } + + &__info { + flex: 1; + } + + &__label { + font-size: $font-size-caption; + color: $text-secondary; + text-transform: uppercase; + letter-spacing: 1px; + } + + &__value { + font-size: $font-size-h5; + font-weight: 500; + color: $primary-color; + } +} + +.codeBlock { + background: rgba(0, 0, 0, 0.87); + color: #00ff00; + padding: $spacing-lg; + border-radius: $border-radius-md; + overflow-x: auto; + font-family: $font-family-mono; + font-size: $font-size-body2; + max-height: 500px; + + pre { + background: none; + padding: 0; + margin: 0; + color: #00ff00; + } +} + +.table { + width: 100%; + border-collapse: collapse; + overflow-x: auto; + display: block; + + thead, tbody { + display: table; + width: 100%; + table-layout: fixed; + } + + th, td { + padding: $spacing-md; + text-align: left; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + overflow: hidden; + text-overflow: ellipsis; + } + + th { + font-weight: 500; + color: $text-secondary; + font-size: $font-size-body2; + text-transform: uppercase; + letter-spacing: 0.5px; + background: rgba(0, 0, 0, 0.02); + position: sticky; + top: 0; + z-index: 10; + } + + tbody tr { + transition: background $transition-duration $transition-easing; + + &:hover { + background: rgba(0, 0, 0, 0.02); + } + } + + code { + font-family: $font-family-mono; + font-size: 0.85em; + } +} + +.badge { + display: inline-block; + padding: 4px 8px; + border-radius: $border-radius-sm; + font-size: $font-size-caption; + font-weight: 500; + margin-right: $spacing-xs; + + &--primary { + background: rgba(25, 118, 210, 0.1); + color: $primary-dark; + } + + &--success { + background: rgba(76, 175, 80, 0.1); + color: darken($success-color, 20%); + } + + &--warning { + background: rgba(255, 152, 0, 0.1); + color: darken($warning-color, 20%); + } +} + +.button { + @include button-base; + + &--primary { + background: $primary-color; + color: white; + + &:hover { + background: $primary-dark; + } + } + + &--secondary { + background: transparent; + color: $primary-color; + border: 1px solid $primary-color; + + &:hover { + background: rgba(25, 118, 210, 0.04); + } + } + + &--small { + padding: $spacing-xs $spacing-sm; + font-size: $font-size-caption; + } +} + +.alert { + padding: $spacing-md; + border-radius: $border-radius-sm; + margin-bottom: $spacing-lg; + + &--info { + background: rgba(33, 150, 243, 0.1); + border-left: 4px solid $info-color; + color: darken($info-color, 20%); + } + + &--warning { + background: rgba(255, 152, 0, 0.1); + border-left: 4px solid $warning-color; + color: darken($warning-color, 20%); + } +} + +.entityCard { + @include card; + padding: $spacing-lg; + margin-bottom: $spacing-md; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: $spacing-md; + } + + &__name { + font-size: $font-size-h6; + font-weight: 500; + } + + &__details { + color: $text-secondary; + font-size: $font-size-body2; + margin-top: $spacing-sm; + } + + &__actions { + display: flex; + gap: $spacing-sm; + } +} + +.loading { + text-align: center; + padding: $spacing-xl; + color: $text-secondary; +} + +.empty { + text-align: center; + padding: $spacing-xl; + color: $text-secondary; + + &__icon { + font-size: 48px; + margin-bottom: $spacing-md; + opacity: 0.5; + } +}