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:
+27
-6
@@ -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
@@ -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
@@ -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
@@ -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
@@ -27,7 +27,7 @@ export function Redemptions() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<RedemptionsProvider>
|
||||
<SectionPageLayout>
|
||||
<SectionPageLayout fixedContent>
|
||||
<SectionPageLayout.Title>
|
||||
{t('Redemption Codes')}
|
||||
</SectionPageLayout.Title>
|
||||
|
||||
+15
-11
@@ -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
@@ -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
@@ -30,7 +30,7 @@ function UsersContent() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionPageLayout>
|
||||
<SectionPageLayout fixedContent>
|
||||
<SectionPageLayout.Title>{t('Users')}</SectionPageLayout.Title>
|
||||
<SectionPageLayout.Actions>
|
||||
<UsersPrimaryButtons />
|
||||
|
||||
Reference in New Issue
Block a user