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, BuildJob } from '../../types/application';
import { LoadingModal, useMinimumLoading } from '../operations/operation-utils';
import { AppSelector, formatDate, useApps } from './ApplicationControls';
import { createRelease, listBuildJobs, listReleases } from './application-api';
import { createRelease, listBuildJobsPage, listReleasesPage } from './application-api';
function statusLabel(status: string) {
return {
@@ -140,7 +140,21 @@ function releaseTime(release: AppRelease) {
return Number.isNaN(value) ? 0 : value;
}
const releasePageSize = 5;
const releasePageSize = 3;
type ReleaseRange = 'week' | 'month' | 'all';
const releaseRangeLabels: Record<ReleaseRange, string> = {
week: '일주일',
month: '한달',
all: '전체'
};
function releaseRangeSince(range: ReleaseRange) {
if (range === 'all') return undefined;
const date = new Date();
date.setDate(date.getDate() - (range === 'week' ? 7 : 30));
return date.toISOString();
}
export function ReleaseManagementPage() {
const { toast } = useToast();
@@ -153,9 +167,13 @@ export function ReleaseManagementPage() {
const [openLogReleaseId, setOpenLogReleaseId] = useState('');
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [range, setRange] = useState<ReleaseRange>('week');
const [totalReleases, setTotalReleases] = useState(0);
const [usedBuildIdValues, setUsedBuildIdValues] = useState<string[]>([]);
const [usedReleaseValueValues, setUsedReleaseValueValues] = useState<string[]>([]);
const loadingVisible = useMinimumLoading(loading);
const usedValues = useMemo(() => usedReleaseValues(releases), [releases]);
const usedBuildIds = useMemo(() => usedReleaseBuildIds(releases), [releases]);
const usedValues = useMemo(() => new Set(usedReleaseValueValues), [usedReleaseValueValues]);
const usedBuildIds = useMemo(() => new Set(usedBuildIdValues), [usedBuildIdValues]);
const candidateBuilds = useMemo(
() => builds.filter((build) => {
const value = buildValue(build);
@@ -168,12 +186,7 @@ export function ReleaseManagementPage() {
() => groupCandidateBuilds(candidateBuilds).filter((group) => !selectedAppId || group.appId === selectedAppId),
[candidateBuilds, selectedAppId]
);
const filteredReleases = useMemo(
() => releases
.filter((release) => !selectedAppId || release.appId === selectedAppId)
.sort((a, b) => releaseTime(b) - releaseTime(a)),
[releases, selectedAppId]
);
const filteredReleases = releases;
const modalBuilds = useMemo(
() => candidateBuilds
.filter((build) => !selectedAppId || build.component.app.id === selectedAppId)
@@ -184,10 +197,26 @@ export function ReleaseManagementPage() {
const refresh = async () => {
setLoading(true);
try {
const [nextReleases, nextBuilds] = await Promise.all([listReleases(), listBuildJobs()]);
const succeededBuilds = nextBuilds.filter((job) => job.status === 'SUCCEEDED');
setReleases(nextReleases);
setBuilds(succeededBuilds);
const [nextReleases, nextBuilds] = await Promise.all([
listReleasesPage({
appKey: apps.find((app) => app.id === selectedAppId)?.key,
page,
limit: releasePageSize,
since: releaseRangeSince(range)
}),
listBuildJobsPage({
appKey: apps.find((app) => app.id === selectedAppId)?.key,
page: 1,
limit: 100,
since: releaseRangeSince(range),
status: 'SUCCEEDED'
})
]);
setReleases(nextReleases.releases);
setTotalReleases(nextReleases.total);
setUsedBuildIdValues(nextReleases.usedBuildIds ?? []);
setUsedReleaseValueValues(nextReleases.usedValues ?? []);
setBuilds(nextBuilds.jobs);
} finally {
setLoading(false);
}
@@ -195,16 +224,15 @@ export function ReleaseManagementPage() {
useEffect(() => {
void refresh();
}, []);
}, [selectedAppId, page, range, apps]);
useEffect(() => {
setPage(1);
}, [selectedAppId]);
}, [selectedAppId, range]);
const totalPages = Math.max(1, Math.ceil(filteredReleases.length / releasePageSize));
const currentPage = Math.min(page, totalPages);
const pageStart = (currentPage - 1) * releasePageSize;
const pagedReleases = filteredReleases.slice(pageStart, pageStart + releasePageSize);
const totalPages = Math.max(1, Math.ceil(totalReleases / releasePageSize));
const currentPage = page;
const pagedReleases = filteredReleases;
const openReleaseModal = (appId?: string) => {
const nextAppId = appId || selectedAppId || candidateGroups[0]?.appId || '';
@@ -252,7 +280,24 @@ export function ReleaseManagementPage() {
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
<Card className="min-w-0 overflow-hidden">
<CardHeader title="릴리즈 목록" description="release 브랜치에서 빌드된 산출물 기준의 배포 후보입니다." />
<CardHeader
title="릴리즈 목록"
description="release 브랜치에서 빌드된 산출물 기준의 배포 후보입니다."
action={
<div className="flex items-center gap-2">
{(Object.keys(releaseRangeLabels) as ReleaseRange[]).map((item) => (
<Button
key={item}
variant={range === item ? 'default' : 'outline'}
className="h-8 px-3 text-[13px]"
onClick={() => setRange(item)}
>
{releaseRangeLabels[item]}
</Button>
))}
</div>
}
/>
<div className="space-y-3 p-4">
{filteredReleases.length === 0 ? (
<div className="flex min-h-44 items-center justify-center text-center text-sm text-muted-foreground">
@@ -306,7 +351,7 @@ export function ReleaseManagementPage() {
</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 ? '불러오는 중' : `총 ${filteredReleases.length}건 · ${currentPage} / ${totalPages}`}
{loading ? '불러오는 중' : `총 ${totalReleases}건 · ${currentPage} / ${totalPages}`}
</span>
<div className="flex gap-2">
<Button