forked from baron/baron-sso
feat(admin): improve tenant bulk import reporting with detailed results
- Backend: Enhanced 'ImportTenantsCSV' to return row-by-row details including action, status, and modified fields. - Backend: Refactored 'upsertTenantCSVRecord' to detect and return specific modified fields (Name, Type, ParentID, Slug, Description, Config, Domains). - Frontend: Added 'TenantImportDetail' and updated 'TenantImportResult' types. - Frontend: Implemented a detailed results modal in 'TenantListPage' showing processing summary and row-level feedback for better transparency.
This commit is contained in:
@@ -285,6 +285,9 @@ function TenantListPage() {
|
||||
Record<number, string>
|
||||
>({});
|
||||
const [previewOpen, setPreviewOpen] = React.useState(false);
|
||||
const [importResult, setImportResult] =
|
||||
React.useState<TenantImportResult | null>(null);
|
||||
const [importResultOpen, setImportResultOpen] = React.useState(false);
|
||||
const [selectedBulkStatus, setSelectedBulkStatus] = React.useState("");
|
||||
const _tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -387,17 +390,8 @@ function TenantListPage() {
|
||||
const importMutation = useMutation({
|
||||
mutationFn: (file: File) => importTenantsCSV(file),
|
||||
onSuccess: (result) => {
|
||||
setImportMessage(
|
||||
t(
|
||||
"msg.admin.tenants.import_result",
|
||||
"생성 {{created}}, 갱신 {{updated}}, 실패 {{failed}}",
|
||||
{
|
||||
created: result.created,
|
||||
updated: result.updated,
|
||||
failed: result.failed,
|
||||
},
|
||||
),
|
||||
);
|
||||
setImportResult(result);
|
||||
setImportResultOpen(true);
|
||||
setPreviewOpen(false);
|
||||
setPreviewRows([]);
|
||||
setSelectedMatches({});
|
||||
@@ -1006,6 +1000,128 @@ function TenantListPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={importResultOpen} onOpenChange={setImportResultOpen}>
|
||||
<DialogContent className="max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t(
|
||||
"ui.admin.tenants.import_result.title",
|
||||
"가져오기 결과 리포트",
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{importResult &&
|
||||
t(
|
||||
"msg.admin.tenants.import_result.summary",
|
||||
"총 {{total}}건 처리: 생성 {{created}}, 갱신 {{updated}}, 실패 {{failed}}",
|
||||
{
|
||||
total: importResult.details.length,
|
||||
created: importResult.created,
|
||||
updated: importResult.updated,
|
||||
failed: importResult.failed,
|
||||
},
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="max-h-[60vh] overflow-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader className={commonStickyTableHeaderClass}>
|
||||
<TableRow>
|
||||
<TableHead className="w-[72px]">
|
||||
{t("ui.common.row", "행")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.tenants.table.name", "NAME")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.tenants.table.slug", "SLUG")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.tenants.import_result.action", "작업")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.tenants.import_result.status", "상태")}
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
{t("ui.admin.tenants.import_result.message", "상세 내용")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importResult?.details.map((detail) => (
|
||||
<TableRow key={detail.row}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{detail.row}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{detail.name}</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{detail.slug}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
detail.action === "created"
|
||||
? "success"
|
||||
: detail.action === "updated"
|
||||
? "warning"
|
||||
: detail.action === "skipped"
|
||||
? "outline"
|
||||
: "destructive"
|
||||
}
|
||||
className="text-[10px]"
|
||||
>
|
||||
{detail.action.toUpperCase()}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{detail.success ? (
|
||||
<Badge variant="success" className="text-[10px]">
|
||||
SUCCESS
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive" className="text-[10px]">
|
||||
FAILED
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{detail.message}
|
||||
{detail.modifiedFields &&
|
||||
detail.modifiedFields.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
<span className="mr-1 text-muted-foreground">
|
||||
{t(
|
||||
"ui.admin.tenants.import_result.modified",
|
||||
"수정됨:",
|
||||
)}
|
||||
</span>
|
||||
{detail.modifiedFields.map((field) => (
|
||||
<Badge
|
||||
key={field}
|
||||
variant="outline"
|
||||
className="bg-muted text-[10px]"
|
||||
>
|
||||
{field}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setImportResultOpen(false)}>
|
||||
{t("ui.common.confirm", "확인")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
|
||||
<DialogContent className="max-w-5xl">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -70,11 +70,22 @@ export type TenantUpdateRequest = {
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TenantImportDetail = {
|
||||
row: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
success: boolean;
|
||||
action: "created" | "updated" | "failed" | "skipped";
|
||||
message: string;
|
||||
modifiedFields?: string[];
|
||||
};
|
||||
|
||||
export type TenantImportResult = {
|
||||
created: number;
|
||||
updated: number;
|
||||
failed: number;
|
||||
errors: string[];
|
||||
details: TenantImportDetail[];
|
||||
};
|
||||
|
||||
export type ApiKeySummary = {
|
||||
|
||||
Reference in New Issue
Block a user