mirror of
https://github.com/johndoe6345789/goodpackagerepo.git
synced 2026-04-24 13:54:59 +00:00
Add SQLite-backed configuration database and initial admin panel structure
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
170
backend/app.py
170
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/<namespace>/<name>/<version>/<variant>/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)
|
||||
|
||||
447
backend/config_db.py
Normal file
447
backend/config_db.py
Normal file
@@ -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)
|
||||
637
frontend/src/app/admin/page.jsx
Normal file
637
frontend/src/app/admin/page.jsx
Normal file
@@ -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 <div className={styles.loading}>Loading admin panel...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<h1>Admin Panel</h1>
|
||||
<p>Repository configuration and management</p>
|
||||
</div>
|
||||
<div className={styles.header__actions}>
|
||||
<button className={`${styles.button} ${styles['button--secondary']}`}>
|
||||
Export Config
|
||||
</button>
|
||||
<button className={`${styles.button} ${styles['button--primary']}`}>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.alert} style={{ background: 'rgba(33, 150, 243, 0.1)', borderLeft: '4px solid #2196f3' }}>
|
||||
ℹ️ <strong>Info:</strong> Configuration loaded from SQLite database. Changes are stored in real-time.
|
||||
</div>
|
||||
|
||||
<div className={styles.tabs}>
|
||||
<button
|
||||
className={`${styles.tabs__tab} ${activeTab === 'overview' ? styles['tabs__tab--active'] : ''}`}
|
||||
onClick={() => setActiveTab('overview')}
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tabs__tab} ${activeTab === 'entities' ? styles['tabs__tab--active'] : ''}`}
|
||||
onClick={() => setActiveTab('entities')}
|
||||
>
|
||||
Entities
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tabs__tab} ${activeTab === 'storage' ? styles['tabs__tab--active'] : ''}`}
|
||||
onClick={() => setActiveTab('storage')}
|
||||
>
|
||||
Storage
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tabs__tab} ${activeTab === 'routes' ? styles['tabs__tab--active'] : ''}`}
|
||||
onClick={() => setActiveTab('routes')}
|
||||
>
|
||||
API Routes
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tabs__tab} ${activeTab === 'auth' ? styles['tabs__tab--active'] : ''}`}
|
||||
onClick={() => setActiveTab('auth')}
|
||||
>
|
||||
Auth & Policies
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tabs__tab} ${activeTab === 'features' ? styles['tabs__tab--active'] : ''}`}
|
||||
onClick={() => setActiveTab('features')}
|
||||
>
|
||||
Features
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tabs__tab} ${activeTab === 'raw' ? styles['tabs__tab--active'] : ''}`}
|
||||
onClick={() => setActiveTab('raw')}
|
||||
>
|
||||
Raw Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'overview' && (
|
||||
<>
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>Repository Information</h2>
|
||||
<div className={styles.section__content}>
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__icon}>📋</div>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Schema Version</div>
|
||||
<div className={styles.stat__value}>{config.schema_version}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__icon}>🔧</div>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Type ID</div>
|
||||
<div className={styles.stat__value} style={{ fontSize: '14px' }}>{config.type_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__icon}>🛣️</div>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>API Routes</div>
|
||||
<div className={styles.stat__value}>{config.api_routes?.length || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__icon}>📦</div>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Entities</div>
|
||||
<div className={styles.stat__value}>{config.entities?.length || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__icon}>💾</div>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Blob Stores</div>
|
||||
<div className={styles.stat__value}>{config.blob_stores?.length || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__icon}>🔐</div>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Auth Scopes</div>
|
||||
<div className={styles.stat__value}>{config.auth_scopes?.length || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ marginTop: '24px', color: '#666' }}>{config.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>Capabilities</h2>
|
||||
<div className={styles.section__content}>
|
||||
{config.capabilities && (
|
||||
<>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<strong>Protocols:</strong>{' '}
|
||||
{JSON.parse(config.capabilities.protocols || '[]').map((p, i) => (
|
||||
<span key={i} className={`${styles.badge} ${styles['badge--primary']}`}>
|
||||
{p}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<strong>Storage:</strong>{' '}
|
||||
{JSON.parse(config.capabilities.storage || '[]').map((s, i) => (
|
||||
<span key={i} className={`${styles.badge} ${styles['badge--primary']}`}>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Features:</strong>{' '}
|
||||
{JSON.parse(config.capabilities.features || '[]').map((f, i) => (
|
||||
<span key={i} className={`${styles.badge} ${styles['badge--success']}`}>
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'entities' && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>
|
||||
Entities
|
||||
<button className={`${styles.button} ${styles['button--primary']} ${styles['button--small']}`}>
|
||||
+ Add Entity
|
||||
</button>
|
||||
</h2>
|
||||
<div className={styles.section__content}>
|
||||
{config.entities && config.entities.length > 0 ? (
|
||||
config.entities.map((entity, i) => (
|
||||
<div key={i} className={styles.entityCard}>
|
||||
<div className={styles.entityCard__header}>
|
||||
<div>
|
||||
<div className={styles.entityCard__name}>{entity.name}</div>
|
||||
<div className={styles.entityCard__details}>
|
||||
Type: {entity.type} • Fields: {entity.fields?.length || 0} • Constraints: {entity.constraints?.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.entityCard__actions}>
|
||||
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
|
||||
Edit
|
||||
</button>
|
||||
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{entity.fields && entity.fields.length > 0 && (
|
||||
<>
|
||||
<h4 style={{ marginTop: '16px', marginBottom: '8px' }}>Fields</h4>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Optional</th>
|
||||
<th>Normalizations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entity.fields.map((field, j) => (
|
||||
<tr key={j}>
|
||||
<td><strong>{field.name}</strong></td>
|
||||
<td>{field.type}</td>
|
||||
<td>{field.optional ? '✓' : '✗'}</td>
|
||||
<td>{JSON.parse(field.normalizations || '[]').join(', ') || 'none'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
|
||||
{entity.constraints && entity.constraints.length > 0 && (
|
||||
<>
|
||||
<h4 style={{ marginTop: '16px', marginBottom: '8px' }}>Constraints</h4>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Pattern</th>
|
||||
<th>When Present</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entity.constraints.map((constraint, j) => (
|
||||
<tr key={j}>
|
||||
<td><strong>{constraint.field}</strong></td>
|
||||
<td><code>{constraint.regex}</code></td>
|
||||
<td>{constraint.when_present ? '✓' : '✗'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<div className={styles.empty__icon}>📦</div>
|
||||
<p>No entities defined</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'storage' && (
|
||||
<>
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>
|
||||
Blob Stores
|
||||
<button className={`${styles.button} ${styles['button--primary']} ${styles['button--small']}`}>
|
||||
+ Add Store
|
||||
</button>
|
||||
</h2>
|
||||
<div className={styles.section__content}>
|
||||
{config.blob_stores && config.blob_stores.length > 0 ? (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Kind</th>
|
||||
<th>Root</th>
|
||||
<th>Addressing Mode</th>
|
||||
<th>Max Size</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{config.blob_stores.map((store, i) => (
|
||||
<tr key={i}>
|
||||
<td><strong>{store.name}</strong></td>
|
||||
<td>{store.kind}</td>
|
||||
<td><code>{store.root}</code></td>
|
||||
<td>{store.addressing_mode}</td>
|
||||
<td>{store.max_blob_bytes ? `${(store.max_blob_bytes / 1024 / 1024).toFixed(0)} MB` : 'N/A'}</td>
|
||||
<td>
|
||||
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<div className={styles.empty__icon}>💾</div>
|
||||
<p>No blob stores defined</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>
|
||||
KV Stores
|
||||
<button className={`${styles.button} ${styles['button--primary']} ${styles['button--small']}`}>
|
||||
+ Add Store
|
||||
</button>
|
||||
</h2>
|
||||
<div className={styles.section__content}>
|
||||
{config.kv_stores && config.kv_stores.length > 0 ? (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Kind</th>
|
||||
<th>Root</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{config.kv_stores.map((store, i) => (
|
||||
<tr key={i}>
|
||||
<td><strong>{store.name}</strong></td>
|
||||
<td>{store.kind}</td>
|
||||
<td><code>{store.root}</code></td>
|
||||
<td>
|
||||
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<div className={styles.empty__icon}>🗄️</div>
|
||||
<p>No KV stores defined</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'routes' && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>
|
||||
API Routes
|
||||
<button className={`${styles.button} ${styles['button--primary']} ${styles['button--small']}`}>
|
||||
+ Add Route
|
||||
</button>
|
||||
</h2>
|
||||
<div className={styles.section__content}>
|
||||
{config.api_routes && config.api_routes.length > 0 ? (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '20%' }}>ID</th>
|
||||
<th style={{ width: '10%' }}>Method</th>
|
||||
<th style={{ width: '30%' }}>Path</th>
|
||||
<th style={{ width: '20%' }}>Tags</th>
|
||||
<th style={{ width: '10%' }}>Pipeline</th>
|
||||
<th style={{ width: '10%' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{config.api_routes.map((route, i) => (
|
||||
<tr key={i}>
|
||||
<td><strong>{route.route_id}</strong></td>
|
||||
<td>
|
||||
<span className={`${styles.badge} ${styles['badge--primary']}`}>
|
||||
{route.method}
|
||||
</span>
|
||||
</td>
|
||||
<td><code>{route.path}</code></td>
|
||||
<td>
|
||||
{JSON.parse(route.tags || '[]').map((tag, j) => (
|
||||
<span key={j} className={`${styles.badge} ${styles['badge--success']}`}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
<td>{JSON.parse(route.pipeline || '[]').length} steps</td>
|
||||
<td>
|
||||
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<div className={styles.empty__icon}>🛣️</div>
|
||||
<p>No API routes defined</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'auth' && (
|
||||
<>
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>
|
||||
Scopes
|
||||
<button className={`${styles.button} ${styles['button--primary']} ${styles['button--small']}`}>
|
||||
+ Add Scope
|
||||
</button>
|
||||
</h2>
|
||||
<div className={styles.section__content}>
|
||||
{config.auth_scopes && config.auth_scopes.length > 0 ? (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Scope</th>
|
||||
<th>Actions</th>
|
||||
<th style={{ width: '120px' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{config.auth_scopes.map((scope, i) => (
|
||||
<tr key={i}>
|
||||
<td><strong>{scope.name}</strong></td>
|
||||
<td>{JSON.parse(scope.actions || '[]').join(', ')}</td>
|
||||
<td>
|
||||
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<div className={styles.empty__icon}>🔐</div>
|
||||
<p>No auth scopes defined</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>
|
||||
Policies
|
||||
<button className={`${styles.button} ${styles['button--primary']} ${styles['button--small']}`}>
|
||||
+ Add Policy
|
||||
</button>
|
||||
</h2>
|
||||
<div className={styles.section__content}>
|
||||
{config.auth_policies && config.auth_policies.length > 0 ? (
|
||||
config.auth_policies.map((policy, i) => (
|
||||
<div key={i} className={styles.entityCard}>
|
||||
<div className={styles.entityCard__header}>
|
||||
<div>
|
||||
<div className={styles.entityCard__name}>{policy.name}</div>
|
||||
<div className={styles.entityCard__details}>
|
||||
Effect: {policy.effect}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.entityCard__actions}>
|
||||
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
|
||||
Edit
|
||||
</button>
|
||||
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.codeBlock}>
|
||||
<pre>{JSON.stringify({
|
||||
conditions: JSON.parse(policy.conditions || '{}'),
|
||||
requirements: JSON.parse(policy.requirements || '{}')
|
||||
}, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.empty}>
|
||||
<div className={styles.empty__icon}>📜</div>
|
||||
<p>No policies defined</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'features' && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>Features Configuration</h2>
|
||||
<div className={styles.section__content}>
|
||||
{config.features && (
|
||||
<div className={styles.grid} style={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Mutable Tags</div>
|
||||
<div className={styles.stat__value}>{config.features.mutable_tags ? '✓ Enabled' : '✗ Disabled'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Allow Overwrite Artifacts</div>
|
||||
<div className={styles.stat__value}>{config.features.allow_overwrite_artifacts ? '✓ Enabled' : '✗ Disabled'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Proxy Enabled</div>
|
||||
<div className={styles.stat__value}>{config.features.proxy_enabled ? '✓ Enabled' : '✗ Disabled'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Garbage Collection</div>
|
||||
<div className={styles.stat__value}>{config.features.gc_enabled ? '✓ Enabled' : '✗ Disabled'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.caching && (
|
||||
<>
|
||||
<h3 style={{ marginTop: '32px', marginBottom: '16px' }}>Caching</h3>
|
||||
<div className={styles.grid} style={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Response Cache</div>
|
||||
<div className={styles.stat__value}>{config.caching.response_cache_enabled ? '✓ Enabled' : '✗ Disabled'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Response Cache TTL</div>
|
||||
<div className={styles.stat__value}>{config.caching.response_cache_ttl}s</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Blob Cache</div>
|
||||
<div className={styles.stat__value}>{config.caching.blob_cache_enabled ? '✓ Enabled' : '✗ Disabled'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<div className={styles.stat__info}>
|
||||
<div className={styles.stat__label}>Blob Cache Max Size</div>
|
||||
<div className={styles.stat__value}>{config.caching.blob_cache_max_bytes ? `${(config.caching.blob_cache_max_bytes / 1024 / 1024 / 1024).toFixed(0)} GB` : 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'raw' && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.section__title}>
|
||||
Raw Configuration Data
|
||||
<button
|
||||
className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
||||
alert('Configuration copied to clipboard!');
|
||||
}}
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
</h2>
|
||||
<div className={styles.section__content}>
|
||||
<div className={styles.codeBlock}>
|
||||
<pre>{JSON.stringify(config, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
292
frontend/src/app/admin/page.module.scss
Normal file
292
frontend/src/app/admin/page.module.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user