forked from baron/baron-sso
Implement tenant import and RP auto login policies
This commit is contained in:
@@ -51,6 +51,7 @@ import {
|
||||
} from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
import {
|
||||
type TenantImportResolution,
|
||||
type TenantImportPreviewRow,
|
||||
buildTenantImportPreview,
|
||||
parseTenantCSV,
|
||||
@@ -58,7 +59,7 @@ import {
|
||||
} from "../utils/tenantCsvImport";
|
||||
|
||||
const tenantCSVTemplate =
|
||||
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain\n";
|
||||
"name,type,parent_tenant_slug,slug,memo,email_domain\n";
|
||||
|
||||
function TenantListPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -72,6 +73,9 @@ function TenantListPage() {
|
||||
const [selectedMatches, setSelectedMatches] = React.useState<
|
||||
Record<number, string>
|
||||
>({});
|
||||
const [selectedCreateSlugs, setSelectedCreateSlugs] = React.useState<
|
||||
Record<number, string>
|
||||
>({});
|
||||
const [previewOpen, setPreviewOpen] = React.useState(false);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
@@ -117,7 +121,7 @@ function TenantListPage() {
|
||||
});
|
||||
|
||||
const exportMutation = useMutation({
|
||||
mutationFn: exportTenantsCSV,
|
||||
mutationFn: (includeIds: boolean) => exportTenantsCSV(includeIds),
|
||||
onSuccess: ({ blob, filename }) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
@@ -265,16 +269,44 @@ function TenantListPage() {
|
||||
setPreviewRows(preview);
|
||||
setSelectedMatches(
|
||||
Object.fromEntries(
|
||||
preview
|
||||
.filter((row) => row.defaultTenantId)
|
||||
.map((row) => [row.row.rowNumber, row.defaultTenantId]),
|
||||
preview.map((row) => [
|
||||
row.row.rowNumber,
|
||||
row.defaultTenantId || "__create__",
|
||||
]),
|
||||
),
|
||||
);
|
||||
setSelectedCreateSlugs(
|
||||
Object.fromEntries(
|
||||
preview.map((row) => [row.row.rowNumber, row.defaultCreateSlug]),
|
||||
),
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleImportConfirm = () => {
|
||||
const csv = serializeTenantImportCSV(previewRows, selectedMatches);
|
||||
const resolutions: Record<number, TenantImportResolution> =
|
||||
Object.fromEntries(
|
||||
previewRows.map((preview) => {
|
||||
const selected = selectedMatches[preview.row.rowNumber] ?? "";
|
||||
if (selected && selected !== "__create__") {
|
||||
return [
|
||||
preview.row.rowNumber,
|
||||
{ mode: "existing", tenantId: selected },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
preview.row.rowNumber,
|
||||
{
|
||||
mode: "create",
|
||||
slug:
|
||||
selectedCreateSlugs[preview.row.rowNumber] ||
|
||||
preview.defaultCreateSlug,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
const csv = serializeTenantImportCSV(previewRows, resolutions);
|
||||
const file = new File([csv], "tenants.csv", { type: "text/csv" });
|
||||
importMutation.mutate(file);
|
||||
};
|
||||
@@ -343,12 +375,21 @@ function TenantListPage() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => exportMutation.mutate()}
|
||||
onClick={() => exportMutation.mutate(false)}
|
||||
disabled={exportMutation.isPending}
|
||||
data-testid="tenant-export-btn"
|
||||
>
|
||||
<Download size={16} />
|
||||
{t("ui.admin.tenants.export", "내보내기")}
|
||||
{t("ui.admin.tenants.export_without_ids", "UUID 제외 내보내기")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => exportMutation.mutate(true)}
|
||||
disabled={exportMutation.isPending}
|
||||
data-testid="tenant-export-with-ids-btn"
|
||||
>
|
||||
<Download size={16} />
|
||||
{t("ui.admin.tenants.export_with_ids", "UUID 포함")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -622,19 +663,41 @@ function TenantListPage() {
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{preview.row.slug}
|
||||
{preview.conflicts.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{preview.conflicts.map((conflict) => (
|
||||
<Badge
|
||||
key={conflict}
|
||||
variant="outline"
|
||||
className="text-[10px]"
|
||||
>
|
||||
{conflict === "external_tenant_id"
|
||||
? t(
|
||||
"ui.admin.tenants.import_preview.external_id",
|
||||
"외부 ID",
|
||||
)
|
||||
: conflict === "slug_exists"
|
||||
? t(
|
||||
"ui.admin.tenants.import_preview.slug_exists",
|
||||
"slug 충돌",
|
||||
)
|
||||
: t(
|
||||
"ui.admin.tenants.import_preview.parent_unresolved",
|
||||
"부모 확인 필요",
|
||||
)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{preview.row.tenantId ? (
|
||||
<Badge variant="outline">
|
||||
{t(
|
||||
"ui.admin.tenants.import_preview.fixed_id",
|
||||
"ID 지정됨",
|
||||
)}
|
||||
</Badge>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<select
|
||||
className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm"
|
||||
value={selectedMatches[preview.row.rowNumber] ?? ""}
|
||||
value={
|
||||
selectedMatches[preview.row.rowNumber] ??
|
||||
"__create__"
|
||||
}
|
||||
data-testid={`tenant-import-match-select-${preview.row.rowNumber}`}
|
||||
onChange={(event) =>
|
||||
setSelectedMatches((prev) => ({
|
||||
@@ -643,10 +706,10 @@ function TenantListPage() {
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="">
|
||||
<option value="__create__">
|
||||
{t(
|
||||
"ui.admin.tenants.import_preview.create_new",
|
||||
"신규 생성",
|
||||
"ui.admin.tenants.import_preview.create_new_reset",
|
||||
"신규 생성 (ID/slug 재설정)",
|
||||
)}
|
||||
</option>
|
||||
{preview.candidates.map((candidate) => (
|
||||
@@ -658,7 +721,22 @@ function TenantListPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{(selectedMatches[preview.row.rowNumber] ??
|
||||
"__create__") === "__create__" && (
|
||||
<Input
|
||||
value={
|
||||
selectedCreateSlugs[preview.row.rowNumber] ?? ""
|
||||
}
|
||||
data-testid={`tenant-import-create-slug-${preview.row.rowNumber}`}
|
||||
onChange={(event) =>
|
||||
setSelectedCreateSlugs((prev) => ({
|
||||
...prev,
|
||||
[preview.row.rowNumber]: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{preview.candidates.length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user