improve app release build selection
This commit is contained in:
@@ -42,6 +42,44 @@ function releaseArtifacts(release: AppRelease) {
|
||||
return release.artifact ? [release.artifact] : [];
|
||||
}
|
||||
|
||||
function buildValue(build: BuildJob) {
|
||||
return build.image || build.artifact || '';
|
||||
}
|
||||
|
||||
function buildTime(build: BuildJob) {
|
||||
const value = new Date(build.finishedAt || build.createdAt).getTime();
|
||||
return Number.isNaN(value) ? 0 : value;
|
||||
}
|
||||
|
||||
function buildVersion(build: BuildJob) {
|
||||
const artifact = build.artifact || '';
|
||||
const match = artifact.match(/\/apps\/[^/]+\/[^/]+\/([^/]+)\//);
|
||||
if (match?.[1]) return match[1];
|
||||
const fileMatch = artifact.match(/[-_]v?(\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?)/);
|
||||
return fileMatch?.[1] ?? null;
|
||||
}
|
||||
|
||||
function usedReleaseValues(releases: AppRelease[]) {
|
||||
return new Set(releases.flatMap(releaseArtifacts));
|
||||
}
|
||||
|
||||
function latestBuildIdsByComponent(builds: BuildJob[]) {
|
||||
const map = new Map<string, BuildJob>();
|
||||
for (const build of builds) {
|
||||
const current = map.get(build.componentId);
|
||||
if (!current || buildTime(build) > buildTime(current)) map.set(build.componentId, build);
|
||||
}
|
||||
return [...map.values()].map((build) => build.id);
|
||||
}
|
||||
|
||||
function newestBuildVersion(builds: BuildJob[]) {
|
||||
return builds
|
||||
.slice()
|
||||
.sort((a, b) => buildTime(b) - buildTime(a))
|
||||
.map(buildVersion)
|
||||
.find((value): value is string => Boolean(value)) ?? '';
|
||||
}
|
||||
|
||||
function buildCount(release: AppRelease) {
|
||||
const imageTags = release.imageTags as ImageTags | null | undefined;
|
||||
const count = Object.keys(imageTags?.images ?? {}).length + Object.keys(imageTags?.artifacts ?? {}).length;
|
||||
@@ -81,10 +119,18 @@ export function ReleaseManagementPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const loadingVisible = useMinimumLoading(loading);
|
||||
const usedValues = useMemo(() => usedReleaseValues(releases), [releases]);
|
||||
const candidateBuilds = useMemo(
|
||||
() => builds.filter((build) => {
|
||||
const value = buildValue(build);
|
||||
return !value || !usedValues.has(value);
|
||||
}),
|
||||
[builds, usedValues]
|
||||
);
|
||||
|
||||
const candidateGroups = useMemo(
|
||||
() => groupCandidateBuilds(builds).filter((group) => !selectedAppId || group.appId === selectedAppId),
|
||||
[builds, selectedAppId]
|
||||
() => groupCandidateBuilds(candidateBuilds).filter((group) => !selectedAppId || group.appId === selectedAppId),
|
||||
[candidateBuilds, selectedAppId]
|
||||
);
|
||||
const filteredReleases = useMemo(
|
||||
() => releases
|
||||
@@ -93,8 +139,10 @@ export function ReleaseManagementPage() {
|
||||
[releases, selectedAppId]
|
||||
);
|
||||
const modalBuilds = useMemo(
|
||||
() => builds.filter((build) => !selectedAppId || build.component.app.id === selectedAppId),
|
||||
[builds, selectedAppId]
|
||||
() => candidateBuilds
|
||||
.filter((build) => !selectedAppId || build.component.app.id === selectedAppId)
|
||||
.sort((a, b) => buildTime(b) - buildTime(a)),
|
||||
[candidateBuilds, selectedAppId]
|
||||
);
|
||||
|
||||
const refresh = async () => {
|
||||
@@ -125,7 +173,9 @@ export function ReleaseManagementPage() {
|
||||
const openReleaseModal = (appId?: string) => {
|
||||
const nextAppId = appId || selectedAppId || candidateGroups[0]?.appId || '';
|
||||
if (nextAppId && nextAppId !== selectedAppId) selectApp(nextAppId);
|
||||
setSelectedBuildIds(builds.filter((build) => build.component.app.id === nextAppId).map((build) => build.id));
|
||||
const appBuilds = candidateBuilds.filter((build) => build.component.app.id === nextAppId);
|
||||
setSelectedBuildIds(latestBuildIdsByComponent(appBuilds));
|
||||
setVersion((prev) => prev || newestBuildVersion(appBuilds));
|
||||
setReleaseModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -154,7 +204,7 @@ export function ReleaseManagementPage() {
|
||||
title="릴리즈 관리"
|
||||
description="브랜치, 커밋, 빌드 산출물을 묶어 환경별로 배포 가능한 릴리즈를 생성합니다."
|
||||
actions={
|
||||
<Button className="h-8 px-3 text-[13px]" disabled={builds.length === 0} onClick={() => openReleaseModal()}>
|
||||
<Button className="h-8 px-3 text-[13px]" disabled={candidateBuilds.length === 0} onClick={() => openReleaseModal()}>
|
||||
<Plus className="size-4" />
|
||||
릴리즈 생성
|
||||
</Button>
|
||||
@@ -255,22 +305,37 @@ export function ReleaseManagementPage() {
|
||||
<div className="space-y-3">
|
||||
<select className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={selectedAppId} onChange={(event) => {
|
||||
selectApp(event.target.value);
|
||||
setSelectedBuildIds(builds.filter((build) => build.component.app.id === event.target.value).map((build) => build.id));
|
||||
const appBuilds = candidateBuilds.filter((build) => build.component.app.id === event.target.value);
|
||||
setSelectedBuildIds(latestBuildIdsByComponent(appBuilds));
|
||||
setVersion(newestBuildVersion(appBuilds));
|
||||
}}>
|
||||
{candidateGroups.map((group) => <option key={group.appId} value={group.appId}>{group.appName}</option>)}
|
||||
</select>
|
||||
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={version} onChange={(event) => setVersion(event.target.value)} placeholder="버전 예: 0.1.1" />
|
||||
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={branch} onChange={(event) => setBranch(event.target.value)} placeholder="브랜치" />
|
||||
<div className="max-h-72 space-y-2 overflow-y-auto rounded-md border border-border p-2">
|
||||
{modalBuilds.map((job) => (
|
||||
<label key={job.id} className="flex items-start gap-2 rounded-md px-2 py-2 text-sm hover:bg-muted/20">
|
||||
<input type="checkbox" checked={selectedBuildIds.includes(job.id)} onChange={() => toggleBuild(job.id)} />
|
||||
<span className="min-w-0">
|
||||
<span className="block font-medium">{job.component.name}</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">{job.image || job.artifact || job.commitSha}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{modalBuilds.length === 0 ? (
|
||||
<div className="px-2 py-8 text-center text-sm text-muted-foreground">
|
||||
등록 가능한 새 빌드 결과가 없습니다.
|
||||
</div>
|
||||
) : null}
|
||||
{modalBuilds.map((job) => {
|
||||
const artifactVersion = buildVersion(job);
|
||||
return (
|
||||
<label key={job.id} className="flex items-start gap-2 rounded-md px-2 py-2 text-sm hover:bg-muted/20">
|
||||
<input type="checkbox" checked={selectedBuildIds.includes(job.id)} onChange={() => toggleBuild(job.id)} />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">{job.component.name}</span>
|
||||
{artifactVersion ? <Badge tone="blue">v{artifactVersion}</Badge> : <Badge tone="neutral">버전 없음</Badge>}
|
||||
<span className="text-xs text-muted-foreground">{job.branch}</span>
|
||||
<span className="text-xs text-muted-foreground">{formatDate(job.finishedAt || job.createdAt)}</span>
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-xs text-muted-foreground">{job.image || job.artifact || job.commitSha}</span>
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user