This commit is contained in:
+50
-8
@@ -285,6 +285,15 @@ type ApiAppRelease = {
|
|||||||
app?: ApiApp;
|
app?: ApiApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LicensePlanSummary = {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
assignedCount: number;
|
||||||
|
activeUserCount: number;
|
||||||
|
expiringSoonCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
type CommonAppPipeline = {
|
type CommonAppPipeline = {
|
||||||
id: string;
|
id: string;
|
||||||
repository: string;
|
repository: string;
|
||||||
@@ -385,6 +394,18 @@ type AdminRow = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const statusTone = {
|
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',
|
'실행 중': 'green',
|
||||||
게시됨: 'green',
|
게시됨: 'green',
|
||||||
@@ -869,16 +890,22 @@ function SkeletonBlock({ className }: { className?: string }) {
|
|||||||
function MetricCards({
|
function MetricCards({
|
||||||
apps: dashboardApps,
|
apps: dashboardApps,
|
||||||
sessions,
|
sessions,
|
||||||
|
licensePlans,
|
||||||
runtimeResources,
|
runtimeResources,
|
||||||
loading
|
loading
|
||||||
}: {
|
}: {
|
||||||
apps: ApiApp[];
|
apps: ApiApp[];
|
||||||
sessions: UserSessionRow[];
|
sessions: UserSessionRow[];
|
||||||
|
licensePlans: LicensePlanSummary[];
|
||||||
runtimeResources: DashboardRuntimeResource[];
|
runtimeResources: DashboardRuntimeResource[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}) {
|
}) {
|
||||||
const activeApps = dashboardApps.filter((app) => isActiveStatus(app.status)).length;
|
const activeApps = dashboardApps.filter((app) => isActiveStatus(app.status)).length;
|
||||||
const activeSessions = sessions.filter((session) => session.state === '활성').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 healthyRuntimeCount = runtimeResources.filter(isRuntimeResourceHealthy).length;
|
||||||
const runtimeRate = runtimeResources.length ? Math.round((healthyRuntimeCount / runtimeResources.length) * 100) : null;
|
const runtimeRate = runtimeResources.length ? Math.round((healthyRuntimeCount / runtimeResources.length) * 100) : null;
|
||||||
const cards = [
|
const cards = [
|
||||||
@@ -898,10 +925,10 @@ function MetricCards({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '라이센스 사용률',
|
label: '라이센스 사용률',
|
||||||
value: '-',
|
value: loading || licenseUsageRate === null ? '-' : `${licenseUsageRate}%`,
|
||||||
delta: '연동 필요',
|
delta: assignedLicenses ? `${activeLicenseUsers}/${assignedLicenses}` : '할당 없음',
|
||||||
hint: '라이센스 백엔드 집계 API 필요',
|
hint: expiringSoonLicenses ? `만료 예정 ${expiringSoonLicenses}명` : '활성 라이센스 사용자 기준',
|
||||||
tone: 'neutral' as const
|
tone: licenseUsageRate !== null && licenseUsageRate >= 90 ? 'amber' as const : 'green' as const
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '런타임 정상률',
|
label: '런타임 정상률',
|
||||||
@@ -1017,7 +1044,7 @@ function LauncherPanel({ versions, loading, onOpen }: { versions: ApiLauncherVer
|
|||||||
function CommonAppsPanel({ pipelines, loading }: { pipelines: CommonAppPipeline[]; loading: boolean }) {
|
function CommonAppsPanel({ pipelines, loading }: { pipelines: CommonAppPipeline[]; loading: boolean }) {
|
||||||
return (
|
return (
|
||||||
<Card className="min-w-0 overflow-hidden">
|
<Card className="min-w-0 overflow-hidden">
|
||||||
<CardHeader title="공통앱" description="Woodpecker의 모든 저장소를 마지막 파이프라인 생성 순으로 표시합니다." />
|
<CardHeader title="공통앱" description="공통앱의 최근 빌드 상태를 확인합니다." />
|
||||||
<div className="max-h-[420px] overflow-y-auto">
|
<div className="max-h-[420px] overflow-y-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="space-y-3 p-5">
|
<div className="space-y-3 p-5">
|
||||||
@@ -2390,6 +2417,17 @@ function loadNotices() {
|
|||||||
.then((data) => data.notices ?? []);
|
.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() {
|
function loadCommonAppPipelines() {
|
||||||
return apiFetch(`${apiBaseUrl}/woodpecker/repos`, { headers: authHeaders() })
|
return apiFetch(`${apiBaseUrl}/woodpecker/repos`, { headers: authHeaders() })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -3261,6 +3299,7 @@ function DashboardPage({
|
|||||||
const [dashboardApps, setDashboardApps] = useState<ApiApp[]>([]);
|
const [dashboardApps, setDashboardApps] = useState<ApiApp[]>([]);
|
||||||
const [dashboardReleases, setDashboardReleases] = useState<ApiAppRelease[]>([]);
|
const [dashboardReleases, setDashboardReleases] = useState<ApiAppRelease[]>([]);
|
||||||
const [recentSessions, setRecentSessions] = useState<UserSessionRow[]>([]);
|
const [recentSessions, setRecentSessions] = useState<UserSessionRow[]>([]);
|
||||||
|
const [licensePlansSummary, setLicensePlansSummary] = useState<LicensePlanSummary[]>([]);
|
||||||
const [commonAppPipelines, setCommonAppPipelines] = useState<CommonAppPipeline[]>([]);
|
const [commonAppPipelines, setCommonAppPipelines] = useState<CommonAppPipeline[]>([]);
|
||||||
const [dashboardNotices, setDashboardNotices] = useState<ApiNotice[]>([]);
|
const [dashboardNotices, setDashboardNotices] = useState<ApiNotice[]>([]);
|
||||||
const [runtimeResources, setRuntimeResources] = useState<DashboardRuntimeResource[]>([]);
|
const [runtimeResources, setRuntimeResources] = useState<DashboardRuntimeResource[]>([]);
|
||||||
@@ -3271,12 +3310,13 @@ function DashboardPage({
|
|||||||
|
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
setDashboardLoading(true);
|
setDashboardLoading(true);
|
||||||
const [versionsResult, appsResult, releasesResult, sessionsResult, noticesResult, commonAppsResult] = await Promise.allSettled([
|
const [versionsResult, appsResult, releasesResult, sessionsResult, noticesResult, licensePlansResult, commonAppsResult] = await Promise.allSettled([
|
||||||
loadLauncherVersions(),
|
loadLauncherVersions(),
|
||||||
loadApps(),
|
loadApps(),
|
||||||
loadAppReleases(),
|
loadAppReleases(),
|
||||||
loadUserSessions(),
|
loadUserSessions(),
|
||||||
loadNotices(),
|
loadNotices(),
|
||||||
|
loadLicensePlans(),
|
||||||
loadCommonAppPipelines()
|
loadCommonAppPipelines()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -3285,6 +3325,7 @@ function DashboardPage({
|
|||||||
const nextReleases = releasesResult.status === 'fulfilled' ? releasesResult.value : [];
|
const nextReleases = releasesResult.status === 'fulfilled' ? releasesResult.value : [];
|
||||||
const nextSessions = sessionsResult.status === 'fulfilled' ? sessionsResult.value : [];
|
const nextSessions = sessionsResult.status === 'fulfilled' ? sessionsResult.value : [];
|
||||||
const nextNotices = noticesResult.status === 'fulfilled' ? noticesResult.value : [];
|
const nextNotices = noticesResult.status === 'fulfilled' ? noticesResult.value : [];
|
||||||
|
const nextLicensePlans = licensePlansResult.status === 'fulfilled' ? licensePlansResult.value : [];
|
||||||
const nextCommonApps = commonAppsResult.status === 'fulfilled' ? commonAppsResult.value : [];
|
const nextCommonApps = commonAppsResult.status === 'fulfilled' ? commonAppsResult.value : [];
|
||||||
const namespaceApps = nextApps.filter((app) => app.namespace);
|
const namespaceApps = nextApps.filter((app) => app.namespace);
|
||||||
const [runtimeResults, metricResults] = await Promise.all([
|
const [runtimeResults, metricResults] = await Promise.all([
|
||||||
@@ -3322,6 +3363,7 @@ function DashboardPage({
|
|||||||
setDashboardApps(nextApps);
|
setDashboardApps(nextApps);
|
||||||
setDashboardReleases(nextReleases);
|
setDashboardReleases(nextReleases);
|
||||||
setRecentSessions(nextSessions);
|
setRecentSessions(nextSessions);
|
||||||
|
setLicensePlansSummary(nextLicensePlans);
|
||||||
setCommonAppPipelines(nextCommonApps);
|
setCommonAppPipelines(nextCommonApps);
|
||||||
setDashboardNotices(nextNotices);
|
setDashboardNotices(nextNotices);
|
||||||
setRuntimeResources(nextRuntimeResources);
|
setRuntimeResources(nextRuntimeResources);
|
||||||
@@ -3343,10 +3385,10 @@ function DashboardPage({
|
|||||||
<PageTitle
|
<PageTitle
|
||||||
eyebrow="플랫폼 운영"
|
eyebrow="플랫폼 운영"
|
||||||
title="대시보드"
|
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">
|
<div className="mt-4 grid gap-4 xl:grid-cols-2">
|
||||||
<LauncherPanel versions={launcherRows} loading={dashboardLoading} onOpen={onLauncherVersionsOpen} />
|
<LauncherPanel versions={launcherRows} loading={dashboardLoading} onOpen={onLauncherVersionsOpen} />
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export function DashboardPage() {
|
|||||||
<Card className="min-w-0 overflow-hidden">
|
<Card className="min-w-0 overflow-hidden">
|
||||||
<CardHeader
|
<CardHeader
|
||||||
title="공통앱"
|
title="공통앱"
|
||||||
description="Woodpecker의 모든 저장소를 마지막 파이프라인 생성 순으로 표시합니다."
|
description="공통앱의 최근 빌드 상태를 확인합니다."
|
||||||
action={
|
action={
|
||||||
<Button variant="outline" className="size-8 px-0" onClick={refreshCommonApps} disabled={commonAppsLoading} title="새로고침" aria-label="새로고침">
|
<Button variant="outline" className="size-8 px-0" onClick={refreshCommonApps} disabled={commonAppsLoading} title="새로고침" aria-label="새로고침">
|
||||||
<RefreshCw className={cn('size-4', commonAppsLoading && 'animate-spin')} />
|
<RefreshCw className={cn('size-4', commonAppsLoading && 'animate-spin')} />
|
||||||
|
|||||||
Reference in New Issue
Block a user