mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-25 07:15:36 +00:00
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:
@@ -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')
|
@Patch('/:tenantId/plan')
|
||||||
async updateTenantPlan(@Param() params: TenantIdParamDto, @Body() dto: UpdateTenantPlanDto) {
|
async updateTenantPlan(@Param() params: TenantIdParamDto, @Body() dto: UpdateTenantPlanDto) {
|
||||||
await this.billingPlanService.updateTenantPlan(params.tenantId, dto.planId as BillingPlanId)
|
await this.billingPlanService.updateTenantPlan(params.tenantId, dto.planId as BillingPlanId)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RESERVED_TENANT_SLUGS } from '@afilmory/utils'
|
|||||||
import { DbAccessor } from 'core/database/database.provider'
|
import { DbAccessor } from 'core/database/database.provider'
|
||||||
import { BizException, ErrorCode } from 'core/errors'
|
import { BizException, ErrorCode } from 'core/errors'
|
||||||
import type { BillingPlanId } from 'core/modules/platform/billing/billing-plan.types'
|
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 { injectable } from 'tsyringe'
|
||||||
|
|
||||||
import type { TenantAggregate, TenantRecord } from './tenant.types'
|
import type { TenantAggregate, TenantRecord } from './tenant.types'
|
||||||
@@ -88,6 +88,7 @@ export class TenantRepository {
|
|||||||
status?: TenantRecord['status']
|
status?: TenantRecord['status']
|
||||||
sortBy?: 'createdAt' | 'name'
|
sortBy?: 'createdAt' | 'name'
|
||||||
sortDir?: 'asc' | 'desc'
|
sortDir?: 'asc' | 'desc'
|
||||||
|
requireStoragePlan?: boolean
|
||||||
}): Promise<{ items: TenantAggregate[]; total: number }> {
|
}): Promise<{ items: TenantAggregate[]; total: number }> {
|
||||||
const db = this.dbAccessor.get()
|
const db = this.dbAccessor.get()
|
||||||
const { page, limit, search, status, sortBy = 'createdAt', sortDir = 'desc' } = options
|
const { page, limit, search, status, sortBy = 'createdAt', sortDir = 'desc' } = options
|
||||||
@@ -98,6 +99,10 @@ export class TenantRepository {
|
|||||||
conditions.push(eq(tenants.status, status))
|
conditions.push(eq(tenants.status, status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.requireStoragePlan) {
|
||||||
|
conditions.push(isNotNull(tenants.storagePlanId))
|
||||||
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
const searchLike = `%${search}%`
|
const searchLike = `%${search}%`
|
||||||
conditions.push(or(ilike(tenants.name, searchLike), ilike(tenants.slug, searchLike)))
|
conditions.push(or(ilike(tenants.name, searchLike), ilike(tenants.slug, searchLike)))
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export class TenantService {
|
|||||||
status?: TenantRecord['status']
|
status?: TenantRecord['status']
|
||||||
sortBy?: 'createdAt' | 'name'
|
sortBy?: 'createdAt' | 'name'
|
||||||
sortDir?: 'asc' | 'desc'
|
sortDir?: 'asc' | 'desc'
|
||||||
|
requireStoragePlan?: boolean
|
||||||
}): Promise<{ items: TenantAggregate[]; total: number }> {
|
}): Promise<{ items: TenantAggregate[]; total: number }> {
|
||||||
return await this.repository.listTenants({
|
return await this.repository.listTenants({
|
||||||
page: options?.page ?? 1,
|
page: options?.page ?? 1,
|
||||||
@@ -130,6 +131,7 @@ export class TenantService {
|
|||||||
status: options?.status,
|
status: options?.status,
|
||||||
sortBy: options?.sortBy,
|
sortBy: options?.sortBy,
|
||||||
sortDir: options?.sortDir,
|
sortDir: options?.sortDir,
|
||||||
|
requireStoragePlan: options?.requireStoragePlan,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
|
|
||||||
const SUPER_ADMIN_SETTINGS_ENDPOINT = '/super-admin/settings'
|
const SUPER_ADMIN_SETTINGS_ENDPOINT = '/super-admin/settings'
|
||||||
const SUPER_ADMIN_TENANTS_ENDPOINT = '/super-admin/tenants'
|
const SUPER_ADMIN_TENANTS_ENDPOINT = '/super-admin/tenants'
|
||||||
|
const SUPER_ADMIN_STORAGE_TENANTS_ENDPOINT = '/super-admin/tenants/storage'
|
||||||
const STABLE_NEWLINE = /\r?\n/
|
const STABLE_NEWLINE = /\r?\n/
|
||||||
|
|
||||||
type RunBuilderDebugOptions = {
|
type RunBuilderDebugOptions = {
|
||||||
@@ -63,6 +64,30 @@ export async function fetchSuperAdminTenants(
|
|||||||
return camelCaseKeys<SuperAdminTenantListResponse>(response)
|
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> {
|
export async function updateSuperAdminTenantPlan(payload: UpdateTenantPlanPayload): Promise<void> {
|
||||||
await coreApi(`${SUPER_ADMIN_TENANTS_ENDPOINT}/${payload.tenantId}/plan`, {
|
await coreApi(`${SUPER_ADMIN_TENANTS_ENDPOINT}/${payload.tenantId}/plan`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { LinearBorderPanel } from '~/components/common/LinearBorderPanel'
|
import { LinearBorderPanel } from '~/components/common/LinearBorderPanel'
|
||||||
import { buildTenantUrl } from '~/modules/auth/utils/domain'
|
import { buildTenantUrl } from '~/modules/auth/utils/domain'
|
||||||
|
|
||||||
import { useSuperAdminTenantsQuery } from '../hooks'
|
import { useSuperAdminStorageTenantsQuery } from '../hooks'
|
||||||
import type { StoragePlanDefinition } from '../types'
|
import type { StoragePlanDefinition } from '../types'
|
||||||
import { formatBytes } from './TenantUsageCell'
|
import { formatBytes } from './TenantUsageCell'
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export function TenantStoragePanel() {
|
|||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [search])
|
}, [search])
|
||||||
|
|
||||||
const tenantsQuery = useSuperAdminTenantsQuery({
|
const tenantsQuery = useSuperAdminStorageTenantsQuery({
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
search: debouncedSearch,
|
search: debouncedSearch,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|||||||
import {
|
import {
|
||||||
deleteSuperAdminTenant,
|
deleteSuperAdminTenant,
|
||||||
fetchSuperAdminSettings,
|
fetchSuperAdminSettings,
|
||||||
|
fetchSuperAdminStorageTenants,
|
||||||
fetchSuperAdminTenantPhotos,
|
fetchSuperAdminTenantPhotos,
|
||||||
fetchSuperAdminTenants,
|
fetchSuperAdminTenants,
|
||||||
updateSuperAdminSettings,
|
updateSuperAdminSettings,
|
||||||
@@ -23,6 +24,7 @@ import type {
|
|||||||
|
|
||||||
export const SUPER_ADMIN_SETTINGS_QUERY_KEY = ['super-admin', 'settings'] as const
|
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_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() {
|
export function useSuperAdminSettingsQuery() {
|
||||||
return useQuery<SuperAdminSettingsResponse>({
|
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 = {
|
type SuperAdminSettingsMutationOptions = {
|
||||||
onSuccess?: (data: SuperAdminSettingsResponse) => void
|
onSuccess?: (data: SuperAdminSettingsResponse) => void
|
||||||
}
|
}
|
||||||
@@ -65,6 +75,7 @@ export function useUpdateTenantPlanMutation() {
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
|
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: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
|
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: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
|
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: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
|
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_TENANTS_QUERY_KEY })
|
||||||
|
void queryClient.invalidateQueries({ queryKey: SUPER_ADMIN_STORAGE_TENANTS_QUERY_KEY })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user