拆分系统监测模块并统一运维化文案

This commit is contained in:
Cheng Zhou
2026-06-11 11:06:59 +08:00
parent b7484c8e01
commit 1886765db8
21 changed files with 222 additions and 143 deletions
+2 -5
View File
@@ -14,7 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_current_user, get_db_session, is_system_admin, list_active_pm_study_ids
from app.core.deps import get_current_user, get_db_session, is_system_admin
from app.core.permission_monitor import get_permission_monitor
from app.models.permission_access_log import PermissionAccessLog
from app.models.permission_metric_snapshot import PermissionMetricSnapshot
@@ -39,10 +39,7 @@ class MonitoringScope:
async def resolve_monitoring_scope(db: AsyncSession, current_user) -> MonitoringScope:
if is_system_admin(current_user):
return MonitoringScope(is_admin=True, study_ids=set())
return MonitoringScope(
is_admin=False,
study_ids=await list_active_pm_study_ids(db, current_user.id),
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
def _apply_monitoring_scope_to_log_query(query, scope: MonitoringScope):
+12 -12
View File
@@ -969,54 +969,54 @@ SYSTEM_PERMISSIONS: dict[str, dict] = {
"description": "管理权限模板",
"roles": ["ADMIN"],
},
# 权限监控
# 系统监测
"system:monitoring:metrics": {
"module": "system_monitoring",
"action": "read",
"description": "查看权限监控指标",
"roles": ["ADMIN", "PM"],
"description": "查看系统监测指标",
"roles": ["ADMIN"],
},
"system:monitoring:cache_stats": {
"module": "system_monitoring",
"action": "read",
"description": "查看缓存统计",
"roles": ["ADMIN", "PM"],
"roles": ["ADMIN"],
},
"system:monitoring:alerts": {
"module": "system_monitoring",
"action": "read",
"description": "查看权限告警",
"roles": ["ADMIN", "PM"],
"roles": ["ADMIN"],
},
"system:monitoring:health": {
"module": "system_monitoring",
"action": "read",
"description": "查看权限系统健康状态",
"roles": ["ADMIN", "PM"],
"roles": ["ADMIN"],
},
"system:monitoring:access_logs": {
"module": "system_monitoring",
"action": "read",
"description": "查看权限访问日志",
"roles": ["ADMIN", "PM"],
"roles": ["ADMIN"],
},
"system:monitoring:trends": {
"module": "system_monitoring",
"action": "read",
"description": "查看权限趋势数据",
"roles": ["ADMIN", "PM"],
"roles": ["ADMIN"],
},
"system:monitoring:reset_metrics": {
"module": "system_monitoring",
"action": "update",
"description": "重置监控指标",
"roles": ["ADMIN", "PM"],
"description": "重置系统监测指标",
"roles": ["ADMIN"],
},
"system:monitoring:clear_alerts": {
"module": "system_monitoring",
"action": "update",
"description": "清除告警",
"roles": ["ADMIN", "PM"],
"roles": ["ADMIN"],
},
"system:monitoring:security_logs": {
"module": "system_monitoring",
@@ -1044,6 +1044,6 @@ SYSTEM_MODULE_LABELS: dict[str, str] = {
"system_users": "账号管理",
"system_projects": "项目管理",
"system_permissions": "权限管理",
"system_monitoring": "权限监控",
"system_monitoring": "系统监测",
"system_audit": "审计日志",
}
@@ -3,10 +3,12 @@
import uuid
import pytest
from fastapi import HTTPException
from sqlalchemy import text
from app.core.permission_monitor import set_permission_monitor, PermissionMonitor
from app.api.v1 import permission_monitoring
from app.api.v1.system_permissions import list_system_permissions
class FakeIpInfo:
@@ -22,6 +24,35 @@ class AdminUserStub:
is_admin = True
class ProjectPmUserStub:
id = uuid.uuid4()
is_admin = False
@pytest.mark.asyncio
async def test_resolve_monitoring_scope_rejects_project_pm(db_session):
"""系统监测模块仅允许系统管理员访问,项目 PM 不再具备监测范围。"""
with pytest.raises(HTTPException) as exc_info:
await permission_monitoring.resolve_monitoring_scope(db_session, ProjectPmUserStub())
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_system_permission_monitoring_definitions_are_admin_only():
"""系统级权限定义中的系统监测模块不应再标注 PM 角色。"""
data = await list_system_permissions()
monitoring_items = [
item for item in data["permissions"]
if item["module"] == "system_monitoring"
]
assert monitoring_items
assert data["module_labels"]["system_monitoring"] == "系统监测"
assert all(item["roles"] == ["ADMIN"] for item in monitoring_items)
assert all("PM" not in item["roles"] for item in monitoring_items)
async def _seed_permission_log(db_session, study_id: uuid.UUID, user_id: uuid.UUID, *, allowed: bool, elapsed_ms: float) -> None:
study_exists = (
await db_session.execute(text("SELECT id FROM studies WHERE id = :id"), {"id": str(study_id)})
+2 -2
View File
@@ -34,7 +34,7 @@ export const updateApiEndpointPermissions = (
export const fetchApiOperations = () =>
apiGet<{ operations: ApiOperation[] }>(`/api/v1/api-permissions/operations`);
// 权限系统监API
// 系统监API
export const fetchPermissionMetrics = () =>
apiGet<PermissionMetricsResponse>(`/api/v1/permission-monitoring/metrics`);
@@ -55,7 +55,7 @@ export const resetPermissionMetrics = () =>
export const clearPermissionAlerts = () =>
apiPost<void>(`/api/v1/permission-monitoring/clear-alerts`);
// 权限监控 - 新增API
// 系统监测 - 新增API
export const fetchAccessLogs = (params: {
study_id?: string;
user_id?: string;
+6 -2
View File
@@ -28,6 +28,10 @@
<el-icon><Document /></el-icon>
<span>{{ TEXT.menu.auditLogs }}</span>
</el-menu-item>
<el-menu-item v-if="isAdmin" index="/admin/system-monitoring">
<el-icon><DataAnalysis /></el-icon>
<span>{{ TEXT.menu.systemMonitoring }}</span>
</el-menu-item>
<el-sub-menu v-if="canAccessAdminPermissions" index="permissions">
<template #title>
<el-icon><Key /></el-icon>
@@ -35,7 +39,6 @@
</template>
<el-menu-item index="/admin/permissions/system">系统级权限</el-menu-item>
<el-menu-item index="/admin/permissions/project">项目权限配置</el-menu-item>
<el-menu-item index="/admin/permissions/monitoring">权限监控</el-menu-item>
</el-sub-menu>
</el-menu-item-group>
@@ -235,7 +238,7 @@ import { fetchSites } from "../api/sites";
import { TEXT } from "../locales";
import {
User, Suitcase, House, Calendar, Flag,
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files, Key
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files, Key, DataAnalysis
} from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { getProjectRoutePermission, hasProjectPermission, projectRouteLandingPaths } from "../utils/projectRoutePermissions";
@@ -301,6 +304,7 @@ const activeMenu = computed(() => {
if (path.startsWith("/knowledge/instruction-files")) return "/knowledge/instruction-files";
if (path.startsWith("/projects/")) return "/admin/projects";
if (path.startsWith("/admin/projects/")) return "/admin/projects";
if (path.startsWith("/admin/system-monitoring") || path.startsWith("/admin/permission-monitoring")) return "/admin/system-monitoring";
if (path.startsWith("/admin/permissions/")) return path;
return path;
});
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readSource = () => readFileSync(resolve(__dirname, "./Layout.vue"), "utf8");
describe("layout system monitoring navigation", () => {
it("shows system monitoring as an admin-only peer module outside permission management", () => {
const source = readSource();
const permissionMenuStart = source.indexOf('<el-sub-menu v-if="canAccessAdminPermissions" index="permissions">');
const permissionMenuEnd = source.indexOf("</el-sub-menu>", permissionMenuStart);
const permissionMenu = source.slice(permissionMenuStart, permissionMenuEnd);
expect(source).toContain('v-if="isAdmin" index="/admin/system-monitoring"');
expect(source).toContain("{{ TEXT.menu.systemMonitoring }}");
expect(permissionMenu).not.toContain("/admin/system-monitoring");
expect(permissionMenu).not.toContain("/admin/permission-monitoring");
expect(permissionMenu).not.toContain("/admin/permissions/monitoring");
expect(source).not.toContain('index="/admin/permissions/monitoring"');
expect(source).toContain('return "/admin/system-monitoring";');
});
});
@@ -21,13 +21,25 @@ describe("PermissionIpLocations", () => {
expect(source).toContain("VChart");
expect(source).toContain("chinaMapOption");
expect(source).toContain("用户分布");
expect(source).toContain("访问来源分布");
expect(source).toContain("访问次数");
expect(source).toContain("访问用户数");
expect(source).toContain("来源 IP 数");
expect(source).toContain("summary");
});
it("uses source-analysis copy instead of IP-location tab wording", () => {
const source = readSource();
expect(source).toContain("访问来源分析");
expect(source).toContain("来源分布");
expect(source).toContain("热点来源地");
expect(source).not.toContain("IP 属地分析");
expect(source).not.toContain("中国地图");
expect(source).not.toContain("热点属地");
expect(source).not.toContain("用户分布图");
});
it("uses the standard geojson.cn China map data instead of generated rectangles", () => {
const source = readSource();
const mapSource = readMapSource();
@@ -3,7 +3,7 @@
<section class="ip-hero">
<div class="ip-locations-toolbar">
<div class="toolbar-left">
<span class="toolbar-title">IP 属地分析</span>
<span class="toolbar-title">访问来源分析</span>
<span class="toolbar-desc">查看访问来源的地理分布</span>
</div>
<div class="toolbar-actions">
@@ -37,7 +37,7 @@
<div class="map-panel">
<div class="section-head">
<div class="section-title-group">
<h4>中国地图</h4>
<h4>来源分布</h4>
<p>访问来源地理分布热力图</p>
</div>
<el-tag type="info" effect="plain" round size="small">{{ currentPeriodLabel }}</el-tag>
@@ -48,7 +48,7 @@
<aside class="rank-panel">
<div class="section-head compact">
<div class="section-title-group">
<h4>热点</h4>
<h4>热点来源</h4>
<p>按访问次数排序</p>
</div>
</div>
@@ -219,7 +219,7 @@ const chinaMapOption = computed(() => ({
},
series: [
{
name: "用户分布",
name: "访问来源分布",
type: "map",
map: "ctms-china",
roam: false,
@@ -77,4 +77,28 @@ describe("PermissionMonitoring.vue", () => {
expect(source).toContain("isAdmin?: boolean");
expect(source).toContain(':show-security-log="props.isAdmin"');
});
it("uses system monitoring tab names and operation-oriented overview copy", () => {
const source = readFileSync(resolve(__dirname, "./PermissionMonitoring.vue"), "utf8");
expect(source).toContain('label="运行概览"');
expect(source).toContain('label="性能趋势"');
expect(source).toContain('label="访问审计"');
expect(source).toContain('label="来源分析"');
expect(source).toContain('label: "今日监测事件"');
expect(source).toContain('label: "每分钟事件"');
expect(source).toContain('label: "今日通过率"');
expect(source).toContain('label: "历史事件总数"');
expect(source).toContain("异常拒绝排行");
expect(source).not.toContain('label="实时概览"');
expect(source).not.toContain('label="趋势分析"');
expect(source).not.toContain('label="访问日志"');
expect(source).not.toContain('label="IP属地"');
expect(source).not.toContain('label: "今日总检查"');
expect(source).not.toContain('label: "每分钟请求"');
expect(source).not.toContain('label: "今日允许率"');
expect(source).not.toContain('label: "历史总记录"');
expect(source).not.toContain("被拒绝最多的权限");
});
});
@@ -1,7 +1,7 @@
<template>
<div class="permission-monitoring">
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane label="实时概览" name="overview">
<el-tab-pane label="运行概览" name="overview">
<div v-if="statsSummary" class="stats-summary">
<div v-for="card in summaryCards" :key="card.label" class="summary-card" :class="card.tone">
<div class="summary-icon">
@@ -83,7 +83,7 @@
<div v-if="topDenied && topDenied.length > 0" class="denied-card">
<div class="card-header">
<h3>被拒绝最多的权限</h3>
<h3>异常拒绝排行</h3>
<el-tag effect="plain" size="small" type="info"> 7 </el-tag>
</div>
<div class="denied-list">
@@ -121,15 +121,15 @@
</div>
</el-tab-pane>
<el-tab-pane label="趋势分析" name="trends">
<el-tab-pane label="性能趋势" name="trends">
<PermissionTrendCharts ref="trendChartsRef" />
</el-tab-pane>
<el-tab-pane label="访问日志" name="logs">
<el-tab-pane label="访问审计" name="logs">
<PermissionAccessLogs ref="accessLogsRef" :show-security-log="props.isAdmin" />
</el-tab-pane>
<el-tab-pane label="IP属地" name="ip-locations">
<el-tab-pane label="来源分析" name="ip-locations">
<PermissionIpLocations ref="ipLocationsRef" />
</el-tab-pane>
</el-tabs>
@@ -226,19 +226,19 @@ const summaryCards = computed(() => {
if (!statsSummary.value) return [];
return [
{
label: "今日总检查",
label: "今日监测事件",
value: statsSummary.value.today.total_checks.toLocaleString(),
tone: "blue",
icon: IconCheck,
},
{
label: "每分钟请求",
label: "每分钟事件",
value: statsSummary.value.last_hour.requests_per_minute,
tone: "cyan",
icon: IconActivity,
},
{
label: "今日允许率",
label: "今日通过率",
value: `${statsSummary.value.today.allow_rate}%`,
tone: statsSummary.value.today.allow_rate >= 80 ? "green" : "warning",
icon: IconPercent,
@@ -250,7 +250,7 @@ const summaryCards = computed(() => {
icon: IconClock,
},
{
label: "历史总记录",
label: "历史事件总数",
value: statsSummary.value.total_logs.toLocaleString(),
tone: "violet",
icon: IconDatabase,
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readSource = () => readFileSync(resolve(__dirname, "./PermissionTrendCharts.vue"), "utf8");
describe("PermissionTrendCharts", () => {
it("uses operation-oriented trend copy", () => {
const source = readSource();
expect(source).toContain("系统性能趋势");
expect(source).toContain("查看系统运行监测指标的变化趋势");
expect(source).toContain("监测事件趋势");
expect(source).toContain("响应时间趋势");
expect(source).toContain("缓存命中率");
expect(source).toContain("异常拒绝率趋势");
expect(source).toContain('name: "异常拒绝率"');
expect(source).not.toContain("数据趋势");
expect(source).not.toContain("查看权限系统各项指标的变化趋势");
expect(source).not.toContain("检查量趋势");
expect(source).not.toContain("<h4>拒绝率趋势</h4>");
expect(source).not.toContain('name: "拒绝率"');
});
});
@@ -2,8 +2,8 @@
<div class="trend-charts">
<div class="trend-toolbar">
<div class="toolbar-left">
<span class="toolbar-title">数据趋势</span>
<span class="toolbar-desc">查看权限系统各项指标的变化趋势</span>
<span class="toolbar-title">系统性能趋势</span>
<span class="toolbar-desc">查看系统运行监测指标的变化趋势</span>
</div>
<el-radio-group v-model="period" size="small" @change="loadData">
<el-radio-button value="24h">24小时</el-radio-button>
@@ -18,7 +18,7 @@
<div class="chart-icon blue">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
</div>
<h4>检查量趋势</h4>
<h4>监测事件趋势</h4>
</div>
<v-chart :option="checksChartOption" autoresize class="chart" />
</div>
@@ -45,7 +45,7 @@
<div class="chart-icon danger">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
</div>
<h4>拒绝率趋势</h4>
<h4>异常拒绝率趋势</h4>
</div>
<v-chart :option="denyRateOption" autoresize class="chart" />
</div>
@@ -181,7 +181,7 @@ const denyRateOption = computed(() => ({
yAxis: { type: "value", max: 100, name: "%", splitLine: { lineStyle: { color: "#f1f5f9" } }, axisLabel: { color: "#64748b" } },
series: [
{
name: "拒绝率",
name: "异常拒绝率",
type: "line",
smooth: true,
symbol: "circle",
+2 -1
View File
@@ -262,6 +262,7 @@ export const TEXT = {
projectManagement: "项目管理",
auditLogs: "审计日志",
permissionManagement: "权限管理",
systemMonitoring: "系统监测",
currentProject: "当前项目",
projectOverview: "项目总览",
projectMilestones: "项目里程碑",
@@ -1135,7 +1136,7 @@ export const TEXT = {
title: "权限管理",
moduleLevel: "模块级权限",
apiLevel: "角色权限",
monitoring: "权限监控",
monitoring: "系统监测",
refreshMetrics: "刷新指标",
save: "保存",
search: "搜索端点",
+18 -1
View File
@@ -45,7 +45,7 @@ describe("admin project route permissions", () => {
it("guards project permission configuration with the system-level project config permission", () => {
const source = readRouter();
const projectPermissionRouteStart = source.indexOf('name: "AdminPermissionsProject"');
const projectPermissionRouteEnd = source.indexOf('name: "AdminPermissionsMonitoring"', projectPermissionRouteStart);
const projectPermissionRouteEnd = source.indexOf('path: "system-monitoring"', projectPermissionRouteStart);
const projectPermissionRoute = source.slice(projectPermissionRouteStart, projectPermissionRouteEnd);
expect(source).toContain('const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config";');
@@ -61,6 +61,23 @@ describe("admin project route permissions", () => {
expect(targetProjectBranch).not.toContain("ensureDefaultPmStudy");
});
it("registers system monitoring as an admin-only peer admin module with legacy redirects", () => {
const source = readRouter();
const monitoringRouteStart = source.indexOf('name: "AdminSystemMonitoring"');
const monitoringRouteEnd = source.indexOf("],", monitoringRouteStart);
const monitoringRoute = source.slice(monitoringRouteStart, monitoringRouteEnd);
expect(source).toContain('import SystemMonitoringPage from "../views/admin/SystemMonitoringPage.vue";');
expect(source).toContain('path: "system-monitoring"');
expect(monitoringRoute).toContain("component: SystemMonitoringPage");
expect(monitoringRoute).toContain("requiresAdmin: true");
expect(source).toContain('path: "permission-monitoring"');
expect(source).toContain('path: "permissions/monitoring"');
expect(source).toContain('redirect: "/admin/system-monitoring"');
expect(source).not.toContain('const SYSTEM_PERMISSION_MONITORING_METRICS = "system:monitoring:metrics";');
expect(source).not.toContain("[SYSTEM_PERMISSION_MONITORING_METRICS]");
});
it("keeps login landing independent from PM management backend access", () => {
const source = readRouter();
+15 -8
View File
@@ -18,6 +18,7 @@ import AdminUserApproval from "../views/admin/AdminUserApproval.vue";
import AdminProjects from "../views/admin/Projects.vue";
import AdminSites from "../views/admin/Sites.vue";
import PermissionManagement from "../views/admin/PermissionManagement.vue";
import SystemMonitoringPage from "../views/admin/SystemMonitoringPage.vue";
import ProjectDetail from "../views/admin/ProjectDetail.vue";
import ProfileSettings from "../views/ProfileSettings.vue";
import ProjectOverview from "../views/ia/ProjectOverview.vue";
@@ -57,7 +58,6 @@ import { TEXT } from "../locales";
const SYSTEM_PERMISSION_READ = "system:permissions:read";
const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config";
const SYSTEM_PERMISSION_MONITORING_METRICS = "system:monitoring:metrics";
const routes: RouteRecordRaw[] = [
{
@@ -372,6 +372,20 @@ const routes: RouteRecordRaw[] = [
component: AuditLogs,
meta: { title: TEXT.menu.auditLogs, adminProjectPermission: { module: "audit_export", action: "read" } },
},
{
path: "system-monitoring",
name: "AdminSystemMonitoring",
component: SystemMonitoringPage,
meta: { title: TEXT.menu.systemMonitoring, requiresAdmin: true },
},
{
path: "permission-monitoring",
redirect: "/admin/system-monitoring",
},
{
path: "permissions/monitoring",
redirect: "/admin/system-monitoring",
},
{
path: "permissions",
redirect: "/admin/permissions/system",
@@ -388,12 +402,6 @@ const routes: RouteRecordRaw[] = [
component: PermissionManagement,
meta: { title: "项目权限配置", systemPermission: SYSTEM_PERMISSION_PROJECT_CONFIG },
},
{
path: "permissions/monitoring",
name: "AdminPermissionsMonitoring",
component: PermissionManagement,
meta: { title: "权限监控", systemPermission: SYSTEM_PERMISSION_MONITORING_METRICS },
},
],
},
{
@@ -416,7 +424,6 @@ const ADMIN_PROJECT_OPERATION_KEYS: Record<string, Record<"read" | "write", stri
const SYSTEM_PERMISSION_ROLES: Record<string, string[]> = {
[SYSTEM_PERMISSION_READ]: ["ADMIN", "PM"],
[SYSTEM_PERMISSION_PROJECT_CONFIG]: ["ADMIN", "PM"],
[SYSTEM_PERMISSION_MONITORING_METRICS]: ["ADMIN", "PM"],
};
const getTargetProjectId = (to: any) => {
+4 -4
View File
@@ -218,7 +218,7 @@ export interface HealthResponse {
cache_stats: CacheStatsResponse;
}
// 权限监控 - 访问日志
// 系统监测 - 访问日志
export interface AccessLogItem {
id: string;
study_id: string;
@@ -293,7 +293,7 @@ export interface SecurityAccessLogsResponse {
items: SecurityAccessLogItem[];
}
// 权限监控 - 趋势数据
// 系统监测 - 趋势数据
export interface TrendDataPoint {
bucket_time: string;
total_checks: number;
@@ -312,7 +312,7 @@ export interface TrendsResponse {
data_points: TrendDataPoint[];
}
// 权限监控 - 被拒绝最多
// 系统监测 - 被拒绝最多
export interface TopDeniedItem {
endpoint_key: string;
role: string;
@@ -349,7 +349,7 @@ export interface IpLocationsResponse {
items: IpLocationStatItem[];
}
// 权限监控 - 统计摘要
// 系统监测 - 统计摘要
export interface StatsSummaryResponse {
total_logs: number;
today: {
@@ -59,7 +59,8 @@ describe("ApiPermissions.vue", () => {
const source = readFileSync(resolve(__dirname, "./ApiPermissions.vue"), "utf8");
expect(source).toContain('label="角色权限"');
expect(source).toContain('label="权限监控"');
expect(source).not.toContain('label="权限监控"');
expect(source).not.toContain("<PermissionMonitoring");
});
it("has save button disabled when not dirty", () => {
+2 -70
View File
@@ -19,10 +19,6 @@
</span>
</div>
<div class="permission-actions">
<el-button @click="resetMetrics" :loading="resetting">
<el-icon><RefreshRight /></el-icon>
刷新指标
</el-button>
<el-button type="primary" :loading="saving" :disabled="!dirty" @click="save">
<el-icon><Check /></el-icon>
保存
@@ -46,17 +42,6 @@
@update="onApiMatrixUpdate"
/>
</el-tab-pane>
<!-- 权限监控 -->
<el-tab-pane label="权限监控" name="monitoring">
<PermissionMonitoring
:is-admin="isAdmin"
:metrics="metrics"
:health="health"
:alerts="alerts"
@refresh="loadMonitoringData"
/>
</el-tab-pane>
</el-tabs>
</div>
</div>
@@ -66,39 +51,24 @@
import { ref, computed, onMounted } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { Key, RefreshRight, Check } from "@element-plus/icons-vue";
import { Key, Check } from "@element-plus/icons-vue";
import type {
ApiEndpointPermissionsResponse,
PermissionMetricsResponse,
CacheStatsResponse,
AlertsResponse,
HealthResponse,
} from "@/types/api";
import {
fetchApiEndpointPermissions,
updateApiEndpointPermissions,
fetchPermissionMetrics,
fetchCacheStats,
fetchPermissionAlerts,
fetchPermissionHealth,
resetPermissionMetrics,
} from "@/api/projectPermissions";
import { useStudyStore } from "@/store/study";
import { useAuthStore } from "@/store/auth";
import { isSystemAdmin } from "@/utils/roles";
import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue";
import PermissionMonitoring from "@/components/PermissionMonitoring.vue";
import PermissionTemplateSelector from "@/components/PermissionTemplateSelector.vue";
const route = useRoute();
const studyStore = useStudyStore();
const auth = useAuthStore();
const isAdmin = computed(() => isSystemAdmin(auth.user));
const activeTab = ref<"api" | "monitoring">("api");
const activeTab = ref<"api">("api");
const loading = ref(false);
const saving = ref(false);
const resetting = ref(false);
const project = computed(() => studyStore.currentStudy);
const studyId = computed(() => {
@@ -109,11 +79,6 @@ const studyId = computed(() => {
// 权限数据
const apiMatrix = ref<ApiEndpointPermissionsResponse | null>(null);
// 监控数据
const metrics = ref<PermissionMetricsResponse | null>(null);
const health = ref<HealthResponse | null>(null);
const alerts = ref<AlertsResponse | null>(null);
// 脏值检测
const dirty = ref(false);
@@ -133,25 +98,6 @@ const loadPermissionData = async () => {
}
};
const loadMonitoringData = async () => {
if (!studyId.value) return;
try {
const [metricsRes, healthRes, alertsRes] = await Promise.all([
fetchPermissionMetrics(),
fetchPermissionHealth(),
fetchPermissionAlerts(undefined, 20),
]);
metrics.value = metricsRes.data;
health.value = healthRes.data;
alerts.value = alertsRes.data;
} catch (error) {
ElMessage.error("加载监控数据失败");
console.error(error);
}
};
const onApiMatrixUpdate = (newMatrix: ApiEndpointPermissionsResponse) => {
apiMatrix.value = newMatrix;
dirty.value = true;
@@ -209,20 +155,6 @@ const save = async () => {
}
};
const resetMetrics = async () => {
resetting.value = true;
try {
await resetPermissionMetrics();
await loadMonitoringData();
ElMessage.success("指标已重置");
} catch (error) {
ElMessage.error("重置指标失败");
console.error(error);
} finally {
resetting.value = false;
}
};
onMounted(() => {
loadPermissionData();
});
@@ -351,7 +351,9 @@ describe("permission management custom roles", () => {
it("does not render the duplicated monitoring header", () => {
const source = readSource();
expect(source).toContain('<PermissionMonitoring :is-admin="isAdmin" />');
expect(source).not.toContain("<PermissionMonitoring");
expect(source).not.toContain('activeTab === "monitoring"');
expect(source).not.toContain("/admin/permissions/monitoring");
expect(source).not.toContain("实时监控权限使用情况、趋势分析与异常告警");
expect(source).not.toContain("重置指标");
expect(source).not.toContain("refreshMonitoring");
@@ -215,18 +215,6 @@
</div>
</template>
<!--
权限监控
-->
<template v-else-if="activeTab === 'monitoring'">
<div class="perm-body">
<div class="perm-body-padded">
<PermissionMonitoring :is-admin="isAdmin" />
</div>
</div>
</template>
<!--
角色管理抽屉
-->
@@ -612,7 +600,6 @@ import {
} from "@/utils/projectPermissionModules";
import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta";
import ApiEndpointPermissions from "@/components/ApiEndpointPermissions.vue";
import PermissionMonitoring from "@/components/PermissionMonitoring.vue";
import PermissionTemplateSelector from "@/components/PermissionTemplateSelector.vue";
const route = useRoute();
@@ -657,16 +644,15 @@ const memberRoleLabels = (row: MemberRoleRow) => {
};
// ── 顶层标签 ──
const tabFromPath = (): "project" | "system" | "monitoring" => {
const tabFromPath = (): "project" | "system" => {
const p = route.path;
if (p.endsWith("/system")) return "system";
if (p.endsWith("/monitoring")) return "monitoring";
return "project";
};
const activeTab = computed({
get: () => tabFromPath(),
set: (tab: "project" | "system" | "monitoring") => {
const pathMap = { system: "/admin/permissions/system", project: "/admin/permissions/project", monitoring: "/admin/permissions/monitoring" };
set: (tab: "project" | "system") => {
const pathMap = { system: "/admin/permissions/system", project: "/admin/permissions/project" };
router.push({ path: pathMap[tab], query: route.query });
},
});
@@ -0,0 +1,18 @@
<template>
<div class="system-monitoring-page">
<PermissionMonitoring :is-admin="true" />
</div>
</template>
<script setup lang="ts">
import PermissionMonitoring from "@/components/PermissionMonitoring.vue";
</script>
<style scoped>
.system-monitoring-page {
margin: -6px -8px;
min-height: calc(100vh - 52px);
min-height: calc(100dvh - 52px);
background: #f5f7fa;
}
</style>