perf(web): keep list tables fixed within page content

- make shared data table pages fill available height and scroll row data inside the table body.
- add a fixed content layout mode so selected list pages avoid page-level scrolling.
- apply the fixed table behavior to keys, logs, channels, models, users, redemptions, and subscriptions.
This commit is contained in:
QuentinHsu
2026-06-09 21:22:55 +08:00
parent 0863ddc3d9
commit a1f7256a05
10 changed files with 78 additions and 44 deletions
+27 -6
View File
@@ -192,6 +192,12 @@ export type DataTablePageProps<TData> = {
*/
className?: string
/**
* Make the desktop table consume the available page height and scroll inside
* the table body while keeping the header fixed. Defaults to `true`.
*/
fixedHeight?: boolean
/**
* Desktop table container className (the bordered scroll wrapper).
*/
@@ -199,7 +205,8 @@ export type DataTablePageProps<TData> = {
/**
* Desktop `<TableHeader>` className override.
* Useful for sticky headers (`'sticky top-0 z-10 bg-muted/30'`) on long lists.
* Use for header color/spacing overrides. Fixed-height pages keep the header
* outside the scrollable body automatically.
*/
tableHeaderClassName?: string
}
@@ -235,7 +242,14 @@ export function DataTablePage<TData>(props: DataTablePageProps<TData>) {
return (
<>
<div className={cn('space-y-2.5 sm:space-y-3', props.className)}>
<div
className={cn(
props.fixedHeight !== false
? 'flex h-full min-h-0 flex-col gap-2.5 sm:gap-3'
: 'space-y-2.5 sm:space-y-3',
props.className
)}
>
{toolbarNode}
{mobileNode}
{desktopNode}
@@ -280,7 +294,6 @@ function renderMobile<TData>(
showMobile: boolean
): React.ReactNode {
if (!showMobile) return null
if (props.mobile !== undefined) return props.mobile
const ownGetRowClassName = props.getRowClassName
const mobileGetRowClassName =
@@ -288,8 +301,7 @@ function renderMobile<TData>(
(ownGetRowClassName
? (row: Row<TData>) => ownGetRowClassName(row, { isMobile: true })
: undefined)
return (
const mobileContent = props.mobile ?? (
<MobileCardList
table={props.table}
isLoading={props.isLoading}
@@ -299,6 +311,8 @@ function renderMobile<TData>(
getRowClassName={mobileGetRowClassName}
/>
)
return <div className='min-h-0 flex-1 overflow-y-auto'>{mobileContent}</div>
}
function renderDesktop<TData>(
@@ -309,6 +323,7 @@ function renderDesktop<TData>(
const rows = props.table.getRowModel().rows
const isFetchingOnly = props.isFetching && !props.isLoading
const fixedHeight = props.fixedHeight !== false
return (
<DataTableView
@@ -322,10 +337,16 @@ function renderDesktop<TData>(
skeletonKeyPrefix={props.skeletonKeyPrefix}
renderRow={props.renderRow}
applyHeaderSize={props.applyHeaderSize}
tableHeaderClassName={props.tableHeaderClassName}
splitHeader={fixedHeight}
tableContainerClassName={fixedHeight ? 'h-full min-h-0' : undefined}
tableHeaderClassName={cn(
fixedHeight && 'bg-muted/30',
props.tableHeaderClassName
)}
getColumnClassName={props.getColumnClassName}
pinnedColumns={props.pinnedColumns}
containerClassName={cn(
fixedHeight && 'min-h-0 flex-1',
'transition-opacity duration-150',
isFetchingOnly && 'pointer-events-none opacity-60',
props.tableClassName
@@ -50,6 +50,7 @@ SectionPageLayoutBreadcrumb.displayName = 'SectionPageLayout.Breadcrumb'
export type SectionPageLayoutProps = {
children: ReactNode
fixedContent?: boolean
}
export function SectionPageLayout(props: SectionPageLayoutProps) {
@@ -95,7 +96,13 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
</div>
</div>
<div className='min-h-0 flex-1 overflow-auto px-3 pt-1 pb-3 sm:px-4 sm:pt-1.5 sm:pb-4'>
<div
className={
props.fixedContent
? 'min-h-0 flex-1 overflow-hidden px-3 pt-1 pb-3 sm:px-4 sm:pt-1.5 sm:pb-4'
: 'min-h-0 flex-1 overflow-auto px-3 pt-1 pb-3 sm:px-4 sm:pt-1.5 sm:pb-4'
}
>
{content}
</div>
+1 -1
View File
@@ -27,7 +27,7 @@ export function Channels() {
const { t } = useTranslation()
return (
<ChannelsProvider>
<SectionPageLayout>
<SectionPageLayout fixedContent>
<SectionPageLayout.Title>{t('Channels')}</SectionPageLayout.Title>
<SectionPageLayout.Actions>
<ChannelsPrimaryButtons />
+1 -1
View File
@@ -27,7 +27,7 @@ export function ApiKeys() {
const { t } = useTranslation()
return (
<ApiKeysProvider>
<SectionPageLayout>
<SectionPageLayout fixedContent>
<SectionPageLayout.Title>{t('API Keys')}</SectionPageLayout.Title>
<SectionPageLayout.Actions>
<ApiKeysPrimaryButtons />
+19 -17
View File
@@ -119,7 +119,7 @@ function ModelsContent() {
return (
<>
<SectionPageLayout>
<SectionPageLayout fixedContent>
<SectionPageLayout.Title>{t(meta.titleKey)}</SectionPageLayout.Title>
<SectionPageLayout.Actions>
{activeSection === 'metadata' ? (
@@ -132,7 +132,7 @@ function ModelsContent() {
)}
</SectionPageLayout.Actions>
<SectionPageLayout.Content>
<div className='space-y-4'>
<div className='flex h-full min-h-0 flex-col gap-4'>
<Tabs value={activeSection} onValueChange={handleSectionChange}>
<TabsList className='max-w-full flex-wrap justify-start group-data-horizontal/tabs:h-auto'>
{MODELS_SECTION_IDS.map((section) => (
@@ -142,21 +142,23 @@ function ModelsContent() {
))}
</TabsList>
</Tabs>
{activeSection === 'metadata' ? (
<ModelsTable />
) : (
<DeploymentAccessGuard
loading={deploymentLoading}
loadingPhase={loadingPhase}
isEnabled={isIoNetEnabled}
connectionLoading={connectionLoading}
connectionOk={connectionOk}
connectionError={connectionError}
onRetry={testConnection}
>
<DeploymentsTable />
</DeploymentAccessGuard>
)}
<div className='min-h-0 flex-1'>
{activeSection === 'metadata' ? (
<ModelsTable />
) : (
<DeploymentAccessGuard
loading={deploymentLoading}
loadingPhase={loadingPhase}
isEnabled={isIoNetEnabled}
connectionLoading={connectionLoading}
connectionOk={connectionOk}
connectionError={connectionError}
onRetry={testConnection}
>
<DeploymentsTable />
</DeploymentAccessGuard>
)}
</div>
</div>
</SectionPageLayout.Content>
</SectionPageLayout>
+1 -1
View File
@@ -27,7 +27,7 @@ export function Redemptions() {
const { t } = useTranslation()
return (
<RedemptionsProvider>
<SectionPageLayout>
<SectionPageLayout fixedContent>
<SectionPageLayout.Title>
{t('Redemption Codes')}
</SectionPageLayout.Title>
+15 -11
View File
@@ -34,7 +34,7 @@ function SubscriptionsContent() {
return (
<>
<SectionPageLayout>
<SectionPageLayout fixedContent>
<SectionPageLayout.Title>
{t('Subscription Management')}
</SectionPageLayout.Title>
@@ -52,16 +52,20 @@ function SubscriptionsContent() {
</div>
</SectionPageLayout.Actions>
<SectionPageLayout.Content>
{!complianceConfirmed ? (
<Alert variant='destructive' className='mb-4'>
<AlertDescription>
{t(
'Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.'
)}
</AlertDescription>
</Alert>
) : null}
<SubscriptionsTable />
<div className='flex h-full min-h-0 flex-col gap-4'>
{!complianceConfirmed ? (
<Alert variant='destructive' className='shrink-0'>
<AlertDescription>
{t(
'Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.'
)}
</AlertDescription>
</Alert>
) : null}
<div className='min-h-0 flex-1'>
<SubscriptionsTable />
</div>
</div>
</SectionPageLayout.Content>
</SectionPageLayout>
@@ -170,10 +170,8 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
)}
skeletonKeyPrefix='usage-log-skeleton'
tableClassName={cn(
'overflow-x-auto',
'[&_[data-slot=table]]:text-[13px] [&_[data-slot=table]_td]:text-[13px] [&_[data-slot=table]_td_*]:text-[13px] [&_[data-slot=table]_th]:text-[13px] [&_[data-slot=table]_th_*]:text-[13px]'
)}
tableHeaderClassName='bg-muted/30 sticky top-0 z-10'
mobile={
<UsageLogsMobileList
table={table}
+5 -3
View File
@@ -110,12 +110,12 @@ function UsageLogsContent() {
return (
<>
<SectionPageLayout>
<SectionPageLayout fixedContent>
<SectionPageLayout.Title>
{t(pageMeta.titleKey)}
</SectionPageLayout.Title>
<SectionPageLayout.Content>
<div className='space-y-4'>
<div className='flex h-full min-h-0 flex-col gap-4'>
{showTaskSwitcher && (
<Tabs value={activeCategory} onValueChange={handleSectionChange}>
<TabsList className='max-w-full flex-wrap justify-start group-data-horizontal/tabs:h-auto'>
@@ -127,7 +127,9 @@ function UsageLogsContent() {
</TabsList>
</Tabs>
)}
<UsageLogsTable logCategory={activeCategory} />
<div className='min-h-0 flex-1'>
<UsageLogsTable logCategory={activeCategory} />
</div>
</div>
</SectionPageLayout.Content>
</SectionPageLayout>
+1 -1
View File
@@ -30,7 +30,7 @@ function UsersContent() {
return (
<>
<SectionPageLayout>
<SectionPageLayout fixedContent>
<SectionPageLayout.Title>{t('Users')}</SectionPageLayout.Title>
<SectionPageLayout.Actions>
<UsersPrimaryButtons />