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:
@@ -102,11 +102,22 @@ func pageTenantsByCursor(tenants []domain.Tenant, limit int, cursorRaw string) (
|
||||
})
|
||||
}
|
||||
|
||||
type tenantImportDetail struct {
|
||||
Row int `json:"row"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Success bool `json:"success"`
|
||||
Action string `json:"action"` // "created", "updated", "failed", "skipped"
|
||||
Message string `json:"message"` // Detailed error or success message
|
||||
ModifiedFields []string `json:"modifiedFields"` // List of fields changed during update
|
||||
}
|
||||
|
||||
type tenantImportResult struct {
|
||||
Created int `json:"created"`
|
||||
Updated int `json:"updated"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []string `json:"errors"`
|
||||
Created int `json:"created"`
|
||||
Updated int `json:"updated"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []string `json:"errors"`
|
||||
Details []tenantImportDetail `json:"details"`
|
||||
}
|
||||
|
||||
type tenantDomainConflict struct {
|
||||
@@ -557,32 +568,64 @@ func (h *TenantHandler) ImportTenantsCSV(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
result := tenantImportResult{Errors: make([]string, 0)}
|
||||
result := tenantImportResult{
|
||||
Errors: make([]string, 0),
|
||||
Details: make([]tenantImportDetail, 0, len(records)),
|
||||
}
|
||||
for i, record := range records {
|
||||
rowNumber := i + 2
|
||||
detail := tenantImportDetail{
|
||||
Row: rowNumber,
|
||||
Slug: record.Slug,
|
||||
Name: record.Name,
|
||||
}
|
||||
|
||||
if record.ParentTenantID == nil && record.ParentTenantSlug != "" {
|
||||
parentID := tenantIDBySlug[strings.ToLower(record.ParentTenantSlug)]
|
||||
if parentID == "" {
|
||||
result.Failed++
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("row %d: parent tenant slug not found: %s", rowNumber, record.ParentTenantSlug))
|
||||
msg := fmt.Sprintf("row %d: parent tenant slug not found: %s", rowNumber, record.ParentTenantSlug)
|
||||
result.Errors = append(result.Errors, msg)
|
||||
detail.Success = false
|
||||
detail.Action = "failed"
|
||||
detail.Message = msg
|
||||
result.Details = append(result.Details, detail)
|
||||
continue
|
||||
}
|
||||
record.ParentTenantID = &parentID
|
||||
}
|
||||
if record.TenantID != "" || (h.DB != nil && record.Slug != "") {
|
||||
tenant, updated, err := h.upsertTenantCSVRecord(c, record)
|
||||
tenant, modifiedFields, err := h.upsertTenantCSVRecord(c, record)
|
||||
if err != nil {
|
||||
result.Failed++
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("row %d: %s", rowNumber, err.Error()))
|
||||
msg := fmt.Sprintf("row %d: %s", rowNumber, err.Error())
|
||||
result.Errors = append(result.Errors, msg)
|
||||
detail.Success = false
|
||||
detail.Action = "failed"
|
||||
detail.Message = msg
|
||||
result.Details = append(result.Details, detail)
|
||||
continue
|
||||
}
|
||||
if updated {
|
||||
tenantIDBySlug[strings.ToLower(record.Slug)] = tenant.ID
|
||||
result.Updated++
|
||||
if h.Worksmobile != nil {
|
||||
_ = h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant)
|
||||
if tenant != nil {
|
||||
if len(modifiedFields) > 0 {
|
||||
tenantIDBySlug[strings.ToLower(record.Slug)] = tenant.ID
|
||||
result.Updated++
|
||||
detail.Success = true
|
||||
detail.Action = "updated"
|
||||
detail.ModifiedFields = modifiedFields
|
||||
if h.Worksmobile != nil {
|
||||
_ = h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant)
|
||||
}
|
||||
result.Details = append(result.Details, detail)
|
||||
continue
|
||||
} else {
|
||||
// No changes, skip
|
||||
detail.Success = true
|
||||
detail.Action = "skipped"
|
||||
detail.Message = "no changes detected"
|
||||
result.Details = append(result.Details, detail)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,19 +637,32 @@ func (h *TenantHandler) ImportTenantsCSV(c *fiber.Ctx) error {
|
||||
tenant, err := h.createTenantCSVRecord(c, record, recordCreatorID)
|
||||
if err != nil {
|
||||
result.Failed++
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("row %d: %s", rowNumber, err.Error()))
|
||||
msg := fmt.Sprintf("row %d: %s", rowNumber, err.Error())
|
||||
result.Errors = append(result.Errors, msg)
|
||||
detail.Success = false
|
||||
detail.Action = "failed"
|
||||
detail.Message = msg
|
||||
result.Details = append(result.Details, detail)
|
||||
continue
|
||||
}
|
||||
if tenant == nil {
|
||||
result.Failed++
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("row %d: tenant creation returned empty result", rowNumber))
|
||||
msg := fmt.Sprintf("row %d: tenant creation returned empty result", rowNumber)
|
||||
result.Errors = append(result.Errors, msg)
|
||||
detail.Success = false
|
||||
detail.Action = "failed"
|
||||
detail.Message = msg
|
||||
result.Details = append(result.Details, detail)
|
||||
continue
|
||||
}
|
||||
tenantIDBySlug[strings.ToLower(record.Slug)] = tenant.ID
|
||||
result.Created++
|
||||
detail.Success = true
|
||||
detail.Action = "created"
|
||||
if h.Worksmobile != nil {
|
||||
_ = h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant)
|
||||
}
|
||||
result.Details = append(result.Details, detail)
|
||||
}
|
||||
|
||||
return c.JSON(result)
|
||||
@@ -1385,12 +1441,12 @@ func (h *TenantHandler) replaceTenantDomains(ctx context.Context, tenantID strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *TenantHandler) upsertTenantCSVRecord(c *fiber.Ctx, record tenantCSVRecord) (*domain.Tenant, bool, error) {
|
||||
func (h *TenantHandler) upsertTenantCSVRecord(c *fiber.Ctx, record tenantCSVRecord) (*domain.Tenant, []string, error) {
|
||||
if h.DB == nil {
|
||||
if record.TenantID != "" {
|
||||
return nil, false, errors.New("database not available for tenant update")
|
||||
return nil, nil, errors.New("database not available for tenant update")
|
||||
}
|
||||
return nil, false, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
var tenant domain.Tenant
|
||||
@@ -1403,42 +1459,85 @@ func (h *TenantHandler) upsertTenantCSVRecord(c *fiber.Ctx, record tenantCSVReco
|
||||
}
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
modifiedFields := []string{}
|
||||
if tenant.Name != record.Name {
|
||||
tenant.Name = record.Name
|
||||
modifiedFields = append(modifiedFields, "Name")
|
||||
}
|
||||
if tenant.Type != record.Type {
|
||||
tenant.Type = record.Type
|
||||
modifiedFields = append(modifiedFields, "Type")
|
||||
}
|
||||
if record.ParentTenantID != nil {
|
||||
oldParentID := ""
|
||||
if tenant.ParentID != nil {
|
||||
oldParentID = *tenant.ParentID
|
||||
}
|
||||
if oldParentID != *record.ParentTenantID {
|
||||
tenant.ParentID = record.ParentTenantID
|
||||
modifiedFields = append(modifiedFields, "ParentID")
|
||||
}
|
||||
} else if tenant.ParentID != nil {
|
||||
tenant.ParentID = nil
|
||||
modifiedFields = append(modifiedFields, "ParentID")
|
||||
}
|
||||
|
||||
if tenant.Slug != record.Slug {
|
||||
tenant.Slug = record.Slug
|
||||
modifiedFields = append(modifiedFields, "Slug")
|
||||
}
|
||||
if tenant.Description != record.Memo {
|
||||
tenant.Description = record.Memo
|
||||
modifiedFields = append(modifiedFields, "Description")
|
||||
}
|
||||
|
||||
tenant.Name = record.Name
|
||||
tenant.Type = record.Type
|
||||
tenant.ParentID = record.ParentTenantID
|
||||
tenant.Slug = record.Slug
|
||||
tenant.Description = record.Memo
|
||||
if tenant.Status == "" {
|
||||
tenant.Status = domain.TenantStatusActive
|
||||
}
|
||||
mergedConfig, changedConfig, err := mergeTenantCSVRecordConfig(tenant.Config, record)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if changedConfig {
|
||||
tenant.Config = mergedConfig
|
||||
modifiedFields = append(modifiedFields, "Config")
|
||||
}
|
||||
|
||||
existingDomains := make([]string, len(tenant.Domains))
|
||||
for i, d := range tenant.Domains {
|
||||
existingDomains[i] = d.Domain
|
||||
}
|
||||
sort.Strings(existingDomains)
|
||||
newDomains := append([]string(nil), record.Domains...)
|
||||
sort.Strings(newDomains)
|
||||
if !reflect.DeepEqual(existingDomains, newDomains) {
|
||||
modifiedFields = append(modifiedFields, "Domains")
|
||||
}
|
||||
|
||||
if len(modifiedFields) == 0 {
|
||||
return &tenant, nil, nil
|
||||
}
|
||||
|
||||
if err := h.DB.Save(&tenant).Error; err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := h.DB.Delete(&domain.TenantDomain{}, "tenant_id = ?", tenant.ID).Error; err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
repo := repository.NewTenantRepository(h.DB)
|
||||
for _, domainName := range record.Domains {
|
||||
if err := repo.AddDomain(c.Context(), tenant.ID, domainName, true); err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &tenant, true, nil
|
||||
return &tenant, modifiedFields, nil
|
||||
}
|
||||
|
||||
func (h *TenantHandler) createTenantCSVRecord(c *fiber.Ctx, record tenantCSVRecord, creatorID string) (*domain.Tenant, error) {
|
||||
|
||||
Reference in New Issue
Block a user