init studio admin frontend
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user