paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
+64
-34
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user