/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useQuery } from '@tanstack/react-query' import { getRouteApi } from '@tanstack/react-router' import type { OnChangeFn, SortingState } from '@tanstack/react-table' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { DISABLED_ROW_DESKTOP, DISABLED_ROW_MOBILE, DataTablePage, useDataTable, } from '@/components/data-table' import { useMediaQuery } from '@/hooks' import { useTableUrlState } from '@/hooks/use-table-url-state' import { getUsers, searchUsers } from '../api' import { USER_STATUS, getUserStatusOptions, getUserRoleOptions, isUserDeleted, } from '../constants' import type { User, UserSortBy } from '../types' import { DataTableBulkActions } from './data-table-bulk-actions' import { useUsersColumns } from './users-columns' import { useUsers } from './users-provider' const route = getRouteApi('/_authenticated/users/') const USER_SORTABLE_COLUMNS = new Set([ 'id', 'username', 'quota', 'group', 'created_at', 'last_login_at', ]) function isDisabledUserRow(user: User) { return isUserDeleted(user) || user.status === USER_STATUS.DISABLED } export function UsersTable() { const { t } = useTranslation() const columns = useUsersColumns() const { refreshTrigger } = useUsers() const isMobile = useMediaQuery('(max-width: 640px)') const [sorting, setSorting] = useState([]) const { globalFilter, onGlobalFilterChange, columnFilters, onColumnFiltersChange, pagination, onPaginationChange, ensurePageInRange, } = useTableUrlState({ search: route.useSearch(), navigate: route.useNavigate(), pagination: { defaultPage: 1, defaultPageSize: isMobile ? 10 : 20 }, globalFilter: { enabled: true, key: 'filter' }, columnFilters: [ { columnId: 'status', searchKey: 'status', type: 'array' }, { columnId: 'role', searchKey: 'role', type: 'array' }, { columnId: 'group', searchKey: 'group', type: 'string' }, ], }) const statusFilter = (columnFilters.find((filter) => filter.id === 'status')?.value as | string[] | undefined) ?? [] const roleFilter = (columnFilters.find((filter) => filter.id === 'role')?.value as | string[] | undefined) ?? [] const groupFilter = (columnFilters.find((filter) => filter.id === 'group')?.value as string) ?? '' const sortParams = useMemo(() => { const activeSort = sorting[0] if ( !activeSort || !USER_SORTABLE_COLUMNS.has(activeSort.id as UserSortBy) ) { return {} } return { sort_by: activeSort.id as UserSortBy, sort_order: activeSort.desc ? 'desc' : 'asc', } as const }, [sorting]) const handleSortingChange: OnChangeFn = (updater) => { setSorting(updater) if (pagination.pageIndex > 0) { onPaginationChange({ ...pagination, pageIndex: 0 }) } } // Fetch data with React Query const { data, isLoading, isFetching } = useQuery({ queryKey: [ 'users', pagination.pageIndex + 1, pagination.pageSize, globalFilter, statusFilter, roleFilter, groupFilter, sortParams, refreshTrigger, ], queryFn: async () => { const hasFilter = globalFilter?.trim() const hasColumnFilter = statusFilter.length > 0 || roleFilter.length > 0 || Boolean(groupFilter) const params = { p: pagination.pageIndex + 1, page_size: pagination.pageSize, ...sortParams, } const result = hasFilter || hasColumnFilter ? await searchUsers({ ...params, keyword: globalFilter, status: statusFilter[0] ?? '', role: roleFilter[0] ?? '', group: groupFilter, }) : await getUsers(params) if (!result.success) { toast.error( result.message || `Failed to ${hasFilter ? 'search' : 'load'} users` ) return { items: [], total: 0 } } return { items: result.data?.items || [], total: result.data?.total || 0, } }, placeholderData: (previousData) => previousData, }) const users = data?.items || [] const { table } = useDataTable({ data: users, columns, enableRowSelection: true, columnFilters, globalFilter, pagination, sorting, globalFilterFn: (row, _columnId, filterValue) => { const searchValue = String(filterValue).toLowerCase() const fields = [ row.getValue('username'), row.original.display_name, row.original.email, ] return fields.some((field) => String(field || '') .toLowerCase() .includes(searchValue) ) }, onPaginationChange, onGlobalFilterChange, onColumnFiltersChange, onSortingChange: handleSortingChange, manualPagination: true, manualFiltering: true, manualSorting: true, totalCount: data?.total || 0, ensurePageInRange, }) return ( isDisabledUserRow(row.original) ? isMobile ? DISABLED_ROW_MOBILE : DISABLED_ROW_DESKTOP : undefined } bulkActions={} /> ) }