260707_2
This commit is contained in:
@@ -207,6 +207,12 @@ export async function fetchCompanyProjects(): Promise<ProjectItem[]> {
|
||||
return data.projects;
|
||||
}
|
||||
|
||||
export async function fetchAllProjects(): Promise<ProjectItem[]> {
|
||||
const data = await request<{ projects: ProjectItem[] }>("/dashboard/admin/projects-all");
|
||||
return data.projects;
|
||||
}
|
||||
|
||||
|
||||
export async function fetchAllCompanies(): Promise<CompanyInfo[]> {
|
||||
const data = await request<{ companies: CompanyInfo[] }>("/dashboard/admin/companies");
|
||||
return data.companies;
|
||||
|
||||
@@ -103,6 +103,19 @@ async def list_company_projects(company_id: int) -> list[dict[str, Any]]:
|
||||
return [_project_row(row) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
async def list_all_projects() -> list[dict[str, Any]]:
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""SELECT p.id, p.name, p.region, p.status, p.updated_at, p.created_at,
|
||||
u.name AS owner_name, u.email AS owner_email
|
||||
FROM projects p LEFT JOIN users u ON u.id = p.user_id
|
||||
WHERE p.deleted_at IS NULL
|
||||
ORDER BY p.updated_at DESC, p.created_at DESC"""
|
||||
)
|
||||
return [_project_row(row) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
async def get_user_company(company_id: int | None) -> dict[str, Any] | None:
|
||||
if company_id is None:
|
||||
return None
|
||||
@@ -399,10 +412,23 @@ async def get_system_resources(days: int = 30) -> dict[str, Any]:
|
||||
# 다운샘플링: 조회 구간을 최대 TARGET_POINTS개 시간버킷으로 나눠 평균.
|
||||
# 계측 간격(2분)보다 버킷이 작으면 원본 그대로(버킷=간격) 반환.
|
||||
target_points = 300
|
||||
bucket_seconds = max(120, (days * 86400) // target_points)
|
||||
await cursor.execute(
|
||||
"""SELECT
|
||||
FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(timestamp) / %s) * %s) AS timestamp,
|
||||
"SELECT COUNT(*) AS cnt FROM system_resources WHERE timestamp >= %s",
|
||||
(since,),
|
||||
)
|
||||
row_cnt_res = await cursor.fetchone()
|
||||
row_count = row_cnt_res["cnt"] if row_cnt_res else 0
|
||||
if row_count <= target_points:
|
||||
bucket_seconds = 120
|
||||
else:
|
||||
bucket_seconds = max(120, (days * 86400) // target_points)
|
||||
date_fmt = "%%Y-%%m-%%dT%%H:%%i:%%s"
|
||||
await cursor.execute(
|
||||
f"""SELECT
|
||||
DATE_FORMAT(
|
||||
FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(timestamp) / %s) * %s),
|
||||
'{date_fmt}'
|
||||
) AS timestamp,
|
||||
ROUND(AVG(cpu_usage_percent), 2) AS cpu_usage_percent,
|
||||
ROUND(AVG(memory_usage_percent), 2) AS memory_usage_percent,
|
||||
ROUND(AVG(disk_usage_percent), 2) AS disk_usage_percent,
|
||||
|
||||
@@ -16,6 +16,7 @@ from .B01_Dashboard_Repository import (
|
||||
get_user_company,
|
||||
join_company,
|
||||
list_all_companies,
|
||||
list_all_projects,
|
||||
list_all_users,
|
||||
list_audit_logs,
|
||||
list_company_members,
|
||||
@@ -236,3 +237,10 @@ async def system_resources(
|
||||
):
|
||||
_ = session
|
||||
return {"status": "success", **await get_system_resources(days)}
|
||||
|
||||
|
||||
@router.get("/admin/projects-all")
|
||||
async def system_projects(session: dict[str, Any] = Depends(require_system_admin)):
|
||||
_ = session
|
||||
return {"status": "success", "projects": await list_all_projects()}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createCompany,
|
||||
fetchAllCompanies,
|
||||
fetchAllJoinRequests,
|
||||
fetchAllProjects,
|
||||
fetchAllUsers,
|
||||
fetchAuditLogs,
|
||||
fetchCompanyJoinRequests,
|
||||
@@ -52,6 +53,7 @@ interface DashboardState {
|
||||
user: DashboardUser;
|
||||
userProjects: ProjectItem[];
|
||||
companyProjects: ProjectItem[];
|
||||
allProjects: ProjectItem[];
|
||||
company: CompanyInfo | null;
|
||||
members: Member[];
|
||||
joinRequests: JoinRequest[];
|
||||
@@ -70,6 +72,7 @@ export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
||||
user,
|
||||
userProjects: [],
|
||||
companyProjects: [],
|
||||
allProjects: [],
|
||||
company: null,
|
||||
members: [],
|
||||
joinRequests: [],
|
||||
@@ -90,18 +93,20 @@ export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
||||
|
||||
async function loadRoleData(state: DashboardState): Promise<void> {
|
||||
if (state.user.role === "SYSTEM_ADMIN") {
|
||||
const [companies, users, requests, logs, resources] = await Promise.all([
|
||||
const [companies, users, requests, logs, resources, projects] = await Promise.all([
|
||||
fetchAllCompanies(),
|
||||
fetchAllUsers(),
|
||||
fetchAllJoinRequests(),
|
||||
fetchAuditLogs(),
|
||||
fetchSystemResources(),
|
||||
fetchSystemResources(7),
|
||||
fetchAllProjects(),
|
||||
]);
|
||||
state.allCompanies = companies;
|
||||
state.allUsers = users;
|
||||
state.allJoinRequests = requests;
|
||||
state.auditLogs = logs;
|
||||
state.resources = resources;
|
||||
state.allProjects = projects;
|
||||
return;
|
||||
}
|
||||
const [projects, company] = await Promise.all([fetchUserProjects(), fetchUserCompany()]);
|
||||
@@ -130,8 +135,9 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
if (state.user.role === "SYSTEM_ADMIN") {
|
||||
grid.append(
|
||||
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true),
|
||||
section(L("B01_Dashboard_Companies"), companyTable(state.allCompanies)),
|
||||
section(L("B01_Dashboard_Users"), userTable(state.allUsers)),
|
||||
section(L("B01_Dashboard_Companies"), companyTable(state.allCompanies), true),
|
||||
section(L("B01_Dashboard_Users"), userTable(state.allUsers), true),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects), true),
|
||||
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.allJoinRequests, true)),
|
||||
section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true),
|
||||
);
|
||||
@@ -415,32 +421,96 @@ function buildResourceMetrics(resources: ResourceData | null): HTMLElement {
|
||||
|
||||
function formatChartTime(iso: string | null | undefined): string {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return "";
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return "";
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function buildResourceChart(resources: ResourceData | null): HTMLElement {
|
||||
const history = resources?.history ?? [];
|
||||
|
||||
// 7일간의 고정 타임라인 생성 (4시간 간격, 총 43개 포인트)
|
||||
const points: { timestamp: Date; cpu: number | null; mem: number | null; disk: number | null }[] = [];
|
||||
const now = new Date();
|
||||
|
||||
// 12시간 단위(정오 또는 자정)로 올림 정렬하여 기준점 계산
|
||||
const baseDate = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
now.getHours() >= 12 ? 24 : 12,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
for (let i = 0; i <= 42; i++) {
|
||||
// 7일 전부터 현재 정렬 시점까지 4시간 단위 버킷 생성
|
||||
const time = new Date(baseDate.getTime() - (42 - i) * 4 * 60 * 60 * 1000);
|
||||
points.push({ timestamp: time, cpu: null, mem: null, disk: null });
|
||||
}
|
||||
|
||||
// DB에서 불러온 계측 이력 매핑
|
||||
history.forEach((h) => {
|
||||
if (!h.timestamp) return;
|
||||
// DB에서 넘어오는 ISO 문자열을 UTC 기준으로 정확히 매핑하기 위해 Z를 보완하여 파싱
|
||||
const tsStr = h.timestamp.endsWith("Z") ? h.timestamp : h.timestamp + "Z";
|
||||
const hTime = new Date(tsStr);
|
||||
|
||||
// 가장 가까운 4시간 단위 버킷 매핑 (±2시간 내)
|
||||
let closestIdx = -1;
|
||||
let minDiff = Infinity;
|
||||
|
||||
points.forEach((p, idx) => {
|
||||
const diff = Math.abs(p.timestamp.getTime() - hTime.getTime());
|
||||
if (diff < minDiff && diff <= 2 * 60 * 60 * 1000) {
|
||||
minDiff = diff;
|
||||
closestIdx = idx;
|
||||
}
|
||||
});
|
||||
|
||||
if (closestIdx !== -1) {
|
||||
const p = points[closestIdx];
|
||||
p.cpu = h.cpu_usage_percent;
|
||||
p.mem = h.memory_usage_percent;
|
||||
p.disk = h.disk_usage_percent;
|
||||
}
|
||||
});
|
||||
|
||||
// xLabels 생성 (Midnight: MM/DD, 그 외는 빈칸)
|
||||
const xLabels = points.map((p) => {
|
||||
const hours = p.timestamp.getHours();
|
||||
if (hours === 0) {
|
||||
return `${p.timestamp.getMonth() + 1}/${p.timestamp.getDate()}`;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const chart = createLineChart({
|
||||
ariaLabel: L("B01_Dashboard_Resources_ChartAria"),
|
||||
xLabels: history.map((p) => formatChartTime(p.timestamp)),
|
||||
xLabels,
|
||||
width: 800,
|
||||
height: 140,
|
||||
series: [
|
||||
{
|
||||
name: L("B01_Dashboard_Metric_Cpu"),
|
||||
colorIndex: 0,
|
||||
values: history.map((p) => p.cpu_usage_percent),
|
||||
values: points.map((p) => p.cpu),
|
||||
},
|
||||
{
|
||||
name: L("B01_Dashboard_Metric_Memory"),
|
||||
colorIndex: 1,
|
||||
values: history.map((p) => p.memory_usage_percent),
|
||||
values: points.map((p) => p.mem),
|
||||
},
|
||||
{
|
||||
name: L("B01_Dashboard_Metric_Disk"),
|
||||
colorIndex: 2,
|
||||
values: history.map((p) => p.disk_usage_percent),
|
||||
values: points.map((p) => p.disk),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -152,6 +152,14 @@
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b01-dashboard__chart .ui-chart__plot {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.b01-dashboard__chart .ui-chart__svg {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.b01-dashboard__chart-caption {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
Reference in New Issue
Block a user