1
0
forked from baron/baron-sso

연동 앱 페이지 UI 정리

This commit is contained in:
2026-06-01 15:47:18 +09:00
parent d40e443d48
commit d2a7ebd82f
5 changed files with 84 additions and 134 deletions

View File

@@ -46,7 +46,6 @@ import {
type ClientSummary, type ClientSummary,
fetchClients, fetchClients,
fetchDeveloperRequestStatus, fetchDeveloperRequestStatus,
fetchDevStats,
fetchMyTenants, fetchMyTenants,
requestDeveloperAccess, requestDeveloperAccess,
} from "../../lib/devApi"; } from "../../lib/devApi";
@@ -79,12 +78,6 @@ function ClientsPage() {
enabled: hasAccessToken, enabled: hasAccessToken,
}); });
const { data: statsData, isLoading: isLoadingStats } = useQuery({
queryKey: ["dev-stats"],
queryFn: fetchDevStats,
enabled: hasAccessToken,
});
const { data: me, isLoading: isLoadingMe } = useQuery({ const { data: me, isLoading: isLoadingMe } = useQuery({
queryKey: ["userMe"], queryKey: ["userMe"],
queryFn: fetchMe, queryFn: fetchMe,
@@ -172,9 +165,6 @@ function ClientsPage() {
typeFilter, typeFilter,
]); ]);
const totalClients = statsData?.total_clients ?? clients.length;
const activeSessions = statsData?.active_sessions ?? 0;
const authFailures = statsData?.auth_failures_24h ?? 0;
const hasFilterResult = filteredClients.length > 0; const hasFilterResult = filteredClients.length > 0;
const isFilteredOut = clients.length > 0 && !hasFilterResult; const isFilteredOut = clients.length > 0 && !hasFilterResult;
const visibleClients = useMemo(() => { const visibleClients = useMemo(() => {
@@ -198,49 +188,8 @@ function ClientsPage() {
""; "";
const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole); const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole);
type StatTone = "up" | "down" | "stable";
type StatItem = {
labelKey: string;
labelFallback: string;
value: string;
deltaKey: string;
deltaFallback: string;
tone: StatTone;
};
const stats: StatItem[] = [
{
labelKey: "ui.dev.clients.stats.total",
labelFallback: "Total Applications",
value: totalClients.toString(),
deltaKey: "ui.dev.clients.stats.realtime",
deltaFallback: "Realtime",
tone: "up" as const,
},
{
labelKey: "ui.dev.clients.stats.active_sessions",
labelFallback: "Active Sessions",
value: activeSessions.toString(),
deltaKey: "ui.dev.clients.stats.realtime",
deltaFallback: "Realtime",
tone: "up" as const,
},
{
labelKey: "ui.dev.clients.stats.auth_failures",
labelFallback: "Auth Failures (24h)",
value: authFailures.toString(),
deltaKey:
authFailures > 0
? "ui.dev.clients.stats.alert"
: "ui.dev.clients.stats.stable",
deltaFallback: authFailures > 0 ? "Check Logs" : "Stable",
tone: authFailures > 0 ? ("down" as const) : ("stable" as const),
},
];
const isLoading = const isLoading =
isLoadingClients || isLoadingClients ||
isLoadingStats ||
isLoadingRequest || isLoadingRequest ||
(hasAccessToken && !profileRole && isLoadingMe); (hasAccessToken && !profileRole && isLoadingMe);
@@ -285,7 +234,7 @@ function ClientsPage() {
canCreateClient ? ( canCreateClient ? (
<Button <Button
size="sm" size="sm"
className="shadow-lg shadow-primary/30" className="mt-1 shadow-lg shadow-primary/30"
onClick={() => navigate("/clients/new")} onClick={() => navigate("/clients/new")}
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
@@ -343,7 +292,19 @@ function ClientsPage() {
/> />
<Card className="glass-panel"> <Card className="glass-panel">
<CardHeader className="pb-4 pt-6"> <CardHeader className="space-y-4 pb-4 pt-6">
<div>
<CardTitle className="text-xl font-semibold">
{t("ui.dev.clients.list.title", "클라이언트 목록")}
</CardTitle>
<CardDescription>
{t(
"msg.dev.clients.showing",
"총 {{shown}}개의 애플리케이션이 등록되어 있습니다.",
{ shown: clients.length },
)}
</CardDescription>
</div>
<SearchFilterBar <SearchFilterBar
primary={ primary={
<div className="relative flex-1"> <div className="relative flex-1">
@@ -360,34 +321,21 @@ function ClientsPage() {
</div> </div>
} }
actions={ actions={
<> <Button
<Button variant="ghost"
variant="ghost" size="sm"
size="sm" className={cn(
className={cn( "gap-1 text-muted-foreground",
"gap-1 text-muted-foreground", isAdvancedFilterOpen && "text-primary bg-primary/10",
isAdvancedFilterOpen && "text-primary bg-primary/10", )}
)} onClick={() => setIsAdvancedFilterOpen(!isAdvancedFilterOpen)}
onClick={() => setIsAdvancedFilterOpen(!isAdvancedFilterOpen)} >
> <Filter className="h-4 w-4" />
<Filter className="h-4 w-4" /> {t(
{t( "ui.dev.clients.consents.filters.advanced",
"ui.dev.clients.consents.filters.advanced", "Advanced Filters",
"Advanced Filters", )}
)} </Button>
</Button>
<div className="hidden items-center gap-2 md:flex">
<Badge variant="muted">
{t(
"ui.dev.clients.badge.tenant_selected",
"테넌트: 선택됨",
)}
</Badge>
<Badge variant="success">
{t("ui.dev.clients.badge.dev_session", "DevFront 세션")}
</Badge>
</div>
</>
} }
advancedOpen={isAdvancedFilterOpen} advancedOpen={isAdvancedFilterOpen}
advanced={ advanced={
@@ -447,59 +395,6 @@ function ClientsPage() {
} }
/> />
</CardHeader> </CardHeader>
<CardContent className="pt-0">
<div className="grid gap-3 md:grid-cols-3">
{stats.map((item) => (
<Card
key={item.labelKey}
className="border border-border/60 bg-background/70 shadow-none"
>
<CardHeader className="space-y-1 p-4">
<CardDescription>
{t(item.labelKey, item.labelFallback)}
</CardDescription>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-semibold tracking-tight">
{item.value}
</span>
<Badge
variant={
item.tone === "up"
? "success"
: item.tone === "down"
? "warning"
: "muted"
}
className={cn(
"px-2 py-0.5 text-[11px]",
item.tone === "stable" && "bg-muted/40 text-foreground",
)}
>
{t(item.deltaKey, item.deltaFallback)}
</Badge>
</div>
</CardHeader>
</Card>
))}
</div>
</CardContent>
</Card>
<Card className="glass-panel">
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
<div>
<CardTitle className="text-xl font-semibold">
{t("ui.dev.clients.list.title", "클라이언트 목록")}
</CardTitle>
<CardDescription>
{t(
"msg.dev.clients.showing",
"총 {{shown}}개의 애플리케이션이 등록되어 있습니다.",
{ shown: totalClients },
)}
</CardDescription>
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col min-h-0 pt-0"> <CardContent className="flex-1 flex flex-col min-h-0 pt-0">
<div className={commonTableShellClass}> <div className={commonTableShellClass}>
<div className={commonTableViewportClass}> <div className={commonTableViewportClass}>

View File

@@ -37,6 +37,7 @@ test("clients page loads correctly", async ({ page }) => {
// 페이지 내 주요 텍스트 확인 // 페이지 내 주요 텍스트 확인
await expect(page.getByText("연동 앱 목록")).toBeVisible(); await expect(page.getByText("연동 앱 목록")).toBeVisible();
await expect(page.getByText("Total Applications", { exact: true })).toHaveCount(0);
// 테이블 헤더 확인 // 테이블 헤더 확인
await expect( await expect(

View File

@@ -595,6 +595,10 @@ description = "Quickly review application types and headless login usage."
empty = "Review the RPs this account can access." empty = "Review the RPs this account can access."
none = "No linked applications are available." none = "No linked applications are available."
[msg.dev.dashboard.recent_changes]
description = "Review trends for changed or deleted applications on the dashboard."
empty = "There are no recent change logs yet."
[msg.dev.dashboard.notice] [msg.dev.dashboard.notice]
consent_audit = "Consent Audit" consent_audit = "Consent Audit"
dev_scope = "Dev Scope" dev_scope = "Dev Scope"
@@ -2308,6 +2312,20 @@ title = "Quick links"
[ui.dev.dashboard.recent] [ui.dev.dashboard.recent]
title = "My Applications" title = "My Applications"
[ui.dev.dashboard.recent_changes]
aria = "Recent changed application status"
deleted_group = "Deleted RPs"
period = "Recent change aggregation period"
series = "Changes {{changes}} / Actors {{actors}}"
title = "Recent Changed Apps"
y_axis = "Y axis: change count"
[ui.dev.dashboard.recent_changes.summary]
changed_clients = "Changed applications"
deleted_clients = "Deleted RPs"
latest_change = "Latest change"
total_changes = "Recent change count"
[ui.dev.dashboard.stack] [ui.dev.dashboard.stack]
notes = "Setup notes" notes = "Setup notes"
subtitle = "Devfront baseline" subtitle = "Devfront baseline"

View File

@@ -1087,6 +1087,10 @@ description = "애플리케이션 유형과 headless login 사용 현황을 빠
empty = "현재 계정이 접근할 수 있는 RP를 확인합니다." empty = "현재 계정이 접근할 수 있는 RP를 확인합니다."
none = "표시할 연동 앱이 없습니다." none = "표시할 연동 앱이 없습니다."
[msg.dev.dashboard.recent_changes]
description = "변경 또는 삭제된 애플리케이션을 대시보드에서 추이를 확인합니다."
empty = "최근 변경 로그가 아직 없습니다."
[msg.dev.dashboard.notice] [msg.dev.dashboard.notice]
consent_audit = "Consent 회수는 감사 로그와 연계" consent_audit = "Consent 회수는 감사 로그와 연계"
dev_scope = "RP 정책은 dev scope에서만 적용" dev_scope = "RP 정책은 dev scope에서만 적용"
@@ -2772,6 +2776,20 @@ title = "빠른 이동"
[ui.dev.dashboard.recent] [ui.dev.dashboard.recent]
title = "내 애플리케이션" title = "내 애플리케이션"
[ui.dev.dashboard.recent_changes]
aria = "최근 변경된 앱 현황"
deleted_group = "삭제된 RP"
period = "최근 변경 집계 단위"
series = "변경 {{changes}} / 작업자 {{actors}}"
title = "최근 변경된 앱"
y_axis = "Y축: 변경 수"
[ui.dev.dashboard.recent_changes.summary]
changed_clients = "변경된 앱 수"
deleted_clients = "삭제된 RP 수"
latest_change = "마지막 변경일"
total_changes = "최근 변경 건수"
[ui.dev.dashboard.stack] [ui.dev.dashboard.stack]
notes = "Setup notes" notes = "Setup notes"
subtitle = "Devfront baseline" subtitle = "Devfront baseline"

View File

@@ -947,6 +947,10 @@ description = ""
empty = "" empty = ""
none = "" none = ""
[msg.dev.dashboard.recent_changes]
description = ""
empty = ""
[msg.dev.dashboard.notice] [msg.dev.dashboard.notice]
consent_audit = "" consent_audit = ""
dev_scope = "" dev_scope = ""
@@ -2653,6 +2657,20 @@ title = ""
[ui.dev.dashboard.recent] [ui.dev.dashboard.recent]
title = "" title = ""
[ui.dev.dashboard.recent_changes]
aria = ""
deleted_group = ""
period = ""
series = ""
title = ""
y_axis = ""
[ui.dev.dashboard.recent_changes.summary]
changed_clients = ""
deleted_clients = ""
latest_change = ""
total_changes = ""
[ui.dev.dashboard.stack] [ui.dev.dashboard.stack]
notes = "" notes = ""
subtitle = "" subtitle = ""