feat: implement storage tenant listing and query enhancements

- Added a new endpoint in SuperAdminTenantController to list tenants with storage plans and their usage statistics.
- Updated TenantRepository and TenantService to support filtering tenants by storage plan requirements.
- Introduced new API functions and hooks in the dashboard for fetching and managing storage tenant data.
- Modified TenantStoragePanel component to utilize the new storage tenant query.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-12-10 15:11:11 +08:00
parent d6b5cf1dda
commit 883fd37e53
6 changed files with 87 additions and 3 deletions

View File

@@ -93,6 +93,44 @@ export class SuperAdminTenantController {
}
}
@Get('/storage')
async listStorageTenants(@Query() query: ListTenantsQueryDto) {
const [tenantResult, storagePlanCatalog, managedProviderKey] = await Promise.all([
this.tenantService.listTenants({
page: query.page,
limit: query.limit,
search: query.search,
status: query.status,
sortBy: query.sortBy,
sortDir: query.sortDir,
requireStoragePlan: true,
}),
this.systemSettings.getStoragePlanCatalog(),
this.systemSettings.getManagedStorageProviderKey(),
])
const { items: tenantAggregates, total } = tenantResult
const tenantIds = tenantAggregates.map((aggregate) => aggregate.tenant.id)
const storageUsageMap =
managedProviderKey && tenantIds.length > 0
? await this.managedStorageService.getUsageTotalsForTenants(managedProviderKey, tenantIds)
: {}
return {
tenants: tenantAggregates.map((aggregate) => ({
...aggregate.tenant,
storageUsage: storageUsageMap[aggregate.tenant.id] ?? null,
})),
plans: [],
storagePlans: Object.entries(storagePlanCatalog).map(([id, def]) => ({
id,
...def,
})),
total,
}
}
@Patch('/:tenantId/plan')
async updateTenantPlan(@Param() params: TenantIdParamDto, @Body() dto: UpdateTenantPlanDto) {
await this.billingPlanService.updateTenantPlan(params.tenantId, dto.planId as BillingPlanId)

View File

@@ -3,7 +3,7 @@ import { RESERVED_TENANT_SLUGS } from '@afilmory/utils'
import { DbAccessor } from 'core/database/database.provider'
import { BizException, ErrorCode } from 'core/errors'
import type { BillingPlanId } from 'core/modules/platform/billing/billing-plan.types'
import { and, asc, count, desc, eq, ilike, notInArray, or } from 'drizzle-orm'
import { and, asc, count, desc, eq, ilike, isNotNull, notInArray, or } from 'drizzle-orm'
import { injectable } from 'tsyringe'
import type { TenantAggregate, TenantRecord } from './tenant.types'
@@ -88,6 +88,7 @@ export class TenantRepository {
status?: TenantRecord['status']
sortBy?: 'createdAt' | 'name'
sortDir?: 'asc' | 'desc'
requireStoragePlan?: boolean
}): Promise<{ items: TenantAggregate[]; total: number }> {
const db = this.dbAccessor.get()
const { page, limit, search, status, sortBy = 'createdAt', sortDir = 'desc' } = options
@@ -98,6 +99,10 @@ export class TenantRepository {
conditions.push(eq(tenants.status, status))
}
if (options.requireStoragePlan) {
conditions.push(isNotNull(tenants.storagePlanId))
}
if (search) {
const searchLike = `%${search}%`
conditions.push(or(ilike(tenants.name, searchLike), ilike(tenants.slug, searchLike)))

View File

@@ -122,6 +122,7 @@ export class TenantService {
status?: TenantRecord['status']
sortBy?: 'createdAt' | 'name'
sortDir?: 'asc' | 'desc'
requireStoragePlan?: boolean
}): Promise<{ items: TenantAggregate[]; total: number }> {
return await this.repository.listTenants({
page: options?.page ?? 1,
@@ -130,6 +131,7 @@ export class TenantService {
status: options?.status,
sortBy: options?.sortBy,
sortDir: options?.sortDir,
requireStoragePlan: options?.requireStoragePlan,
})
}

View File

@@ -18,6 +18,7 @@ import type {
const SUPER_ADMIN_SETTINGS_ENDPOINT = '/super-admin/settings'
const SUPER_ADMIN_TENANTS_ENDPOINT = '/super-admin/tenants'
const SUPER_ADMIN_STORAGE_TENANTS_ENDPOINT = '/super-admin/tenants/storage'
const STABLE_NEWLINE = /\r?\n/
type RunBuilderDebugOptions = {
@@ -63,6 +64,30 @@ export async function fetchSuperAdminTenants(
return camelCaseKeys<SuperAdminTenantListResponse>(response)
}
export async function fetchSuperAdminStorageTenants(
params?: SuperAdminTenantListParams,
): Promise<SuperAdminTenantListResponse> {
const query = new URLSearchParams()
if (params) {
if (params.page) query.set('page', String(params.page))
if (params.limit) query.set('limit', String(params.limit))
if (params.search) query.set('search', params.search)
if (params.sortBy) query.set('sortBy', params.sortBy)
if (params.sortDir) query.set('sortDir', params.sortDir)
}
const queryString = query.toString()
const url = queryString
? `${SUPER_ADMIN_STORAGE_TENANTS_ENDPOINT}?${queryString}`
: SUPER_ADMIN_STORAGE_TENANTS_ENDPOINT
const response = await coreApi<SuperAdminTenantListResponse>(url, {
method: 'GET',
})
return camelCaseKeys<SuperAdminTenantListResponse>(response)
}
export async function updateSuperAdminTenantPlan(payload: UpdateTenantPlanPayload): Promise<void> {
await coreApi(`${SUPER_ADMIN_TENANTS_ENDPOINT}/${payload.tenantId}/plan`, {
method: 'PATCH',

View File

@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
import { LinearBorderPanel } from '~/components/common/LinearBorderPanel'
import { buildTenantUrl } from '~/modules/auth/utils/domain'
import { useSuperAdminTenantsQuery } from '../hooks'
import { useSuperAdminStorageTenantsQuery } from '../hooks'
import type { StoragePlanDefinition } from '../types'
import { formatBytes } from './TenantUsageCell'
@@ -27,7 +27,7 @@ export function TenantStoragePanel() {
return () => clearTimeout(timer)
}, [search])
const tenantsQuery = useSuperAdminTenantsQuery({
const tenantsQuery = useSuperAdminStorageTenantsQuery({
page,
limit,
search: debouncedSearch,

View File

@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
deleteSuperAdminTenant,
fetchSuperAdminSettings,
fetchSuperAdminStorageTenants,
fetchSuperAdminTenantPhotos,
fetchSuperAdminTenants,
updateSuperAdminSettings,
@@ -23,6 +24,7 @@ import type {
export const SUPER_ADMIN_SETTINGS_QUERY_KEY = ['super-admin', 'settings'] as const
export const SUPER_ADMIN_TENANTS_QUERY_KEY = ['super-admin', 'tenants'] as const
export const SUPER_ADMIN_STORAGE_TENANTS_QUERY_KEY = ['super-admin', 'tenants', 'storage'] as const
export function useSuperAdminSettingsQuery() {
return useQuery<SuperAdminSettingsResponse>({
@@ -40,6 +42,14 @@ export function useSuperAdminTenantsQuery(params?: SuperAdminTenantListParams) {
})
}
export function useSuperAdminStorageTenantsQuery(params?: SuperAdminTenantListParams) {
return useQuery<SuperAdminTenantListResponse>({
queryKey: [...SUPER_ADMIN_STORAGE_TENANTS_QUERY_KEY, params],
queryFn: () => fetchSuperAdminStorageTenants(params),
placeholderData: (previousData) => previousData,
})
}
type SuperAdminSettingsMutationOptions = {
onSuccess?: (data: SuperAdminSettingsResponse) => void
}
@@ -65,6 +75,7 @@ export function useUpdateTenantPlanMutation() {
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_STORAGE_TENANTS_QUERY_KEY })
},
})
}
@@ -78,6 +89,7 @@ export function useUpdateTenantStoragePlanMutation() {
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_STORAGE_TENANTS_QUERY_KEY })
},
})
}
@@ -91,6 +103,7 @@ export function useUpdateTenantBanMutation() {
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_STORAGE_TENANTS_QUERY_KEY })
},
})
}
@@ -104,6 +117,7 @@ export function useDeleteTenantMutation() {
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_STORAGE_TENANTS_QUERY_KEY })
},
})
}