mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 14:41:21 +00:00
perf(web): debounce server and large-list searches (#6727)
This commit is contained in:
@@ -307,6 +307,7 @@ export function ApiKeysTable() {
|
|||||||
applyHeaderSize
|
applyHeaderSize
|
||||||
toolbarProps={{
|
toolbarProps={{
|
||||||
searchPlaceholder: t('Filter by name...'),
|
searchPlaceholder: t('Filter by name...'),
|
||||||
|
searchDebounceMs: 500,
|
||||||
additionalSearch: (
|
additionalSearch: (
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('Filter by API key...')}
|
placeholder={t('Filter by API key...')}
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ export function DeploymentsTable() {
|
|||||||
applyHeaderSize
|
applyHeaderSize
|
||||||
toolbarProps={{
|
toolbarProps={{
|
||||||
searchPlaceholder: t('Search deployments...'),
|
searchPlaceholder: t('Search deployments...'),
|
||||||
|
searchDebounceMs: 500,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
columnId: 'status',
|
columnId: 'status',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { type ColumnDef, type RowSelectionState } from '@tanstack/react-table'
|
import type { ColumnDef, RowSelectionState } from '@tanstack/react-table'
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Info,
|
Info,
|
||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce'
|
||||||
import { useIsMobile } from '@/hooks/use-mobile'
|
import { useIsMobile } from '@/hooks/use-mobile'
|
||||||
|
|
||||||
import { applyUpstreamOverwrite } from '../../api'
|
import { applyUpstreamOverwrite } from '../../api'
|
||||||
@@ -117,6 +118,7 @@ export function UpstreamConflictDialog({
|
|||||||
} = useModels()
|
} = useModels()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const debouncedSearch = useDebounce(search, 200)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
const [pageSize, setPageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
@@ -149,7 +151,7 @@ export function UpstreamConflictDialog({
|
|||||||
|
|
||||||
const totalModels = upstreamConflicts.length
|
const totalModels = upstreamConflicts.length
|
||||||
const totalFields = conflictRows.length
|
const totalFields = conflictRows.length
|
||||||
const normalizedSearch = search.trim().toLowerCase()
|
const normalizedSearch = debouncedSearch.trim().toLowerCase()
|
||||||
|
|
||||||
const { matchingModelNames, visibleRowIds } = useMemo(() => {
|
const { matchingModelNames, visibleRowIds } = useMemo(() => {
|
||||||
if (!normalizedSearch) {
|
if (!normalizedSearch) {
|
||||||
@@ -396,7 +398,7 @@ export function UpstreamConflictDialog({
|
|||||||
const payload: SyncOverwritePayload[] = Object.entries(groupedSelections)
|
const payload: SyncOverwritePayload[] = Object.entries(groupedSelections)
|
||||||
.map(([modelName, fields]) => ({
|
.map(([modelName, fields]) => ({
|
||||||
model_name: modelName,
|
model_name: modelName,
|
||||||
fields: Array.from(fields),
|
fields: [...fields],
|
||||||
}))
|
}))
|
||||||
.filter((item) => item.fields.length > 0)
|
.filter((item) => item.fields.length > 0)
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export function ModelsTable() {
|
|||||||
applyHeaderSize
|
applyHeaderSize
|
||||||
toolbarProps={{
|
toolbarProps={{
|
||||||
searchPlaceholder: t('Filter by model name...'),
|
searchPlaceholder: t('Filter by model name...'),
|
||||||
|
searchDebounceMs: 500,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
columnId: 'status',
|
columnId: 'status',
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import { useSearch } from '@tanstack/react-router'
|
import { useSearch } from '@tanstack/react-router'
|
||||||
import { useMemo, useCallback, useState } from 'react'
|
import { useMemo, useCallback, useState } from 'react'
|
||||||
|
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FILTER_ALL,
|
FILTER_ALL,
|
||||||
SORT_OPTIONS,
|
SORT_OPTIONS,
|
||||||
@@ -67,6 +69,7 @@ export function useFilters(models: PricingModel[]) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const searchInput = filterState.search || ''
|
const searchInput = filterState.search || ''
|
||||||
|
const debouncedSearchInput = useDebounce(searchInput, 200)
|
||||||
const sortBy = filterState.sort || SORT_OPTIONS.NAME
|
const sortBy = filterState.sort || SORT_OPTIONS.NAME
|
||||||
const vendorFilter = filterState.vendor || FILTER_ALL
|
const vendorFilter = filterState.vendor || FILTER_ALL
|
||||||
const groupFilter = filterState.group || FILTER_ALL
|
const groupFilter = filterState.group || FILTER_ALL
|
||||||
@@ -147,7 +150,7 @@ export function useFilters(models: PricingModel[]) {
|
|||||||
if (!models || models.length === 0) return []
|
if (!models || models.length === 0) return []
|
||||||
|
|
||||||
return filterAndSortModels(models, {
|
return filterAndSortModels(models, {
|
||||||
search: searchInput,
|
search: debouncedSearchInput,
|
||||||
vendor: vendorFilter,
|
vendor: vendorFilter,
|
||||||
group: groupFilter,
|
group: groupFilter,
|
||||||
quotaType: quotaTypeFilter,
|
quotaType: quotaTypeFilter,
|
||||||
@@ -157,7 +160,7 @@ export function useFilters(models: PricingModel[]) {
|
|||||||
})
|
})
|
||||||
}, [
|
}, [
|
||||||
models,
|
models,
|
||||||
searchInput,
|
debouncedSearchInput,
|
||||||
vendorFilter,
|
vendorFilter,
|
||||||
groupFilter,
|
groupFilter,
|
||||||
quotaTypeFilter,
|
quotaTypeFilter,
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export function RedemptionsTable() {
|
|||||||
applyHeaderSize
|
applyHeaderSize
|
||||||
toolbarProps={{
|
toolbarProps={{
|
||||||
searchPlaceholder: t('Filter by name or ID...'),
|
searchPlaceholder: t('Filter by name or ID...'),
|
||||||
|
searchDebounceMs: 500,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
columnId: 'status',
|
columnId: 'status',
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce'
|
||||||
|
|
||||||
import type { UpstreamChannel } from '../types'
|
import type { UpstreamChannel } from '../types'
|
||||||
import {
|
import {
|
||||||
@@ -80,6 +81,7 @@ export function ChannelSelectorDialog({
|
|||||||
}: ChannelSelectorDialogProps) {
|
}: ChannelSelectorDialogProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const debouncedSearch = useDebounce(search, 200)
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -273,15 +275,15 @@ export function ChannelSelectorDialog({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const filteredChannels = useMemo(() => {
|
const filteredChannels = useMemo(() => {
|
||||||
if (!search.trim()) return channels
|
if (!debouncedSearch.trim()) return channels
|
||||||
|
|
||||||
const searchLower = search.toLowerCase()
|
const searchLower = debouncedSearch.toLowerCase()
|
||||||
return channels.filter(
|
return channels.filter(
|
||||||
(ch) =>
|
(ch) =>
|
||||||
ch.name.toLowerCase().includes(searchLower) ||
|
ch.name.toLowerCase().includes(searchLower) ||
|
||||||
ch.base_url.toLowerCase().includes(searchLower)
|
ch.base_url.toLowerCase().includes(searchLower)
|
||||||
)
|
)
|
||||||
}, [channels, search])
|
}, [channels, debouncedSearch])
|
||||||
|
|
||||||
const sortedChannels = useMemo(() => {
|
const sortedChannels = useMemo(() => {
|
||||||
return [...filteredChannels].sort((a, b) => {
|
return [...filteredChannels].sort((a, b) => {
|
||||||
|
|||||||
@@ -682,6 +682,7 @@ const ModelRatioVisualEditorComponent = forwardRef<
|
|||||||
<DataTableToolbar
|
<DataTableToolbar
|
||||||
table={table}
|
table={table}
|
||||||
searchPlaceholder={t('Search models...')}
|
searchPlaceholder={t('Search models...')}
|
||||||
|
searchDebounceMs={250}
|
||||||
filters={[
|
filters={[
|
||||||
{
|
{
|
||||||
columnId: 'billingMode',
|
columnId: 'billingMode',
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce'
|
||||||
|
|
||||||
import type { DifferencesMap, RatioType } from '../types'
|
import type { DifferencesMap, RatioType } from '../types'
|
||||||
import { RATIO_TYPE_OPTIONS } from './constants'
|
import { RATIO_TYPE_OPTIONS } from './constants'
|
||||||
@@ -88,6 +89,7 @@ export function UpstreamRatioSyncTable({
|
|||||||
}: UpstreamRatioSyncTableProps) {
|
}: UpstreamRatioSyncTableProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const debouncedSearch = useDebounce(search, 250)
|
||||||
const [ratioTypeFilter, setRatioTypeFilter] = useState<string>('')
|
const [ratioTypeFilter, setRatioTypeFilter] = useState<string>('')
|
||||||
|
|
||||||
const dataSource = useMemo<ModelRow[]>(() => {
|
const dataSource = useMemo<ModelRow[]>(() => {
|
||||||
@@ -106,8 +108,8 @@ export function UpstreamRatioSyncTable({
|
|||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
let data = dataSource
|
let data = dataSource
|
||||||
|
|
||||||
if (search.trim()) {
|
if (debouncedSearch.trim()) {
|
||||||
const lower = search.toLowerCase()
|
const lower = debouncedSearch.toLowerCase()
|
||||||
data = data.filter((row) => row.model.toLowerCase().includes(lower))
|
data = data.filter((row) => row.model.toLowerCase().includes(lower))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +118,7 @@ export function UpstreamRatioSyncTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}, [dataSource, search, ratioTypeFilter])
|
}, [dataSource, debouncedSearch, ratioTypeFilter])
|
||||||
|
|
||||||
const upstreamNames = useMemo(() => {
|
const upstreamNames = useMemo(() => {
|
||||||
const set = new Set<string>()
|
const set = new Set<string>()
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { useIsAdmin } from '@/hooks/use-admin'
|
import { useIsAdmin } from '@/hooks/use-admin'
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getUserBillingHistory,
|
getUserBillingHistory,
|
||||||
@@ -50,6 +51,8 @@ export function useBillingHistory(options: UseBillingHistoryOptions = {}) {
|
|||||||
const [page, setPage] = useState(initialPage)
|
const [page, setPage] = useState(initialPage)
|
||||||
const [pageSize, setPageSize] = useState(initialPageSize)
|
const [pageSize, setPageSize] = useState(initialPageSize)
|
||||||
const [keyword, setKeyword] = useState('')
|
const [keyword, setKeyword] = useState('')
|
||||||
|
const debouncedKeyword = useDebounce(keyword)
|
||||||
|
const requestIdRef = useRef(0)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [completing, setCompleting] = useState(false)
|
const [completing, setCompleting] = useState(false)
|
||||||
|
|
||||||
@@ -57,11 +60,14 @@ export function useBillingHistory(options: UseBillingHistoryOptions = {}) {
|
|||||||
* Fetch billing history
|
* Fetch billing history
|
||||||
*/
|
*/
|
||||||
const fetchBillingHistory = useCallback(async () => {
|
const fetchBillingHistory = useCallback(async () => {
|
||||||
|
const requestId = ++requestIdRef.current
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = isAdmin
|
const response = isAdmin
|
||||||
? await getAllBillingHistory(page, pageSize, keyword)
|
? await getAllBillingHistory(page, pageSize, debouncedKeyword)
|
||||||
: await getUserBillingHistory(page, pageSize, keyword)
|
: await getUserBillingHistory(page, pageSize, debouncedKeyword)
|
||||||
|
|
||||||
|
if (requestId !== requestIdRef.current) return
|
||||||
|
|
||||||
if (isApiSuccess(response) && response.data) {
|
if (isApiSuccess(response) && response.data) {
|
||||||
setRecords(response.data.items || [])
|
setRecords(response.data.items || [])
|
||||||
@@ -74,15 +80,19 @@ export function useBillingHistory(options: UseBillingHistoryOptions = {}) {
|
|||||||
setTotal(0)
|
setTotal(0)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (requestId !== requestIdRef.current) return
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('Failed to fetch billing history:', error)
|
console.error('Failed to fetch billing history:', error)
|
||||||
toast.error(i18next.t('Failed to load billing history'))
|
toast.error(i18next.t('Failed to load billing history'))
|
||||||
setRecords([])
|
setRecords([])
|
||||||
setTotal(0)
|
setTotal(0)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
if (requestId === requestIdRef.current) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isAdmin, page, pageSize, keyword])
|
}, [debouncedKeyword, isAdmin, page, pageSize])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete a pending order (admin only)
|
* Complete a pending order (admin only)
|
||||||
@@ -137,14 +147,17 @@ export function useBillingHistory(options: UseBillingHistoryOptions = {}) {
|
|||||||
* Search by keyword
|
* Search by keyword
|
||||||
*/
|
*/
|
||||||
const handleSearch = useCallback((newKeyword: string) => {
|
const handleSearch = useCallback((newKeyword: string) => {
|
||||||
|
requestIdRef.current += 1
|
||||||
setKeyword(newKeyword)
|
setKeyword(newKeyword)
|
||||||
setPage(1) // Reset to first page when searching
|
setPage(1) // Reset to first page when searching
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Fetch data when dependencies change
|
// Fetch data after the search draft has settled.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (keyword !== debouncedKeyword) return
|
||||||
|
|
||||||
fetchBillingHistory()
|
fetchBillingHistory()
|
||||||
}, [fetchBillingHistory])
|
}, [debouncedKeyword, fetchBillingHistory, keyword])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
records,
|
records,
|
||||||
|
|||||||
Reference in New Issue
Block a user