Files
codex-proxy-rs/frontend/src/views/usage/composables/useOpsErrorsTable.ts
T
Yanshan ChenandCodex 7b03fdc8ab feat(keys): support custom keys and search by name
Support optional custom client keys across creation, storage, and authentication. Search keys and request records by name, remove credential and prefix searches, and reject duplicate names during create and update using the existing control-plane transaction lock.

Simplify the request diagnostics panel and use the shared empty state.

Validated with 1,029 backend tests using dedicated PostgreSQL and Redis, strict workspace Clippy, frontend lint/typecheck/build, and browser checks for key creation, name validation, search, diagnostics refresh/export, and mobile layouts.

Co-authored-by: Codex <noreply@openai.com>
2026-09-13 03:21:42 +08:00

112 lines
2.9 KiB
TypeScript

import type { Ref } from 'vue'
import type { UsageTimeRangeParams } from './useUsageTimeRange'
import { watchDebounced } from '@vueuse/core'
import { computed, onScopeDispose, shallowRef, watch } from 'vue'
import { getOpsErrors } from '@/api'
import { useStablePagedQuery } from '@/composables/useStablePagedQuery'
import { withMinimumDuration } from '@/utils/async'
interface UseOpsErrorsTableOptions {
timeRangeParams: Readonly<Ref<UsageTimeRangeParams>>
latestTimeRangeParams: () => UsageTimeRangeParams
provider: Readonly<Ref<string>>
active: Readonly<Ref<boolean>>
}
export function useOpsErrorsTable(options: UseOpsErrorsTableOptions) {
const refreshing = shallowRef(false)
const searchQuery = shallowRef('')
const search = computed(() => searchQuery.value.trim() || undefined)
let disposed = false
// 时间、平台和搜索共同构成分页快照,避免翻页混入另一组筛选结果。
let tableParams = snapshot()
function snapshot() {
return {
...options.latestTimeRangeParams(),
provider: options.provider.value || undefined,
search: search.value,
}
}
const query = useStablePagedQuery({
initialPageSize: 10,
load: ({ currentPage, pageSize }, requestOptions) => getOpsErrors({
currentPage,
pageSize,
...tableParams,
}, requestOptions),
})
const pagination = computed(() => ({
currentPage: query.currentPage.value,
pageSize: query.pageSize.value,
total: query.total.value,
}))
function handlePageChange(nextPage: number) {
if (tableParams.search !== search.value) {
void reloadLatest()
return
}
void query.execute(nextPage)
}
function handlePageSizeChange(nextPageSize: number) {
query.pageSize.value = nextPageSize
if (tableParams.search !== search.value)
void reloadLatest()
else
void query.reloadFromStart()
}
function reloadLatest() {
tableParams = snapshot()
// 筛选失败不能继续展示上一范围的数据,错误态由面板单独呈现。
query.items.value = []
return query.reloadFromStart()
}
async function refresh() {
if (refreshing.value || query.loading.value)
return
refreshing.value = true
try {
await withMinimumDuration(reloadLatest)
}
finally {
refreshing.value = false
}
}
watchDebounced(
search,
() => {
if (!disposed && options.active.value && tableParams.search !== search.value)
void reloadLatest()
},
{ debounce: 250 },
)
watch([options.timeRangeParams, options.provider, options.active], () => {
if (options.active.value)
void reloadLatest()
else
query.invalidate()
}, { immediate: true })
onScopeDispose(() => disposed = true)
return {
loading: query.loading,
error: query.error,
refreshing,
records: query.items,
searchQuery,
pagination,
handlePageChange,
handlePageSizeChange,
refresh,
}
}