diff --git a/src/App.tsx b/src/App.tsx index ae617fc..4e9c38a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -285,6 +285,15 @@ type ApiAppRelease = { app?: ApiApp; }; +type LicensePlanSummary = { + id: string; + code: string; + name: string; + assignedCount: number; + activeUserCount: number; + expiringSoonCount: number; +}; + type CommonAppPipeline = { id: string; repository: string; @@ -385,6 +394,18 @@ type AdminRow = { }; const statusTone = { + success: 'green', + succeeded: 'green', + done: 'green', + running: 'blue', + started: 'blue', + pending: 'amber', + blocked: 'amber', + created: 'amber', + failure: 'red', + failed: 'red', + error: 'red', + killed: 'red', 활성: 'green', '실행 중': 'green', 게시됨: 'green', @@ -869,16 +890,22 @@ function SkeletonBlock({ className }: { className?: string }) { function MetricCards({ apps: dashboardApps, sessions, + licensePlans, runtimeResources, loading }: { apps: ApiApp[]; sessions: UserSessionRow[]; + licensePlans: LicensePlanSummary[]; runtimeResources: DashboardRuntimeResource[]; loading: boolean; }) { const activeApps = dashboardApps.filter((app) => isActiveStatus(app.status)).length; const activeSessions = sessions.filter((session) => session.state === '활성').length; + const assignedLicenses = licensePlans.reduce((sum, plan) => sum + plan.assignedCount, 0); + const activeLicenseUsers = licensePlans.reduce((sum, plan) => sum + plan.activeUserCount, 0); + const expiringSoonLicenses = licensePlans.reduce((sum, plan) => sum + plan.expiringSoonCount, 0); + const licenseUsageRate = assignedLicenses ? Math.round((activeLicenseUsers / assignedLicenses) * 100) : null; const healthyRuntimeCount = runtimeResources.filter(isRuntimeResourceHealthy).length; const runtimeRate = runtimeResources.length ? Math.round((healthyRuntimeCount / runtimeResources.length) * 100) : null; const cards = [ @@ -898,10 +925,10 @@ function MetricCards({ }, { label: '라이센스 사용률', - value: '-', - delta: '연동 필요', - hint: '라이센스 백엔드 집계 API 필요', - tone: 'neutral' as const + value: loading || licenseUsageRate === null ? '-' : `${licenseUsageRate}%`, + delta: assignedLicenses ? `${activeLicenseUsers}/${assignedLicenses}` : '할당 없음', + hint: expiringSoonLicenses ? `만료 예정 ${expiringSoonLicenses}명` : '활성 라이센스 사용자 기준', + tone: licenseUsageRate !== null && licenseUsageRate >= 90 ? 'amber' as const : 'green' as const }, { label: '런타임 정상률', @@ -1017,7 +1044,7 @@ function LauncherPanel({ versions, loading, onOpen }: { versions: ApiLauncherVer function CommonAppsPanel({ pipelines, loading }: { pipelines: CommonAppPipeline[]; loading: boolean }) { return ( - +
{loading ? (
@@ -2390,6 +2417,17 @@ function loadNotices() { .then((data) => data.notices ?? []); } +function loadLicensePlans() { + return apiFetch(`${apiBaseUrl}/licenses/plans`, { headers: authHeaders() }) + .then((response) => { + if (!response.ok) { + throw new Error('License plans request failed'); + } + return response.json() as Promise<{ plans?: LicensePlanSummary[] }>; + }) + .then((data) => data.plans ?? []); +} + function loadCommonAppPipelines() { return apiFetch(`${apiBaseUrl}/woodpecker/repos`, { headers: authHeaders() }) .then((response) => { @@ -3261,6 +3299,7 @@ function DashboardPage({ const [dashboardApps, setDashboardApps] = useState([]); const [dashboardReleases, setDashboardReleases] = useState([]); const [recentSessions, setRecentSessions] = useState([]); + const [licensePlansSummary, setLicensePlansSummary] = useState([]); const [commonAppPipelines, setCommonAppPipelines] = useState([]); const [dashboardNotices, setDashboardNotices] = useState([]); const [runtimeResources, setRuntimeResources] = useState([]); @@ -3271,12 +3310,13 @@ function DashboardPage({ async function loadDashboard() { setDashboardLoading(true); - const [versionsResult, appsResult, releasesResult, sessionsResult, noticesResult, commonAppsResult] = await Promise.allSettled([ + const [versionsResult, appsResult, releasesResult, sessionsResult, noticesResult, licensePlansResult, commonAppsResult] = await Promise.allSettled([ loadLauncherVersions(), loadApps(), loadAppReleases(), loadUserSessions(), loadNotices(), + loadLicensePlans(), loadCommonAppPipelines() ]); @@ -3285,6 +3325,7 @@ function DashboardPage({ const nextReleases = releasesResult.status === 'fulfilled' ? releasesResult.value : []; const nextSessions = sessionsResult.status === 'fulfilled' ? sessionsResult.value : []; const nextNotices = noticesResult.status === 'fulfilled' ? noticesResult.value : []; + const nextLicensePlans = licensePlansResult.status === 'fulfilled' ? licensePlansResult.value : []; const nextCommonApps = commonAppsResult.status === 'fulfilled' ? commonAppsResult.value : []; const namespaceApps = nextApps.filter((app) => app.namespace); const [runtimeResults, metricResults] = await Promise.all([ @@ -3322,6 +3363,7 @@ function DashboardPage({ setDashboardApps(nextApps); setDashboardReleases(nextReleases); setRecentSessions(nextSessions); + setLicensePlansSummary(nextLicensePlans); setCommonAppPipelines(nextCommonApps); setDashboardNotices(nextNotices); setRuntimeResources(nextRuntimeResources); @@ -3343,10 +3385,10 @@ function DashboardPage({ - +
diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 9d0b0e0..b1a8613 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -111,7 +111,7 @@ export function DashboardPage() {