perf(channels): optimize card view layout and reduce re-renders
- Show 3-column card grid from xl breakpoint instead of 2xl - Cap inline priority/weight width to avoid huge values stretching cards - Collapse right-column grid to content-sized columns, removing wasted space - Memoize channel columns, context value, upstream-update result, and ChannelCard to avoid rebuilding/re-rendering all cards on unrelated state changes
This commit is contained in:
+25
-19
@@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { memo } from 'react'
|
||||
import { flexRender, type Row } from '@tanstack/react-table'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -36,7 +37,7 @@ const SENSITIVE_MASK = '••••'
|
||||
* priority/weight spinners, balance refresh, response/test times, tag
|
||||
* expand-collapse, and the per-row (or per-tag) actions menu.
|
||||
*/
|
||||
export function ChannelCard({ row }: { row: Row<Channel> }) {
|
||||
function ChannelCardComponent({ row }: { row: Row<Channel> }) {
|
||||
const { t } = useTranslation()
|
||||
const { sensitiveVisible } = useChannels()
|
||||
const isTagRow = isTagAggregateRow(row.original)
|
||||
@@ -118,25 +119,23 @@ export function ChannelCard({ row }: { row: Row<Channel> }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column (sits on the right, content left-aligned) */}
|
||||
<div className='flex flex-shrink-0 flex-col gap-3'>
|
||||
<div className='grid grid-cols-2 items-center gap-x-3'>
|
||||
<span className={labelClass}>{t('Priority')}</span>
|
||||
<span className={labelClass}>{t('Weight')}</span>
|
||||
<div className='flex justify-start'>{priorityCell}</div>
|
||||
<div className='flex justify-start'>{weightCell}</div>
|
||||
{/* Right column (sits on the right, content left-aligned). A single
|
||||
grid with content-sized columns keeps Priority/Weight and
|
||||
Response/Last Tested aligned without wasting horizontal space. */}
|
||||
<div className='grid flex-shrink-0 grid-cols-[auto_auto] items-center gap-x-3 gap-y-1'>
|
||||
<span className={labelClass}>{t('Priority')}</span>
|
||||
<span className={labelClass}>{t('Weight')}</span>
|
||||
<div className='flex justify-start'>{priorityCell}</div>
|
||||
<div className='flex justify-start'>{weightCell}</div>
|
||||
<span className={cn('mt-2', labelClass)}>
|
||||
{fieldLabels.response_time}
|
||||
</span>
|
||||
<span className={cn('mt-2', labelClass)}>{fieldLabels.test_time}</span>
|
||||
<div className='overflow-hidden text-sm'>
|
||||
{responseCell ?? <span className='text-muted-foreground'>-</span>}
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-x-3'>
|
||||
<div className={cn('mb-1', labelClass)}>
|
||||
{fieldLabels.response_time}
|
||||
</div>
|
||||
<div className={cn('mb-1', labelClass)}>{fieldLabels.test_time}</div>
|
||||
<div className='overflow-hidden text-sm'>
|
||||
{responseCell ?? <span className='text-muted-foreground'>-</span>}
|
||||
</div>
|
||||
<div className='overflow-hidden text-sm'>
|
||||
{testCell ?? <span className='text-muted-foreground'>-</span>}
|
||||
</div>
|
||||
<div className='overflow-hidden text-sm'>
|
||||
{testCell ?? <span className='text-muted-foreground'>-</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,3 +160,10 @@ export function ChannelCard({ row }: { row: Row<Channel> }) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoized so each card only re-renders when its own react-table row reference
|
||||
* changes, instead of every card re-rendering whenever the parent table state
|
||||
* (filters, pagination, sensitive toggle, etc.) updates.
|
||||
*/
|
||||
export const ChannelCard = memo(ChannelCardComponent)
|
||||
|
||||
+584
-574
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react'
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useChannelUpstreamUpdates } from '../hooks/use-channel-upstream-updates'
|
||||
import { channelsQueryKeys } from '../lib'
|
||||
@@ -88,24 +94,38 @@ export function ChannelsProvider({ children }: { children: React.ReactNode }) {
|
||||
}, [queryClient])
|
||||
const upstream = useChannelUpstreamUpdates(refreshChannels)
|
||||
|
||||
// useState setters are stable, so the context value only needs to change when
|
||||
// an actual state value changes. Memoizing avoids handing every consumer
|
||||
// (including all channel cards/cells) a brand-new object on each render.
|
||||
const value = useMemo<ChannelsContextType>(
|
||||
() => ({
|
||||
open,
|
||||
setOpen,
|
||||
currentRow,
|
||||
setCurrentRow,
|
||||
currentTag,
|
||||
setCurrentTag,
|
||||
enableTagMode,
|
||||
setEnableTagMode,
|
||||
idSort,
|
||||
setIdSort,
|
||||
sensitiveVisible,
|
||||
setSensitiveVisible,
|
||||
upstream,
|
||||
}),
|
||||
[
|
||||
open,
|
||||
currentRow,
|
||||
currentTag,
|
||||
enableTagMode,
|
||||
idSort,
|
||||
sensitiveVisible,
|
||||
upstream,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<ChannelsContext.Provider
|
||||
value={{
|
||||
open,
|
||||
setOpen,
|
||||
currentRow,
|
||||
setCurrentRow,
|
||||
currentTag,
|
||||
setCurrentTag,
|
||||
enableTagMode,
|
||||
setEnableTagMode,
|
||||
idSort,
|
||||
setIdSort,
|
||||
sensitiveVisible,
|
||||
setSensitiveVisible,
|
||||
upstream,
|
||||
}}
|
||||
>
|
||||
<ChannelsContext.Provider value={value}>
|
||||
{children}
|
||||
</ChannelsContext.Provider>
|
||||
)
|
||||
|
||||
@@ -371,7 +371,7 @@ export function ChannelsTable() {
|
||||
enableCardView
|
||||
viewModeStorageKey={CHANNELS_VIEW_MODE_STORAGE_KEY}
|
||||
renderCard={(row) => <ChannelCard row={row} />}
|
||||
cardGridClassName='grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-2 2xl:grid-cols-3'
|
||||
cardGridClassName='grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-3'
|
||||
applyHeaderSize
|
||||
toolbarProps={{
|
||||
searchPlaceholder: t('Filter by name, ID, or key...'),
|
||||
|
||||
@@ -164,8 +164,9 @@ export function NumericSpinnerInput({
|
||||
type='button'
|
||||
onClick={handleStartEdit}
|
||||
disabled={disabled}
|
||||
title={localValue}
|
||||
className={cn(
|
||||
'h-7 min-w-8 cursor-text px-1 text-center font-mono text-sm tabular-nums',
|
||||
'h-7 min-w-8 max-w-16 cursor-text truncate px-1 text-center font-mono text-sm tabular-nums',
|
||||
disabled && 'cursor-default opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { useRef, useState, useCallback } from 'react'
|
||||
import { useRef, useState, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { api, type ApiRequestConfig } from '@/lib/api'
|
||||
@@ -285,20 +285,41 @@ export function useChannelUpstreamUpdates(refresh: () => Promise<void>) {
|
||||
}
|
||||
}, [refresh, t])
|
||||
|
||||
return {
|
||||
showModal,
|
||||
channel,
|
||||
addModels,
|
||||
removeModels,
|
||||
preferredTab,
|
||||
applyLoading,
|
||||
detectAllLoading,
|
||||
applyAllLoading,
|
||||
openModal,
|
||||
closeModal,
|
||||
applyUpdates,
|
||||
applyAllUpdates,
|
||||
detectChannelUpdates,
|
||||
detectAllUpdates,
|
||||
}
|
||||
// Memoized so consumers (and the channels context value built from this) get
|
||||
// a stable reference unless an actual field changes. Callbacks above are all
|
||||
// useCallback-stable, so this only changes when relevant state changes.
|
||||
return useMemo(
|
||||
() => ({
|
||||
showModal,
|
||||
channel,
|
||||
addModels,
|
||||
removeModels,
|
||||
preferredTab,
|
||||
applyLoading,
|
||||
detectAllLoading,
|
||||
applyAllLoading,
|
||||
openModal,
|
||||
closeModal,
|
||||
applyUpdates,
|
||||
applyAllUpdates,
|
||||
detectChannelUpdates,
|
||||
detectAllUpdates,
|
||||
}),
|
||||
[
|
||||
showModal,
|
||||
channel,
|
||||
addModels,
|
||||
removeModels,
|
||||
preferredTab,
|
||||
applyLoading,
|
||||
detectAllLoading,
|
||||
applyAllLoading,
|
||||
openModal,
|
||||
closeModal,
|
||||
applyUpdates,
|
||||
applyAllUpdates,
|
||||
detectChannelUpdates,
|
||||
detectAllUpdates,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user