connect tailscale node status through platform gateway
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+62
-35
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, type FormEvent, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState, type FormEvent, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Activity,
|
||||
@@ -236,15 +236,6 @@ type ApiAppRelease = {
|
||||
app?: ApiApp;
|
||||
};
|
||||
|
||||
type LicensePlanSummary = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
assignedCount: number;
|
||||
activeUserCount: number;
|
||||
expiringSoonCount: number;
|
||||
};
|
||||
|
||||
type CommonAppPipeline = {
|
||||
id: string;
|
||||
repository: string;
|
||||
@@ -300,6 +291,17 @@ type RuntimePodMetric = {
|
||||
memory: string;
|
||||
};
|
||||
|
||||
type DashboardJob = {
|
||||
id: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type ActiveWorkSummary = {
|
||||
builds: number;
|
||||
releases: number;
|
||||
deployments: number;
|
||||
};
|
||||
|
||||
type DashboardRuntimeResource = RuntimeResource & {
|
||||
namespace: string;
|
||||
appName: string;
|
||||
@@ -349,22 +351,37 @@ function loadNotices() {
|
||||
.then((response) => response.notices ?? []);
|
||||
}
|
||||
|
||||
function loadLicensePlans() {
|
||||
return apiRequest<{ plans?: LicensePlanSummary[] }>('/licenses/plans')
|
||||
.then((response) => response.plans ?? []);
|
||||
}
|
||||
|
||||
function loadCommonAppPipelines() {
|
||||
return apiRequest<{ repos: CommonAppPipeline[] }>('/woodpecker/repos')
|
||||
.then((result) => result.repos ?? []);
|
||||
}
|
||||
|
||||
function isActiveWorkStatus(status?: string | null) {
|
||||
return status === 'PENDING' || status === 'RUNNING';
|
||||
}
|
||||
|
||||
function loadActiveWorkSummary(releases: ApiAppRelease[]): Promise<ActiveWorkSummary> {
|
||||
const activeReleases = releases.filter((release) => isActiveWorkStatus(release.status)).length;
|
||||
return Promise.allSettled([
|
||||
apiRequest<{ jobs?: DashboardJob[] }>('/apps/builds?page=1&limit=20'),
|
||||
apiRequest<{ deployments?: DashboardJob[] }>('/apps/deployments?page=1&limit=20')
|
||||
]).then(([buildsResult, deploymentsResult]) => ({
|
||||
builds: buildsResult.status === 'fulfilled'
|
||||
? (buildsResult.value.jobs ?? []).filter((job) => isActiveWorkStatus(job.status)).length
|
||||
: 0,
|
||||
releases: activeReleases,
|
||||
deployments: deploymentsResult.status === 'fulfilled'
|
||||
? (deploymentsResult.value.deployments ?? []).filter((job) => isActiveWorkStatus(job.status)).length
|
||||
: 0
|
||||
}));
|
||||
}
|
||||
|
||||
function loadRuntimeNamespaceResources(namespace: string) {
|
||||
return apiRequest<RuntimeResourceResponse>(`/runtime/namespaces/${encodeURIComponent(namespace)}/resources`);
|
||||
return apiRequest<RuntimeResourceResponse>(`/apps/runtime-namespaces/${encodeURIComponent(namespace)}/resources`);
|
||||
}
|
||||
|
||||
function loadRuntimePodMetrics(namespace: string) {
|
||||
return apiRequest<RuntimePodMetric[]>(`/runtime/namespaces/${encodeURIComponent(namespace)}/pod-metrics`);
|
||||
return apiRequest<RuntimePodMetric[]>(`/runtime/metrics/pods?namespace=${encodeURIComponent(namespace)}`);
|
||||
}
|
||||
|
||||
const statusTone = {
|
||||
@@ -473,7 +490,21 @@ function Sidebar({
|
||||
}) {
|
||||
const [openMenus, setOpenMenus] = useState<Record<string, boolean>>({});
|
||||
const normalizedMenuSearch = menuSearch.trim().toLowerCase();
|
||||
const roleNavGroups = filterNavGroupsByRole(navGroups, currentAdmin?.role) as typeof navGroups;
|
||||
const roleNavGroups = useMemo(
|
||||
() => filterNavGroupsByRole(navGroups, currentAdmin?.role) as typeof navGroups,
|
||||
[currentAdmin?.role]
|
||||
);
|
||||
useEffect(() => {
|
||||
for (const group of roleNavGroups) {
|
||||
for (const item of group.items) {
|
||||
if (!('children' in item) || !item.children) continue;
|
||||
const hasActiveChild = item.children.some((child) => resolveNavView(child) === activeView);
|
||||
if (!hasActiveChild) continue;
|
||||
setOpenMenus((prev) => (prev[item.label] ? prev : { ...prev, [item.label]: true }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [activeView, roleNavGroups]);
|
||||
const visibleGroups = normalizedMenuSearch
|
||||
? roleNavGroups
|
||||
.map((group) => {
|
||||
@@ -846,22 +877,19 @@ function SkeletonBlock({ className }: { className?: string }) {
|
||||
function MetricCards({
|
||||
apps: dashboardApps,
|
||||
sessions,
|
||||
licensePlans,
|
||||
activeWork,
|
||||
runtimeResources,
|
||||
loading
|
||||
}: {
|
||||
apps: ApiApp[];
|
||||
sessions: UserSessionRow[];
|
||||
licensePlans: LicensePlanSummary[];
|
||||
activeWork: ActiveWorkSummary;
|
||||
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 activeWorkTotal = activeWork.builds + activeWork.releases + activeWork.deployments;
|
||||
const healthyRuntimeCount = runtimeResources.filter(isRuntimeResourceHealthy).length;
|
||||
const runtimeRate = runtimeResources.length ? Math.round((healthyRuntimeCount / runtimeResources.length) * 100) : null;
|
||||
const cards = [
|
||||
@@ -880,11 +908,11 @@ function MetricCards({
|
||||
tone: 'green' as const
|
||||
},
|
||||
{
|
||||
label: '라이센스 사용률',
|
||||
value: loading || licenseUsageRate === null ? '-' : `${licenseUsageRate}%`,
|
||||
delta: assignedLicenses ? `${activeLicenseUsers}/${assignedLicenses}` : '할당 없음',
|
||||
hint: expiringSoonLicenses ? `만료 예정 ${expiringSoonLicenses}명` : '활성 라이센스 사용자 기준',
|
||||
tone: licenseUsageRate !== null && licenseUsageRate >= 90 ? 'amber' as const : 'green' as const
|
||||
label: '진행 중 작업',
|
||||
value: loading ? '-' : `${activeWorkTotal}건`,
|
||||
delta: `빌드 ${activeWork.builds} · 릴리즈 ${activeWork.releases} · 배포 ${activeWork.deployments}`,
|
||||
hint: '대기/실행 중 작업 기준',
|
||||
tone: activeWorkTotal > 0 ? 'amber' as const : 'green' as const
|
||||
},
|
||||
{
|
||||
label: '런타임 정상률',
|
||||
@@ -1355,7 +1383,7 @@ function DashboardPage({
|
||||
const [dashboardApps, setDashboardApps] = useState<ApiApp[]>([]);
|
||||
const [dashboardReleases, setDashboardReleases] = useState<ApiAppRelease[]>([]);
|
||||
const [recentSessions, setRecentSessions] = useState<UserSessionRow[]>([]);
|
||||
const [licensePlansSummary, setLicensePlansSummary] = useState<LicensePlanSummary[]>([]);
|
||||
const [activeWorkSummary, setActiveWorkSummary] = useState<ActiveWorkSummary>({ builds: 0, releases: 0, deployments: 0 });
|
||||
const [commonAppPipelines, setCommonAppPipelines] = useState<CommonAppPipeline[]>([]);
|
||||
const [dashboardNotices, setDashboardNotices] = useState<ApiNotice[]>([]);
|
||||
const [runtimeResources, setRuntimeResources] = useState<DashboardRuntimeResource[]>([]);
|
||||
@@ -1366,13 +1394,12 @@ function DashboardPage({
|
||||
|
||||
async function loadDashboard() {
|
||||
setDashboardLoading(true);
|
||||
const [versionsResult, appsResult, releasesResult, sessionsResult, noticesResult, licensePlansResult, commonAppsResult] = await Promise.allSettled([
|
||||
const [versionsResult, appsResult, releasesResult, sessionsResult, noticesResult, commonAppsResult] = await Promise.allSettled([
|
||||
loadLauncherVersions(),
|
||||
loadApps(),
|
||||
loadAppReleases(),
|
||||
loadUserSessions(),
|
||||
loadNotices(),
|
||||
loadLicensePlans(),
|
||||
loadCommonAppPipelines()
|
||||
]);
|
||||
|
||||
@@ -1381,8 +1408,8 @@ 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 nextActiveWork = await loadActiveWorkSummary(nextReleases);
|
||||
const namespaceApps = nextApps.filter((app) => app.namespace);
|
||||
const [runtimeResults, metricResults] = await Promise.all([
|
||||
Promise.allSettled(
|
||||
@@ -1419,7 +1446,7 @@ function DashboardPage({
|
||||
setDashboardApps(nextApps);
|
||||
setDashboardReleases(nextReleases);
|
||||
setRecentSessions(nextSessions);
|
||||
setLicensePlansSummary(nextLicensePlans);
|
||||
setActiveWorkSummary(nextActiveWork);
|
||||
setCommonAppPipelines(nextCommonApps);
|
||||
setDashboardNotices(nextNotices);
|
||||
setRuntimeResources(nextRuntimeResources);
|
||||
@@ -1444,7 +1471,7 @@ function DashboardPage({
|
||||
description="플랫폼의 현재 상태와 운영 흐름을 빠르게 파악합니다."
|
||||
/>
|
||||
|
||||
<MetricCards apps={dashboardApps} sessions={recentSessions} licensePlans={licensePlansSummary} runtimeResources={runtimeResources} loading={dashboardLoading} />
|
||||
<MetricCards apps={dashboardApps} sessions={recentSessions} activeWork={activeWorkSummary} runtimeResources={runtimeResources} loading={dashboardLoading} />
|
||||
|
||||
<div className="mt-4 grid gap-4 xl:grid-cols-2">
|
||||
<LauncherPanel versions={launcherRows} loading={dashboardLoading} onOpen={onLauncherVersionsOpen} />
|
||||
|
||||
Reference in New Issue
Block a user