refine app management and source repository displays
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -73,12 +73,12 @@ function getTone(status: string) {
|
|||||||
return 'neutral';
|
return 'neutral';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatStatus(status?: string | null) {
|
function formatStatus(status?: string | null, launcherLinked?: boolean) {
|
||||||
|
if (status === 'DISABLED' || status === 'INACTIVE') return '비활성';
|
||||||
|
if (launcherLinked) return '활성';
|
||||||
const statusMap: Record<string, string> = {
|
const statusMap: Record<string, string> = {
|
||||||
ACTIVE: '준비됨',
|
ACTIVE: '활성',
|
||||||
READY: '준비됨',
|
READY: '준비됨',
|
||||||
INACTIVE: '비활성',
|
|
||||||
DISABLED: '비활성',
|
|
||||||
ERROR: '오류'
|
ERROR: '오류'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -906,7 +906,9 @@ export function AppManagementPage() {
|
|||||||
<td className="px-5 py-3">{app.name}</td>
|
<td className="px-5 py-3">{app.name}</td>
|
||||||
<td className="px-5 py-3 text-muted-foreground">{app.namespace || '-'}</td>
|
<td className="px-5 py-3 text-muted-foreground">{app.namespace || '-'}</td>
|
||||||
<td className="px-5 py-3">{app.components?.length ?? 0}</td>
|
<td className="px-5 py-3">{app.components?.length ?? 0}</td>
|
||||||
<td className="px-5 py-3"><Badge tone={getTone(app.status)}>{formatStatus(app.status)}</Badge></td>
|
<td className="px-5 py-3">
|
||||||
|
<Badge tone={getTone(formatStatus(app.status, app.launcherLinked))}>{formatStatus(app.status, app.launcherLinked)}</Badge>
|
||||||
|
</td>
|
||||||
<td className="px-5 py-3">
|
<td className="px-5 py-3">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
@@ -952,7 +954,7 @@ export function AppManagementPage() {
|
|||||||
['네임스페이스', selectedApp.namespace],
|
['네임스페이스', selectedApp.namespace],
|
||||||
['연결 소스', `${linkedComponents}개`],
|
['연결 소스', `${linkedComponents}개`],
|
||||||
['구성요소', `${totalComponents}개`],
|
['구성요소', `${totalComponents}개`],
|
||||||
['상태', formatStatus(selectedApp.status)]
|
['상태', formatStatus(selectedApp.status, selectedApp.launcherLinked)]
|
||||||
].map(([label, value]) => (
|
].map(([label, value]) => (
|
||||||
<div key={label} className="rounded-md border border-border bg-muted/20 p-4">
|
<div key={label} className="rounded-md border border-border bg-muted/20 p-4">
|
||||||
<p className="text-xs text-muted-foreground">{label}</p>
|
<p className="text-xs text-muted-foreground">{label}</p>
|
||||||
@@ -1008,7 +1010,6 @@ export function AppManagementPage() {
|
|||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<span className={cn('size-2.5 shrink-0 rounded-full', running ? statusDotToneClass('green') : component.repository ? statusDotToneClass('blue') : statusDotToneClass('neutral'))} />
|
<span className={cn('size-2.5 shrink-0 rounded-full', running ? statusDotToneClass('green') : component.repository ? statusDotToneClass('blue') : statusDotToneClass('neutral'))} />
|
||||||
{component.name}
|
{component.name}
|
||||||
{running ? <Badge tone="green">실행 중</Badge> : null}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3 text-muted-foreground">{imageSourceLabel}</td>
|
<td className="px-5 py-3 text-muted-foreground">{imageSourceLabel}</td>
|
||||||
@@ -1091,7 +1092,6 @@ export function AppManagementPage() {
|
|||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<span className={cn('block size-2.5 shrink-0 rounded-full', stopped ? statusDotToneClass('neutral') : runtimeStatusDotClass(resource.status))} />
|
<span className={cn('block size-2.5 shrink-0 rounded-full', stopped ? statusDotToneClass('neutral') : runtimeStatusDotClass(resource.status))} />
|
||||||
<span className="text-sm text-foreground">{resource.name}</span>
|
<span className="text-sm text-foreground">{resource.name}</span>
|
||||||
<Badge tone={runtimeStatusTone(resource, stopped)}>{runtimeStatusLabel(resource, stopped)}</Badge>
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3 text-foreground">{resource.kind}</td>
|
<td className="px-5 py-3 text-foreground">{resource.kind}</td>
|
||||||
|
|||||||
@@ -389,7 +389,6 @@ export function RepositoryManagementPage() {
|
|||||||
<input
|
<input
|
||||||
value={repoName}
|
value={repoName}
|
||||||
onChange={(event) => handleRepoNameChange(event.target.value)}
|
onChange={(event) => handleRepoNameChange(event.target.value)}
|
||||||
placeholder="lms-fe"
|
|
||||||
className="h-full min-w-0 flex-1 bg-transparent px-3 text-sm outline-none placeholder:text-muted-foreground"
|
className="h-full min-w-0 flex-1 bg-transparent px-3 text-sm outline-none placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,11 +62,19 @@ function splitRepository(repository?: string | null) {
|
|||||||
return owner && repo ? { owner, repo } : null;
|
return owner && repo ? { owner, repo } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function repositoryGitUrl(repository?: string | null) {
|
function httpCloneUrl(repository?: string | null) {
|
||||||
const normalized = normalizeRepository(repository);
|
const normalized = normalizeRepository(repository);
|
||||||
return normalized ? `${giteaBaseUrl.replace(/\/$/, '')}/${normalized}.git` : '-';
|
return normalized ? `${giteaBaseUrl.replace(/\/$/, '')}/${normalized}.git` : '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sshCloneUrl(repository?: string | null) {
|
||||||
|
const normalized = normalizeRepository(repository);
|
||||||
|
if (!normalized) return '-';
|
||||||
|
const base = giteaBaseUrl.replace(/\/$/, '');
|
||||||
|
const host = base.replace(/^https?:\/\//, '').split('/')[0]?.split(':')[0] || 'localhost';
|
||||||
|
return `ssh://git@${host}:2222/${normalized}.git`;
|
||||||
|
}
|
||||||
|
|
||||||
function commitAuthor(commit: GiteaCommit) {
|
function commitAuthor(commit: GiteaCommit) {
|
||||||
return commit.author?.login
|
return commit.author?.login
|
||||||
|| commit.author?.username
|
|| commit.author?.username
|
||||||
@@ -321,13 +329,13 @@ export function SourceManagementPage() {
|
|||||||
const selectedSource = filteredSources.find((source) => source.id === selectedSourceId) ?? filteredSources[0] ?? null;
|
const selectedSource = filteredSources.find((source) => source.id === selectedSourceId) ?? filteredSources[0] ?? null;
|
||||||
const repository = splitRepository(selectedSource?.repository);
|
const repository = splitRepository(selectedSource?.repository);
|
||||||
const branch = selectedBranch || 'develop';
|
const branch = selectedBranch || 'develop';
|
||||||
const latestBranchSha = commits.length > 0 ? commits[0]?.sha ?? '' : '';
|
|
||||||
const branchOptions = useMemo(
|
const branchOptions = useMemo(
|
||||||
() => [...new Set(['develop', 'release', 'main', selectedSource?.branch].filter((item): item is string => Boolean(item)))],
|
() => [...new Set(['develop', 'release', 'main', selectedSource?.branch].filter((item): item is string => Boolean(item)))],
|
||||||
[selectedSource?.branch]
|
[selectedSource?.branch]
|
||||||
);
|
);
|
||||||
const totalCommitPages = totalCommits === null ? null : Math.max(1, Math.ceil(totalCommits / commitPageSize));
|
const totalCommitPages = totalCommits === null ? null : Math.max(1, Math.ceil(totalCommits / commitPageSize));
|
||||||
const selectedRepositoryGitUrl = repositoryGitUrl(selectedSource?.repository);
|
const selectedHttpCloneUrl = httpCloneUrl(selectedSource?.repository);
|
||||||
|
const selectedSshCloneUrl = sshCloneUrl(selectedSource?.repository);
|
||||||
|
|
||||||
const refreshSources = () => {
|
const refreshSources = () => {
|
||||||
setSourcesLoading(true);
|
setSourcesLoading(true);
|
||||||
@@ -426,11 +434,11 @@ export function SourceManagementPage() {
|
|||||||
.finally(() => setDiffLoading(false));
|
.finally(() => setDiffLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyGitUrl = () => {
|
const copyCloneUrl = (value: string, label: string) => {
|
||||||
if (!selectedSource?.repository || selectedRepositoryGitUrl === '-') return;
|
if (!selectedSource?.repository || value === '-') return;
|
||||||
navigator.clipboard?.writeText(selectedRepositoryGitUrl)
|
navigator.clipboard?.writeText(value)
|
||||||
.then(() => toast({ type: 'success', message: 'Git URL을 복사했습니다.' }))
|
.then(() => toast({ type: 'success', message: `${label}을 복사했습니다.` }))
|
||||||
.catch(() => toast({ type: 'error', message: 'Git URL 복사에 실패했습니다.' }));
|
.catch(() => toast({ type: 'error', message: `${label} 복사에 실패했습니다.` }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -472,7 +480,7 @@ export function SourceManagementPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 rounded-md border border-border bg-muted/20 p-3 text-sm md:grid-cols-2">
|
<div className="space-y-3 rounded-md border border-border bg-muted/20 p-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">조회 브랜치</p>
|
<p className="text-xs text-muted-foreground">조회 브랜치</p>
|
||||||
<select
|
<select
|
||||||
@@ -485,27 +493,28 @@ export function SourceManagementPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 md:col-span-2">
|
<div className="min-w-0">
|
||||||
<p className="text-xs text-muted-foreground">저장소</p>
|
<p className="text-xs text-muted-foreground">저장소</p>
|
||||||
<div className="mt-2 rounded-md border border-border bg-background px-3 py-2">
|
<div className="mt-2 rounded-md border border-border bg-background px-3 py-2">
|
||||||
<p className="truncate text-sm font-medium">{normalizeRepository(selectedSource?.repository) || '-'}</p>
|
<p className="truncate text-sm font-medium">{normalizeRepository(selectedSource?.repository) || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 md:col-span-2">
|
<div className="min-w-0">
|
||||||
<p className="text-xs text-muted-foreground">Git URL</p>
|
<p className="text-xs text-muted-foreground">HTTP Clone URL</p>
|
||||||
<div className="mt-2 flex min-w-0 items-center gap-2 rounded-md border border-border bg-background px-3 py-2">
|
<div className="mt-2 flex min-w-0 items-center gap-2 rounded-md border border-border bg-background px-3 py-2">
|
||||||
<p className="min-w-0 flex-1 truncate text-sm font-medium" title={selectedRepositoryGitUrl}>{selectedRepositoryGitUrl}</p>
|
<p className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground" title={selectedHttpCloneUrl}>{selectedHttpCloneUrl}</p>
|
||||||
<Button variant="ghost" className="size-8 shrink-0 px-0" title="Git URL 복사" aria-label="Git URL 복사" onClick={copyGitUrl} disabled={!selectedSource?.repository}>
|
<Button variant="ghost" className="size-8 shrink-0 px-0" title="HTTP Clone URL 복사" aria-label="HTTP Clone URL 복사" onClick={() => copyCloneUrl(selectedHttpCloneUrl, 'HTTP Clone URL')} disabled={!selectedSource?.repository}>
|
||||||
<Copy className="size-4" />
|
<Copy className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 md:col-span-2">
|
<div className="min-w-0">
|
||||||
<p className="text-xs text-muted-foreground">SHA1</p>
|
<p className="text-xs text-muted-foreground">SSH Clone URL</p>
|
||||||
<div className="mt-2 rounded-md border border-border bg-background px-3 py-2">
|
<div className="mt-2 flex min-w-0 items-center gap-2 rounded-md border border-border bg-background px-3 py-2">
|
||||||
<p className="truncate font-mono text-sm font-medium" title={latestBranchSha || '-'}>
|
<p className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground" title={selectedSshCloneUrl}>{selectedSshCloneUrl}</p>
|
||||||
{latestBranchSha || '-'}
|
<Button variant="ghost" className="size-8 shrink-0 px-0" title="SSH Clone URL 복사" aria-label="SSH Clone URL 복사" onClick={() => copyCloneUrl(selectedSshCloneUrl, 'SSH Clone URL')} disabled={!selectedSource?.repository}>
|
||||||
</p>
|
<Copy className="size-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,6 +64,23 @@ function statusTone(status?: string): 'green' | 'amber' | 'red' | 'neutral' {
|
|||||||
return 'neutral';
|
return 'neutral';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildStatusText(status?: string) {
|
||||||
|
if (status === 'SUCCEEDED') return '성공';
|
||||||
|
if (status === 'FAILED') return '실패';
|
||||||
|
if (status === 'RUNNING') return '진행 중';
|
||||||
|
if (status === 'PENDING') return '대기';
|
||||||
|
if (status === 'CANCELED') return '취소';
|
||||||
|
return status ?? '빌드 이력 없음';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStatusTone(status?: string): 'green' | 'amber' | 'red' | 'blue' | 'neutral' {
|
||||||
|
if (status === 'SUCCEEDED') return 'green';
|
||||||
|
if (status === 'FAILED' || status === 'CANCELED') return 'red';
|
||||||
|
if (status === 'RUNNING') return 'blue';
|
||||||
|
if (status === 'PENDING') return 'amber';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
function componentIcon(type: LauncherComponentDefinition['type']) {
|
function componentIcon(type: LauncherComponentDefinition['type']) {
|
||||||
if (type === 'client') return FileArchive;
|
if (type === 'client') return FileArchive;
|
||||||
if (type === 'database') return Database;
|
if (type === 'database') return Database;
|
||||||
@@ -249,7 +266,9 @@ export function LauncherComponentsPage() {
|
|||||||
<Card className="p-5">
|
<Card className="p-5">
|
||||||
<p className="text-sm text-muted-foreground">최근 클라이언트 빌드</p>
|
<p className="text-sm text-muted-foreground">최근 클라이언트 빌드</p>
|
||||||
<p className="mt-3 text-2xl font-semibold">{latestBuild?.version ?? '-'}</p>
|
<p className="mt-3 text-2xl font-semibold">{latestBuild?.version ?? '-'}</p>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">{latestBuild?.status ?? '빌드 이력 없음'}</p>
|
<div className="mt-2">
|
||||||
|
<Badge tone={buildStatusTone(latestBuild?.status)}>{buildStatusText(latestBuild?.status)}</Badge>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-5">
|
<Card className="p-5">
|
||||||
<p className="text-sm text-muted-foreground">Namespace</p>
|
<p className="text-sm text-muted-foreground">Namespace</p>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export type ApiApp = {
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
namespace?: string | null;
|
namespace?: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
|
launcherLinked?: boolean;
|
||||||
components?: ApiAppComponent[];
|
components?: ApiAppComponent[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user