forked from baron/baron-sso
연동 앱 페이지 레이아웃 개선
This commit is contained in:
@@ -82,6 +82,7 @@ type RecentClientChange = {
|
|||||||
|
|
||||||
const recentClientChangesInitialCount = 5;
|
const recentClientChangesInitialCount = 5;
|
||||||
const recentClientChangesBatchSize = 5;
|
const recentClientChangesBatchSize = 5;
|
||||||
|
const clientListPreviewCount = 5;
|
||||||
|
|
||||||
const recentClientActions = new Set([
|
const recentClientActions = new Set([
|
||||||
"CREATE_CLIENT",
|
"CREATE_CLIENT",
|
||||||
@@ -316,6 +317,7 @@ function ClientsPage() {
|
|||||||
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
||||||
const [isRecentChangesGuideOpen, setIsRecentChangesGuideOpen] =
|
const [isRecentChangesGuideOpen, setIsRecentChangesGuideOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isClientListExpanded, setIsClientListExpanded] = useState(false);
|
||||||
const [visibleRecentClientChangesCount, setVisibleRecentClientChangesCount] =
|
const [visibleRecentClientChangesCount, setVisibleRecentClientChangesCount] =
|
||||||
useState(recentClientChangesInitialCount);
|
useState(recentClientChangesInitialCount);
|
||||||
const [sortConfig, setSortConfig] =
|
const [sortConfig, setSortConfig] =
|
||||||
@@ -425,6 +427,14 @@ function ClientsPage() {
|
|||||||
const authFailures = statsData?.auth_failures_24h ?? 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(() => {
|
||||||
|
if (isClientListExpanded) {
|
||||||
|
return filteredClients;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredClients.slice(0, clientListPreviewCount);
|
||||||
|
}, [filteredClients, isClientListExpanded]);
|
||||||
|
const canToggleClientList = filteredClients.length > clientListPreviewCount;
|
||||||
const currentTenant = tenants?.find(
|
const currentTenant = tenants?.find(
|
||||||
(tenant) => tenant.id === tenantId || tenant.slug === companyCode,
|
(tenant) => tenant.id === tenantId || tenant.slug === companyCode,
|
||||||
);
|
);
|
||||||
@@ -784,15 +794,20 @@ function ClientsPage() {
|
|||||||
/>
|
/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<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) => (
|
{stats.map((item) => (
|
||||||
<Card key={item.labelKey} className="border border-border/60">
|
<Card
|
||||||
<CardHeader className="pb-2">
|
key={item.labelKey}
|
||||||
|
className="border border-border/60 bg-background/70 shadow-none"
|
||||||
|
>
|
||||||
|
<CardHeader className="space-y-1 p-4">
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t(item.labelKey, item.labelFallback)}
|
{t(item.labelKey, item.labelFallback)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
<div className="mt-1 flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<span className="text-3xl font-bold">{item.value}</span>
|
<span className="text-2xl font-semibold tracking-tight">
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
item.tone === "up"
|
item.tone === "up"
|
||||||
@@ -802,7 +817,7 @@ function ClientsPage() {
|
|||||||
: "muted"
|
: "muted"
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2",
|
"px-2 py-0.5 text-[11px]",
|
||||||
item.tone === "stable" && "bg-muted/40 text-foreground",
|
item.tone === "stable" && "bg-muted/40 text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -954,7 +969,7 @@ function ClientsPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{filteredClients.map((client) => (
|
{visibleClients.map((client) => (
|
||||||
<TableRow key={client.id}>
|
<TableRow key={client.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Link
|
<Link
|
||||||
@@ -1039,6 +1054,33 @@ function ClientsPage() {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,59 @@ test("clients page shows recent RP changes", async ({ page }) => {
|
|||||||
).toBeVisible();
|
).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 ({
|
test("clients page shows user-delete relation cleanup in recent changes", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user