拆分系统监测模块并统一运维化文案
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "搜索端点",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user