From a65d476f25ddcd5ef961f18b80fc640cb7341ecd Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Fri, 11 Sep 2026 19:32:20 +0800 Subject: [PATCH] fix(keys): sync status after resetting quota --- frontend/src/views/user/KeysView.vue | 14 ++++-- .../src/views/user/__tests__/KeysView.spec.ts | 48 ++++++++++++++++++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/user/KeysView.vue b/frontend/src/views/user/KeysView.vue index 023757650a..e915e8148f 100644 --- a/frontend/src/views/user/KeysView.vue +++ b/frontend/src/views/user/KeysView.vue @@ -1823,14 +1823,18 @@ const setExpirationDays = (days: number) => { // Reset quota used for an API key const resetQuotaUsed = async () => { - if (!selectedKey.value) return + const key = selectedKey.value + if (!key) return showResetQuotaDialog.value = false try { - await keysAPI.update(selectedKey.value.id, { reset_quota: true }) + const updatedKey = await keysAPI.update(key.id, { reset_quota: true }) appStore.showSuccess(t('keys.quotaResetSuccess')) - // Update local state - if (selectedKey.value) { - selectedKey.value.quota_used = 0 + key.quota_used = updatedKey.quota_used + if (key.status !== updatedKey.status) { + key.status = updatedKey.status + if (selectedKey.value?.id === key.id) { + formData.value.status = updatedKey.status === 'active' ? 'active' : 'inactive' + } } } catch (error: any) { const errorMsg = error.response?.data?.detail || t('keys.failedToResetQuota') diff --git a/frontend/src/views/user/__tests__/KeysView.spec.ts b/frontend/src/views/user/__tests__/KeysView.spec.ts index b66072e94f..4ec5b4c53b 100644 --- a/frontend/src/views/user/__tests__/KeysView.spec.ts +++ b/frontend/src/views/user/__tests__/KeysView.spec.ts @@ -7,6 +7,7 @@ import KeysView from '../KeysView.vue' const { listKeys, + updateKey, getPublicSettings, getDashboardApiKeysUsage, getAvailableGroups, @@ -18,6 +19,7 @@ const { nextStep, } = vi.hoisted(() => ({ listKeys: vi.fn(), + updateKey: vi.fn(), getPublicSettings: vi.fn(), getDashboardApiKeysUsage: vi.fn(), getAvailableGroups: vi.fn(), @@ -59,7 +61,7 @@ vi.mock('@/api', () => ({ keysAPI: { list: listKeys, create: vi.fn(), - update: vi.fn(), + update: updateKey, delete: vi.fn(), toggleStatus: vi.fn(), }, @@ -170,6 +172,7 @@ const DataTableStub = { +
@@ -223,7 +226,10 @@ const mountView = async () => { TablePageLayout: TablePageLayoutStub, DataTable: DataTableStub, Pagination: PaginationStub, - BaseDialog: true, + BaseDialog: { + props: ['show'], + template: '
', + }, ConfirmDialog: true, EmptyState: true, Select: SelectStub, @@ -261,6 +267,7 @@ describe('user KeysView column settings', () => { localStorage.clear() listKeys.mockReset() + updateKey.mockReset() getPublicSettings.mockReset() getDashboardApiKeysUsage.mockReset() getAvailableGroups.mockReset() @@ -285,6 +292,43 @@ describe('user KeysView column settings', () => { isCurrentStep.mockReturnValue(false) }) + it.each([ + { initialStatus: 'quota_exhausted', status: 'active', formStatus: 'active' }, + { initialStatus: 'inactive', status: 'inactive', formStatus: 'inactive' }, + { initialStatus: 'active', status: 'active', formStatus: 'inactive' }, + ] as const)('syncs quota reset from $initialStatus to $status with form status $formStatus', async ({ initialStatus, status, formStatus }) => { + const key: ApiKey = { + ...createApiKey(), group_id: 1, quota: 10, quota_used: 10, + status: initialStatus, + } + listKeys.mockResolvedValueOnce({ items: [key], total: 1, page: 1, page_size: 20, pages: 1 }) + updateKey.mockResolvedValue({ ...key, status, quota_used: 0 }) + const wrapper = await mountView() + await getButtonByText(wrapper, 'common.edit').trigger('click') + await wrapper.get('[data-tour="key-form-name"]').setValue('Unsaved name') + const statusSelect = wrapper.findAllComponents({ name: 'Select' }) + .find((select) => select.props('options').length === 2 && + select.props('options')[0].value === 'active')! + statusSelect.vm.$emit('update:modelValue', 'inactive') + await wrapper.get('button[title="keys.resetQuotaUsed"]').trigger('click') + const confirmation = wrapper.findAllComponents({ name: 'ConfirmDialog' }) + .find((dialog) => dialog.props('title') === 'keys.resetQuotaTitle')! + confirmation.vm.$emit('confirm') + await flushPromises() + + expect(updateKey).toHaveBeenNthCalledWith(1, key.id, { reset_quota: true }) + expect(wrapper.findComponent({ name: 'DataTable' }).props('data')[0]) + .toMatchObject({ status, quota_used: 0 }) + expect(statusSelect.props('modelValue')).toBe(formStatus) + expect((wrapper.get('[data-tour="key-form-name"]').element as HTMLInputElement).value) + .toBe('Unsaved name') + + await wrapper.get('#key-form').trigger('submit') + await flushPromises() + expect(updateKey).toHaveBeenNthCalledWith(2, key.id, expect.objectContaining({ name: 'Unsaved name', status: formStatus })) + wrapper.unmount() + }) + it('uses the default API key columns with low-frequency columns hidden', async () => { const wrapper = await mountView()