forked from baron/baron-sso
연동 앱 페이지 레이아웃 개선
This commit is contained in:
@@ -82,6 +82,7 @@ type RecentClientChange = {
|
||||
|
||||
const recentClientChangesInitialCount = 5;
|
||||
const recentClientChangesBatchSize = 5;
|
||||
const clientListPreviewCount = 5;
|
||||
|
||||
const recentClientActions = new Set([
|
||||
"CREATE_CLIENT",
|
||||
@@ -316,6 +317,7 @@ function ClientsPage() {
|
||||
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
||||
const [isRecentChangesGuideOpen, setIsRecentChangesGuideOpen] =
|
||||
useState(false);
|
||||
const [isClientListExpanded, setIsClientListExpanded] = useState(false);
|
||||
const [visibleRecentClientChangesCount, setVisibleRecentClientChangesCount] =
|
||||
useState(recentClientChangesInitialCount);
|
||||
const [sortConfig, setSortConfig] =
|
||||
@@ -425,6 +427,14 @@ function ClientsPage() {
|
||||
const authFailures = statsData?.auth_failures_24h ?? 0;
|
||||
const hasFilterResult = filteredClients.length > 0;
|
||||
const isFilteredOut = clients.length > 0 && !hasFilterResult;
|
||||
const visibleClients = useMemo(() => {
|
||||
if (isClientListExpanded) {
|
||||
return filteredClients;
|
||||
}
|
||||
|
||||
return filteredClients.slice(0, clientListPreviewCount);
|
||||
}, [filteredClients, isClientListExpanded]);
|
||||
const canToggleClientList = filteredClients.length > clientListPreviewCount;
|
||||
const currentTenant = tenants?.find(
|
||||
(tenant) => tenant.id === tenantId || tenant.slug === companyCode,
|
||||
);
|
||||
@@ -784,15 +794,20 @@ function ClientsPage() {
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<Card key={item.labelKey} className="border border-border/60">
|
||||
<CardHeader className="pb-2">
|
||||
<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="mt-1 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold">{item.value}</span>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-2xl font-semibold tracking-tight">
|
||||
{item.value}
|
||||
</span>
|
||||
<Badge
|
||||
variant={
|
||||
item.tone === "up"
|
||||
@@ -802,7 +817,7 @@ function ClientsPage() {
|
||||
: "muted"
|
||||
}
|
||||
className={cn(
|
||||
"px-2",
|
||||
"px-2 py-0.5 text-[11px]",
|
||||
item.tone === "stable" && "bg-muted/40 text-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -954,7 +969,7 @@ function ClientsPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{filteredClients.map((client) => (
|
||||
{visibleClients.map((client) => (
|
||||
<TableRow key={client.id}>
|
||||
<TableCell>
|
||||
<Link
|
||||
@@ -1039,6 +1054,33 @@ function ClientsPage() {
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
{canToggleClientList ? (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={
|
||||
isClientListExpanded
|
||||
? t(
|
||||
"ui.dev.clients.list.collapse_aria",
|
||||
"연동 앱 목록 접기",
|
||||
)
|
||||
: t(
|
||||
"ui.dev.clients.list.more_aria",
|
||||
"연동 앱 목록 더보기",
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
setIsClientListExpanded((current) => !current)
|
||||
}
|
||||
>
|
||||
{isClientListExpanded
|
||||
? t("ui.common.collapse", "접기")
|
||||
: t("ui.common.load_more", "더보기")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -100,6 +100,59 @@ test("clients page shows recent RP changes", async ({ page }) => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("clients page shows only five apps by default and expands with more button", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedAuth(page, "super_admin");
|
||||
const clients = Array.from({ length: 6 }, (_, index) =>
|
||||
makeClient(`client-${index + 1}`, {
|
||||
name: `Preview App ${index + 1}`,
|
||||
createdAt: new Date(
|
||||
Date.UTC(2026, 2, 3, 9, 10 - index, 0),
|
||||
).toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
await installDevApiMock(page, {
|
||||
clients,
|
||||
consents: [] as Consent[],
|
||||
auditLogs: [] as AuditLog[],
|
||||
auditLogsByCursor: undefined,
|
||||
});
|
||||
|
||||
await page.goto("/clients");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "연동 앱 목록" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator("table").first().locator("tbody tr").filter({
|
||||
hasText: /Preview App \d/,
|
||||
}),
|
||||
).toHaveCount(5);
|
||||
await expect(
|
||||
page.getByText("Preview App 6", { exact: true }),
|
||||
).not.toBeVisible();
|
||||
|
||||
const moreButton = page.getByRole("button", {
|
||||
name: "연동 앱 목록 더보기",
|
||||
});
|
||||
await expect(moreButton).toBeVisible();
|
||||
await expect(moreButton).toHaveCount(1);
|
||||
await moreButton.click();
|
||||
|
||||
await expect(
|
||||
page.locator("table").first().locator("tbody tr").filter({
|
||||
hasText: /Preview App \d/,
|
||||
}),
|
||||
).toHaveCount(6);
|
||||
await expect(
|
||||
page.getByText("Preview App 6", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "연동 앱 목록 더보기" }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("clients page shows user-delete relation cleanup in recent changes", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user