diff --git a/public/assets/brand/logo.png b/public/assets/brand/logo.png deleted file mode 100644 index 5c87d02..0000000 Binary files a/public/assets/brand/logo.png and /dev/null differ diff --git a/src/App.tsx b/src/App.tsx index 4e9c38a..0ed1bb1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,52 +7,16 @@ import { CircleHelp, ExternalLink, GitBranch, - Copy, - KeyRound, - Lock, LogOut, - MoreHorizontal, - Plus, - Play, - RotateCcw, Search, - Settings2, - ShieldCheck, - SlidersHorizontal, - Trash2 + SlidersHorizontal } from 'lucide-react'; -import { - alerts, - authApiEndpoints, - appBundles, - appDeploymentJobs, - appMigrationJobs, - appReleasePipelines, - appSourceConfigs, - apps, - containerBlueprints, - k3sWorkloads, - agentDevices, - gatewayConnections, - launcherCommonSettings, - launcherVersions, - licensePlans, - metrics, - navGroups, - notices, - tailscaleNodes, - tailscaleSettings, - runtimeCommands, - userLicenseAssignments -} from './data/mock'; +import { navGroups } from './data/mock'; import { Badge, Button, Card, CardHeader, Progress } from './components/ui'; -import { useToast } from './components/toast'; -import { DataTableCard, type DataTableColumn, type DataTableFilter } from './components/data-table'; import { formatDateTime as formatDateTimeValue } from './lib/date-format'; -import { apiFetch, authExpiredEventName, authHeaders, clearAdminSession } from './lib/api'; +import { apiBaseUrl, apiRequest, authExpiredEventName, clearAdminSession } from './lib/api'; import { filterAppsByCurrentAdmin } from './lib/admin-app-permissions'; import { cn } from './lib/utils'; -import { getStoredSelectedAppId, setStoredSelectedAppId } from './lib/selected-app'; import { AppManagementPage as AppManagementApiPage } from './pages/applications/AppManagementPage'; import { BuildManagementPage } from './pages/applications/BuildManagementPage'; import { DeploymentManagementPage } from './pages/applications/DeploymentManagementPage'; @@ -81,7 +45,6 @@ import { GatewayAgentsPage as GatewayAgentsRestoredPage, GatewayDevicesPage as GatewayDevicesRestoredPage, GatewayStatusPage as GatewayStatusRestoredPage, - LauncherCommonSettingsPage as LauncherCommonSettingsRestoredPage, TailscaleAclPage as TailscaleAclRestoredPage, TailscaleDiagnosticsPage as TailscaleDiagnosticsRestoredPage, TailscaleRoutesPage as TailscaleRoutesRestoredPage, @@ -199,27 +162,6 @@ function viewFromPath(pathname: string): ViewKey { return viewByPath[pathname] ?? 'dashboard'; } -const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:4500/api'; -const giteaBaseUrl = import.meta.env.VITE_GITEA_BASE_URL ?? 'http://localhost:3000'; -const giteaSshBaseUrl = import.meta.env.VITE_GITEA_SSH_BASE_URL ?? 'ssh://git@localhost:2222'; - -type GiteaRepo = { - id: number; - owner: string; - name: string; - fullName: string; - description?: string; - private: boolean; - htmlUrl: string; - cloneUrl: string; - sshUrl: string; - defaultBranch: string; - updatedAt: string; - language?: string; - branchCount?: number; - releaseCount?: number; -}; - type CurrentAdmin = { email: string; name?: string; @@ -358,40 +300,63 @@ type DashboardRuntimeResource = RuntimeResource & { type UserSessionRow = { id: string; - no: number; + no?: number; sessionId: string | null; userId: string; user: string; - email: string; + email?: string; role: string; session: string; - createdAt: string | null; + createdAt?: string | null; expiresAt: string | null; lastSeen: string | null; state: string; }; -type AdminRow = { - id: string; - email: string; - name: string; - role: 'OWNER' | 'ADMIN' | 'OPERATOR' | 'VIEWER'; - isActive: boolean; - lastLoginAt: string | null; - createdAt: string; - updatedAt: string; - sessions?: Array<{ - id: string; - expiresAt: string | null; - revokedAt: string | null; - createdAt: string; - }>; - sshKeys?: Array<{ - id: string; - label?: string | null; - fingerprint?: string | null; - }>; -}; +function formatDateTime(value?: string | null) { + return formatDateTimeValue(value); +} + +function loadLauncherVersions() { + return apiRequest('/launcher/versions'); +} + +function loadApps() { + return apiRequest('/apps') + .then(filterAppsByCurrentAdmin); +} + +function loadAppReleases() { + return apiRequest('/apps/releases'); +} + +function loadUserSessions() { + return apiRequest<{ sessions?: UserSessionRow[] }>('/users/sessions') + .then((response) => response.sessions ?? []); +} + +function loadNotices() { + return apiRequest<{ notices?: ApiNotice[] }>('/notices') + .then((response) => response.notices ?? []); +} + +function loadLicensePlans() { + return apiRequest<{ plans?: LicensePlanSummary[] }>('/licenses/plans') + .then((response) => response.plans ?? []); +} + +function loadCommonAppPipelines() { + return apiRequest<{ repos: CommonAppPipeline[] }>('/woodpecker/repos') + .then((result) => result.repos ?? []); +} + +function loadRuntimeNamespaceResources(namespace: string) { + return apiRequest(`/runtime/namespaces/${encodeURIComponent(namespace)}/resources`); +} + +function loadRuntimePodMetrics(namespace: string) { + return apiRequest(`/runtime/namespaces/${encodeURIComponent(namespace)}/pod-metrics`); +} const statusTone = { success: 'green', @@ -619,45 +584,6 @@ function PageTitle({ ); } -function LoadingModal({ open, label = '데이터를 불러오는 중입니다.' }: { open: boolean; label?: string }) { - if (!open) return null; - - return ( -
-
-
-

{label}

-
-
- ); -} - -function useMinimumLoading(loading: boolean, minimumMs = 500) { - const [visible, setVisible] = useState(loading); - const startedAtRef = useRef(loading ? Date.now() : null); - - useEffect(() => { - if (loading) { - startedAtRef.current = Date.now(); - setVisible(true); - return; - } - - if (!visible) return; - - const elapsed = startedAtRef.current ? Date.now() - startedAtRef.current : minimumMs; - const remaining = Math.max(minimumMs - elapsed, 0); - const timer = window.setTimeout(() => { - startedAtRef.current = null; - setVisible(false); - }, remaining); - - return () => window.clearTimeout(timer); - }, [loading, minimumMs, visible]); - - return visible; -} - function Topbar({ menuSearch, onMenuSearchChange, @@ -1374,1914 +1300,6 @@ function NoticePanel({ notices: dashboardNotices, loading, onOpen }: { notices: ); } -function LauncherManagementPage() { - const [launcherRows, setLauncherRows] = useState([]); - const [launcherRepos, setLauncherRepos] = useState([]); - const [launcherLoading, setLauncherLoading] = useState(true); - const launcherLoadingVisible = useMinimumLoading(launcherLoading); - const [launcherError, setLauncherError] = useState(false); - const [sourceModalOpen, setSourceModalOpen] = useState(false); - const [sourceRepository, setSourceRepository] = useState(''); - const [sourceBranch, setSourceBranch] = useState('release'); - const [sourceCommitSha, setSourceCommitSha] = useState(''); - const [sourceSaveStatus, setSourceSaveStatus] = useState<'idle' | 'saving' | 'error'>('idle'); - const [releaseModalOpen, setReleaseModalOpen] = useState(false); - const [releaseSaveStatus, setReleaseSaveStatus] = useState<'idle' | 'saving' | 'error'>('idle'); - const [newLauncherRelease, setNewLauncherRelease] = useState({ - version: '', - minimumVersion: '', - artifact: '', - downloadUrl: '', - releaseNotes: '', - repository: '', - branch: 'release', - commitSha: '' - }); - const launcher = launcherRows[0]; - - const refreshLauncherVersions = () => { - setLauncherLoading(true); - loadLauncherVersions() - .then((versions) => { - setLauncherRows(versions); - setLauncherError(false); - setLauncherLoading(false); - }) - .catch(() => { - setLauncherRows([]); - setLauncherError(true); - setLauncherLoading(false); - }); - }; - - useEffect(() => { - let mounted = true; - - refreshLauncherVersions(); - loadPlatformRepos() - .then((response) => { - if (!response.ok) throw new Error('Gitea repositories request failed'); - return response.json() as Promise<{ repos: GiteaRepo[] }>; - }) - .then((data) => { - if (!mounted) return; - setLauncherRepos(data.repos ?? []); - }) - .catch(() => { - if (!mounted) return; - setLauncherRepos([]); - }); - - return () => { - mounted = false; - }; - }, []); - - const openSourceModal = () => { - if (!launcher) return; - - setSourceRepository(launcher.repository ?? launcherRepos[0]?.fullName ?? ''); - setSourceBranch(launcher.branch ?? 'release'); - setSourceCommitSha(launcher.commitSha ?? ''); - setSourceSaveStatus('idle'); - setSourceModalOpen(true); - }; - - const saveLauncherSource = () => { - if (!launcher || !sourceRepository.trim()) return; - - setSourceSaveStatus('saving'); - apiFetch(`${apiBaseUrl}/launcher/versions/${launcher.id}/source`, { - method: 'PATCH', - headers: authHeaders({ 'Content-Type': 'application/json' }), - body: JSON.stringify({ - repository: sourceRepository.trim(), - branch: sourceBranch.trim() || undefined, - commitSha: sourceCommitSha.trim() || undefined - }) - }) - .then((response) => { - if (!response.ok) throw new Error('Launcher source link failed'); - return response.json(); - }) - .then(() => { - setSourceModalOpen(false); - setSourceSaveStatus('idle'); - refreshLauncherVersions(); - }) - .catch(() => setSourceSaveStatus('error')); - }; - - const openReleaseModal = () => { - setNewLauncherRelease({ - version: '', - minimumVersion: launcher?.minimumVersion ?? '', - artifact: '', - downloadUrl: '', - releaseNotes: '', - repository: launcher?.repository ?? launcherRepos[0]?.fullName ?? '', - branch: launcher?.branch ?? 'release', - commitSha: '' - }); - setReleaseSaveStatus('idle'); - setReleaseModalOpen(true); - }; - - const createLauncherRelease = () => { - if (!newLauncherRelease.version.trim() || !newLauncherRelease.artifact.trim() || !newLauncherRelease.downloadUrl.trim()) return; - - setReleaseSaveStatus('saving'); - apiFetch(`${apiBaseUrl}/launcher/versions`, { - method: 'POST', - headers: authHeaders({ 'Content-Type': 'application/json' }), - body: JSON.stringify({ - version: newLauncherRelease.version.trim(), - minimumVersion: newLauncherRelease.minimumVersion.trim() || undefined, - artifact: newLauncherRelease.artifact.trim(), - downloadUrl: newLauncherRelease.downloadUrl.trim(), - releaseNotes: newLauncherRelease.releaseNotes.trim() || undefined, - repository: newLauncherRelease.repository || undefined, - branch: newLauncherRelease.branch || undefined, - commitSha: newLauncherRelease.commitSha.trim() || undefined - }) - }) - .then((response) => { - if (!response.ok) throw new Error('Launcher release create failed'); - return response.json(); - }) - .then(() => { - setReleaseModalOpen(false); - setReleaseSaveStatus('idle'); - refreshLauncherVersions(); - }) - .catch(() => setReleaseSaveStatus('error')); - }; - - return ( - <> - - - - - } - /> - - {launcherError ? ( -
- 런처 버전 데이터를 불러오지 못했습니다. -
- ) : null} - - {!launcherLoading && !launcher ? ( - - 등록된 런처 버전이 없습니다. - - ) : null} - - {launcher ? ( -
- - -
-
-
-
-

Autonomy Studio Launcher

- {launcher.isActive ? '활성' : '비활성'} -
-

- 최신 {launcher.version} · 최소 지원 {launcher.minimumVersion || '-'} -

-
-
- -
-
-

배포 파일

-

{launcher.artifact}

-
-
-

다운로드 URL

-

{launcher.downloadUrl}

-
-
- -
-
-

소스 연결

- -
- {launcher.repository ? ( -
-
-

Repository

-

{launcher.repository}

-
-
-

Branch

-

{launcher.branch || '-'}

-
-
-

Commit

-

{launcher.commitSha || '-'}

-
-
-

Clone URL

-
-

{getHttpCloneUrl(launcher.repository)}

- -
-
-
- ) : ( -

연결된 Gitea 저장소가 없습니다.

- )} -
- -
-

릴리즈 노트

-

{launcher.releaseNotes || '-'}

-
-
-
- - - -
- {[ - ['최신 버전', launcher.version], - ['최소 런처 버전', launcher.minimumVersion || '-'] - ].map(([label, value]) => ( -
- {label} - {value} -
- ))} -
-
-
- ) : null} - - {sourceModalOpen && launcher ? ( -
-
-
-

런처 소스 연결

-
-
- - - - {sourceSaveStatus === 'error' ?

런처 소스 연결 저장에 실패했습니다.

: null} -
-
- - -
-
-
- ) : null} - {releaseModalOpen ? ( -
-
-
-

릴리즈 등록

-
-
- - - - - - - -