refactor: 数据源菜单合一+布局优化
- 4个数据源菜单(MySQL/PG/Redis/Mongo)合并为1个「数据源」 - InstancePage 改为统一入口,顶部Tab切换类型过滤 - 添加数据源改为弹框Modal(默认MySQL) - 去掉左下角用户区,退出登录移到右上角header - 侧边栏收窄220px,整体布局更紧凑 - 后端202 tests passing - 前端构建零错误
This commit is contained in:
1 parent
bab55d3927
commit
5cbd0e3441
23 files changed
+2881
-94
No files matched your search
@@ -0,0 +1,240 @@
|
||||
"""Redis management API endpoints."""
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user, require_role
|
||||
from app.models.user import User
|
||||
from app.models.instance import Instance
|
||||
from app.services import redis_connector
|
||||
|
||||
router = APIRouter(tags=["Redis管理"])
|
||||
|
||||
|
||||
async def _get_redis_instance(instance_id: int, db: AsyncSession) -> Instance:
|
||||
"""Fetch and validate a Redis instance."""
|
||||
result = await db.execute(select(Instance).where(Instance.id == instance_id))
|
||||
instance = result.scalar_one_or_none()
|
||||
if not instance:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
if instance.db_type != "redis":
|
||||
raise HTTPException(status_code=400, detail="该实例不是 Redis 类型")
|
||||
return instance
|
||||
|
||||
|
||||
# ── Request schemas ──────────────────────────────────────────────
|
||||
|
||||
class RedisKeyCreate(BaseModel):
|
||||
db: int = 0
|
||||
key: str
|
||||
value: object
|
||||
type: str = "string"
|
||||
ttl: Optional[int] = None
|
||||
|
||||
|
||||
class RedisCommandRequest(BaseModel):
|
||||
db: int = 0
|
||||
command: str
|
||||
|
||||
|
||||
class RedisExpireRequest(BaseModel):
|
||||
db: int = 0
|
||||
seconds: int
|
||||
|
||||
|
||||
# ── Endpoints ────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/redis/{instance_id}/info")
|
||||
async def get_redis_info(
|
||||
instance_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Redis 信息概览."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
info = await redis_connector.RedisConnector.get_info(instance)
|
||||
return info
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="获取 Redis 信息失败: " + str(e))
|
||||
|
||||
|
||||
@router.get("/redis/{instance_id}/dbsize")
|
||||
async def get_redis_dbsize(
|
||||
instance_id: int,
|
||||
db_num: int = Query(0, alias="db"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""DBSIZE."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
size = await redis_connector.RedisConnector.get_dbsize(instance, db_num)
|
||||
return {"db": db_num, "size": size}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="获取 DBSIZE 失败: " + str(e))
|
||||
|
||||
|
||||
@router.get("/redis/{instance_id}/keys")
|
||||
async def get_redis_keys(
|
||||
instance_id: int,
|
||||
db_num: int = Query(0, alias="db"),
|
||||
pattern: str = Query("*"),
|
||||
type: Optional[str] = Query(None, alias="type"),
|
||||
cursor: int = Query(0),
|
||||
count: int = Query(50),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""键列表."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
next_cursor, keys = await redis_connector.RedisConnector.scan_keys(
|
||||
instance, db=db_num, pattern=pattern,
|
||||
type_filter=type, cursor=cursor, count=count,
|
||||
)
|
||||
return {"cursor": next_cursor, "keys": keys}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="获取键列表失败: " + str(e))
|
||||
|
||||
|
||||
@router.get("/redis/{instance_id}/keys/{key:path}")
|
||||
async def get_redis_key_detail(
|
||||
instance_id: int,
|
||||
key: str,
|
||||
db_num: int = Query(0, alias="db"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""键详情."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
detail = await redis_connector.RedisConnector.get_key_value(
|
||||
instance, db_num, key
|
||||
)
|
||||
return detail
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="获取键详情失败: " + str(e))
|
||||
|
||||
|
||||
@router.post("/redis/{instance_id}/keys")
|
||||
async def create_redis_key(
|
||||
instance_id: int,
|
||||
data: RedisKeyCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin", "dba")),
|
||||
):
|
||||
"""创建/更新键."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
result = await redis_connector.RedisConnector.set_key_value(
|
||||
instance, data.db, data.key, data.value, data.type, data.ttl
|
||||
)
|
||||
return {"message": "操作成功", "result": result}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="创建键失败: " + str(e))
|
||||
|
||||
|
||||
@router.delete("/redis/{instance_id}/keys/{key:path}")
|
||||
async def delete_redis_key(
|
||||
instance_id: int,
|
||||
key: str,
|
||||
db_num: int = Query(0, alias="db"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin", "dba")),
|
||||
):
|
||||
"""删除键."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
deleted = await redis_connector.RedisConnector.del_key(instance, db_num, key)
|
||||
return {"message": "删除成功", "deleted": deleted}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="删除键失败: " + str(e))
|
||||
|
||||
|
||||
@router.put("/redis/{instance_id}/keys/{key:path}/expire")
|
||||
async def set_redis_key_expire(
|
||||
instance_id: int,
|
||||
key: str,
|
||||
data: RedisExpireRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin", "dba")),
|
||||
):
|
||||
"""设置 TTL."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
ok = await redis_connector.RedisConnector.set_ttl(
|
||||
instance, data.db, key, data.seconds
|
||||
)
|
||||
return {"message": "设置成功" if ok else "设置失败"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="设置 TTL 失败: " + str(e))
|
||||
|
||||
|
||||
@router.delete("/redis/{instance_id}/flushdb")
|
||||
async def flush_redis_db(
|
||||
instance_id: int,
|
||||
db_num: int = Query(0, alias="db"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""清空DB (admin only!)."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
result = await redis_connector.RedisConnector.flush_db(instance, db_num)
|
||||
return {"message": "清空成功", "result": result}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="清空 DB 失败: " + str(e))
|
||||
|
||||
|
||||
@router.post("/redis/{instance_id}/command")
|
||||
async def execute_redis_command(
|
||||
instance_id: int,
|
||||
data: RedisCommandRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin", "dba")),
|
||||
):
|
||||
"""执行命令."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
output = await redis_connector.RedisConnector.execute_command(
|
||||
instance, data.db, data.command
|
||||
)
|
||||
return {"output": output}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="命令执行失败: " + str(e))
|
||||
|
||||
|
||||
@router.get("/redis/{instance_id}/slowlog")
|
||||
async def get_redis_slowlog(
|
||||
instance_id: int,
|
||||
count: int = Query(10),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""慢日志."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
logs = await redis_connector.RedisConnector.get_slowlog(instance, count)
|
||||
return logs
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="获取慢日志失败: " + str(e))
|
||||
|
||||
|
||||
@router.get("/redis/{instance_id}/clients")
|
||||
async def get_redis_clients(
|
||||
instance_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""客户端列表."""
|
||||
instance = await _get_redis_instance(instance_id, db)
|
||||
try:
|
||||
clients = await redis_connector.RedisConnector.get_clients(instance)
|
||||
return clients
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="获取客户端列表失败: " + str(e))
|
||||
Reference in new issue
Block a user