mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
feat(keys): sort by current concurrency
This commit is contained in:
@@ -388,10 +388,9 @@ func (r *apiKeyRepository) deleteWithAudit(ctx context.Context, exec *dbent.Clie
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, filters service.APIKeyListFilters) ([]service.APIKey, *pagination.PaginationResult, error) {
|
||||
func (r *apiKeyRepository) apiKeyListByUserIDQuery(userID int64, filters service.APIKeyListFilters) *dbent.APIKeyQuery {
|
||||
q := r.activeQuery().Where(apikey.UserIDEQ(userID))
|
||||
|
||||
// Apply filters
|
||||
if filters.Search != "" {
|
||||
q = q.Where(apikey.Or(
|
||||
apikey.NameContainsFold(filters.Search),
|
||||
@@ -409,6 +408,12 @@ func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, param
|
||||
}
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, filters service.APIKeyListFilters) ([]service.APIKey, *pagination.PaginationResult, error) {
|
||||
q := r.apiKeyListByUserIDQuery(userID, filters)
|
||||
|
||||
total, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -435,6 +440,22 @@ func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, param
|
||||
return outKeys, paginationResultFromTotal(int64(total), params), nil
|
||||
}
|
||||
|
||||
func (r *apiKeyRepository) ListAllByUserID(ctx context.Context, userID int64, filters service.APIKeyListFilters) ([]service.APIKey, error) {
|
||||
keys, err := r.apiKeyListByUserIDQuery(userID, filters).
|
||||
WithGroup().
|
||||
Order(dbent.Asc(apikey.FieldID)).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outKeys := make([]service.APIKey, 0, len(keys))
|
||||
for i := range keys {
|
||||
outKeys = append(outKeys, *apiKeyEntityToService(keys[i]))
|
||||
}
|
||||
return outKeys, nil
|
||||
}
|
||||
|
||||
func (r *apiKeyRepository) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
|
||||
if len(apiKeyIDs) == 0 {
|
||||
return []int64{}, nil
|
||||
@@ -504,14 +525,24 @@ func apiKeyListOrder(params pagination.PaginationParams) []func(*entsql.Selector
|
||||
field = apikey.FieldLastUsedAt
|
||||
case "created_at":
|
||||
field = apikey.FieldCreatedAt
|
||||
case "id":
|
||||
field = apikey.FieldID
|
||||
default:
|
||||
field = apikey.FieldID
|
||||
}
|
||||
|
||||
if sortOrder == pagination.SortOrderAsc {
|
||||
return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(apikey.FieldID)}
|
||||
orders := []func(*entsql.Selector){dbent.Asc(field)}
|
||||
if field != apikey.FieldID {
|
||||
orders = append(orders, dbent.Asc(apikey.FieldID))
|
||||
}
|
||||
return orders
|
||||
}
|
||||
return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(apikey.FieldID)}
|
||||
orders := []func(*entsql.Selector){dbent.Desc(field)}
|
||||
if field != apikey.FieldID {
|
||||
orders = append(orders, dbent.Desc(apikey.FieldID))
|
||||
}
|
||||
return orders
|
||||
}
|
||||
|
||||
// SearchAPIKeys searches API keys by user ID and/or keyword (name)
|
||||
|
||||
@@ -23,3 +23,20 @@ func (s *APIKeyRepoSuite) TestListByUserID_SortByNameAsc() {
|
||||
s.Require().Equal("a-key", keys[0].Name)
|
||||
s.Require().Equal("z-key", keys[1].Name)
|
||||
}
|
||||
|
||||
func (s *APIKeyRepoSuite) TestListByUserID_SortByID() {
|
||||
user := s.mustCreateUser("sort-id@example.com")
|
||||
first := s.mustCreateApiKey(user.ID, "sk-id-a", "a-key", nil)
|
||||
second := s.mustCreateApiKey(user.ID, "sk-id-b", "b-key", nil)
|
||||
|
||||
keys, _, err := s.repo.ListByUserID(s.ctx, user.ID, pagination.PaginationParams{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
SortBy: "id",
|
||||
SortOrder: "desc",
|
||||
}, service.APIKeyListFilters{})
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(keys, 2)
|
||||
s.Require().Equal(second.ID, keys[0].ID)
|
||||
s.Require().Equal(first.ID, keys[1].ID)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -40,8 +41,9 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
apiKeyMaxErrorsPerHour = 20
|
||||
apiKeyLastUsedMinTouch = 30 * time.Second
|
||||
apiKeyMaxErrorsPerHour = 20
|
||||
apiKeyLastUsedMinTouch = 30 * time.Second
|
||||
apiKeySortCurrentConcurrency = "current_concurrency"
|
||||
// DB 写失败后的短退避,避免请求路径持续同步重试造成写风暴与高延迟。
|
||||
apiKeyLastUsedFailBackoff = 5 * time.Second
|
||||
)
|
||||
@@ -82,6 +84,10 @@ type APIKeyRepository interface {
|
||||
GetRateLimitData(ctx context.Context, id int64) (*APIKeyRateLimitData, error)
|
||||
}
|
||||
|
||||
type apiKeyAllByUserIDLister interface {
|
||||
ListAllByUserID(ctx context.Context, userID int64, filters APIKeyListFilters) ([]APIKey, error)
|
||||
}
|
||||
|
||||
// APIKeyRateLimitData holds rate limit usage and window state for an API key.
|
||||
type APIKeyRateLimitData struct {
|
||||
Usage5h float64
|
||||
@@ -437,6 +443,10 @@ func (s *APIKeyService) Create(ctx context.Context, userID int64, req CreateAPIK
|
||||
|
||||
// List 获取用户的API Key列表
|
||||
func (s *APIKeyService) List(ctx context.Context, userID int64, params pagination.PaginationParams, filters APIKeyListFilters) ([]APIKey, *pagination.PaginationResult, error) {
|
||||
if normalizedAPIKeySortBy(params.SortBy) == apiKeySortCurrentConcurrency {
|
||||
return s.listByCurrentConcurrency(ctx, userID, params, filters)
|
||||
}
|
||||
|
||||
keys, pagination, err := s.apiKeyRepo.ListByUserID(ctx, userID, params, filters)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("list api keys: %w", err)
|
||||
@@ -445,6 +455,75 @@ func (s *APIKeyService) List(ctx context.Context, userID int64, params paginatio
|
||||
return keys, pagination, nil
|
||||
}
|
||||
|
||||
func (s *APIKeyService) listByCurrentConcurrency(ctx context.Context, userID int64, params pagination.PaginationParams, filters APIKeyListFilters) ([]APIKey, *pagination.PaginationResult, error) {
|
||||
repo, ok := s.apiKeyRepo.(apiKeyAllByUserIDLister)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("list api keys by current concurrency: repository does not support unpaginated API key listing")
|
||||
}
|
||||
|
||||
keys, err := repo.ListAllByUserID(ctx, userID, filters)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("list api keys: %w", err)
|
||||
}
|
||||
s.fillCurrentConcurrency(ctx, keys)
|
||||
sortAPIKeysByCurrentConcurrency(keys, params.NormalizedSortOrder(pagination.SortOrderDesc))
|
||||
return paginateAPIKeys(keys, params), apiKeyPaginationResult(int64(len(keys)), params), nil
|
||||
}
|
||||
|
||||
func normalizedAPIKeySortBy(sortBy string) string {
|
||||
return strings.ToLower(strings.TrimSpace(sortBy))
|
||||
}
|
||||
|
||||
func sortAPIKeysByCurrentConcurrency(keys []APIKey, sortOrder string) {
|
||||
desc := sortOrder != pagination.SortOrderAsc
|
||||
sort.SliceStable(keys, func(i, j int) bool {
|
||||
if keys[i].CurrentConcurrency == keys[j].CurrentConcurrency {
|
||||
if desc {
|
||||
return keys[i].ID > keys[j].ID
|
||||
}
|
||||
return keys[i].ID < keys[j].ID
|
||||
}
|
||||
if desc {
|
||||
return keys[i].CurrentConcurrency > keys[j].CurrentConcurrency
|
||||
}
|
||||
return keys[i].CurrentConcurrency < keys[j].CurrentConcurrency
|
||||
})
|
||||
}
|
||||
|
||||
func paginateAPIKeys(keys []APIKey, params pagination.PaginationParams) []APIKey {
|
||||
if len(keys) == 0 {
|
||||
return []APIKey{}
|
||||
}
|
||||
limit := params.Limit()
|
||||
page := params.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
if offset >= len(keys) {
|
||||
return []APIKey{}
|
||||
}
|
||||
end := offset + limit
|
||||
if end > len(keys) {
|
||||
end = len(keys)
|
||||
}
|
||||
return keys[offset:end]
|
||||
}
|
||||
|
||||
func apiKeyPaginationResult(total int64, params pagination.PaginationParams) *pagination.PaginationResult {
|
||||
limit := params.Limit()
|
||||
pages := int(total) / limit
|
||||
if int(total)%limit > 0 {
|
||||
pages++
|
||||
}
|
||||
return &pagination.PaginationResult{
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
PageSize: limit,
|
||||
Pages: pages,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIKeyService) fillCurrentConcurrency(ctx context.Context, keys []APIKey) {
|
||||
if s == nil || s.concurrencyService == nil || len(keys) == 0 {
|
||||
return
|
||||
|
||||
@@ -9,6 +9,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -24,20 +25,26 @@ import (
|
||||
// - deleteErr: 模拟 Delete 返回的错误
|
||||
// - deletedIDs: 记录被调用删除的 API Key ID,用于断言验证
|
||||
type apiKeyRepoStub struct {
|
||||
apiKey *APIKey // GetKeyAndOwnerID 的返回值
|
||||
getByIDErr error // GetKeyAndOwnerID 的错误返回值
|
||||
deleteErr error // Delete 的错误返回值
|
||||
updateErr error // Update 的错误返回值
|
||||
deletedIDs []int64 // 记录已删除的 API Key ID 列表
|
||||
updatedKeys []APIKey
|
||||
allowListByUserID bool
|
||||
listByUserIDKeys []APIKey
|
||||
listByUserIDErr error
|
||||
listByUserIDCalls []int64
|
||||
listByUserIDParams []pagination.PaginationParams
|
||||
updateLastUsed func(ctx context.Context, id int64, usedAt time.Time) error
|
||||
touchedIDs []int64
|
||||
touchedUsedAts []time.Time
|
||||
apiKey *APIKey // GetKeyAndOwnerID 的返回值
|
||||
getByIDErr error // GetKeyAndOwnerID 的错误返回值
|
||||
deleteErr error // Delete 的错误返回值
|
||||
updateErr error // Update 的错误返回值
|
||||
deletedIDs []int64 // 记录已删除的 API Key ID 列表
|
||||
updatedKeys []APIKey
|
||||
allowListByUserID bool
|
||||
listByUserIDKeys []APIKey
|
||||
listByUserIDErr error
|
||||
listByUserIDCalls []int64
|
||||
listByUserIDParams []pagination.PaginationParams
|
||||
listByUserIDFilters []APIKeyListFilters
|
||||
allowListAllByUserID bool
|
||||
listAllByUserIDKeys []APIKey
|
||||
listAllByUserIDErr error
|
||||
listAllByUserIDCalls []int64
|
||||
listAllByUserIDFilters []APIKeyListFilters
|
||||
updateLastUsed func(ctx context.Context, id int64, usedAt time.Time) error
|
||||
touchedIDs []int64
|
||||
touchedUsedAts []time.Time
|
||||
}
|
||||
|
||||
// 以下方法在本测试中不应被调用,使用 panic 确保测试失败时能快速定位问题
|
||||
@@ -103,6 +110,7 @@ func (s *apiKeyRepoStub) ListByUserID(ctx context.Context, userID int64, params
|
||||
}
|
||||
s.listByUserIDCalls = append(s.listByUserIDCalls, userID)
|
||||
s.listByUserIDParams = append(s.listByUserIDParams, params)
|
||||
s.listByUserIDFilters = append(s.listByUserIDFilters, filters)
|
||||
if s.listByUserIDErr != nil {
|
||||
return nil, nil, s.listByUserIDErr
|
||||
}
|
||||
@@ -115,6 +123,51 @@ func (s *apiKeyRepoStub) ListByUserID(ctx context.Context, userID int64, params
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *apiKeyRepoStub) ListAllByUserID(ctx context.Context, userID int64, filters APIKeyListFilters) ([]APIKey, error) {
|
||||
if !s.allowListAllByUserID {
|
||||
panic("unexpected ListAllByUserID call")
|
||||
}
|
||||
s.listAllByUserIDCalls = append(s.listAllByUserIDCalls, userID)
|
||||
s.listAllByUserIDFilters = append(s.listAllByUserIDFilters, filters)
|
||||
if s.listAllByUserIDErr != nil {
|
||||
return nil, s.listAllByUserIDErr
|
||||
}
|
||||
source := s.listByUserIDKeys
|
||||
if s.listAllByUserIDKeys != nil {
|
||||
source = s.listAllByUserIDKeys
|
||||
}
|
||||
return filterAPIKeyStubKeys(userID, source, filters), nil
|
||||
}
|
||||
|
||||
func filterAPIKeyStubKeys(userID int64, keys []APIKey, filters APIKeyListFilters) []APIKey {
|
||||
result := make([]APIKey, 0, len(keys))
|
||||
search := strings.ToLower(filters.Search)
|
||||
for _, key := range keys {
|
||||
if key.UserID != userID {
|
||||
continue
|
||||
}
|
||||
if search != "" &&
|
||||
!strings.Contains(strings.ToLower(key.Name), search) &&
|
||||
!strings.Contains(strings.ToLower(key.Key), search) {
|
||||
continue
|
||||
}
|
||||
if filters.Status != "" && key.Status != filters.Status {
|
||||
continue
|
||||
}
|
||||
if filters.GroupID != nil {
|
||||
if *filters.GroupID == 0 {
|
||||
if key.GroupID != nil {
|
||||
continue
|
||||
}
|
||||
} else if key.GroupID == nil || *key.GroupID != *filters.GroupID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
result = append(result, key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *apiKeyRepoStub) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
|
||||
panic("unexpected VerifyOwnership call")
|
||||
}
|
||||
@@ -320,6 +373,96 @@ func TestAPIKeyService_List_FillsCurrentConcurrency(t *testing.T) {
|
||||
require.Equal(t, 0, keys[1].CurrentConcurrency)
|
||||
}
|
||||
|
||||
func TestAPIKeyService_List_SortByCurrentConcurrency(t *testing.T) {
|
||||
groupID := int64(42)
|
||||
keys := []APIKey{
|
||||
{ID: 1, UserID: 7, Key: "sk-target-1", Name: "target-one", GroupID: &groupID, Status: StatusActive},
|
||||
{ID: 2, UserID: 7, Key: "sk-target-2", Name: "target-two", GroupID: &groupID, Status: StatusActive},
|
||||
{ID: 3, UserID: 7, Key: "sk-target-3", Name: "target-three", GroupID: &groupID, Status: StatusActive},
|
||||
{ID: 4, UserID: 7, Key: "sk-target-4", Name: "target-four", GroupID: &groupID, Status: StatusActive},
|
||||
{ID: 9, UserID: 7, Key: "sk-target-9", Name: "target-inactive", GroupID: &groupID, Status: StatusDisabled},
|
||||
{ID: 10, UserID: 7, Key: "sk-other-10", Name: "other", GroupID: &groupID, Status: StatusActive},
|
||||
{ID: 11, UserID: 7, Key: "sk-target-11", Name: "target-no-group", Status: StatusActive},
|
||||
{ID: 12, UserID: 8, Key: "sk-target-12", Name: "target-other-user", GroupID: &groupID, Status: StatusActive},
|
||||
}
|
||||
filters := APIKeyListFilters{
|
||||
Search: "target",
|
||||
Status: StatusActive,
|
||||
GroupID: &groupID,
|
||||
}
|
||||
repo := &apiKeyRepoStub{
|
||||
allowListAllByUserID: true,
|
||||
listAllByUserIDKeys: keys,
|
||||
}
|
||||
concurrency := NewConcurrencyService(&stubConcurrencyCacheForTest{
|
||||
apiKeyConcurrency: map[int64]int{
|
||||
1: 5,
|
||||
2: 5,
|
||||
3: 2,
|
||||
4: 8,
|
||||
9: 99,
|
||||
10: 99,
|
||||
11: 99,
|
||||
12: 99,
|
||||
},
|
||||
})
|
||||
svc := &APIKeyService{apiKeyRepo: repo, concurrencyService: concurrency}
|
||||
|
||||
got, page, err := svc.List(context.Background(), 7, pagination.PaginationParams{
|
||||
Page: 2,
|
||||
PageSize: 2,
|
||||
SortBy: "current_concurrency",
|
||||
SortOrder: "desc",
|
||||
}, filters)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []int64{1, 3}, apiKeyTestIDs(got))
|
||||
require.Equal(t, int64(4), page.Total)
|
||||
require.Equal(t, 2, page.Page)
|
||||
require.Equal(t, 2, page.PageSize)
|
||||
require.Equal(t, 2, page.Pages)
|
||||
require.Empty(t, repo.listByUserIDCalls)
|
||||
require.Equal(t, []int64{7}, repo.listAllByUserIDCalls)
|
||||
require.Len(t, repo.listAllByUserIDFilters, 1)
|
||||
require.Equal(t, filters.Search, repo.listAllByUserIDFilters[0].Search)
|
||||
require.Equal(t, filters.Status, repo.listAllByUserIDFilters[0].Status)
|
||||
require.NotNil(t, repo.listAllByUserIDFilters[0].GroupID)
|
||||
require.Equal(t, groupID, *repo.listAllByUserIDFilters[0].GroupID)
|
||||
}
|
||||
|
||||
func TestAPIKeyService_List_SortByCurrentConcurrencyAscTiesByID(t *testing.T) {
|
||||
repo := &apiKeyRepoStub{
|
||||
allowListAllByUserID: true,
|
||||
listAllByUserIDKeys: []APIKey{
|
||||
{ID: 1, UserID: 7, Key: "sk-1", Name: "one", Status: StatusActive},
|
||||
{ID: 2, UserID: 7, Key: "sk-2", Name: "two", Status: StatusActive},
|
||||
{ID: 3, UserID: 7, Key: "sk-3", Name: "three", Status: StatusActive},
|
||||
{ID: 4, UserID: 7, Key: "sk-4", Name: "four", Status: StatusActive},
|
||||
},
|
||||
}
|
||||
concurrency := NewConcurrencyService(&stubConcurrencyCacheForTest{
|
||||
apiKeyConcurrency: map[int64]int{1: 5, 2: 5, 3: 2, 4: 8},
|
||||
})
|
||||
svc := &APIKeyService{apiKeyRepo: repo, concurrencyService: concurrency}
|
||||
|
||||
got, page, err := svc.List(context.Background(), 7, pagination.PaginationParams{
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
SortBy: "current_concurrency",
|
||||
SortOrder: "asc",
|
||||
}, APIKeyListFilters{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []int64{3, 1, 2, 4}, apiKeyTestIDs(got))
|
||||
require.Equal(t, 4, page.PageSize)
|
||||
}
|
||||
|
||||
func apiKeyTestIDs(keys []APIKey) []int64 {
|
||||
ids := make([]int64, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
ids = append(ids, key.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func TestAPIKeyService_GetByID_FillsCurrentConcurrency(t *testing.T) {
|
||||
repo := &apiKeyRepoStub{
|
||||
apiKey: &APIKey{ID: 10, UserID: 7, Key: "sk-10", Name: "key-10"},
|
||||
|
||||
@@ -1168,7 +1168,7 @@ const allColumns = computed<Column[]>(() => [
|
||||
{ key: 'name', label: t('common.name'), sortable: true },
|
||||
{ key: 'key', label: t('keys.apiKey'), sortable: false },
|
||||
{ key: 'group', label: t('keys.group'), sortable: false },
|
||||
{ key: 'current_concurrency', label: t('keys.currentConcurrency'), sortable: false },
|
||||
{ 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 },
|
||||
{ key: 'expires_at', label: t('keys.expiresAt'), sortable: true },
|
||||
|
||||
@@ -149,11 +149,16 @@ const TablePageLayoutStub = {
|
||||
}
|
||||
|
||||
const DataTableStub = {
|
||||
name: 'DataTable',
|
||||
props: ['columns', 'data'],
|
||||
emits: ['sort'],
|
||||
template: `
|
||||
<div>
|
||||
<div data-test="columns">{{ columns.map((col) => col.key).join(',') }}</div>
|
||||
<div data-test="columns-meta">{{ JSON.stringify(columns.map((col) => ({ key: col.key, sortable: !!col.sortable }))) }}</div>
|
||||
<button data-test="sort-current-concurrency" @click="$emit('sort', 'current_concurrency', 'asc')">
|
||||
Sort Current Concurrency
|
||||
</button>
|
||||
<div v-for="row in data" :key="row.id">
|
||||
<slot name="cell-name" :value="row.name" :row="row" />
|
||||
<div data-test="current-concurrency">
|
||||
@@ -166,17 +171,30 @@ const DataTableStub = {
|
||||
}
|
||||
|
||||
const SelectStub = {
|
||||
name: 'Select',
|
||||
props: ['modelValue', 'options'],
|
||||
emits: ['update:modelValue'],
|
||||
template: '<select :value="modelValue" @change="$emit(\'update:modelValue\', $event.target.value)"></select>',
|
||||
}
|
||||
|
||||
const SearchInputStub = {
|
||||
name: 'SearchInput',
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue', 'search'],
|
||||
template: '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
|
||||
}
|
||||
|
||||
const PaginationStub = {
|
||||
name: 'Pagination',
|
||||
props: ['page', 'total', 'pageSize'],
|
||||
emits: ['update:page', 'update:pageSize'],
|
||||
template: `
|
||||
<div>
|
||||
<button data-test="page-size-50" @click="$emit('update:pageSize', 50)">50</button>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
||||
const IconStub = {
|
||||
props: ['name'],
|
||||
template: '<span data-test="icon">{{ name }}</span>',
|
||||
@@ -189,7 +207,7 @@ const mountView = async () => {
|
||||
AppLayout: AppLayoutStub,
|
||||
TablePageLayout: TablePageLayoutStub,
|
||||
DataTable: DataTableStub,
|
||||
Pagination: true,
|
||||
Pagination: PaginationStub,
|
||||
BaseDialog: true,
|
||||
ConfirmDialog: true,
|
||||
EmptyState: true,
|
||||
@@ -212,6 +230,9 @@ const mountView = async () => {
|
||||
const visibleColumnKeys = (wrapper: VueWrapper) =>
|
||||
wrapper.get('[data-test="columns"]').text().split(',').filter(Boolean)
|
||||
|
||||
const visibleColumnMeta = (wrapper: VueWrapper): Array<{ key: string; sortable: boolean }> =>
|
||||
JSON.parse(wrapper.get('[data-test="columns-meta"]').text())
|
||||
|
||||
const getButtonByText = (wrapper: VueWrapper, text: string) => {
|
||||
const button = wrapper.findAll('button').find((item) => item.text().includes(text))
|
||||
if (!button) {
|
||||
@@ -317,4 +338,49 @@ describe('user KeysView column settings', () => {
|
||||
|
||||
expect(wrapper.get('[data-test="current-concurrency"]').text()).toBe('3')
|
||||
})
|
||||
|
||||
it('marks current concurrency as sortable', async () => {
|
||||
const wrapper = await mountView()
|
||||
|
||||
const currentConcurrencyColumn = visibleColumnMeta(wrapper).find(
|
||||
(column) => column.key === 'current_concurrency'
|
||||
)
|
||||
expect(currentConcurrencyColumn?.sortable).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps filters and selected page size when sorting by current concurrency', async () => {
|
||||
getAvailableGroups.mockResolvedValue([{ id: 42, name: 'OpenAI' }])
|
||||
const wrapper = await mountView()
|
||||
|
||||
await wrapper.get('[data-test="page-size-50"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.findComponent({ name: 'SearchInput' }).vm.$emit('update:modelValue', 'target')
|
||||
await wrapper.findComponent({ name: 'SearchInput' }).vm.$emit('search')
|
||||
await flushPromises()
|
||||
|
||||
const selects = wrapper.findAllComponents({ name: 'Select' })
|
||||
await selects[0].vm.$emit('update:modelValue', 42)
|
||||
await flushPromises()
|
||||
await selects[1].vm.$emit('update:modelValue', 'active')
|
||||
await flushPromises()
|
||||
|
||||
listKeys.mockClear()
|
||||
|
||||
await wrapper.get('[data-test="sort-current-concurrency"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(listKeys).toHaveBeenLastCalledWith(
|
||||
1,
|
||||
50,
|
||||
{
|
||||
search: 'target',
|
||||
status: 'active',
|
||||
group_id: 42,
|
||||
sort_by: 'current_concurrency',
|
||||
sort_order: 'asc',
|
||||
},
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user