Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,8 @@ func Register(c *gin.Context) {

func GetAllUsers(c *gin.Context) {
pageInfo := common.GetPageQuery(c)
users, total, err := model.GetAllUsers(pageInfo)
sortOptions := model.NewUserSortOptions(c.Query("sort_by"), c.Query("sort_order"))
users, total, err := model.GetAllUsers(pageInfo, sortOptions)
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -337,7 +338,8 @@ func SearchUsers(c *gin.Context) {
}
}
pageInfo := common.GetPageQuery(c)
users, total, err := model.SearchUsers(keyword, group, role, status, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
sortOptions := model.NewUserSortOptions(c.Query("sort_by"), c.Query("sort_order"))
users, total, err := model.SearchUsers(keyword, group, role, status, pageInfo.GetStartIdx(), pageInfo.GetPageSize(), sortOptions)
if err != nil {
common.ApiError(c, err)
return
Expand Down
66 changes: 62 additions & 4 deletions model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,66 @@ import (

"github.com/bytedance/gopkg/util/gopool"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)

const UserNameMaxLength = 20

var userSortColumns = map[string]string{
"id": "id",
"username": "username",
"quota": "quota",
"group": "group",
"created_at": "created_at",
"last_login_at": "last_login_at",
}

type UserSortOptions struct {
SortBy string
SortOrder string
}

func NewUserSortOptions(sortBy string, sortOrder string) UserSortOptions {
normalizedSortBy := strings.ToLower(strings.TrimSpace(sortBy))
normalizedSortOrder := strings.ToLower(strings.TrimSpace(sortOrder))
if _, ok := userSortColumns[normalizedSortBy]; !ok {
normalizedSortBy = "id"
normalizedSortOrder = "desc"
} else if normalizedSortOrder != "asc" {
normalizedSortOrder = "desc"
}

return UserSortOptions{
SortBy: normalizedSortBy,
SortOrder: normalizedSortOrder,
}
}

func (options UserSortOptions) Apply(query *gorm.DB) *gorm.DB {
columnName, ok := userSortColumns[options.SortBy]
if !ok {
columnName = "id"
}
q := query.Order(clause.OrderByColumn{
Column: clause.Column{Name: columnName},
Desc: options.SortOrder != "asc",
})
if columnName != "id" {
q = q.Order(clause.OrderByColumn{
Column: clause.Column{Name: "id"},
Desc: true,
})
}
return q
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func resolveUserSortOptions(sortOptions []UserSortOptions) UserSortOptions {
if len(sortOptions) == 0 {
return NewUserSortOptions("", "")
}
return sortOptions[0]
}

// User if you add sensitive fields, don't forget to clean them in setupLogin function.
// Otherwise, the sensitive information will be saved on local storage in plain text!
type User struct {
Expand Down Expand Up @@ -286,7 +342,7 @@ func GetMaxUserId() int {
return user.Id
}

func GetAllUsers(pageInfo *common.PageInfo) (users []*User, total int64, err error) {
func GetAllUsers(pageInfo *common.PageInfo, sortOptions ...UserSortOptions) (users []*User, total int64, err error) {
// Start transaction
tx := DB.Begin()
if tx.Error != nil {
Expand All @@ -306,7 +362,8 @@ func GetAllUsers(pageInfo *common.PageInfo) (users []*User, total int64, err err
}

// Get paginated users within same transaction
err = tx.Unscoped().Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("password", "access_token").Find(&users).Error
order := resolveUserSortOptions(sortOptions)
err = order.Apply(tx.Unscoped()).Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Omit("password", "access_token").Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err
Expand All @@ -320,7 +377,7 @@ func GetAllUsers(pageInfo *common.PageInfo) (users []*User, total int64, err err
return users, total, nil
}

func SearchUsers(keyword string, group string, role *int, status *int, startIdx int, num int) ([]*User, int64, error) {
func SearchUsers(keyword string, group string, role *int, status *int, startIdx int, num int, sortOptions ...UserSortOptions) ([]*User, int64, error) {
var users []*User
var total int64
var err error
Expand Down Expand Up @@ -374,7 +431,8 @@ func SearchUsers(keyword string, group string, role *int, status *int, startIdx
}

// 获取分页数据
err = query.Omit("password", "access_token").Order("id desc").Limit(num).Offset(startIdx).Find(&users).Error
order := resolveUserSortOptions(sortOptions)
err = order.Apply(query.Omit("password", "access_token")).Limit(num).Offset(startIdx).Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err
Expand Down
66 changes: 66 additions & 0 deletions model/user_pagination_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package model

import (
"fmt"
"testing"

"github.com/QuantumNous/new-api/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func insertUsersForPaginationTest(t *testing.T, total int) {
t.Helper()
for id := 1; id <= total; id++ {
user := &User{
Id: id,
Username: fmt.Sprintf("user%02d", id),
Password: "password123",
DisplayName: fmt.Sprintf("User %02d", id),
Email: fmt.Sprintf("user%02d@example.com", id),
Role: common.RoleCommonUser,
Status: common.UserStatusEnabled,
Group: "default",
AffCode: fmt.Sprintf("aff%02d", id),
}
require.NoError(t, DB.Create(user).Error)
}
}

func collectUserIDs(users []*User) []int {
ids := make([]int, 0, len(users))
for _, user := range users {
ids = append(ids, user.Id)
}
return ids
}

func TestGetAllUsersSortsBeforePagination(t *testing.T) {
truncateTables(t)
insertUsersForPaginationTest(t, 42)

pageOne, total, err := GetAllUsers(&common.PageInfo{Page: 1, PageSize: 20}, NewUserSortOptions("id", "asc"))
require.NoError(t, err)
assert.Equal(t, int64(42), total)
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, collectUserIDs(pageOne))

pageTwo, total, err := GetAllUsers(&common.PageInfo{Page: 2, PageSize: 20}, NewUserSortOptions("id", "asc"))
require.NoError(t, err)
assert.Equal(t, int64(42), total)
assert.Equal(t, []int{21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40}, collectUserIDs(pageTwo))

pageThree, total, err := GetAllUsers(&common.PageInfo{Page: 3, PageSize: 20}, NewUserSortOptions("id", "asc"))
require.NoError(t, err)
assert.Equal(t, int64(42), total)
assert.Equal(t, []int{41, 42}, collectUserIDs(pageThree))
}

func TestSearchUsersSortsBeforePagination(t *testing.T) {
truncateTables(t)
insertUsersForPaginationTest(t, 42)

users, total, err := SearchUsers("user", "", nil, nil, 20, 20, NewUserSortOptions("id", "asc"))
require.NoError(t, err)
assert.Equal(t, int64(42), total)
assert.Equal(t, []int{21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40}, collectUserIDs(users))
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type DataTableFeatureOptions<TData> = Pick<
| 'manualFiltering'
| 'manualPagination'
| 'manualSorting'
| 'enableSorting'
| 'enableColumnResizing'
>

Expand Down Expand Up @@ -292,7 +293,7 @@ export function useDataTable<TData>(options: UseDataTableOptions<TData>) {
initialPagination = { pageIndex: 0, pageSize: 20 },
withFilteredRowModel = !manualFiltering,
withPaginationRowModel = !manualPagination,
withSortedRowModel = !manualSorting,
withSortedRowModel = !manualSorting && !manualPagination,
withFacetedRowModel = !manualFiltering,
withExpandedRowModel = false,
} = options
Expand Down Expand Up @@ -370,6 +371,11 @@ export function useDataTable<TData>(options: UseDataTableOptions<TData>) {
(totalCount !== undefined
? Math.ceil(totalCount / pagination.pageSize)
: undefined)
const resolvedEnableSorting =
options.enableSorting ??
(!manualPagination ||
Boolean(options.sorting) ||
Boolean(options.onSortingChange))

const table = useReactTable({
data,
Expand All @@ -387,6 +393,7 @@ export function useDataTable<TData>(options: UseDataTableOptions<TData>) {
pagination,
},
enableRowSelection: options.enableRowSelection,
enableSorting: resolvedEnableSorting,
getRowId: options.getRowId,
getSubRows: options.getSubRows,
globalFilterFn: options.globalFilterFn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ export function ModelsTable() {
onPaginationChange,
onGlobalFilterChange,
manualPagination: true,
manualSorting: true,
manualFiltering: true,
ensurePageInRange,
})
Expand Down
15 changes: 13 additions & 2 deletions web/default/src/features/users/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,15 @@ import type {
export async function getUsers(
params: GetUsersParams = {}
): Promise<GetUsersResponse> {
const { p = 1, page_size = 10 } = params
const res = await api.get(`/api/user/?p=${p}&page_size=${page_size}`)
const { p = 1, page_size = 10, sort_by, sort_order } = params
const res = await api.get('/api/user/', {
params: {
p,
page_size,
sort_by,
sort_order,
},
})
return res.data
}

Expand All @@ -58,6 +65,8 @@ export async function searchUsers(
status = '',
p = 1,
page_size = 10,
sort_by,
sort_order,
} = params
const queryParams = new URLSearchParams()
queryParams.set('keyword', keyword)
Expand All @@ -66,6 +75,8 @@ export async function searchUsers(
if (status) queryParams.set('status', status)
queryParams.set('p', String(p))
queryParams.set('page_size', String(page_size))
if (sort_by) queryParams.set('sort_by', sort_by)
if (sort_order) queryParams.set('sort_order', sort_order)
const res = await api.get(`/api/user/search?${queryParams.toString()}`)
return res.data
}
Expand Down
41 changes: 40 additions & 1 deletion web/default/src/features/users/components/users-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ 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'

Expand All @@ -37,13 +39,22 @@ import {
getUserRoleOptions,
isUserDeleted,
} from '../constants'
import type { User } from '../types'
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<UserSortBy>([
'id',
'username',
'quota',
'group',
'created_at',
'last_login_at',
])

function isDisabledUserRow(user: User) {
return isUserDeleted(user) || user.status === USER_STATUS.DISABLED
}
Expand All @@ -53,6 +64,7 @@ export function UsersTable() {
const columns = useUsersColumns()
const { refreshTrigger } = useUsers()
const isMobile = useMediaQuery('(max-width: 640px)')
const [sorting, setSorting] = useState<SortingState>([])

const {
globalFilter,
Expand Down Expand Up @@ -85,6 +97,28 @@ export function UsersTable() {
(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<SortingState> = (updater) => {
setSorting(updater)
if (pagination.pageIndex > 0) {
onPaginationChange({ ...pagination, pageIndex: 0 })
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Fetch data with React Query
const { data, isLoading, isFetching } = useQuery({
queryKey: [
Expand All @@ -95,6 +129,7 @@ export function UsersTable() {
statusFilter,
roleFilter,
groupFilter,
sortParams,
refreshTrigger,
],
queryFn: async () => {
Expand All @@ -104,6 +139,7 @@ export function UsersTable() {
const params = {
p: pagination.pageIndex + 1,
page_size: pagination.pageSize,
...sortParams,
}

const result =
Expand Down Expand Up @@ -141,6 +177,7 @@ export function UsersTable() {
columnFilters,
globalFilter,
pagination,
sorting,
globalFilterFn: (row, _columnId, filterValue) => {
const searchValue = String(filterValue).toLowerCase()
const fields = [
Expand All @@ -157,8 +194,10 @@ export function UsersTable() {
onPaginationChange,
onGlobalFilterChange,
onColumnFiltersChange,
onSortingChange: handleSortingChange,
manualPagination: true,
manualFiltering: true,
manualSorting: true,
totalCount: data?.total || 0,
ensurePageInRange,
})
Expand Down
Loading