mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
feat(keys): support sorting API keys by group name
This commit is contained in:
@@ -652,6 +652,17 @@ func apiKeyListOrder(params pagination.PaginationParams) []func(*entsql.Selector
|
||||
sortBy := strings.ToLower(strings.TrimSpace(params.SortBy))
|
||||
sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc)
|
||||
|
||||
if sortBy == "group" {
|
||||
// Sort before pagination, keeping ungrouped keys last in either direction.
|
||||
opts := []entsql.OrderTermOption{entsql.OrderNullsLast()}
|
||||
tieOrder := dbent.Asc(apikey.FieldID)
|
||||
if sortOrder == pagination.SortOrderDesc {
|
||||
opts = append(opts, entsql.OrderDesc())
|
||||
tieOrder = dbent.Desc(apikey.FieldID)
|
||||
}
|
||||
return []func(*entsql.Selector){apikey.ByGroupField(group.FieldName, opts...), tieOrder}
|
||||
}
|
||||
|
||||
var field string
|
||||
switch sortBy {
|
||||
case "name":
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIKeyRepositoryListByUserIDSortByGroup(t *testing.T) {
|
||||
repo, client := newAPIKeyRepoSQLite(t)
|
||||
ctx := context.Background()
|
||||
user := mustCreateAPIKeyRepoUser(t, ctx, client, "group-sort@test.com")
|
||||
otherUser := mustCreateAPIKeyRepoUser(t, ctx, client, "other-group-sort@test.com")
|
||||
createGroup := func(name string) *dbent.Group {
|
||||
g, err := client.Group.Create().SetName(name).Save(ctx)
|
||||
require.NoError(t, err)
|
||||
return g
|
||||
}
|
||||
// Create groups in reverse name order so sorting by group ID cannot pass.
|
||||
zulu := createGroup("Zulu")
|
||||
alpha := createGroup("Alpha")
|
||||
createKey := func(userID int64, name string, groupID *int64, status string) int64 {
|
||||
key := &service.APIKey{
|
||||
UserID: userID, Key: "sk-" + name, Name: name, GroupID: groupID, Status: status,
|
||||
}
|
||||
require.NoError(t, repo.Create(ctx, key))
|
||||
return key.ID
|
||||
}
|
||||
zuluFirst := createKey(user.ID, "match-zulu-first", &zulu.ID, service.StatusActive)
|
||||
ungroupedFirst := createKey(user.ID, "match-ungrouped-first", nil, service.StatusActive)
|
||||
alphaFirst := createKey(user.ID, "match-alpha-first", &alpha.ID, service.StatusActive)
|
||||
zuluSecond := createKey(user.ID, "match-zulu-second", &zulu.ID, service.StatusActive)
|
||||
alphaSecond := createKey(user.ID, "match-alpha-second", &alpha.ID, service.StatusActive)
|
||||
ungroupedSecond := createKey(user.ID, "match-ungrouped-second", nil, service.StatusActive)
|
||||
createKey(otherUser.ID, "match-other-user", &alpha.ID, service.StatusActive)
|
||||
createKey(user.ID, "excluded-search", &alpha.ID, service.StatusActive)
|
||||
createKey(user.ID, "match-inactive", &alpha.ID, service.StatusDisabled)
|
||||
deleted := createKey(user.ID, "match-deleted", &alpha.ID, service.StatusActive)
|
||||
require.NoError(t, repo.Delete(ctx, deleted))
|
||||
|
||||
ungroupedID := int64(0)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
order string
|
||||
groupID *int64
|
||||
want []int64
|
||||
}{
|
||||
{"ascending", "asc", nil, []int64{alphaFirst, alphaSecond, zuluFirst, zuluSecond, ungroupedFirst, ungroupedSecond}},
|
||||
{"descending", "desc", nil, []int64{zuluSecond, zuluFirst, alphaSecond, alphaFirst, ungroupedSecond, ungroupedFirst}},
|
||||
{"group filter", "asc", &alpha.ID, []int64{alphaFirst, alphaSecond}},
|
||||
{"ungrouped filter", "desc", &ungroupedID, []int64{ungroupedSecond, ungroupedFirst}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got []int64
|
||||
const pageSize = 3
|
||||
pages := (len(tc.want) + pageSize - 1) / pageSize
|
||||
for page := 1; page <= pages; page++ {
|
||||
keys, result, err := repo.ListByUserID(ctx, user.ID, pagination.PaginationParams{
|
||||
Page: page, PageSize: pageSize, SortBy: "group", SortOrder: tc.order,
|
||||
}, service.APIKeyListFilters{Search: "match", Status: service.StatusActive, GroupID: tc.groupID})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, len(tc.want), result.Total)
|
||||
require.Equal(t, pages, result.Pages)
|
||||
for _, key := range keys {
|
||||
got = append(got, key.ID)
|
||||
if key.GroupID != nil {
|
||||
require.NotNil(t, key.Group, "sorting must preserve group preloading")
|
||||
}
|
||||
}
|
||||
}
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1266,7 +1266,7 @@ const allColumns = computed<Column[]>(() => [
|
||||
{ key: 'name', label: t('common.name'), sortable: true },
|
||||
{ key: 'id', label: t('keys.id'), sortable: true },
|
||||
{ key: 'key', label: t('keys.apiKey'), sortable: false },
|
||||
{ key: 'group', label: t('keys.group'), sortable: false },
|
||||
{ key: 'group', label: t('keys.group'), sortable: true },
|
||||
{ key: 'current_concurrency', label: t('keys.currentConcurrency'), sortable: true },
|
||||
{ key: 'usage', label: t('keys.usage'), sortable: false },
|
||||
{ key: 'rate_limit', label: t('keys.rateLimitColumn'), sortable: false },
|
||||
|
||||
@@ -210,6 +210,7 @@ const PaginationStub = {
|
||||
template: `
|
||||
<div>
|
||||
<button data-test="page-size-50" @click="$emit('update:pageSize', 50)">50</button>
|
||||
<button data-test="page-2" @click="$emit('update:page', 2)">Page 2</button>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
@@ -511,7 +512,11 @@ describe('user KeysView column settings', () => {
|
||||
expect(currentConcurrencyColumn?.sortable).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps filters and selected page size when sorting by current concurrency', async () => {
|
||||
it.each([
|
||||
{ key: 'current_concurrency', order: 'asc' },
|
||||
{ key: 'group', order: 'asc' },
|
||||
{ key: 'group', order: 'desc' },
|
||||
] as const)('keeps filters and resets pagination and selection when sorting $key $order', async ({ key, order }) => {
|
||||
getAvailableGroups.mockResolvedValue([{ id: 42, name: 'OpenAI' }])
|
||||
const wrapper = await mountView()
|
||||
|
||||
@@ -528,11 +533,19 @@ describe('user KeysView column settings', () => {
|
||||
await selects[1].vm.$emit('update:modelValue', 'active')
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('[data-test="page-2"]').trigger('click')
|
||||
await flushPromises()
|
||||
const table = wrapper.findComponent({ name: 'DataTable' })
|
||||
table.vm.$emit('update:selectedKeys', [1])
|
||||
await nextTick()
|
||||
expect(table.props('selectedKeys')).toEqual([1])
|
||||
expect(visibleColumnMeta(wrapper).find((column) => column.key === key)?.sortable).toBe(true)
|
||||
listKeys.mockClear()
|
||||
|
||||
await wrapper.get('[data-test="sort-current-concurrency"]').trigger('click')
|
||||
table.vm.$emit('sort', key, order)
|
||||
await flushPromises()
|
||||
|
||||
expect(table.props('selectedKeys')).toEqual([])
|
||||
expect(listKeys).toHaveBeenLastCalledWith(
|
||||
1,
|
||||
50,
|
||||
@@ -540,8 +553,8 @@ describe('user KeysView column settings', () => {
|
||||
search: 'target',
|
||||
status: 'active',
|
||||
group_id: 42,
|
||||
sort_by: 'current_concurrency',
|
||||
sort_order: 'asc',
|
||||
sort_by: key,
|
||||
sort_order: order,
|
||||
},
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) })
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user