forked from baron/baron-sso
perf(admin): implement server-side search and virtualization for tenant list
- Backend: Added 'search' parameter to TenantRepository and TenantService. - Backend: Updated all Tenant list calls to support searching. - Backend: Enhanced UserRepository.List to support cursor-based pagination and search. - Frontend: Switched TenantListPage to use useInfiniteQuery for lazy loading. - Frontend: Implemented list virtualization in TenantHierarchyView using @tanstack/react-virtual. - Frontend: Added server-side search with debouncing (useDeferredValue). - Fixed various Go compilation errors caused by method signature changes.
This commit is contained in:
@@ -235,7 +235,7 @@ func (h *AdminHandler) countTenants(ctx context.Context) int64 {
|
||||
if h == nil || h.TenantRepo == nil {
|
||||
return 0
|
||||
}
|
||||
_, total, err := h.TenantRepo.List(ctx, 1, 0, "")
|
||||
_, total, err := h.TenantRepo.List(ctx, 1, 0, "", "")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -629,7 +629,7 @@ func (h *AuthHandler) GetActiveTenants(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 3. List and Filter Tenants
|
||||
tenants, _, err := h.TenantService.ListTenants(c.Context(), 1000, 0, "")
|
||||
tenants, _, err := h.TenantService.ListTenants(c.Context(), 1000, 0, "", "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "Failed to fetch tenants")
|
||||
}
|
||||
|
||||
@@ -3571,7 +3571,7 @@ func (h *DevHandler) ListMyTenants(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
if role == domain.RoleSuperAdmin {
|
||||
tenants, _, err := h.TenantSvc.ListTenants(c.Context(), 100, 0, "")
|
||||
tenants, _, err := h.TenantSvc.ListTenants(c.Context(), 100, 0, "", "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "failed to list tenants")
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func (h *UserHandler) resolveHanmacEmailScope(ctx context.Context) (*hanmacEmail
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tenants, _, err := h.TenantService.ListTenants(ctx, 10000, 0, "")
|
||||
tenants, _, err := h.TenantService.ListTenants(ctx, 10000, 0, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
|
||||
if role != domain.RoleSuperAdmin {
|
||||
// Not a super admin: Only return the entire tree(s) of the tenants they belong to
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", "")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -343,13 +343,13 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
} else {
|
||||
// Super Admin case
|
||||
if cursorRaw != "" && h.DB != nil {
|
||||
tenants, total, nextCursor, err = h.listTenantsByCursor(c.Context(), limit, parentId, cursorRaw)
|
||||
tenants, total, nextCursor, err = h.listTenantsByCursor(c.Context(), limit, parentId, cursorRaw, "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
|
||||
}
|
||||
offset = 0
|
||||
} else {
|
||||
tenants, total, err = h.Service.ListTenants(c.Context(), limit, offset, parentId)
|
||||
tenants, total, err = h.Service.ListTenants(c.Context(), limit, offset, parentId, "")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -382,7 +382,7 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, parentID string, cursorRaw string) ([]domain.Tenant, int64, string, error) {
|
||||
func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, parentID string, cursorRaw string, search string) ([]domain.Tenant, int64, string, error) {
|
||||
cursor, err := pagination.Decode(cursorRaw)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
@@ -395,6 +395,12 @@ func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, pare
|
||||
pageQuery = pageQuery.Where("parent_id = ?", parentID)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
searchTerm := "%" + strings.ToLower(search) + "%"
|
||||
countQuery = countQuery.Where("LOWER(name) LIKE ? OR LOWER(slug) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm, searchTerm)
|
||||
pageQuery = pageQuery.Where("LOWER(name) LIKE ? OR LOWER(slug) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm, searchTerm)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := countQuery.Count(&total).Error; err != nil {
|
||||
return nil, 0, "", err
|
||||
@@ -422,7 +428,7 @@ func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, pare
|
||||
|
||||
func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
parentID := strings.TrimSpace(c.Query("parentId"))
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -566,7 +572,7 @@ func (h *TenantHandler) ImportTenantsCSV(c *fiber.Ctx) error {
|
||||
|
||||
tenantIDBySlug := make(map[string]string)
|
||||
if h.Service != nil {
|
||||
if tenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, ""); err == nil {
|
||||
if tenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", ""); err == nil {
|
||||
for _, tenant := range tenants {
|
||||
tenantIDBySlug[strings.ToLower(tenant.Slug)] = tenant.ID
|
||||
}
|
||||
@@ -2336,7 +2342,7 @@ func (h *TenantHandler) GetOrgContext(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "tenant service is not configured")
|
||||
}
|
||||
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
@@ -2410,7 +2416,7 @@ func (h *TenantHandler) loadOrgContextMembers(ctx context.Context, tenantIDs, te
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersByAppointment, _, err := h.UserRepo.List(ctx, 0, 10000, "", "")
|
||||
usersByAppointment, _, _, err := h.UserRepo.List(ctx, 0, 10000, "", "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2741,7 +2747,7 @@ func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, err.Error())
|
||||
}
|
||||
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "", "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
|
||||
// Expand manageableSlugs to the entire tenant tree (root + all descendants)
|
||||
if h.TenantService != nil && len(baseTenantIDs) > 0 {
|
||||
allTenants, _, err := h.TenantService.ListTenants(c.Context(), 10000, 0, "")
|
||||
allTenants, _, err := h.TenantService.ListTenants(c.Context(), 10000, 0, "", "")
|
||||
if err == nil {
|
||||
parentMap := make(map[string]string)
|
||||
for _, t := range allTenants {
|
||||
@@ -1614,7 +1614,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 1. Fetch Users using Repo for efficiency
|
||||
users, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, tenantSlug)
|
||||
users, _, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, tenantSlug, "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch users for export")
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type TenantRepository interface {
|
||||
FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error)
|
||||
FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error)
|
||||
AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error
|
||||
List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error)
|
||||
List(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error)
|
||||
ListByType(ctx context.Context, tenantType string) ([]domain.Tenant, error)
|
||||
DeleteBulk(ctx context.Context, ids []string) error
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (r *tenantRepository) AddDomain(ctx context.Context, tenantID string, domai
|
||||
return r.db.WithContext(ctx).Create(&td).Error
|
||||
}
|
||||
|
||||
func (r *tenantRepository) List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
func (r *tenantRepository) List(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
var tenants []domain.Tenant
|
||||
var total int64
|
||||
db := r.db.WithContext(ctx).Model(&domain.Tenant{})
|
||||
@@ -133,6 +133,11 @@ func (r *tenantRepository) List(ctx context.Context, limit, offset int, parentID
|
||||
db = db.Where("parent_id = ?", parentID)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
searchTerm := "%" + strings.ToLower(search) + "%"
|
||||
db = db.Where("LOWER(name) LIKE ? OR LOWER(slug) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm, searchTerm)
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/pagination"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -17,7 +18,7 @@ type UserRepository interface {
|
||||
FindByID(ctx context.Context, id string) (*domain.User, error)
|
||||
FindByIDs(ctx context.Context, ids []string) ([]domain.User, error)
|
||||
ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error)
|
||||
List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error)
|
||||
List(ctx context.Context, offset, limit int, search string, tenantSlug string, cursor string) ([]domain.User, int64, string, error)
|
||||
CountByTenant(ctx context.Context, tenantID string) (int64, error)
|
||||
CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error)
|
||||
CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error)
|
||||
@@ -215,7 +216,7 @@ func lowerStrings(arr []string) []string {
|
||||
return res
|
||||
}
|
||||
|
||||
func (r *userRepository) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
|
||||
func (r *userRepository) List(ctx context.Context, offset, limit int, search string, tenantSlug string, cursorRaw string) ([]domain.User, int64, string, error) {
|
||||
var users []domain.User
|
||||
var total int64
|
||||
db := r.db.WithContext(ctx).Model(&domain.User{})
|
||||
@@ -232,14 +233,34 @@ func (r *userRepository) List(ctx context.Context, offset, limit int, search str
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
if err := db.Offset(offset).Limit(limit).Preload("Tenant").Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
if cursorRaw != "" {
|
||||
cursor, err := pagination.Decode(cursorRaw)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
db = pagination.ApplyCreatedAtIDCursor(db, cursor, "created_at", "id")
|
||||
} else {
|
||||
db = db.Offset(offset)
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
if err := db.Order("created_at desc, id desc").Limit(limit + 1).Preload("Tenant").Find(&users).Error; err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
var items []domain.User
|
||||
var nextCursor string
|
||||
if len(users) > limit {
|
||||
items = users[:limit]
|
||||
last := items[limit-1]
|
||||
nextCursor = pagination.Encode(last.CreatedAt, last.ID)
|
||||
} else {
|
||||
items = users
|
||||
}
|
||||
|
||||
return items, total, nextCursor, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(ctx context.Context, id string) error {
|
||||
|
||||
@@ -18,7 +18,7 @@ type TenantService interface {
|
||||
GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error)
|
||||
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
|
||||
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
|
||||
ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error)
|
||||
ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error)
|
||||
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
|
||||
ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
|
||||
IsDomainAllowed(ctx context.Context, domainName string) (bool, error)
|
||||
@@ -314,8 +314,8 @@ func (s *tenantService) GetTenantBySlug(ctx context.Context, slug string) (*doma
|
||||
return s.repo.FindBySlug(ctx, slug)
|
||||
}
|
||||
|
||||
func (s *tenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
return s.repo.List(ctx, limit, offset, parentID)
|
||||
func (s *tenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
return s.repo.List(ctx, limit, offset, parentID, search)
|
||||
}
|
||||
|
||||
func (s *tenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
|
||||
|
||||
@@ -938,7 +938,7 @@ func (s *worksmobileSyncService) hanmacRoot(ctx context.Context, tenantID string
|
||||
}
|
||||
|
||||
func (s *worksmobileSyncService) hanmacSubtree(ctx context.Context, rootID string) ([]domain.Tenant, error) {
|
||||
all, _, err := s.tenantService.ListTenants(ctx, 10000, 0, "")
|
||||
all, _, err := s.tenantService.ListTenants(ctx, 10000, 0, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user