paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
2026-07-07 15:32:56 +09:00
parent 1de085790d
commit 9cb82249d7
14 changed files with 805 additions and 214 deletions
@@ -6,7 +6,7 @@ import { PageTitle } from '../../layout/AppLayout';
import type { AppRelease, DeploymentJob } from '../../types/application';
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
import { AppSelector, formatDate, useApps } from './ApplicationControls';
import { createDeployment, listDeployments, listReleases } from './application-api';
import { createDeployment, listDeploymentsPage, listReleases } from './application-api';
function statusLabel(status: string) {
return {
@@ -53,11 +53,25 @@ function deploymentTime(deployment: DeploymentJob) {
return Number.isNaN(value) ? 0 : value;
}
const deploymentPageSize = 5;
const deploymentPageSize = 3;
type DeploymentRange = 'week' | 'month' | 'all';
const deploymentRangeLabels: Record<DeploymentRange, string> = {
week: '일주일',
month: '한달',
all: '전체'
};
function deploymentRangeSince(range: DeploymentRange) {
if (range === 'all') return undefined;
const date = new Date();
date.setDate(date.getDate() - (range === 'week' ? 7 : 30));
return date.toISOString();
}
export function DeploymentManagementPage() {
const { toast } = useToast();
const { apps, selectedAppId, loading: appsLoading, selectApp } = useApps();
const { apps, selectedApp, selectedAppId, loading: appsLoading, selectApp } = useApps();
const [deployments, setDeployments] = useState<DeploymentJob[]>([]);
const [releases, setReleases] = useState<AppRelease[]>([]);
const [releaseId, setReleaseId] = useState('');
@@ -67,6 +81,9 @@ export function DeploymentManagementPage() {
const [selectedDeployment, setSelectedDeployment] = useState<DeploymentJob | null>(null);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [range, setRange] = useState<DeploymentRange>('week');
const [totalDeployments, setTotalDeployments] = useState(0);
const [deployedReleaseIdValues, setDeployedReleaseIdValues] = useState<string[]>([]);
const loadingVisible = useMinimumLoading(loading);
const selectedRelease = releases.find((release) => release.id === releaseId);
@@ -75,32 +92,37 @@ export function DeploymentManagementPage() {
[releases, selectedAppId]
);
const deployedReleaseIds = useMemo(
() => new Set(deployments
.filter((deployment) => deployment.status === 'SUCCEEDED' && deployment.releaseId)
.map((deployment) => deployment.releaseId!)),
[deployments]
() => new Set(deployedReleaseIdValues),
[deployedReleaseIdValues]
);
const deployableReleases = useMemo(
() => filteredReleases.filter((release) => !deployedReleaseIds.has(release.id)),
[filteredReleases, deployedReleaseIds]
);
const filteredDeployments = useMemo(
() => deployments
.filter((deployment) => !selectedAppId || deployment.appId === selectedAppId)
.sort((a, b) => deploymentTime(b) - deploymentTime(a)),
[deployments, selectedAppId]
() => deployments,
[deployments]
);
const totalPages = Math.max(1, Math.ceil(filteredDeployments.length / deploymentPageSize));
const currentPage = Math.min(page, totalPages);
const pageStart = (currentPage - 1) * deploymentPageSize;
const pagedDeployments = filteredDeployments.slice(pageStart, pageStart + deploymentPageSize);
const totalPages = Math.max(1, Math.ceil(totalDeployments / deploymentPageSize));
const currentPage = page;
const pagedDeployments = filteredDeployments;
const refresh = async () => {
setLoading(true);
try {
const [nextDeployments, nextReleases] = await Promise.all([listDeployments(), listReleases()]);
const [nextDeployments, nextReleases] = await Promise.all([
listDeploymentsPage({
appKey: selectedApp?.key,
page,
limit: deploymentPageSize,
since: deploymentRangeSince(range)
}),
listReleases(selectedApp?.key)
]);
const deployableReleases = nextReleases.filter((release) => release.status === 'SUCCEEDED');
setDeployments(nextDeployments);
setDeployments(nextDeployments.deployments);
setTotalDeployments(nextDeployments.total);
setDeployedReleaseIdValues(nextDeployments.deployedReleaseIds ?? []);
setReleases(deployableReleases);
} finally {
setLoading(false);
@@ -109,11 +131,11 @@ export function DeploymentManagementPage() {
useEffect(() => {
void refresh();
}, []);
}, [selectedApp?.key, selectedAppId, page, range]);
useEffect(() => {
setPage(1);
}, [selectedAppId]);
}, [selectedAppId, range]);
useEffect(() => {
if (selectedRelease?.app?.namespace) setNamespace((prev) => prev || selectedRelease.app.namespace || '');
@@ -166,9 +188,26 @@ export function DeploymentManagementPage() {
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
<Card className="min-w-0 overflow-hidden">
<CardHeader title="배포 작업" description="릴리즈별 배포 실행 결과와 rollout 로그를 확인합니다." />
<CardHeader
title="배포 작업"
description="선택한 기간의 배포 실행 결과와 rollout 로그를 최신순으로 표시합니다."
action={
<div className="flex items-center gap-2">
{(Object.keys(deploymentRangeLabels) as DeploymentRange[]).map((item) => (
<Button
key={item}
variant={range === item ? 'default' : 'outline'}
className="h-8 px-3 text-[13px]"
onClick={() => setRange(item)}
>
{deploymentRangeLabels[item]}
</Button>
))}
</div>
}
/>
<div className="space-y-3 p-4">
{filteredDeployments.length === 0 ? (
{!loading && filteredDeployments.length === 0 ? (
<div className="flex min-h-44 items-center justify-center text-center text-sm text-muted-foreground">
배포 작업이 없습니다.
</div>
@@ -180,7 +219,7 @@ export function DeploymentManagementPage() {
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-medium text-muted-foreground">No. {filteredDeployments.length - pageStart - index}</span>
<span className="text-xs font-medium text-muted-foreground">No. {totalDeployments - ((currentPage - 1) * deploymentPageSize + index)}</span>
<Badge tone={statusTone(deployment.status)}>{statusLabel(deployment.status)}</Badge>
<span className="text-base font-medium">{deployment.app.name}</span>
<span className="text-base font-medium">{deployment.release?.version || '-'}</span>
@@ -220,7 +259,7 @@ export function DeploymentManagementPage() {
</div>
<div className="flex items-center justify-between gap-4 border-t border-border px-5 py-3 text-sm">
<span className="text-xs text-muted-foreground">
{loading ? '불러오는 중' : `총 ${filteredDeployments.length}건 · ${currentPage} / ${totalPages}`}
{loading ? '불러오는 중' : `총 ${totalDeployments}건 · ${currentPage} / ${totalPages}`}
</span>
<div className="flex gap-2">
<Button