refactor(auth): replace dashboard sessions with stateless tokens and session control (#6329)

* refactor(auth): replace dashboard sessions with stateless tokens

* feat(auth): harden session issuance and distributed enforcement

* fix(proxy): preserve trusted proxy compatibility defaults

* refactor: address dashboard auth review feedback

* refactor: remove classic frontend and flatten web app
This commit is contained in:
Calcium-Ion
2026-07-20 16:48:43 +08:00
committed by GitHub
parent 5a6c53d496
commit 31d70fca39
1605 changed files with 17511 additions and 147913 deletions
@@ -0,0 +1,36 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { cn } from '@/lib/utils'
type BadgeCellProps = React.HTMLAttributes<HTMLDivElement>
export function BadgeCell({ className, ...props }: BadgeCellProps) {
return (
<div
data-slot='badge-cell'
className={cn(
'-ml-1.5 flex max-w-full min-w-0 items-center gap-1 overflow-hidden [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:min-w-0',
className
)}
{...props}
/>
)
}
@@ -0,0 +1,75 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { StatusBadgeList } from '@/components/status-badge'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
interface BadgeListCellProps {
items: React.ReactNode[]
max?: number
tooltipClassName?: string
}
/**
* Table cell renderer for a list of badges with overflow tooltip.
* Displays up to `max` badges inline; remaining items appear in a tooltip.
* Applies -ml-1.5 to compensate for badge px-1.5 and align with column header.
*/
export function BadgeListCell({
items,
max = 2,
tooltipClassName,
}: BadgeListCellProps) {
if (items.length === 0) {
return <span className='text-muted-foreground text-xs'>-</span>
}
const showTooltip = items.length > max
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<div className='-ml-1.5 max-w-full' />}>
<StatusBadgeList
items={items}
max={max}
renderItem={(item) => item}
/>
</TooltipTrigger>
{showTooltip && (
<TooltipContent
side='top'
className={
tooltipClassName ??
'border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
}
>
<div className='flex flex-wrap gap-1'>{items}</div>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}
@@ -0,0 +1,97 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { type Column } from '@tanstack/react-table'
import {
ArrowDown as ArrowDownIcon,
ArrowUp as ArrowUpIcon,
ChevronsUpDown as CaretSortIcon,
EyeOff as EyeNoneIcon,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
type DataTableColumnHeaderProps<TData, TValue> =
React.HTMLAttributes<HTMLDivElement> & {
column: Column<TData, TValue>
title: React.ReactNode
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
const { t } = useTranslation()
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<div className={cn('flex items-center space-x-2', className)}>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant='ghost'
size='sm'
className='data-popup-open:bg-accent -ms-3 h-8'
/>
}
>
<span>{title}</span>
{column.getIsSorted() === 'desc' ? (
<ArrowDownIcon className='ms-2 h-4 w-4' />
) : column.getIsSorted() === 'asc' ? (
<ArrowUpIcon className='ms-2 h-4 w-4' />
) : (
<CaretSortIcon className='ms-2 h-4 w-4' />
)}
</DropdownMenuTrigger>
<DropdownMenuContent align='start'>
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className='text-muted-foreground/70 size-3.5' />
{t('Asc')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className='text-muted-foreground/70 size-3.5' />
{t('Desc')}
</DropdownMenuItem>
{column.getCanHide() && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className='text-muted-foreground/70 size-3.5' />
{t('Hide')}
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
@@ -0,0 +1,78 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { cn } from '@/lib/utils'
import type { DataTableColumnClassName, DataTablePinnedColumn } from './types'
export function getResolvedColumnClassName(
getColumnClassName?: DataTableColumnClassName,
pinnedColumns?: DataTablePinnedColumn[]
): DataTableColumnClassName {
return getResolvedColumnClassNameFromMap(
getColumnClassName,
getPinnedColumnMap(pinnedColumns)
)
}
export function getResolvedColumnClassNameFromMap(
getColumnClassName?: DataTableColumnClassName,
pinnedColumnById?: Map<string, DataTablePinnedColumn>
): DataTableColumnClassName {
return (columnId, kind) => {
const customClassName = getColumnClassName?.(columnId, kind)
const pinnedColumn = pinnedColumnById?.get(columnId)
if (!pinnedColumn) {
return customClassName
}
return cn(customClassName, getPinnedColumnClassName(pinnedColumn, kind))
}
}
export function getPinnedColumnMap(pinnedColumns?: DataTablePinnedColumn[]) {
if (!pinnedColumns?.length) {
return undefined
}
return new Map(pinnedColumns.map((column) => [column.columnId, column]))
}
function getPinnedColumnClassName(
pinnedColumn: DataTablePinnedColumn,
kind: 'header' | 'cell'
) {
const edgeClassName =
pinnedColumn.side === 'left'
? 'shadow-[8px_0_10px_-10px_hsl(var(--foreground))]'
: 'shadow-[-8px_0_10px_-10px_hsl(var(--foreground))]'
return cn(
'sticky whitespace-nowrap',
pinnedColumn.side === 'left' ? 'left-0' : 'right-0',
edgeClassName,
kind === 'header'
? '[background-color:var(--table-header-bg,var(--table-header))] group-hover:[background-color:var(--table-header-hover)] z-30'
: 'bg-background z-10 group-hover:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] group-data-[state=selected]:bg-muted',
pinnedColumn.className,
kind === 'header'
? pinnedColumn.headerClassName
: pinnedColumn.cellClassName
)
}
@@ -0,0 +1,21 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export function isContentSizedColumn(columnId: string): boolean {
return columnId === 'actions'
}
@@ -0,0 +1,69 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Table as TanstackTable } from '@tanstack/react-table'
import { isContentSizedColumn } from './content-sized-columns'
export function DataTableColgroup<TData>({
table,
}: {
table: TanstackTable<TData>
}) {
const columns = table.getVisibleLeafColumns()
const sizedColumns = columns.filter(
(column) => !isContentSizedColumn(column.id)
)
const totalSize = sizedColumns.reduce((sum, col) => sum + col.getSize(), 0)
return (
<colgroup>
{columns.map((column) => {
const width = getColumnWidth(
table,
column.id,
column.getSize(),
totalSize
)
return <col key={column.id} style={{ width }} />
})}
</colgroup>
)
}
function getColumnWidth<TData>(
table: TanstackTable<TData>,
columnId: string,
columnSize: number,
totalSize: number
) {
if (isContentSizedColumn(columnId)) {
return undefined
}
if (table.options.enableColumnResizing === true) {
return `${columnSize}px`
}
if (totalSize <= 0) {
return undefined
}
return `${(columnSize / totalSize) * 100}%`
}
@@ -0,0 +1,277 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
flexRender,
type Header,
type Table as TanstackTable,
} from '@tanstack/react-table'
import type { KeyboardEvent, MouseEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
import { DataTableColumnHeader } from './column-header'
import { isContentSizedColumn } from './content-sized-columns'
import type { DataTableColumnClassName } from './types'
type DataTableHeaderProps<TData> = {
table: TanstackTable<TData>
applyHeaderSize?: boolean
className?: string
rowClassName?: string
getColumnClassName?: DataTableColumnClassName
}
export function DataTableHeader<TData>({
table,
applyHeaderSize,
className,
rowClassName,
getColumnClassName,
}: DataTableHeaderProps<TData>) {
const { t } = useTranslation()
return (
<TableHeader className={className}>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className={rowClassName}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
colSpan={header.colSpan}
data-column-id={header.column.id}
className={cn(
'relative',
getColumnClassName?.(header.column.id, 'header')
)}
style={getHeaderSizeStyle(header, applyHeaderSize)}
>
{renderHeaderContent(header)}
{shouldRenderColumnResizer(table, header) && (
<div
role='separator'
aria-orientation='vertical'
aria-label={t('Resize column')}
data-column-resizer
tabIndex={0}
onDoubleClick={(event) =>
handleColumnAutoSize(event, table, header)
}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
onKeyDown={(event) =>
handleColumnResizeKeyDown(event, table, header)
}
className={cn(
'absolute top-0 right-0 h-full w-2 cursor-col-resize touch-none select-none',
'after:bg-border hover:after:bg-primary after:absolute after:top-2 after:right-0 after:h-[calc(100%-1rem)] after:w-px after:transition-colors',
header.column.getIsResizing() && 'after:bg-primary'
)}
/>
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
)
}
function handleColumnResizeKeyDown<TData>(
event: KeyboardEvent<HTMLDivElement>,
table: TanstackTable<TData>,
header: Header<TData, unknown>
) {
const step = event.shiftKey ? 50 : 10
if (event.key === 'ArrowLeft') {
event.preventDefault()
resizeColumnByKeyboard(table, header, -step)
return
}
if (event.key === 'ArrowRight') {
event.preventDefault()
resizeColumnByKeyboard(table, header, step)
return
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
autoSizeColumn(event.currentTarget, table, header)
}
}
function resizeColumnByKeyboard<TData>(
table: TanstackTable<TData>,
header: Header<TData, unknown>,
delta: number
) {
table.setColumnSizing((previous) => ({
...previous,
[header.column.id]: getClampedColumnSize(
header,
header.column.getSize() + delta
),
}))
}
function handleColumnAutoSize<TData>(
event: MouseEvent<HTMLDivElement>,
table: TanstackTable<TData>,
header: Header<TData, unknown>
) {
event.preventDefault()
autoSizeColumn(event.currentTarget, table, header)
}
function autoSizeColumn<TData>(
resizerElement: HTMLElement,
table: TanstackTable<TData>,
header: Header<TData, unknown>
) {
const measuredSize = measureColumnContentWidth(
resizerElement,
header.column.id
)
if (measuredSize === undefined) {
return
}
table.setColumnSizing((previous) => ({
...previous,
[header.column.id]: getClampedColumnSize(header, measuredSize),
}))
}
function getClampedColumnSize<TData>(
header: Header<TData, unknown>,
nextSize: number
) {
const { minSize, maxSize } = header.column.columnDef
if (typeof minSize === 'number' && nextSize < minSize) {
return minSize
}
if (typeof maxSize === 'number' && nextSize > maxSize) {
return maxSize
}
return nextSize
}
function measureColumnContentWidth(
resizerElement: HTMLElement,
columnId: string
) {
const tableElement = resizerElement.closest('table')
if (!tableElement) {
return undefined
}
const cells = tableElement.querySelectorAll<HTMLElement>(
getColumnElementSelector(columnId)
)
if (cells.length === 0) {
return undefined
}
const measuredWidth = [...cells].reduce(
(maxWidth, cell) => Math.max(maxWidth, measureElementWidth(cell)),
0
)
return measuredWidth > 0 ? Math.ceil(measuredWidth) : undefined
}
function measureElementWidth(element: HTMLElement) {
const clone = element.cloneNode(true) as HTMLElement
clone.querySelectorAll('[data-column-resizer]').forEach((resizer) => {
resizer.remove()
})
clone.style.position = 'absolute'
clone.style.visibility = 'hidden'
clone.style.pointerEvents = 'none'
clone.style.left = '-10000px'
clone.style.top = '0'
clone.style.width = 'max-content'
clone.style.minWidth = '0'
clone.style.maxWidth = 'none'
clone.style.height = 'auto'
clone.style.whiteSpace = 'nowrap'
document.body.append(clone)
const width = clone.scrollWidth
clone.remove()
return width
}
function getColumnElementSelector(columnId: string) {
const escapedColumnId =
typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
? CSS.escape(columnId)
: columnId.replaceAll('\\', '\\\\').replaceAll('"', '\\"')
return `[data-column-id="${escapedColumnId}"]`
}
function shouldRenderColumnResizer<TData>(
table: TanstackTable<TData>,
header: Header<TData, unknown>
) {
return (
table.options.enableColumnResizing === true &&
!header.isPlaceholder &&
header.column.getCanResize() &&
!isContentSizedColumn(header.column.id)
)
}
function getHeaderSizeStyle<TData>(
header: Header<TData, unknown>,
applyHeaderSize: boolean | undefined
) {
if (!applyHeaderSize || isContentSizedColumn(header.column.id)) {
return undefined
}
return { width: header.getSize() }
}
function renderHeaderContent<TData>(header: Header<TData, unknown>) {
if (header.isPlaceholder) return null
const { header: headerDef, meta } = header.column.columnDef
// A string header means the user wrote e.g. `header: t('Name')` — auto-render
// with DataTableColumnHeader so sorting works without boilerplate.
// A function (including TanStack's default accessor-key fallback) is passed
// through as-is. meta.label is kept as a fallback for legacy columns.
if (typeof headerDef === 'string') {
return <DataTableColumnHeader column={header.column} title={headerDef} />
}
if (meta?.label) {
return <DataTableColumnHeader column={header.column} title={meta.label} />
}
return flexRender(headerDef, header.getContext())
}
@@ -0,0 +1,148 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
flexRender,
type Cell,
type Row,
type Table as TanstackTable,
} from '@tanstack/react-table'
import * as React from 'react'
import { TableCell, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
import { TruncatedCell } from './truncated-cell'
import type { DataTableColumnClassName } from './types'
type DataTableRowProps<TData> = {
row: Row<TData>
className?: string
getColumnClassName?: DataTableColumnClassName
cellRenderColumns?: TanstackTable<TData>['options']['columns']
} & Omit<React.ComponentProps<typeof TableRow>, 'children'>
type DataTableRowInnerProps<TData> = DataTableRowProps<TData> & {
isSelected: boolean
/**
* Stable signature of currently visible leaf columns for this row.
* Captured outside the memo comparator so visibility toggles re-render
* even when the TanStack row object reference stays the same.
*/
visibleColumnIds: string
}
function DataTableRowInner<TData>({
row,
isSelected,
className,
getColumnClassName,
cellRenderColumns,
visibleColumnIds,
...rowProps
}: DataTableRowInnerProps<TData>) {
// Destructured only to keep them out of `rowProps` (not valid DOM attrs)
// and to feed the memo comparator below; intentionally unused here.
void cellRenderColumns
void visibleColumnIds
return (
<TableRow
data-state={isSelected ? 'selected' : undefined}
className={className}
{...rowProps}
>
{row.getVisibleCells().map((cell) => {
const renderedCell = renderCellContent(cell)
return (
<TableCell
key={cell.id}
data-column-id={cell.column.id}
className={cn(
'max-w-full min-w-0',
renderedCell.isPrimitive && 'overflow-hidden',
getColumnClassName?.(cell.column.id, 'cell')
)}
>
{renderedCell.content}
</TableCell>
)
})}
</TableRow>
)
}
const MemoizedDataTableRow = React.memo(DataTableRowInner, (prev, next) => {
// Do not read row.getIsSelected() / row.getVisibleCells() inside the
// comparator: TanStack row objects keep a stable reference while selection
// and columnVisibility mutate on the table instance. Reading them here would
// compare identical live values and miss those updates. Both are lifted to
// explicit props, captured per render in DataTableRow.
//
// Column cell renderers (and getColumnClassName) can close over external
// state while the row stays stable, so column definitions and the class
// resolver are part of the render identity and must be compared too.
return (
prev.row === next.row &&
prev.className === next.className &&
prev.isSelected === next.isSelected &&
prev.visibleColumnIds === next.visibleColumnIds &&
prev.getColumnClassName === next.getColumnClassName &&
prev.cellRenderColumns === next.cellRenderColumns
)
}) as typeof DataTableRowInner
export function DataTableRow<TData>(props: DataTableRowProps<TData>) {
const visibleColumnIds = props.row
.getVisibleCells()
.map((cell) => cell.column.id)
.join('\0')
return (
<MemoizedDataTableRow
{...props}
isSelected={props.row.getIsSelected()}
visibleColumnIds={visibleColumnIds}
/>
)
}
function renderCellContent<TData>(cell: Cell<TData, unknown>) {
const content = flexRender(cell.column.columnDef.cell, cell.getContext())
const textContent = getPrimitiveTextContent(content)
if (!textContent) {
return { content, isPrimitive: false }
}
return {
content: (
<TruncatedCell tooltipContent={textContent}>{content}</TruncatedCell>
),
isPrimitive: true,
}
}
function getPrimitiveTextContent(content: React.ReactNode): string | null {
if (typeof content === 'string' || typeof content === 'number') {
return String(content)
}
return null
}
@@ -0,0 +1,331 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Row, Table as TanstackTable } from '@tanstack/react-table'
import * as React from 'react'
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
import {
getPinnedColumnMap,
getResolvedColumnClassNameFromMap,
} from './column-pinning'
import { DataTableColgroup } from './data-table-colgroup'
import { DataTableHeader } from './data-table-header'
import { DataTableRow } from './data-table-row'
import { TableEmpty } from './table-empty'
import { getTableSizeStyle } from './table-sizing'
import { TableSkeleton } from './table-skeleton'
import type {
DataTableColumnClassName,
DataTablePinnedColumn,
DataTableViewProps,
} from './types'
export type {
DataTableColumnClassName,
DataTablePinnedColumn,
DataTableRenderRowHelpers,
DataTableViewProps,
} from './types'
export { DataTableRow } from './data-table-row'
export { DataTableRowActionMenu } from './row-action-menu'
export function DataTableView<TData>(props: DataTableViewProps<TData>) {
const rows = props.rows ?? props.table.getRowModel().rows
const colSpan = React.useMemo(
() => props.table.getVisibleLeafColumns().length,
[props.table]
)
const columnClassName = useResolvedColumnClassName(
props.table,
props.getColumnClassName,
props.pinnedColumns
)
return (
<div
className={cn(
'overflow-hidden rounded-lg border',
props.containerClassName
)}
{...props.containerProps}
>
{props.splitHeader ? (
<SplitHeaderTableView
props={props}
rows={rows}
colSpan={colSpan}
getColumnClassName={columnClassName}
/>
) : (
<UnifiedTableView
props={props}
rows={rows}
colSpan={colSpan}
getColumnClassName={columnClassName}
/>
)}
</div>
)
}
function UnifiedTableView<TData>({
props,
rows,
colSpan,
getColumnClassName,
}: {
props: DataTableViewProps<TData>
rows: Row<TData>[]
colSpan: number
getColumnClassName: DataTableColumnClassName
}) {
const tableSizing = getTableSizing(props)
return (
<div className={props.tableContainerClassName}>
<Table className={props.tableClassName} style={tableSizing.style}>
{tableSizing.colgroup}
<DataTableHeader
table={props.table}
applyHeaderSize={props.applyHeaderSize}
className={props.tableHeaderClassName}
rowClassName={props.tableHeaderRowClassName}
getColumnClassName={getColumnClassName}
/>
{renderTableBody(props, rows, colSpan, getColumnClassName)}
</Table>
</div>
)
}
function SplitHeaderTableView<TData>({
props,
rows,
colSpan,
getColumnClassName,
}: {
props: DataTableViewProps<TData>
rows: Row<TData>[]
colSpan: number
getColumnClassName: DataTableColumnClassName
}) {
const tableSizing = getTableSizing(props)
return (
<div
className={cn(
'flex h-full min-h-0 flex-col',
props.tableContainerClassName
)}
>
<div
className={cn(
'min-h-0 flex-1 overflow-auto',
'**:data-[slot=table-header]:[--table-header-bg:var(--table-header)]',
'**:data-[slot=table-header]:bg-(--table-header-bg)',
props.splitHeaderScrollClassName,
props.bodyContainerClassName
)}
>
<table
data-slot='table'
className={cn(
'w-full caption-bottom text-sm tabular-nums [&_td]:text-sm [&_td_*]:text-sm [&_th]:text-sm [&_th_*]:text-sm',
props.tableClassName
)}
style={tableSizing.style}
>
{tableSizing.colgroup}
<DataTableHeader
table={props.table}
applyHeaderSize={props.applyHeaderSize}
className={cn('sticky top-0 z-10', props.tableHeaderClassName)}
rowClassName={props.tableHeaderRowClassName}
getColumnClassName={getColumnClassName}
/>
{renderTableBody(props, rows, colSpan, getColumnClassName)}
</table>
</div>
</div>
)
}
function useResolvedColumnClassName<TData>(
table: TanstackTable<TData>,
getColumnClassName?: DataTableColumnClassName,
pinnedColumns?: DataTablePinnedColumn[]
) {
const allPinnedColumns = React.useMemo(() => {
const metaPinnedColumns = getMetaPinnedColumns(table)
return mergePinnedColumns(pinnedColumns, metaPinnedColumns)
}, [table, pinnedColumns])
const pinnedColumnById = React.useMemo(
() => getPinnedColumnMap(allPinnedColumns),
[allPinnedColumns]
)
return React.useMemo(
() =>
getResolvedColumnClassNameFromMap(getColumnClassName, pinnedColumnById),
[getColumnClassName, pinnedColumnById]
)
}
function getMetaPinnedColumns<TData>(
table: TanstackTable<TData>
): DataTablePinnedColumn[] {
return table.getAllColumns().flatMap((column) => {
const side = column.columnDef.meta?.pinned
if (!side) {
return []
}
return [{ columnId: column.id, side }]
})
}
function mergePinnedColumns(
explicitPinnedColumns: DataTablePinnedColumn[] | undefined,
metaPinnedColumns: DataTablePinnedColumn[]
): DataTablePinnedColumn[] | undefined {
if (!metaPinnedColumns.length) {
return explicitPinnedColumns
}
if (!explicitPinnedColumns?.length) {
return metaPinnedColumns
}
const explicitColumnIds = new Set(
explicitPinnedColumns.map((column) => column.columnId)
)
return [
...explicitPinnedColumns,
...metaPinnedColumns.filter(
(column) => !explicitColumnIds.has(column.columnId)
),
]
}
function getTableSizing<TData>(props: DataTableViewProps<TData>): {
colgroup?: React.ReactNode
style?: React.CSSProperties
} {
if (props.colgroup) {
return { colgroup: props.colgroup }
}
if (!props.splitHeader && !props.applyHeaderSize) {
return {}
}
return {
colgroup: <DataTableColgroup table={props.table} />,
style: getTableSizeStyle(props.table),
}
}
function renderTableBody<TData>(
props: DataTableViewProps<TData>,
rows: Row<TData>[],
colSpan: number,
getColumnClassName: DataTableColumnClassName
) {
return (
<TableBody className={props.tableBodyClassName}>
{renderTableBodyContent(props, rows, colSpan, getColumnClassName)}
</TableBody>
)
}
function renderTableBodyContent<TData>(
props: DataTableViewProps<TData>,
rows: Row<TData>[],
colSpan: number,
getColumnClassName: DataTableColumnClassName
) {
if (props.isLoading) {
return (
<TableSkeleton
table={props.table}
keyPrefix={props.skeletonKeyPrefix}
rowHeight={props.skeletonRowHeight}
/>
)
}
if (rows.length === 0) {
return renderEmptyState(props, colSpan)
}
return rows.map((row) =>
props.renderRow
? props.renderRow(row, {
getCellClassName: (columnId, className) =>
cn(getColumnClassName(columnId, 'cell'), className),
})
: renderDefaultRow(props, row, getColumnClassName)
)
}
function renderEmptyState<TData>(
props: DataTableViewProps<TData>,
colSpan: number
) {
if (props.emptyContent) {
return (
<TableRow>
<TableCell colSpan={colSpan} className={props.emptyCellClassName}>
{props.emptyContent}
</TableCell>
</TableRow>
)
}
return (
<TableEmpty
colSpan={colSpan}
title={props.emptyTitle}
description={props.emptyDescription}
icon={props.emptyIcon}
>
{props.emptyAction}
</TableEmpty>
)
}
function renderDefaultRow<TData>(
props: DataTableViewProps<TData>,
row: Row<TData>,
getColumnClassName: DataTableColumnClassName
) {
return (
<DataTableRow
key={row.id}
row={row}
className={cn(props.tableBodyRowClassName, props.getRowClassName?.(row))}
getColumnClassName={getColumnClassName}
cellRenderColumns={props.table.options.columns}
/>
)
}
@@ -0,0 +1,169 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { type Table } from '@tanstack/react-table'
import {
ChevronLeft as ChevronLeftIcon,
ChevronRight as ChevronRightIcon,
ChevronsLeft as DoubleArrowLeftIcon,
ChevronsRight as DoubleArrowRightIcon,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { cn, getPageNumbers } from '@/lib/utils'
type DataTablePaginationProps<TData> = {
table: Table<TData>
}
const PAGE_SIZE_OPTIONS = [10, 20, 30, 40, 50, 100] as const
const PAGE_SIZE_SELECT_ITEMS = PAGE_SIZE_OPTIONS.map((pageSize) => ({
value: `${pageSize}`,
label: pageSize,
}))
export function DataTablePagination<TData>({
table,
}: DataTablePaginationProps<TData>) {
const { t } = useTranslation()
const pagination = table.getState().pagination
const currentPage = pagination.pageIndex + 1
const pageSize = pagination.pageSize
const totalPages = table.getPageCount()
const totalRows = table.getRowCount()
const pageNumbers = getPageNumbers(currentPage, totalPages)
return (
<div
className={cn(
'@container/pagination flex min-w-0 items-center justify-end overflow-clip'
)}
style={{ overflowClipMargin: 1 }}
>
<div className='flex min-w-0 shrink-0 items-center gap-2 @xl/pagination:gap-3'>
<div className='flex shrink-0 items-baseline gap-1.5 text-xs font-medium whitespace-nowrap sm:text-sm'>
<span className='text-muted-foreground/80'>{t('Total:')}</span>
<span className='text-foreground tabular-nums'>
{totalRows.toLocaleString()}
</span>
</div>
<div className='flex shrink-0 items-center gap-1.5 @lg/pagination:gap-2'>
<p className='text-muted-foreground/80 hidden text-sm font-medium whitespace-nowrap @2xl/pagination:block'>
{t('Rows per page')}
</p>
<Select
items={PAGE_SIZE_SELECT_ITEMS}
value={`${pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className='text-foreground h-8 w-[64px] font-medium tabular-nums sm:w-[70px]'>
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side='top' alignItemWithTrigger={false}>
<SelectGroup>
{PAGE_SIZE_OPTIONS.map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className='flex min-w-0 shrink-0 items-center gap-1 @lg/pagination:gap-1.5 @xl/pagination:gap-2'>
<Button
variant='outline'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0 @max-lg/pagination:hidden'
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className='sr-only'>{t('Go to first page')}</span>
<DoubleArrowLeftIcon className='h-4 w-4' />
</Button>
<Button
variant='outline'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className='sr-only'>{t('Go to previous page')}</span>
<ChevronLeftIcon className='h-4 w-4' />
</Button>
{pageNumbers.map((pageNumber, index) => (
<div key={`${pageNumber}-${index}`} className='flex items-center'>
{pageNumber === '...' ? (
<span className='text-muted-foreground/60 px-0.5 text-sm @lg/pagination:px-1'>
...
</span>
) : (
<Button
variant={currentPage === pageNumber ? 'default' : 'outline'}
className={cn(
'h-8 min-w-8 px-2 tabular-nums',
currentPage === pageNumber
? 'font-semibold'
: 'text-muted-foreground hover:text-foreground'
)}
onClick={() => table.setPageIndex((pageNumber as number) - 1)}
>
<span className='sr-only'>
{t('Go to page {{page}}', { page: pageNumber })}
</span>
{pageNumber}
</Button>
)}
</div>
))}
<Button
variant='outline'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className='sr-only'>{t('Go to next page')}</span>
<ChevronRightIcon className='h-4 w-4' />
</Button>
<Button
variant='outline'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0 @max-lg/pagination:hidden'
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className='sr-only'>{t('Go to last page')}</span>
<DoubleArrowRightIcon className='h-4 w-4' />
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,61 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { MoreHorizontal } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
type DataTableRowActionMenuProps = {
children: React.ReactNode
ariaLabel: string
contentClassName?: string
modal?: boolean
onOpenChange?: (open: boolean) => void
}
export function DataTableRowActionMenu(props: DataTableRowActionMenuProps) {
return (
<DropdownMenu modal={props.modal} onOpenChange={props.onOpenChange}>
<DropdownMenuTrigger
render={
<Button
variant='ghost'
size='icon'
className='data-popup-open:bg-muted'
aria-label={props.ariaLabel}
/>
}
>
<MoreHorizontal aria-hidden='true' />
</DropdownMenuTrigger>
<DropdownMenuContent
align='end'
className={cn('w-48', props.contentClassName)}
>
{props.children}
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,88 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Database } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { TableRow, TableCell } from '@/components/ui/table'
interface TableEmptyProps {
/**
* Number of columns to span
*/
colSpan: number
/**
* Custom title for empty state
* @default 'No Data'
*/
title?: string
/**
* Custom description for empty state
* @default 'No records found. Try adjusting your filters.'
*/
description?: string
/**
* Custom icon component
* @default Database icon
*/
icon?: React.ReactNode
/**
* Additional content to display (e.g., buttons)
*/
children?: React.ReactNode
}
/**
* Generic table empty state component
* Displays a centered empty state message when table has no data
*/
export function TableEmpty({
colSpan,
title,
description,
icon,
children,
}: TableEmptyProps) {
const { t } = useTranslation()
const resolvedTitle = title ?? t('No Data')
const resolvedDescription =
description ?? t('No records found. Try adjusting your filters.')
return (
<TableRow>
<TableCell colSpan={colSpan} className='h-[400px] p-0'>
<Empty>
<EmptyHeader>
<EmptyMedia variant='icon'>
{icon || <Database className='size-6' />}
</EmptyMedia>
<EmptyTitle>{resolvedTitle}</EmptyTitle>
<EmptyDescription>{resolvedDescription}</EmptyDescription>
</EmptyHeader>
{children}
</Empty>
</TableCell>
</TableRow>
)
}
@@ -0,0 +1,37 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Table as TanstackTable } from '@tanstack/react-table'
import type * as React from 'react'
import { isContentSizedColumn } from './content-sized-columns'
export function getTableSizeStyle<TData>(
table: TanstackTable<TData>
): React.CSSProperties {
const width = table
.getVisibleLeafColumns()
.filter((column) => !isContentSizedColumn(column.id))
.reduce((total, column) => total + column.getSize(), 0)
return {
minWidth: `max(100%, ${width}px)`,
tableLayout: 'auto',
width: '100%',
}
}
@@ -0,0 +1,89 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Table } from '@tanstack/react-table'
import { Skeleton } from '@/components/ui/skeleton'
import { TableRow, TableCell } from '@/components/ui/table'
import { cn } from '@/lib/utils'
const SKELETON_WIDTHS = [
'75%',
'60%',
'85%',
'50%',
'70%',
'90%',
'55%',
'80%',
'65%',
'45%',
]
interface TableSkeletonProps<TData> {
table: Table<TData>
rowCount?: number
rowHeight?: string
keyPrefix?: string
}
export function TableSkeleton<TData>({
table,
rowCount,
rowHeight = 'h-[52px]',
keyPrefix = 'skeleton',
}: TableSkeletonProps<TData>) {
const visibleColumns = table.getVisibleLeafColumns()
const finalRowCount =
rowCount ?? Math.min(table.getState().pagination?.pageSize || 20, 20)
return (
<>
{Array.from({ length: finalRowCount }, (_, rowIndex) => (
<TableRow
key={`${keyPrefix}-${rowIndex}`}
className={cn(rowHeight, 'border-b')}
>
{visibleColumns.map((column, colIndex) => {
const isSelectColumn = column.id === 'select'
const widthIndex =
(rowIndex * visibleColumns.length + colIndex) %
SKELETON_WIDTHS.length
return (
<TableCell key={column.id} className='py-3'>
<Skeleton
className={cn(
'h-4 rounded-sm',
isSelectColumn ? 'size-4' : undefined
)}
style={
isSelectColumn
? undefined
: { width: SKELETON_WIDTHS[widthIndex] }
}
/>
</TableCell>
)
})}
</TableRow>
))}
</>
)
}
@@ -0,0 +1,92 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
type TruncatedCellProps = {
children: React.ReactNode
cellClassName?: string
className?: string
contentClassName?: string
side?: 'top' | 'bottom' | 'left' | 'right'
tooltipClassName?: string
tooltipContent?: React.ReactNode
}
export function TruncatedCell({
children,
cellClassName,
className,
contentClassName,
side = 'top',
tooltipClassName,
tooltipContent,
}: TruncatedCellProps) {
const content = tooltipContent ?? getTextContent(children)
if (!content) {
return (
<div
className={cn(
'block max-w-full min-w-0 truncate',
cellClassName,
className
)}
>
{children}
</div>
)
}
return (
<Tooltip>
<TooltipTrigger
render={
<div
className={cn(
'block max-w-full min-w-0 truncate',
cellClassName,
className
)}
/>
}
>
<div className={cn('truncate', contentClassName)}>{children}</div>
</TooltipTrigger>
<TooltipContent
side={side}
className={cn('max-w-xs break-all', tooltipClassName)}
>
{content}
</TooltipContent>
</Tooltip>
)
}
function getTextContent(node: React.ReactNode): string {
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(getTextContent).join('')
return ''
}
@@ -0,0 +1,71 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Row, Table as TanstackTable } from '@tanstack/react-table'
import type * as React from 'react'
export type DataTableColumnClassName = (
columnId: string,
kind: 'header' | 'cell'
) => string | undefined
export type DataTablePinnedColumn = {
columnId: string
side: 'left' | 'right'
className?: string
headerClassName?: string
cellClassName?: string
}
export type DataTableRenderRowHelpers = {
getCellClassName: (columnId: string, className?: string) => string | undefined
}
export type DataTableViewProps<TData> = {
table: TanstackTable<TData>
isLoading?: boolean
rows?: Row<TData>[]
emptyTitle?: string
emptyDescription?: string
emptyIcon?: React.ReactNode
emptyAction?: React.ReactNode
emptyContent?: React.ReactNode
emptyCellClassName?: string
skeletonKeyPrefix?: string
skeletonRowHeight?: string
renderRow?: (
row: Row<TData>,
helpers: DataTableRenderRowHelpers
) => React.ReactNode
getRowClassName?: (row: Row<TData>) => string | undefined
getColumnClassName?: DataTableColumnClassName
pinnedColumns?: DataTablePinnedColumn[]
applyHeaderSize?: boolean
tableClassName?: string
tableHeaderClassName?: string
tableHeaderRowClassName?: string
tableBodyClassName?: string
tableBodyRowClassName?: string
splitHeader?: boolean
splitHeaderScrollClassName?: string
bodyContainerClassName?: string
containerClassName?: string
containerProps?: Omit<React.ComponentProps<'div'>, 'className' | 'children'>
tableContainerClassName?: string
colgroup?: React.ReactNode
}