forked from baron/baron-sso
adminfront 태넌트 화면 기능 누락 복구
This commit is contained in:
@@ -763,7 +763,7 @@ function TenantListPage() {
|
||||
<Input
|
||||
placeholder={t(
|
||||
"ui.admin.tenants.list.search_placeholder",
|
||||
"테넌트 이름 또는 슬러그 검색...",
|
||||
"테넌트 이름, 슬러그, UUID 검색...",
|
||||
)}
|
||||
className="h-9 pl-9"
|
||||
value={search}
|
||||
@@ -771,6 +771,64 @@ function TenantListPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex rounded-md border bg-background p-0.5"
|
||||
data-testid="tenant-view-mode-toggle"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant={viewMode === "tree" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="h-8 gap-1.5"
|
||||
aria-pressed={viewMode === "tree"}
|
||||
onClick={() => setViewMode("tree")}
|
||||
data-testid="tenant-view-tree-btn"
|
||||
>
|
||||
<Network size={14} />
|
||||
{t("ui.admin.tenants.view.tree", "트리")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={viewMode === "table" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="h-8 gap-1.5"
|
||||
aria-pressed={viewMode === "table"}
|
||||
onClick={() => setViewMode("table")}
|
||||
data-testid="tenant-view-table-btn"
|
||||
>
|
||||
<List size={14} />
|
||||
{t("ui.admin.tenants.view.table", "평면")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant={scopeTenantId ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-9 gap-2"
|
||||
onClick={() => setScopePickerOpen(true)}
|
||||
data-testid="tenant-scope-picker-btn"
|
||||
>
|
||||
<Network size={16} />
|
||||
{selectedScopeTenant
|
||||
? t("ui.admin.tenants.scope.active", "{{name}} 하위", {
|
||||
name: selectedScopeTenant.name,
|
||||
})
|
||||
: t("ui.admin.tenants.scope.pick", "상위 범위 선택")}
|
||||
</Button>
|
||||
{scopeTenantId ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9"
|
||||
onClick={() => setScopeTenantId("")}
|
||||
data-testid="tenant-scope-clear-btn"
|
||||
>
|
||||
{t("ui.common.clear", "초기화")}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<RoleGuard roles={["super_admin"]}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
|
||||
@@ -120,8 +120,8 @@ test.describe("Tenants Management", () => {
|
||||
items: [
|
||||
{
|
||||
id: "company-1",
|
||||
name: "Hanmac",
|
||||
slug: "hanmac",
|
||||
name: "Acme",
|
||||
slug: "acme",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
memberCount: 0,
|
||||
@@ -162,7 +162,7 @@ test.describe("Tenants Management", () => {
|
||||
.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i)
|
||||
.fill("team-1");
|
||||
await expect(page.locator("table")).toContainText("Platform");
|
||||
await expect(page.locator("table")).toContainText("Hanmac");
|
||||
await expect(page.locator("table")).toContainText("Acme");
|
||||
|
||||
await page.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i).fill("");
|
||||
await page
|
||||
@@ -176,6 +176,82 @@ test.describe("Tenants Management", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("switches tree and flat views, searches UUID, and selects descendants", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1100, height: 760 });
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
if (route.request().method() !== "GET") {
|
||||
return route.continue();
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "company-1",
|
||||
name: "Acme",
|
||||
slug: "acme",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "dept-1",
|
||||
name: "Planning",
|
||||
slug: "planning",
|
||||
status: "active",
|
||||
type: "ORGANIZATION",
|
||||
parentId: "company-1",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "team-1",
|
||||
name: "Platform",
|
||||
slug: "platform",
|
||||
status: "active",
|
||||
type: "USER_GROUP",
|
||||
parentId: "dept-1",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 3,
|
||||
limit: 500,
|
||||
offset: 0,
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/tenants");
|
||||
|
||||
await expect(page.getByTestId("tenant-view-tree-btn")).toBeVisible();
|
||||
await page.getByTestId("tenant-view-table-btn").click();
|
||||
await expect(page.getByTestId("tenant-view-table-btn")).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
|
||||
await page.getByPlaceholder(/UUID|슬러그|slug/i).fill("team-1");
|
||||
await expect(page.locator("table")).toContainText("Platform");
|
||||
await expect(page.locator("table")).not.toContainText("Acme");
|
||||
|
||||
await page.getByPlaceholder(/UUID|슬러그|slug/i).fill("");
|
||||
await page
|
||||
.locator("tbody tr")
|
||||
.filter({ hasText: "Acme" })
|
||||
.getByRole("checkbox")
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId("tenant-bulk-action-bar")).toContainText(
|
||||
"3개 선택됨",
|
||||
);
|
||||
});
|
||||
|
||||
test("should virtualize large tenant lists and load next pages automatically", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -565,6 +641,13 @@ test.describe("Tenants Management", () => {
|
||||
let exportUrl = "";
|
||||
let importRequested = false;
|
||||
let importBody = "";
|
||||
const openDataManagementMenu = async () => {
|
||||
const exportMenuItem = page.getByTestId("tenant-export-menu-item");
|
||||
if (!(await exportMenuItem.isVisible().catch(() => false))) {
|
||||
await page.getByTestId("tenant-data-mgmt-btn").click();
|
||||
}
|
||||
await expect(exportMenuItem).toBeVisible();
|
||||
};
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
const url = route.request().url();
|
||||
@@ -627,18 +710,34 @@ test.describe("Tenants Management", () => {
|
||||
|
||||
await expect(page.getByText(/조직\/사용자 통합/)).toHaveCount(0);
|
||||
|
||||
// Open Data Management dropdown for export check
|
||||
await page.getByTestId("tenant-data-mgmt-btn").click();
|
||||
await openDataManagementMenu();
|
||||
await expect(page.getByTestId("tenant-template-menu-item")).toBeVisible();
|
||||
await expect(page.getByTestId("tenant-export-menu-item")).toBeVisible();
|
||||
await expect(page.getByTestId("tenant-import-menu-item")).toBeVisible();
|
||||
|
||||
const download = page.waitForEvent("download");
|
||||
await page.getByTestId("tenant-export-menu-item").click();
|
||||
await download;
|
||||
await page.getByTestId("tenant-export-menu-item").dispatchEvent("click");
|
||||
const exportDownload = await download;
|
||||
expect(exportRequested).toBe(true);
|
||||
expect(exportDownload.suggestedFilename()).toBe("tenants.csv");
|
||||
expect(exportUrl).toContain("includeIds=false");
|
||||
|
||||
await openDataManagementMenu();
|
||||
await expect(
|
||||
page.getByTestId("tenant-export-with-ids-menu-item"),
|
||||
).toBeVisible();
|
||||
const exportWithIdsDownload = page.waitForEvent("download");
|
||||
await page
|
||||
.getByTestId("tenant-export-with-ids-menu-item")
|
||||
.dispatchEvent("click");
|
||||
await exportWithIdsDownload;
|
||||
expect(exportUrl).toContain("includeIds=true");
|
||||
|
||||
await openDataManagementMenu();
|
||||
const templateDownload = page.waitForEvent("download");
|
||||
await page.getByTestId("tenant-template-menu-item").dispatchEvent("click");
|
||||
const template = await templateDownload;
|
||||
expect(template.suggestedFilename()).toBe("tenant-import-template.csv");
|
||||
|
||||
// Upload directly via setInputFiles (Playwright supports hidden inputs)
|
||||
await page.getByTestId("tenant-import-input").setInputFiles({
|
||||
name: "tenants.csv",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Worksmobile tenant management", () => {
|
||||
@@ -558,4 +559,163 @@ test.describe("Worksmobile tenant management", () => {
|
||||
immutableRow.getByRole("button", { name: /비밀번호 관리/ }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test("downloads initial password CSV and enqueues WORKS admin jobs", async ({
|
||||
page,
|
||||
}) => {
|
||||
const requests: string[] = [];
|
||||
const headers = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||
};
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const method = route.request().method();
|
||||
|
||||
if (url.pathname.endsWith("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: { id: "admin-user", name: "Admin", role: "super_admin" },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.endsWith("/admin/tenants/hanmac-family-id/worksmobile") &&
|
||||
method === "GET"
|
||||
) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
tenant: {
|
||||
id: "hanmac-family-id",
|
||||
name: "한맥 가족",
|
||||
slug: "hanmac-family",
|
||||
parentId: null,
|
||||
},
|
||||
config: {
|
||||
enabled: true,
|
||||
tokenConfigured: true,
|
||||
adminTenantId: "works-tenant-1",
|
||||
},
|
||||
recentJobs: [
|
||||
{
|
||||
id: "job-retry",
|
||||
resourceType: "USER",
|
||||
resourceId: "user-failed",
|
||||
action: "sync",
|
||||
status: "failed",
|
||||
retryCount: 1,
|
||||
createdAt: "2026-05-01T00:00:00Z",
|
||||
updatedAt: "2026-05-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.endsWith(
|
||||
"/admin/tenants/hanmac-family-id/worksmobile/comparison",
|
||||
) &&
|
||||
method === "GET"
|
||||
) {
|
||||
return route.fulfill({
|
||||
json: { users: [], groups: [] },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.endsWith(
|
||||
"/admin/tenants/hanmac-family-id/worksmobile/initial-passwords.csv",
|
||||
) &&
|
||||
method === "GET"
|
||||
) {
|
||||
requests.push("download-passwords");
|
||||
return route.fulfill({
|
||||
body: "email,password\nuser@example.com,Secret123!\n",
|
||||
contentType: "text/csv",
|
||||
headers: {
|
||||
...headers,
|
||||
"Content-Disposition":
|
||||
'attachment; filename="worksmobile-passwords.csv"',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.endsWith(
|
||||
"/admin/tenants/hanmac-family-id/worksmobile/backfill/dry-run",
|
||||
) &&
|
||||
method === "POST"
|
||||
) {
|
||||
requests.push("dry-run");
|
||||
return route.fulfill({ json: { id: "job-dry-run" }, headers });
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.endsWith(
|
||||
"/admin/tenants/hanmac-family-id/worksmobile/orgunits/org-1/sync",
|
||||
) &&
|
||||
method === "POST"
|
||||
) {
|
||||
requests.push("org-sync");
|
||||
return route.fulfill({ json: { id: "job-org-sync" }, headers });
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.endsWith(
|
||||
"/admin/tenants/hanmac-family-id/worksmobile/users/user-1/sync",
|
||||
) &&
|
||||
method === "POST"
|
||||
) {
|
||||
requests.push("user-sync");
|
||||
return route.fulfill({ json: { id: "job-user-sync" }, headers });
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.endsWith(
|
||||
"/admin/tenants/hanmac-family-id/worksmobile/jobs/job-retry/retry",
|
||||
) &&
|
||||
method === "POST"
|
||||
) {
|
||||
requests.push("retry");
|
||||
return route.fulfill({ json: { id: "job-retry-next" }, headers });
|
||||
}
|
||||
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.goto("/tenants/hanmac-family-id/worksmobile");
|
||||
await expect(page.getByText("Worksmobile 연동")).toBeVisible();
|
||||
|
||||
const download = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: "초기 비밀번호 CSV" }).click();
|
||||
const passwordCsv = await download;
|
||||
expect(passwordCsv.suggestedFilename()).toBe("worksmobile-passwords.csv");
|
||||
const passwordCsvPath = await passwordCsv.path();
|
||||
expect(passwordCsvPath).toBeTruthy();
|
||||
expect(await readFile(passwordCsvPath ?? "", "utf8")).toContain(
|
||||
"user@example.com,Secret123!",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Backfill Dry-run" }).click();
|
||||
await expect.poll(() => requests).toContain("dry-run");
|
||||
|
||||
await page.getByPlaceholder("orgUnit tenant UUID").fill("org-1");
|
||||
await page.getByRole("button", { name: "조직 Sync" }).click();
|
||||
await expect.poll(() => requests).toContain("org-sync");
|
||||
|
||||
await page.getByPlaceholder("Kratos user UUID").fill("user-1");
|
||||
await page.getByRole("button", { name: "구성원 Sync" }).click();
|
||||
await expect.poll(() => requests).toContain("user-sync");
|
||||
|
||||
await page
|
||||
.getByRole("row", { name: /USER:user-failed/ })
|
||||
.getByRole("button")
|
||||
.click();
|
||||
await expect.poll(() => requests).toContain("retry");
|
||||
expect(requests).toContain("download-passwords");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user