paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
2026-07-07 15:32:56 +09:00
parent 1de085790d
commit 9cb82249d7
14 changed files with 805 additions and 214 deletions
+64 -34
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react';
import { useEffect, useRef, useState, type FormEvent, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Activity,
@@ -17,6 +17,7 @@ import { formatDateTime as formatDateTimeValue } from './lib/date-format';
import { apiBaseUrl, apiRequest, authExpiredEventName, clearAdminSession } from './lib/api';
import { filterAppsByCurrentAdmin } from './lib/admin-app-permissions';
import { cn } from './lib/utils';
import { openPageAgent } from './lib/page-agent';
import { AppManagementPage as AppManagementApiPage } from './pages/applications/AppManagementPage';
import { BuildManagementPage } from './pages/applications/BuildManagementPage';
import { DeploymentManagementPage } from './pages/applications/DeploymentManagementPage';
@@ -446,6 +447,19 @@ function filterNavGroupsByRole(groups: typeof navGroups, role?: string | null) {
.filter((group) => group.items.length > 0);
}
function pageAgentAccessAttrs(allowed: boolean) {
return allowed
? {}
: {
'data-page-agent-ignore': 'true',
'data-page-agent-not-interactive': 'true'
};
}
function isTrustedClick(event: ReactMouseEvent<HTMLElement>) {
return event.nativeEvent.isTrusted;
}
function Sidebar({
activeView,
menuSearch,
@@ -491,8 +505,11 @@ function Sidebar({
</div>
<nav className="scrollbar-stable min-h-0 flex-1 space-y-5 overflow-y-auto pr-2">
{visibleGroups.map((group) => (
<div key={group.label}>
{visibleGroups.map((group) => {
const isPageAgentAllowedGroup = group.label === '플랫폼';
return (
<div key={group.label} {...pageAgentAccessAttrs(isPageAgentAllowedGroup)}>
<p className="mb-2 px-2 text-xs font-medium text-muted-foreground">{group.label}</p>
<div className="space-y-1">
{group.items.map((item) => {
@@ -504,12 +521,14 @@ function Sidebar({
return (
<div key={item.label}>
<button
type="button"
onClick={() => {
if (hasChildren) {
setOpenMenus((prev) => ({ ...prev, [item.label]: !prev[item.label] }));
return;
<button
type="button"
{...pageAgentAccessAttrs(isPageAgentAllowedGroup)}
onClick={(event) => {
if (!isPageAgentAllowedGroup && !isTrustedClick(event)) return;
if (hasChildren) {
setOpenMenus((prev) => ({ ...prev, [item.label]: !prev[item.label] }));
return;
}
const view = resolveNavView(item);
if (view) onViewChange(view);
@@ -535,13 +554,15 @@ function Sidebar({
const isChildActive = activeView === resolveNavView(child);
return (
<button
type="button"
key={child.label}
onClick={() => {
const view = resolveNavView(child);
if (view) onViewChange(view);
}}
<button
type="button"
key={child.label}
{...pageAgentAccessAttrs(isPageAgentAllowedGroup)}
onClick={(event) => {
if (!isPageAgentAllowedGroup && !isTrustedClick(event)) return;
const view = resolveNavView(child);
if (view) onViewChange(view);
}}
className={cn(
'flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-[12.5px] font-medium text-muted-foreground transition-colors hover:bg-sidebar-accent hover:text-white',
isChildActive && 'font-semibold text-white'
@@ -559,7 +580,8 @@ function Sidebar({
})}
</div>
</div>
))}
);
})}
{visibleGroups.length === 0 ? (
<div className="px-2 py-6 text-center text-xs text-muted-foreground">검색된 메뉴가 없습니다.</div>
) : null}
@@ -633,7 +655,7 @@ function Topbar({
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" className="size-9 px-0">
<Button variant="ghost" className="size-9 px-0" title="AI 도움말" aria-label="AI 도움말" onClick={openPageAgent}>
<CircleHelp className="size-4" />
</Button>
<div ref={profileRef} className="relative">
@@ -1028,16 +1050,20 @@ function SessionsPanel({ sessions, loading, onMore }: { sessions: UserSessionRow
<Card>
<CardHeader
title="사용자/세션"
description="실시간 접근 상태와 세션 제어 항목입니다."
action={
<button
type="button"
className="flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
onClick={onMore}
title="사용자/세션으로 이동"
aria-label="사용자/세션으로 이동"
>
<ChevronRight className="size-4" />
description="실시간 접근 상태와 세션 제어 항목입니다."
action={
<button
type="button"
{...pageAgentAccessAttrs(false)}
className="flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
onClick={(event) => {
if (!isTrustedClick(event)) return;
onMore();
}}
title="사용자/세션으로 이동"
aria-label="사용자/세션으로 이동"
>
<ChevronRight className="size-4" />
</button>
}
/>
@@ -1077,13 +1103,17 @@ function SessionsPanel({ sessions, loading, onMore }: { sessions: UserSessionRow
);
}
function DashboardLinkButton({ onClick, label }: { onClick: () => void; label: string }) {
function DashboardLinkButton({ onClick, label, pageAgentAllowed = true }: { onClick: () => void; label: string; pageAgentAllowed?: boolean }) {
return (
<button
type="button"
className="flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
onClick={onClick}
title={label}
type="button"
{...pageAgentAccessAttrs(pageAgentAllowed)}
className="flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
onClick={(event) => {
if (!pageAgentAllowed && !isTrustedClick(event)) return;
onClick();
}}
title={label}
aria-label={label}
>
<ChevronRight className="size-4" />
@@ -1270,7 +1300,7 @@ function NoticePanel({ notices: dashboardNotices, loading, onOpen }: { notices:
<CardHeader
title="공지사항"
description="런처 공지와 앱별 공지사항을 관리합니다."
action={<DashboardLinkButton onClick={onOpen} label="공지사항으로 이동" />}
action={<DashboardLinkButton onClick={onOpen} label="공지사항으로 이동" pageAgentAllowed={false} />}
/>
<div className="max-h-[520px] divide-y divide-border overflow-y-auto">
{loading ? (