connect dashboard license usage
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-07 09:05:17 +09:00
parent bf833f772c
commit 3930da8ef2
2 changed files with 51 additions and 9 deletions
+50 -8
View File
@@ -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 (
<Card className="min-w-0 overflow-hidden">
<CardHeader title="공통앱" description="Woodpecker의 모든 저장소를 마지막 파이프라인 생성 순으로 표시합니다." />
<CardHeader title="공통앱" description="공통앱의 최근 빌드 상태를 확인합니다." />
<div className="max-h-[420px] overflow-y-auto">
{loading ? (
<div className="space-y-3 p-5">
@@ -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<ApiApp[]>([]);
const [dashboardReleases, setDashboardReleases] = useState<ApiAppRelease[]>([]);
const [recentSessions, setRecentSessions] = useState<UserSessionRow[]>([]);
const [licensePlansSummary, setLicensePlansSummary] = useState<LicensePlanSummary[]>([]);
const [commonAppPipelines, setCommonAppPipelines] = useState<CommonAppPipeline[]>([]);
const [dashboardNotices, setDashboardNotices] = useState<ApiNotice[]>([]);
const [runtimeResources, setRuntimeResources] = useState<DashboardRuntimeResource[]>([]);
@@ -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({
<PageTitle
eyebrow="플랫폼 운영"
title="대시보드"
description="플랫폼 운영에 필요한 주요 현황과 작업 흐름을 한눈에 확인합니다."
description="플랫폼의 현재 상태와 운영 흐름을 빠르게 파악합니다."
/>
<MetricCards apps={dashboardApps} sessions={recentSessions} runtimeResources={runtimeResources} loading={dashboardLoading} />
<MetricCards apps={dashboardApps} sessions={recentSessions} licensePlans={licensePlansSummary} runtimeResources={runtimeResources} loading={dashboardLoading} />
<div className="mt-4 grid gap-4 xl:grid-cols-2">
<LauncherPanel versions={launcherRows} loading={dashboardLoading} onOpen={onLauncherVersionsOpen} />
+1 -1
View File
@@ -111,7 +111,7 @@ export function DashboardPage() {
<Card className="min-w-0 overflow-hidden">
<CardHeader
title="공통앱"
description="Woodpecker의 모든 저장소를 마지막 파이프라인 생성 순으로 표시합니다."
description="공통앱의 최근 빌드 상태를 확인합니다."
action={
<Button variant="outline" className="size-8 px-0" onClick={refreshCommonApps} disabled={commonAppsLoading} title="새로고침" aria-label="새로고침">
<RefreshCw className={cn('size-4', commonAppsLoading && 'animate-spin')} />