112 lines
3.9 KiB
TypeScript
112 lines
3.9 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { Badge } from '../../components/ui';
|
|
import type { AppWithComponents, JobStatus } from '../../types/application';
|
|
import { listApps } from './application-api';
|
|
import { getStoredSelectedAppId, setStoredSelectedAppId } from '../../lib/selected-app';
|
|
import { formatDateTime } from '../../lib/date-format';
|
|
|
|
export function useApps() {
|
|
const [apps, setApps] = useState<AppWithComponents[]>([]);
|
|
const [selectedAppId, setSelectedAppId] = useState(() => getStoredSelectedAppId());
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const refreshApps = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const nextApps = await listApps();
|
|
setApps(nextApps);
|
|
const storedAppId = getStoredSelectedAppId();
|
|
const validAppId = nextApps.some((app) => app.id === storedAppId) ? storedAppId : nextApps[0]?.id ?? '';
|
|
if (validAppId) {
|
|
setSelectedAppId(validAppId);
|
|
setStoredSelectedAppId(validAppId);
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
void refreshApps();
|
|
}, []);
|
|
|
|
const selectedApp = useMemo(
|
|
() => apps.find((app) => app.id === selectedAppId) ?? apps[0] ?? null,
|
|
[apps, selectedAppId]
|
|
);
|
|
|
|
const selectApp = (appId: string) => {
|
|
setSelectedAppId(appId);
|
|
setStoredSelectedAppId(appId);
|
|
};
|
|
|
|
return { apps, selectedApp, selectedAppId: selectedApp?.id ?? '', loading, refreshApps, selectApp };
|
|
}
|
|
|
|
export function AppSelector({
|
|
apps,
|
|
value,
|
|
onChange,
|
|
loading
|
|
}: {
|
|
apps: AppWithComponents[];
|
|
value: string;
|
|
onChange: (appId: string) => void;
|
|
loading?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="mb-5 flex flex-col gap-2 rounded-lg border border-border bg-card px-5 py-4 md:flex-row md:items-center md:justify-between">
|
|
<div>
|
|
<p className="text-sm font-semibold text-foreground">앱 선택</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">선택한 앱 기준으로 소스, 빌드, 릴리즈, 배포 데이터를 봅니다.</p>
|
|
</div>
|
|
<select
|
|
className="h-9 min-w-64 rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring"
|
|
value={value}
|
|
disabled={loading || apps.length === 0}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
>
|
|
{apps.length === 0 ? <option value="">등록된 앱을 선택해주세요</option> : null}
|
|
{apps.map((app) => (
|
|
<option key={app.id} value={app.id}>
|
|
{app.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StatusBadge({ status }: { status: string }) {
|
|
const normalized = status.toUpperCase();
|
|
const label = {
|
|
SUCCEEDED: '성공',
|
|
ACTIVE: '활성',
|
|
READY: '준비',
|
|
RUNNING: '실행 중',
|
|
PENDING: '대기',
|
|
FAILED: '실패',
|
|
DISABLED: '비활성',
|
|
CANCELED: '취소'
|
|
}[normalized] ?? status;
|
|
|
|
if (normalized === 'SUCCEEDED' || normalized === 'ACTIVE' || normalized === 'READY' || normalized === 'RUNNING' || status === '성공' || status === '활성' || status === '준비' || status === '실행 중') {
|
|
return <Badge tone={normalized === 'RUNNING' || status === '실행 중' ? 'blue' : 'green'}>{label}</Badge>;
|
|
}
|
|
if (normalized === 'PENDING' || status === '대기') return <Badge tone="amber">{label}</Badge>;
|
|
if (normalized === 'FAILED' || normalized === 'DISABLED' || normalized === 'CANCELED' || status === '실패' || status === '비활성' || status === '취소') return <Badge tone="red">{label}</Badge>;
|
|
return <Badge>{label}</Badge>;
|
|
}
|
|
|
|
export function isBuildDone(status: JobStatus) {
|
|
return !['PENDING', 'RUNNING'].includes(status);
|
|
}
|
|
|
|
export function formatDate(value?: string | null) {
|
|
return formatDateTime(value);
|
|
}
|
|
|
|
export function EmptyState({ children }: { children: string }) {
|
|
return <div className="flex min-h-40 items-center justify-center text-sm text-muted-foreground">{children}</div>;
|
|
}
|