diff --git a/src/App.tsx b/src/App.tsx index 899f06d..b0a2ad3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import { SlidersHorizontal, Trash2 } from 'lucide-react'; -import { navGroups } from './data/mock'; +import { navGroups } from './layout/navigation'; import { Badge, Button, Card, CardHeader, Modal, Progress } from './components/ui'; import { formatDateTime as formatDateTimeValue } from './lib/date-format'; import { apiBaseUrl, apiFetch, apiRequest, authExpiredEventName, authHeaders, clearAdminSession } from './lib/api'; @@ -58,7 +58,6 @@ import { MonitoringPage } from './pages/system/MonitoringPage'; import { LoadingModal, useMinimumLoading } from './pages/operations/operation-utils'; import { AgentCommandsPage as AgentCommandsRestoredPage, - AgentInstallStatusPage as AgentInstallStatusRestoredPage, AgentRegistrationPage as AgentRegistrationRestoredPage, AgentSshKeysPage as AgentSshKeysRestoredPage, TailscaleAclPage as TailscaleAclRestoredPage, @@ -132,12 +131,11 @@ const viewByLabel: Record = { 네트워크: 'infraNetwork', 스토리지: 'infraStorage', 모니터링: 'monitoring', - '장비 등록': 'agentRegistration', - '설치 상태': 'agentInstallStatus', - 'SSH 키 배포': 'agentSshKeys', + '플랫폼 등록': 'agentRegistration', + 'SSH 키': 'agentSshKeys', '원격 명령': 'agentCommands', '로그/이벤트': 'runtimeLogs', - '장비 제어': 'runtimeDeviceControl', + '연결 제어': 'runtimeDeviceControl', '관리자 관리': 'adminManagement', '조직 관리': 'organizations', '사용자 관리': 'userSessions', @@ -578,10 +576,11 @@ function formatRelativeTime(timestamp: number) { type NavGroup = (typeof navGroups)[number]; type NavItem = NavGroup['items'][number]; -type NavChild = { label: string; view?: string }; +type NavChild = { label: string; path?: string }; function resolveNavView(item: NavItem | NavChild): ViewKey | undefined { - return 'view' in item && item.view ? item.view as ViewKey : viewByLabel[item.label]; + const path = 'path' in item ? item.path : undefined; + return path ? viewByPath[path] : viewByLabel[item.label]; } function isOwnerRole(role?: string | null) { @@ -686,9 +685,10 @@ function Sidebar({

{group.label}

{group.items.map((item) => { - const hasChildren = 'children' in item && item.children; + const children = item.children; + const hasChildren = Boolean(children?.length); const isParentActive = hasChildren - ? item.children.some((child) => resolveNavView(child) === activeView) + ? children!.some((child) => resolveNavView(child) === activeView) : activeView === resolveNavView(item); const isMenuOpen = Boolean(normalizedMenuSearch) || (openMenus[item.label] ?? isParentActive); @@ -723,7 +723,7 @@ function Sidebar({ {hasChildren && isMenuOpen ? (
- {item.children.map((child) => { + {children!.map((child) => { const isChildActive = activeView === resolveNavView(child); return ( @@ -2333,8 +2333,6 @@ function App() { return ; case 'agentRegistration': return ; - case 'agentInstallStatus': - return ; case 'agentSshKeys': return ; case 'agentCommands': diff --git a/src/App.tsx.backup-before-page-split b/src/App.tsx.backup-before-page-split deleted file mode 100644 index 920ee58..0000000 --- a/src/App.tsx.backup-before-page-split +++ /dev/null @@ -1,4510 +0,0 @@ -import { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react'; -import { - Activity, - Bell, - ChevronDown, - ChevronRight, - CircleHelp, - ExternalLink, - Copy, - KeyRound, - Lock, - LogOut, - MoreHorizontal, - Plus, - Play, - RotateCcw, - Search, - Settings2, - ShieldCheck, - SlidersHorizontal, - Trash2 -} 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 { Badge, Button, Card, CardHeader, Progress } from './components/ui'; -import { useToast } from './components/toast'; -import { DataTableCard, type DataTableColumn, type DataTableFilter } from './components/data-table'; -import { cn } from './lib/utils'; -import { getStoredSelectedAppId, setStoredSelectedAppId } from './lib/selected-app'; -import { BuildManagementPage } from './pages/applications/BuildManagementPage'; -import { DeploymentManagementPage } from './pages/applications/DeploymentManagementPage'; -import { MigrationManagementPage } from './pages/applications/MigrationManagementPage'; -import { ReleaseManagementPage } from './pages/applications/ReleaseManagementPage'; -import { RuntimeConfigPage as RuntimeConfigApiPage } from './pages/runtime/RuntimeConfigPage'; -import { RuntimeLogsPage as RuntimeLogsApiPage } from './pages/runtime/RuntimeLogsPage'; - -type ViewKey = - | 'dashboard' - | 'launcherVersions' - | 'launcherApps' - | 'launcherCommonSettings' - | 'notices' - | 'appManagement' - | 'appSources' - | 'appBuilds' - | 'appReleases' - | 'appDeployments' - | 'appMigrations' - | 'gatewayStatus' - | 'gatewayAgents' - | 'gatewayDevices' - | 'tailscaleStatus' - | 'tailscaleRoutes' - | 'tailscaleAcl' - | 'tailscaleDiagnostics' - | 'agentRegistration' - | 'agentInstallStatus' - | 'agentSshKeys' - | 'agentWorkspace' - | 'agentCommands' - | 'runtimeConfig' - | 'runtimeLogs' - | 'runtimeDeviceControl' - | 'adminManagement' - | 'userSessions' - | 'licenses'; - -const viewByLabel: Record = { - 대시보드: 'dashboard', - '버전 관리': 'launcherVersions', - '앱 연결': 'launcherApps', - '공통 설정': 'launcherCommonSettings', - 공지사항: 'notices', - '앱 관리': 'appManagement', - '소스 관리': 'appSources', - '빌드 관리': 'appBuilds', - '릴리즈 관리': 'appReleases', - '배포 관리': 'appDeployments', - 마이그레이션: 'appMigrations', - '연결 상태': 'gatewayStatus', - 'Agent 관리': 'gatewayAgents', - '장비 동기화': 'gatewayDevices', - '노드 현황': 'tailscaleStatus', - 라우트: 'tailscaleRoutes', - ACL: 'tailscaleAcl', - '연결 진단': 'tailscaleDiagnostics', - '장비 등록': 'agentRegistration', - '설치 상태': 'agentInstallStatus', - 'SSH 키 배포': 'agentSshKeys', - 'Workspace 설정': 'agentWorkspace', - '원격 명령': 'agentCommands', - '런타임 구성': 'runtimeConfig', - '로그/이벤트': 'runtimeLogs', - '장비 제어': 'runtimeDeviceControl', - '관리자 관리': 'adminManagement', - '사용자/세션': 'userSessions', - 라이센스: 'licenses' -}; - -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 ApiAppComponent = { - id: string; - appId: string; - name: string; - type: string; - repository?: string | null; - branch?: string | null; - buildTarget?: string | null; - app?: ApiApp; -}; - -type ApiApp = { - id: string; - key: string; - name: string; - description?: string | null; - namespace?: string | null; - status: string; - components?: ApiAppComponent[]; -}; - -type ApiLauncherVersion = { - id: string; - version: string; - minimumVersion?: string | null; - repository?: string | null; - branch?: string | null; - commitSha?: string | null; - artifact: string; - downloadUrl: string; - releaseNotes?: string | null; - isActive: boolean; - createdAt: string; - updatedAt: string; -}; - -type UserSessionRow = { - id: string; - no: number; - sessionId: string | null; - userId: string; - user: string; - email: string; - role: string; - session: string; - 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; - }>; -}; - -const statusTone = { - 활성: 'green', - '실행 중': 'green', - 게시됨: 'green', - 카나리: 'blue', - 예약됨: 'blue', - 점검: 'amber', - 주의: 'amber', - 드레이닝: 'amber', - 준비중: 'amber', - 배포됨: 'green', - 초안: 'neutral', - 대기: 'neutral', - 만료: 'neutral', - 종료: 'neutral', - '세션 없음': 'neutral', - 비활성: 'neutral', - '지원 종료': 'red', - 잠김: 'red', - 구성완료: 'green', - 검토필요: 'amber', - 완료: 'green' -} as const; - -function getTone(status: string) { - return statusTone[status as keyof typeof statusTone] ?? 'neutral'; -} - -type NavGroup = (typeof navGroups)[number]; -type NavItem = NavGroup['items'][number]; - -function Sidebar({ - activeView, - menuSearch, - onViewChange -}: { - activeView: ViewKey; - menuSearch: string; - onViewChange: (view: ViewKey) => void; -}) { - const [openMenus, setOpenMenus] = useState>({}); - const normalizedMenuSearch = menuSearch.trim().toLowerCase(); - const visibleGroups = normalizedMenuSearch - ? navGroups - .map((group) => { - const groupMatches = group.label.toLowerCase().includes(normalizedMenuSearch); - const items = groupMatches - ? group.items - : group.items.flatMap((item) => { - const itemMatches = item.label.toLowerCase().includes(normalizedMenuSearch); - - if ('children' in item && item.children) { - const matchedChildren = item.children.filter((child) => child.label.toLowerCase().includes(normalizedMenuSearch)); - if (itemMatches) return [item]; - if (matchedChildren.length) return [{ ...item, children: matchedChildren } as NavItem]; - return []; - } - - return itemMatches ? [item] : []; - }); - - return { ...group, items }; - }) - .filter((group) => group.items.length > 0) - : navGroups; - - return ( - - ); -} - -function PageTitle({ - eyebrow, - title, - description, - actions -}: { - eyebrow: string; - title: string; - description: string; - actions?: ReactNode; -}) { - return ( -
-
-

{eyebrow}

-

{title}

-

{description}

-
- {actions ?
{actions}
: null} -
- ); -} - -function LoadingModal({ open, label = '데이터를 불러오는 중입니다.' }: { open: boolean; label?: string }) { - if (!open) return null; - - return ( -
-
-
-

{label}

-
-
- ); -} - -function useMinimumLoading(loading: boolean, minimumMs = 800) { - 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 -}: { - menuSearch: string; - onMenuSearchChange: (value: string) => void; -}) { - return ( -
-
- -
- - onMenuSearchChange(event.target.value)} - /> -
-
-
- - - -
-
- ); -} - -function LoginPage({ onLogin }: { onLogin: () => void }) { - const [email, setEmail] = useState('admin@futuredrive.net'); - const [password, setPassword] = useState(''); - const [loginLoading, setLoginLoading] = useState(false); - const [loginError, setLoginError] = useState(false); - const passwordInputRef = useRef(null); - const passwordMask = '•'.repeat(password.length); - - const submitLogin = (event: FormEvent) => { - event.preventDefault(); - if (!email.trim() || !password.trim()) return; - - setLoginLoading(true); - setLoginError(false); - fetch(`${apiBaseUrl}/auth/admin/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: email.trim(), password }) - }) - .then((response) => { - if (!response.ok) throw new Error('Admin login failed'); - return response.json() as Promise<{ token: string; expiresAt: string; admin: { email: string; name: string; role: string } }>; - }) - .then((data) => { - localStorage.setItem('studio-admin-token', data.token); - localStorage.setItem('studio-admin', JSON.stringify(data.admin)); - setLoginLoading(false); - onLogin(); - }) - .catch(() => { - setLoginLoading(false); - setLoginError(true); - }); - }; - - return ( -
-
-
-
-
- Autonomy Studio -
-
-

관리자 로그인

-

계정 정보를 입력해 주세요.

-
-
-
- - - {loginError ? ( -

이메일 또는 비밀번호를 확인해 주세요.

- ) : null} - -
-
-
-
- ); -} - -function MetricCards() { - return ( -
- {metrics.map((metric) => ( - -
-

{metric.label}

- {metric.delta} -
-
{metric.value}
-

{metric.hint}

-
- ))} -
- ); -} - -function LauncherPanel() { - return ( - - - - 배포 정책 - - } - /> -
-
- {launcherVersions.map((item) => ( -
-
-
-
-

{item.name}

- {item.status} -
-

- 최신 {item.latestVersion} · 최소 지원 {item.minimumSupportedVersion} -

-
-
-
-
-

배포 파일

-

{item.artifact}

-
-
-

다운로드 URL

-

{item.downloadUrl}

-
-
-

{item.releaseNotes}

-
- ))} -
-
-

운영 알림

-
- {alerts.map((alert) => ( -
- -

{alert.label}

-
- ))} -
-
-
-
- ); -} - -function SessionsPanel({ onMore }: { onMore: () => void }) { - const [recentSessions, setRecentSessions] = useState([]); - const [sessionsLoading, setSessionsLoading] = useState(true); - - useEffect(() => { - loadUserSessions() - .then((rows) => { - setRecentSessions(rows.slice(0, 5)); - setSessionsLoading(false); - }) - .catch(() => { - setRecentSessions([]); - setSessionsLoading(false); - }); - }, []); - - return ( - - - 더보기 - - } - /> -
- {sessionsLoading ? ( -
-
- 불러오는 중 -
- ) : recentSessions.length === 0 ? ( -
표시할 사용자 세션이 없습니다.
- ) : null} - {!sessionsLoading && recentSessions.map((session) => ( -
-
-
-

{session.user}

- {session.state} -
-

- {session.email} · {session.role} -

-
- {formatDateTime(session.lastSeen)} -
- ))} -
- - ); -} - -function AppRegistryTable() { - return ( - - - - 앱 등록 - - } - /> -
- - - - - - - - - - - - - - - {apps.map((app) => ( - - - - - - - - - - - ))} - -
앱상태릴리즈저장소/경로워크로드시크릿사용량
-

{app.name}

-

{app.key} · {app.version}

-
- {app.status} - {app.release} - - {app.gitea} - - - {app.containers}{app.secrets} -
- 70 ? 'amber' : 'primary'} /> - 75 ? 'red' : 'green'} /> -
-
- -
-
-
- ); -} - -function ContainerPanel() { - return ( - - -
- {k3sWorkloads.map((item) => ( -
-
-
-
-

{item.workload}

- {item.status} -
-

{item.app} · {item.node}

-
- -
-
-
-
- CPU - {item.cpu}% -
- 80 ? 'red' : item.cpu > 65 ? 'amber' : 'primary'} /> -
-
-
- 메모리 - {item.memory}% -
- 80 ? 'red' : item.memory > 65 ? 'amber' : 'green'} /> -
-
-
- ))} -
-
- ); -} - -function NoticePanel() { - return ( - - - - 공지 작성 - - } - /> -
- {notices.map((notice) => ( -
-
-
- {notice.scope} - {notice.status} -
-

{notice.title}

-

{notice.audience}

-
- {notice.date} -
- ))} -
-
- ); -} - -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'); - fetch(`${apiBaseUrl}/launcher/versions/${launcher.id}/source`, { - method: 'PATCH', - headers: { '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'); - fetch(`${apiBaseUrl}/launcher/versions`, { - method: 'POST', - headers: { '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 ? ( -
-
-
-

런처 릴리즈 등록

-
-
- - - - - - - -