Files
EvoBGP/apps/web/src/components/reui/data-grid/data-grid-table.tsx
T
Denozordec a3f3ffd672
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 52s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m7s
refactor: integrate PanelCard and enhance dashboard components for improved layout
Refactored multiple dashboard components to utilize the new PanelCard for better organization and presentation. Updated the DashboardFramePanel, DashboardQuickLinks, and DashboardOperationsBreakdown components to streamline layouts and enhance user experience. Removed deprecated components and improved loading states in various sections, ensuring a cohesive interface throughout the application.
2026-07-09 21:39:20 +07:00

1650 lines
48 KiB
TypeScript

import {
CSSProperties,
Fragment,
memo,
MouseEvent as ReactMouseEvent,
ReactNode,
TouchEvent as ReactTouchEvent,
Ref,
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import { useDataGrid } from "@/components/reui/data-grid/data-grid"
import {
Cell,
Column,
flexRender,
Header,
Row,
Table,
} from "@tanstack/react-table"
import { cva } from "class-variance-authority"
import { cn } from "@evobgp/ui/lib/utils"
import { Checkbox } from "@evobgp/ui/components/checkbox"
import { Spinner } from "@evobgp/ui/components/spinner"
const headerCellSpacingVariants = cva("", {
variants: {
size: {
dense: "px-2 h-8",
default: "px-3",
},
},
defaultVariants: {
size: "default",
},
})
const bodyCellSpacingVariants = cva("", {
variants: {
size: {
dense: "px-2 py-1.5",
default: "px-3 py-2",
},
},
defaultVariants: {
size: "default",
},
})
const footerCellSpacingVariants = cva("", {
variants: {
size: {
dense: "px-2 py-1.5",
default: "px-3 py-2",
},
},
defaultVariants: {
size: "default",
},
})
function getPinningStyles<TData>(column: Column<TData>): CSSProperties {
const isPinned = column.getIsPinned()
return {
left: isPinned === "left" ? `${column.getStart("left")}px` : undefined,
right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined,
position: isPinned ? "sticky" : undefined,
transform: isPinned ? "translateZ(0)" : undefined,
contain: isPinned ? "paint" : undefined,
width: column.getSize(),
zIndex: isPinned ? 30 : undefined,
backgroundClip: isPinned ? "padding-box" : undefined,
}
}
function assignRef<T>(ref: Ref<T> | undefined, value: T | null) {
if (!ref) return
if (typeof ref === "function") {
ref(value)
return
}
;(ref as { current: T | null }).current = value
}
type DataGridResizeStartEvent =
| ReactMouseEvent<HTMLDivElement>
| ReactTouchEvent<HTMLDivElement>
type DataGridResizeDocumentEvent = globalThis.MouseEvent | globalThis.TouchEvent
function isDataGridTouchEvent(
event: DataGridResizeStartEvent | DataGridResizeDocumentEvent
): event is ReactTouchEvent<HTMLDivElement> | globalThis.TouchEvent {
return "touches" in event
}
function getDataGridResizeEventClientX(
event: DataGridResizeStartEvent | DataGridResizeDocumentEvent
) {
if (isDataGridTouchEvent(event)) {
return event.touches[0]?.clientX ?? event.changedTouches[0]?.clientX
}
return event.clientX
}
function startDataGridColumnResizeOnEnd<TData>(
event: DataGridResizeStartEvent,
header: Header<TData, unknown>,
table: Table<TData>
) {
const column = table.getColumn(header.column.id)
if (!column || !column.getCanResize()) return
if (isDataGridTouchEvent(event) && event.touches.length > 1) return
event.persist?.()
const ownerDocument = event.currentTarget.ownerDocument
const previousBodyCursor = ownerDocument.body.style.cursor
const previousDocumentCursor = ownerDocument.documentElement.style.cursor
const startSize = header.getSize()
const dragStartClientX = getDataGridResizeEventClientX(event)
const headerCell = event.currentTarget.closest("th")
const headerRect = headerCell?.getBoundingClientRect()
const startOffset =
headerRect &&
Number.isFinite(
table.options.columnResizeDirection === "rtl"
? headerRect.left
: headerRect.right
)
? table.options.columnResizeDirection === "rtl"
? headerRect.left
: headerRect.right
: dragStartClientX
if (typeof dragStartClientX !== "number" || typeof startOffset !== "number") {
return
}
ownerDocument.body.style.cursor = "col-resize"
ownerDocument.documentElement.style.cursor = "col-resize"
const columnSizingStart = header
.getLeafHeaders()
.map(
(leafHeader) =>
[leafHeader.column.id, leafHeader.column.getSize()] as [string, number]
)
const directionMultiplier =
table.options.columnResizeDirection === "rtl" ? -1 : 1
const updateOffset = (clientXPos?: number, commit = false) => {
if (typeof clientXPos !== "number") return
const nextColumnSizing: Record<string, number> = {}
const deltaOffset = (clientXPos - dragStartClientX) * directionMultiplier
const deltaPercentage = Math.max(deltaOffset / startSize, -0.999999)
columnSizingStart.forEach(([columnId, headerSize]) => {
nextColumnSizing[columnId] =
Math.round(
Math.max(headerSize + headerSize * deltaPercentage, 0) * 100
) / 100
})
table.setColumnSizingInfo((old) => ({
...old,
startOffset,
startSize,
deltaOffset,
deltaPercentage,
columnSizingStart,
isResizingColumn: column.id,
}))
if (commit) {
table.setColumnSizing((old) => ({
...old,
...nextColumnSizing,
}))
}
}
const endResize = (clientXPos?: number) => {
updateOffset(clientXPos, true)
table.setColumnSizingInfo((old) => ({
...old,
isResizingColumn: false,
startOffset: null,
startSize: null,
deltaOffset: null,
deltaPercentage: null,
columnSizingStart: [],
}))
ownerDocument.body.style.cursor = previousBodyCursor
ownerDocument.documentElement.style.cursor = previousDocumentCursor
}
const mouseMoveHandler = (moveEvent: globalThis.MouseEvent) => {
updateOffset(moveEvent.clientX)
}
const mouseUpHandler = (upEvent: globalThis.MouseEvent) => {
ownerDocument.removeEventListener("mousemove", mouseMoveHandler)
ownerDocument.removeEventListener("mouseup", mouseUpHandler)
endResize(upEvent.clientX)
}
const touchMoveHandler = (moveEvent: globalThis.TouchEvent) => {
if (moveEvent.cancelable) {
moveEvent.preventDefault()
moveEvent.stopPropagation()
}
updateOffset(getDataGridResizeEventClientX(moveEvent))
}
const touchEndHandler = (endEvent: globalThis.TouchEvent) => {
ownerDocument.removeEventListener("touchmove", touchMoveHandler)
ownerDocument.removeEventListener("touchend", touchEndHandler)
if (endEvent.cancelable) {
endEvent.preventDefault()
endEvent.stopPropagation()
}
endResize(getDataGridResizeEventClientX(endEvent))
}
const passiveIfSupported = { passive: false } as const
if (isDataGridTouchEvent(event)) {
ownerDocument.addEventListener(
"touchmove",
touchMoveHandler,
passiveIfSupported
)
ownerDocument.addEventListener(
"touchend",
touchEndHandler,
passiveIfSupported
)
} else {
ownerDocument.addEventListener(
"mousemove",
mouseMoveHandler,
passiveIfSupported
)
ownerDocument.addEventListener(
"mouseup",
mouseUpHandler,
passiveIfSupported
)
}
table.setColumnSizingInfo((old) => ({
...old,
startOffset,
startSize,
deltaOffset: 0,
deltaPercentage: 0,
columnSizingStart,
isResizingColumn: column.id,
}))
}
type DataGridTablePinnedBoundary = "top" | "bottom"
function getDataGridTableRowSections<TData>(
table: Table<TData>,
rowsPinnable?: boolean
) {
if (!rowsPinnable) {
return {
topRows: [] as Row<TData>[],
centerRows: table.getRowModel().rows as Row<TData>[],
bottomRows: [] as Row<TData>[],
}
}
return {
topRows: table.getTopRows() as Row<TData>[],
centerRows: table.getCenterRows() as Row<TData>[],
bottomRows: table.getBottomRows() as Row<TData>[],
}
}
function getDataGridTableResolvedRows<TData>(
table: Table<TData>,
rowsPinnable?: boolean
) {
const { topRows, centerRows, bottomRows } = getDataGridTableRowSections(
table,
rowsPinnable
)
const resolvedRows: Array<{
row: Row<TData>
pinnedBoundary?: DataGridTablePinnedBoundary
}> = []
topRows.forEach((row, index) => {
resolvedRows.push({
row,
pinnedBoundary:
index === topRows.length - 1 &&
(centerRows.length > 0 || bottomRows.length > 0)
? "top"
: undefined,
})
})
centerRows.forEach((row) => {
resolvedRows.push({ row })
})
bottomRows.forEach((row, index) => {
resolvedRows.push({
row,
pinnedBoundary:
index === 0 && (centerRows.length > 0 || topRows.length > 0)
? "bottom"
: undefined,
})
})
return resolvedRows
}
function getDataGridTableOrderedVisibleColumns<TData>(table: Table<TData>) {
return [
...table.getLeftVisibleLeafColumns(),
...table.getCenterVisibleLeafColumns(),
...table.getRightVisibleLeafColumns(),
] as Column<TData>[]
}
function getDataGridTableOrderedVisibleCells<TData>(row: Row<TData>) {
return [
...row.getLeftVisibleCells(),
...row.getCenterVisibleCells(),
...row.getRightVisibleCells(),
] as Cell<TData, unknown>[]
}
function getDataGridTableMergedHeaderGroups<TData>(table: Table<TData>) {
const leftHeaderGroups = table.getLeftHeaderGroups()
const centerHeaderGroups = table.getCenterHeaderGroups()
const rightHeaderGroups = table.getRightHeaderGroups()
const headerGroupCount = Math.max(
leftHeaderGroups.length,
centerHeaderGroups.length,
rightHeaderGroups.length
)
return Array.from({ length: headerGroupCount }, (_, index) => {
const leftGroup = leftHeaderGroups[index]
const centerGroup = centerHeaderGroups[index]
const rightGroup = rightHeaderGroups[index]
return {
id:
[leftGroup?.id, centerGroup?.id, rightGroup?.id]
.filter(Boolean)
.join(":") || `header-group-${index}`,
headers: [
...(leftGroup?.headers ?? []),
...(centerGroup?.headers ?? []),
...(rightGroup?.headers ?? []),
] as Header<TData, unknown>[],
}
})
}
function hasDataGridTableRightPinnedColumns<TData>(table: Table<TData>) {
return (table.getState().columnPinning.right?.length ?? 0) > 0
}
function DataGridTableFillCol() {
const { props } = useDataGrid()
if (!props.tableLayout?.columnsResizable) return null
return (
<col
data-slot="data-grid-table-fill-col"
style={{ width: "var(--data-grid-fill-size, 0px)" }}
/>
)
}
function DataGridTableFillHeadCell() {
const { props } = useDataGrid()
if (!props.tableLayout?.columnsResizable) return null
return (
<th
aria-hidden="true"
data-slot="data-grid-table-fill-head-cell"
style={{ width: "var(--data-grid-fill-size, 0px)" }}
className="p-0"
/>
)
}
function DataGridTableFillBodyCell() {
const { props } = useDataGrid()
if (!props.tableLayout?.columnsResizable) return null
return (
<td
aria-hidden="true"
data-slot="data-grid-table-fill-body-cell"
style={{ width: "var(--data-grid-fill-size, 0px)" }}
className="p-0"
/>
)
}
function DataGridTableFillFootCell() {
const { props } = useDataGrid()
if (!props.tableLayout?.columnsResizable) return null
return (
<td
aria-hidden="true"
data-slot="data-grid-table-fill-foot-cell"
style={{ width: "var(--data-grid-fill-size, 0px)" }}
className="p-0"
/>
)
}
function DataGridTableBase({ children }: { children: ReactNode }) {
const { props, table } = useDataGrid()
const leftVisibleColumns = table.getLeftVisibleLeafColumns()
const centerVisibleColumns = table.getCenterVisibleLeafColumns()
const rightVisibleColumns = table.getRightVisibleLeafColumns()
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
/**
* Compute column widths as CSS custom properties once upfront (memoized).
* Cells reference these via calc(var(--col-X-size) * 1px) so the browser
* handles width propagation without per-cell getSize() calls or React
* re-renders of the body.
*/
const columnSizeVars = useMemo(() => {
if (!props.tableLayout?.columnsResizable) return undefined
const headers = table.getFlatHeaders()
const colSizes: Record<string, number> = {}
for (let i = 0; i < headers.length; i++) {
const header = headers[i]!
colSizes[`--header-${header.id}-size`] = header.getSize()
colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
}
return colSizes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
props.tableLayout?.columnsResizable,
// eslint-disable-next-line react-hooks/exhaustive-deps
table.getState().columnSizing,
])
return (
<table
data-slot="data-grid-table"
className={cn(
"text-foreground caption-bottom text-left align-middle text-sm font-normal rtl:text-right",
props.tableLayout?.columnsResizable ? "min-w-0" : "w-full min-w-full",
props.tableLayout?.width === "auto" ? "table-auto" : "table-fixed",
!props.tableLayout?.columnsResizable && "",
!props.tableLayout?.columnsDraggable &&
"border-separate border-spacing-0",
props.tableClassNames?.base
)}
style={
props.tableLayout?.columnsResizable
? {
...columnSizeVars,
width: `calc(${table.getTotalSize()}px + var(--data-grid-fill-size, 0px))`,
}
: undefined
}
>
<colgroup>
{[...leftVisibleColumns, ...centerVisibleColumns].map((column) => (
<col
key={column.id}
style={
props.tableLayout?.columnsResizable
? { width: `calc(var(--col-${column.id}-size) * 1px)` }
: props.tableLayout?.width === "fixed"
? { width: column.getSize() }
: undefined
}
/>
))}
{hasRightPinnedColumns ? <DataGridTableFillCol /> : null}
{rightVisibleColumns.map((column) => (
<col
key={column.id}
style={
props.tableLayout?.columnsResizable
? { width: `calc(var(--col-${column.id}-size) * 1px)` }
: props.tableLayout?.width === "fixed"
? { width: column.getSize() }
: undefined
}
/>
))}
{!hasRightPinnedColumns ? <DataGridTableFillCol /> : null}
</colgroup>
{children}
</table>
)
}
function DataGridTableViewport({
children,
className,
viewportRef,
style,
}: {
children: ReactNode
className?: string
viewportRef?: Ref<HTMLDivElement>
style?: CSSProperties
}) {
const { props, table } = useDataGrid()
const didApplyAutoSizeColumnRef = useRef<string | null>(null)
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(
null
)
const [containerWidth, setContainerWidth] = useState(0)
const handleViewportRef = useCallback(
(node: HTMLDivElement | null) => {
setViewportElement(node)
if (props.tableLayout?.columnsResizable && node) {
const scrollViewport =
(node.closest(
'[data-slot="scroll-area-viewport"]'
) as HTMLElement | null) ?? node.parentElement
const measurementTarget = scrollViewport ?? node
setContainerWidth(measurementTarget.clientWidth)
} else if (!node) {
setContainerWidth(0)
}
assignRef(viewportRef, node)
},
[props.tableLayout?.columnsResizable, viewportRef]
)
const fillWidth =
props.tableLayout?.columnsResizable && containerWidth > 0
? Math.max(0, containerWidth - table.getTotalSize())
: 0
const autoSizeColumnId = table
.getVisibleLeafColumns()
.find(
(column) => column.columnDef.meta?.autoSize && column.getCanResize()
)?.id
useLayoutEffect(() => {
if (!viewportElement || !props.tableLayout?.columnsResizable) {
setContainerWidth(0)
return
}
const scrollViewport =
(viewportElement.closest(
'[data-slot="scroll-area-viewport"]'
) as HTMLElement | null) ?? viewportElement.parentElement
const measurementTarget = scrollViewport ?? viewportElement
const syncContainerWidth = () => {
setContainerWidth(measurementTarget.clientWidth)
}
syncContainerWidth()
if (typeof ResizeObserver === "undefined") return
const observer = new ResizeObserver(syncContainerWidth)
observer.observe(measurementTarget)
return () => {
observer.disconnect()
}
}, [props.tableLayout?.columnsResizable, viewportElement])
useLayoutEffect(() => {
if (!props.tableLayout?.columnsResizable) return
if (!autoSizeColumnId || fillWidth <= 0) return
if (didApplyAutoSizeColumnRef.current === autoSizeColumnId) return
const autoSizeColumn = table.getColumn(autoSizeColumnId)
if (!autoSizeColumn) return
didApplyAutoSizeColumnRef.current = autoSizeColumnId
table.setColumnSizing((old) => ({
...old,
[autoSizeColumnId]:
(old[autoSizeColumnId] ?? autoSizeColumn.getSize()) + fillWidth,
}))
}, [autoSizeColumnId, fillWidth, props.tableLayout?.columnsResizable, table])
return (
<div
data-slot="data-grid-table-viewport"
ref={handleViewportRef}
className={cn("relative min-w-full align-top", className)}
style={{
...(props.tableLayout?.columnsResizable
? {
width: `calc(${table.getTotalSize()}px + var(--data-grid-fill-size, 0px))`,
["--data-grid-fill-size" as string]: `${fillWidth}px`,
}
: undefined),
...style,
}}
>
{children}
<DataGridTableResizeIndicator viewportElement={viewportElement} />
</div>
)
}
function DataGridTableHead({ children }: { children: ReactNode }) {
const { props } = useDataGrid()
return (
<thead
className={cn(
props.tableClassNames?.header,
props.tableLayout?.headerSticky && props.tableClassNames?.headerSticky
)}
>
{children}
</thead>
)
}
function DataGridTableHeadRow({
children,
rowId,
}: {
children: ReactNode
rowId: string
}) {
const { props } = useDataGrid()
return (
<tr
className={cn(
props.tableLayout?.headerBorder && "[&>th]:border-b",
props.tableLayout?.cellBorder && "*:last:border-e-0",
props.tableLayout?.stripped && "bg-transparent",
props.tableLayout?.headerBackground === false && "bg-transparent",
props.tableClassNames?.headerRow
)}
>
{children}
</tr>
)
}
function DataGridTableHeadRowCell<TData>({
children,
header,
dndRef,
dndStyle,
}: {
children: ReactNode
header: Header<TData, unknown>
dndRef?: React.Ref<HTMLTableCellElement>
dndStyle?: CSSProperties
}) {
const { props } = useDataGrid()
const { column } = header
const isPinned = column.getIsPinned()
const isFirstLeftPinned =
isPinned === "left" && column.getIsFirstColumn("left")
const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left")
const isFirstRightPinned =
isPinned === "right" && column.getIsFirstColumn("right")
const isLastRightPinned =
isPinned === "right" && column.getIsLastColumn("right")
const isLastVisibleColumn =
column.getIndex() ===
header.getContext().table.getVisibleLeafColumns().length - 1
const headerCellSpacing = headerCellSpacingVariants({
size: props.tableLayout?.dense ? "dense" : "default",
})
return (
<th
ref={dndRef}
style={{
...(props.tableLayout?.width === "fixed" &&
!props.tableLayout?.columnsResizable && {
width: header.getSize(),
}),
...(props.tableLayout?.columnsPinnable &&
column.getCanPin() &&
getPinningStyles(column)),
...(props.tableLayout?.columnsResizable && {
width: `calc(var(--header-${header.id}-size) * 1px)`,
}),
...(dndStyle ? dndStyle : null),
}}
data-pinned={isPinned || undefined}
data-outer-pinned-col={
isFirstLeftPinned ? "left" : isLastRightPinned ? "right" : undefined
}
data-last-col={
isLastLeftPinned ? "left" : isFirstRightPinned ? "right" : undefined
}
className={cn(
"text-foreground relative h-10 text-left align-middle font-medium rtl:text-right [&:has([role=checkbox])]:pe-0",
headerCellSpacing,
props.tableLayout?.headerBackground && "bg-muted",
props.tableLayout?.cellBorder && "border-e",
props.tableLayout?.columnsResizable &&
column.getCanResize() &&
(isPinned ? "overflow-hidden" : "overflow-visible"),
props.tableLayout?.columnsResizable &&
column.getCanResize() &&
isLastVisibleColumn &&
"pe-8",
props.tableLayout?.columnsPinnable &&
column.getCanPin() &&
cn(
"data-pinned:bg-muted data-outer-pinned-col:bg-clip-padding data-pinned:isolate",
"[&[data-pinned=left][data-last-col=left]]:shadow-[inset_-1px_0_0_0_var(--border)] [&[data-pinned=right]:last-child_div.cursor-col-resize:last-child]:opacity-0 [&[data-pinned=right][data-last-col=right]]:shadow-[inset_1px_0_0_0_var(--border)]",
"[&:not([data-pinned]):has(+[data-pinned])_div.cursor-col-resize:last-child]:opacity-0 [&[data-last-col=left]_div.cursor-col-resize:last-child]:opacity-0"
),
header.column.columnDef.meta?.headerClassName,
column.getIndex() === 0 ||
column.getIndex() === header.headerGroup.headers.length - 1
? props.tableClassNames?.edgeCell
: ""
)}
>
{children}
</th>
)
}
function DataGridTableHeadRowCellResize<TData>({
header,
}: {
header: Header<TData, unknown>
}) {
const { props, table } = useDataGrid()
const { column } = header
const isPinned = column.getIsPinned()
const isLastVisibleColumn =
column.getIndex() ===
header.getContext().table.getVisibleLeafColumns().length - 1
const isResizeModeOnEnd =
(props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode) ===
"onEnd"
const handleMouseDown = (event: ReactMouseEvent<HTMLDivElement>) => {
event.preventDefault()
event.stopPropagation()
if (isResizeModeOnEnd) {
startDataGridColumnResizeOnEnd(event, header, table)
return
}
header.getResizeHandler()(event)
}
const handleTouchStart = (event: ReactTouchEvent<HTMLDivElement>) => {
event.preventDefault()
event.stopPropagation()
if (isResizeModeOnEnd) {
startDataGridColumnResizeOnEnd(event, header, table)
return
}
header.getResizeHandler()(event)
}
return (
<div
{...{
onDoubleClick: () => column.resetSize(),
onMouseDown: handleMouseDown,
onTouchStart: handleTouchStart,
className: cn(
"absolute top-0 h-full cursor-col-resize user-select-none touch-none z-10 flex",
isLastVisibleColumn
? "end-0 w-5 justify-end before:hidden"
: isPinned
? "end-0 w-5 justify-end before:hidden"
: "-end-2 w-5 justify-center before:absolute before:inset-y-0 before:w-px before:-translate-x-px before:bg-border",
column.getIsResizing() &&
(isResizeModeOnEnd
? "opacity-100"
: isLastVisibleColumn
? "before:absolute before:end-0 before:block before:inset-y-0 before:w-0.5 before:bg-primary opacity-100"
: "before:block before:bg-primary before:w-0.5 opacity-100")
),
}}
/>
)
}
function DataGridTableResizeIndicator({
viewportElement,
}: {
viewportElement: HTMLDivElement | null
}) {
const { props, table } = useDataGrid()
const columnSizingInfo = table.getState().columnSizingInfo
const resizingColumnId = columnSizingInfo.isResizingColumn
const resizeMode =
props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode
if (
!props.tableLayout?.columnsResizable ||
resizeMode !== "onEnd" ||
!resizingColumnId
) {
return null
}
const resizingHeader = table
.getFlatHeaders()
.find(
(header) =>
header.column.id === resizingColumnId || header.id === resizingColumnId
)
if (!resizingHeader) return null
const deltaOffset = columnSizingInfo.deltaOffset ?? 0
const headerHeight =
viewportElement
?.querySelector('[data-slot="data-grid-table"] thead')
?.getBoundingClientRect().height ?? 0
const indicatorLeft =
typeof columnSizingInfo.startOffset === "number" && viewportElement
? columnSizingInfo.startOffset -
viewportElement.getBoundingClientRect().left
: resizingHeader.getStart() + resizingHeader.getSize()
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-y-0 z-20"
style={{
left: indicatorLeft,
transform: `translateX(${deltaOffset}px)`,
}}
>
<div className="bg-primary/85 absolute inset-y-0 left-0 w-px -translate-x-1/2" />
<div
className="bg-primary rounded-b-sm absolute top-0 left-0 -translate-x-1/2 shadow-xs"
style={{
width: 5,
height: Math.max(headerHeight, 6),
}}
/>
</div>
)
}
function DataGridTableRowSpacer() {
return (
<tbody
aria-hidden="true"
className="h-2"
data-slot="data-grid-table-body-spacer"
></tbody>
)
}
function DataGridTableBody({ children }: { children: ReactNode }) {
const { props } = useDataGrid()
return (
<tbody
data-slot="data-grid-table-body"
className={cn(
props.tableLayout?.rowRounded &&
"[&_td:first-child]:rounded-l-lg",
props.tableLayout?.rowRounded &&
"[&_td:last-child]:rounded-r-lg",
props.tableClassNames?.body
)}
>
{children}
</tbody>
)
}
function DataGridTableFoot({ children }: { children: ReactNode }) {
const { props } = useDataGrid()
return (
<tfoot
data-slot="data-grid-table-foot"
className={cn(props.tableClassNames?.footer)}
>
{children}
</tfoot>
)
}
function DataGridTableFootRow({ children }: { children: ReactNode }) {
const { props } = useDataGrid()
const footRowBottomBorderClasses = "[&:not(:last-child)>td]:border-b"
return (
<tr
data-slot="data-grid-table-foot-row"
className={cn(
props.tableLayout?.footerBackground && "bg-muted/40 dark:bg-background",
props.tableLayout?.rowBorder && footRowBottomBorderClasses,
props.tableLayout?.cellBorder && "*:last:border-e-0"
)}
>
{children}
<DataGridTableFillFootCell />
</tr>
)
}
function DataGridTableFootRowCell({
children,
colSpan,
className,
}: {
children?: ReactNode
colSpan?: number
className?: string
}) {
const { props } = useDataGrid()
const spacing = footerCellSpacingVariants({
size: props.tableLayout?.dense ? "dense" : "default",
})
return (
<td
colSpan={colSpan}
className={cn(
"text-secondary-foreground/80 align-middle font-medium",
spacing,
props.tableLayout?.footerBackground && "bg-muted/40 dark:bg-background",
props.tableLayout?.cellBorder && "border-e",
className
)}
>
{children}
</td>
)
}
function DataGridTableBodyRowSkeleton({ children }: { children: ReactNode }) {
const { table, props } = useDataGrid()
return (
<tr
className={cn(
"hover:bg-muted/40 data-[state=selected]:bg-muted/50",
props.onRowClick && "cursor-pointer",
!props.tableLayout?.stripped &&
props.tableLayout?.rowBorder &&
"border-border border-b [&:not(:last-child)>td]:border-b",
props.tableLayout?.cellBorder && "*:last:border-e-0",
props.tableLayout?.stripped &&
"odd:bg-muted/90 odd:hover:bg-muted hover:bg-transparent",
table.options.enableRowSelection && "*:first:relative",
props.tableClassNames?.bodyRow
)}
>
{children}
</tr>
)
}
function DataGridTableBodyRowSkeletonCell<TData>({
children,
column,
}: {
children: ReactNode
column: Column<TData>
}) {
const { props, table } = useDataGrid()
const bodyCellSpacing = bodyCellSpacingVariants({
size: props.tableLayout?.dense ? "dense" : "default",
})
return (
<td
style={
props.tableLayout?.columnsResizable
? { width: `calc(var(--col-${column.id}-size) * 1px)` }
: undefined
}
className={cn(
"align-middle",
bodyCellSpacing,
props.tableLayout?.cellBorder && "border-e",
props.tableLayout?.columnsResizable &&
column.getCanResize() &&
"truncate",
column.columnDef.meta?.cellClassName,
props.tableLayout?.columnsPinnable &&
column.getCanPin() &&
"data-pinned:bg-background data-pinned:isolate [&[data-pinned=left][data-last-col=left]]:shadow-[inset_-1px_0_0_0_var(--border)] [&[data-pinned=right][data-last-col=right]]:shadow-[inset_1px_0_0_0_var(--border)]",
column.getIndex() === 0 ||
column.getIndex() === table.getVisibleFlatColumns().length - 1
? props.tableClassNames?.edgeCell
: ""
)}
>
{children}
</td>
)
}
function DataGridTableBodyRow<TData>({
children,
row,
pinnedBoundary,
rowRef,
dndRef,
dndStyle,
}: {
children: ReactNode
row: Row<TData>
pinnedBoundary?: DataGridTablePinnedBoundary
rowRef?: React.Ref<HTMLTableRowElement>
dndRef?: React.Ref<HTMLTableRowElement>
dndStyle?: CSSProperties
}) {
const { props, table } = useDataGrid()
const isRowPinned = row.getIsPinned()
const bodyRowBottomBorderClasses =
"[&:not(:last-child)>td]:border-b [tbody:has(+tfoot)_&:last-child>td]:border-b [*:has(>[data-slot=data-grid]+[data-slot=data-grid-pagination])_[data-slot=data-grid]_&:last-child>td]:border-b"
return (
<tr
ref={(node) => {
assignRef(rowRef, node)
assignRef(dndRef, node)
}}
style={{ ...(dndStyle ? dndStyle : null) }}
data-state={
table.options.enableRowSelection && row.getIsSelected()
? "selected"
: undefined
}
data-row-pinned={isRowPinned || undefined}
data-row-pinned-boundary={pinnedBoundary}
onClick={() => props.onRowClick && props.onRowClick(row.original)}
className={cn(
"hover:bg-muted/40 data-[state=selected]:bg-muted/50",
props.onRowClick && "cursor-pointer",
!props.tableLayout?.stripped &&
props.tableLayout?.rowBorder &&
bodyRowBottomBorderClasses,
props.tableLayout?.cellBorder &&
`*:last:border-e-0 ${bodyRowBottomBorderClasses}`,
props.tableLayout?.stripped &&
"odd:bg-muted/90 odd:hover:bg-muted hover:bg-transparent",
table.options.enableRowSelection && "*:first:relative",
props.tableLayout?.rowsPinnable &&
isRowPinned &&
"bg-muted/30 hover:bg-muted/50",
pinnedBoundary === "top" && "[&>td]:shadow-[0_2px_0_rgba(0,0,0,0.03)]",
pinnedBoundary === "bottom" &&
"[&>td]:shadow-[0_2px_0_rgba(0,0,0,0.03)]",
props.tableClassNames?.bodyRow
)}
>
{children}
</tr>
)
}
function DataGridTableBodyRowExpandded<TData>({ row }: { row: Row<TData> }) {
const { props, table } = useDataGrid()
const bodyRowBottomBorderClasses =
"[&:not(:last-child)>td]:border-b [tbody:has(+tfoot)_&:last-child>td]:border-b [*:has(>[data-slot=data-grid]+[data-slot=data-grid-pagination])_[data-slot=data-grid]_&:last-child>td]:border-b"
return (
<tr
className={cn(props.tableLayout?.rowBorder && bodyRowBottomBorderClasses)}
>
<td
colSpan={
getDataGridTableOrderedVisibleCells(row).length +
(props.tableLayout?.columnsResizable ? 1 : 0)
}
>
{table
.getAllColumns()
.find((column) => column.columnDef.meta?.expandedContent)
?.columnDef.meta?.expandedContent?.(row.original)}
</td>
</tr>
)
}
function DataGridTableBodyRowCell<TData>({
children,
cell,
dndRef,
dndStyle,
}: {
children: ReactNode
cell: Cell<TData, unknown>
dndRef?: React.Ref<HTMLTableCellElement>
dndStyle?: CSSProperties
}) {
const { props } = useDataGrid()
const { column, row } = cell
const isPinned = column.getIsPinned()
const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left")
const isFirstRightPinned =
isPinned === "right" && column.getIsFirstColumn("right")
const bodyCellSpacing = bodyCellSpacingVariants({
size: props.tableLayout?.dense ? "dense" : "default",
})
return (
<td
ref={dndRef}
style={{
...(props.tableLayout?.columnsPinnable &&
column.getCanPin() &&
getPinningStyles(column)),
...(props.tableLayout?.columnsResizable && {
width: `calc(var(--col-${column.id}-size) * 1px)`,
}),
...(dndStyle ? dndStyle : null),
}}
data-pinned={isPinned || undefined}
data-last-col={
isLastLeftPinned ? "left" : isFirstRightPinned ? "right" : undefined
}
className={cn(
"align-middle",
bodyCellSpacing,
props.tableLayout?.cellBorder && "border-e",
props.tableLayout?.columnsResizable &&
column.getCanResize() &&
"truncate",
cell.column.columnDef.meta?.cellClassName,
props.tableLayout?.columnsPinnable &&
column.getCanPin() &&
cn(
"data-pinned:bg-background data-pinned:isolate",
"[&[data-pinned=left][data-last-col=left]]:shadow-[inset_-1px_0_0_0_var(--border)]",
"[&[data-pinned=right][data-last-col=right]]:shadow-[inset_1px_0_0_0_var(--border)]"
),
column.getIndex() === 0 ||
column.getIndex() === row.getVisibleCells().length - 1
? props.tableClassNames?.edgeCell
: ""
)}
>
{children}
</td>
)
}
function DataGridTableRenderedRow<TData>({
row,
pinnedBoundary,
rowRef,
}: {
row: Row<TData>
pinnedBoundary?: DataGridTablePinnedBoundary
rowRef?: React.Ref<HTMLTableRowElement>
}) {
const { props, table } = useDataGrid()
const leftVisibleCells = row.getLeftVisibleCells()
const centerVisibleCells = row.getCenterVisibleCells()
const rightVisibleCells = row.getRightVisibleCells()
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
return (
<Fragment>
<DataGridTableBodyRow
row={row}
pinnedBoundary={pinnedBoundary}
rowRef={rowRef}
>
{[...leftVisibleCells, ...centerVisibleCells].map(
(cell: Cell<TData, unknown>) => (
<DataGridTableBodyRowCell cell={cell} key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</DataGridTableBodyRowCell>
)
)}
{props.tableLayout?.columnsResizable && hasRightPinnedColumns ? (
<DataGridTableFillBodyCell />
) : null}
{rightVisibleCells.map((cell: Cell<TData, unknown>) => (
<DataGridTableBodyRowCell cell={cell} key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</DataGridTableBodyRowCell>
))}
{props.tableLayout?.columnsResizable && !hasRightPinnedColumns ? (
<DataGridTableFillBodyCell />
) : null}
</DataGridTableBodyRow>
{row.getIsExpanded() && <DataGridTableBodyRowExpandded row={row} />}
</Fragment>
)
}
function DataGridTableEmpty() {
const { table, props } = useDataGrid()
const visibleColumnCount =
getDataGridTableOrderedVisibleColumns(table).length +
(props.tableLayout?.columnsResizable ? 1 : 0)
return (
<tr>
<td
colSpan={Math.max(visibleColumnCount, 1)}
className="text-muted-foreground py-6 text-center text-sm"
>
{props.emptyMessage || "No data available"}
</td>
</tr>
)
}
function DataGridTableLoader() {
const { props } = useDataGrid()
return (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="text-muted-foreground bg-card rounded-lg flex items-center gap-2 border px-4 py-2 text-sm leading-none font-medium">
<Spinner className="size-5 opacity-60" />
{props.loadingMessage || "Loading..."}
</div>
</div>
)
}
function DataGridTableRowPin<TData>({ row }: { row: Row<TData> }) {
const isPinned = row.getIsPinned()
return (
<button
type="button"
aria-label={isPinned ? "Unpin row" : "Pin row"}
onClick={() => {
if (isPinned) {
row.pin(false)
} else {
row.pin("top")
}
}}
className={cn(
"text-muted-foreground hover:text-foreground rounded-lg inline-flex size-7 items-center justify-center transition-colors",
isPinned && "text-primary hover:text-primary/80"
)}
>
{isPinned ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
stroke="none"
>
<path d="M16 2l4.585 4.586-2.122 2.121L17.05 7.293l-3.535 3.536 1.413 5.658-2.12 2.121-4.244-4.243L4.322 18.6l-1.414-1.41 4.242-4.244-4.243-4.243 2.122-2.121 5.656 1.414 3.536-3.536-1.414-1.414z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="12" y1="17" x2="12" y2="22" />
<path d="M5 17h14v-1.76a2 2 0 00-1.11-1.79l-1.78-.9A2 2 0 0115 10.76V6h1a2 2 0 000-4H8a2 2 0 000 4h1v4.76a2 2 0 01-1.11 1.79l-1.78.9A2 2 0 005 15.24z" />
</svg>
)}
</button>
)
}
function DataGridTableRowSelect<TData>({ row }: { row: Row<TData> }) {
return (
<>
<div
className={cn(
"bg-primary absolute inset-s-0 top-0 bottom-0 hidden w-[2px]",
row.getIsSelected() && "block"
)}
></div>
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="align-[inherit]"
/>
</>
)
}
function DataGridTableRowSelectAll() {
const { table, recordCount, isLoading } = useDataGrid()
const isAllSelected = table.getIsAllPageRowsSelected()
const isSomeSelected = table.getIsSomePageRowsSelected()
return (
<Checkbox
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
disabled={isLoading || recordCount === 0}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
className="align-[inherit]"
/>
)
}
function DataGridTableBodyRows<TData>({ table }: { table: Table<TData> }) {
const { isLoading, props } = useDataGrid()
const pagination = table.getState().pagination
if (isLoading && props.loadingMode === "skeleton" && pagination?.pageSize) {
const leftVisibleColumns = table.getLeftVisibleLeafColumns()
const centerVisibleColumns = table.getCenterVisibleLeafColumns()
const rightVisibleColumns = table.getRightVisibleLeafColumns()
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
return (
<>
{Array.from({ length: pagination.pageSize }).map((_, rowIndex) => (
<DataGridTableBodyRowSkeleton key={rowIndex}>
{[...leftVisibleColumns, ...centerVisibleColumns].map((column) => (
<DataGridTableBodyRowSkeletonCell column={column} key={column.id}>
{column.columnDef.meta?.skeleton}
</DataGridTableBodyRowSkeletonCell>
))}
{props.tableLayout?.columnsResizable && hasRightPinnedColumns ? (
<DataGridTableFillBodyCell />
) : null}
{rightVisibleColumns.map((column) => (
<DataGridTableBodyRowSkeletonCell column={column} key={column.id}>
{column.columnDef.meta?.skeleton}
</DataGridTableBodyRowSkeletonCell>
))}
{props.tableLayout?.columnsResizable && !hasRightPinnedColumns ? (
<DataGridTableFillBodyCell />
) : null}
</DataGridTableBodyRowSkeleton>
))}
</>
)
}
if (isLoading && props.loadingMode === "spinner") {
return (
<tr>
<td colSpan={table.getVisibleFlatColumns().length} className="p-8">
<div className="flex items-center justify-center">
<svg
className="text-muted-foreground mr-3 -ml-1 h-5 w-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{props.loadingMessage || "Loading..."}
</div>
</td>
</tr>
)
}
const resolvedRows = getDataGridTableResolvedRows(
table,
props.tableLayout?.rowsPinnable
)
if (!resolvedRows.length) return <DataGridTableEmpty />
return (
<>
{resolvedRows.map(({ row, pinnedBoundary }) => (
<DataGridTableRenderedRow
key={row.id}
row={row}
pinnedBoundary={pinnedBoundary}
/>
))}
</>
)
}
/**
* Memoized body rows: skip re-renders during active column resize.
* Column widths update via CSS variables on the <table> element,
* so the browser handles width changes without React re-renders.
*/
const MemoizedDataGridTableBodyRows = memo(
DataGridTableBodyRows,
(_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn
) as typeof DataGridTableBodyRows
function DataGridTableHeader<TData>() {
const { table, props } = useDataGrid()
const mergedHeaderGroups = getDataGridTableMergedHeaderGroups(table)
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
return (
<DataGridTableViewport>
<DataGridTableBase>
<DataGridTableHead>
{mergedHeaderGroups.map((headerGroup) => {
return (
<DataGridTableHeadRow key={headerGroup.id} rowId={headerGroup.id}>
{headerGroup.headers
.filter((header) => header.column.getIsPinned() !== "right")
.map((header) => {
const { column } = header
return (
<DataGridTableHeadRowCell header={header} key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{props.tableLayout?.columnsResizable &&
column.getCanResize() && (
<DataGridTableHeadRowCellResize header={header} />
)}
</DataGridTableHeadRowCell>
)
})}
{props.tableLayout?.columnsResizable &&
hasRightPinnedColumns ? (
<DataGridTableFillHeadCell />
) : null}
{headerGroup.headers
.filter((header) => header.column.getIsPinned() === "right")
.map((header) => {
const { column } = header
return (
<DataGridTableHeadRowCell header={header} key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{props.tableLayout?.columnsResizable &&
column.getCanResize() && (
<DataGridTableHeadRowCellResize header={header} />
)}
</DataGridTableHeadRowCell>
)
})}
{props.tableLayout?.columnsResizable &&
!hasRightPinnedColumns ? (
<DataGridTableFillHeadCell />
) : null}
</DataGridTableHeadRow>
)
})}
</DataGridTableHead>
</DataGridTableBase>
</DataGridTableViewport>
)
}
function DataGridTable<TData>({
footerContent,
renderHeader = true,
}: {
footerContent?: ReactNode
renderHeader?: boolean
}) {
const { table, props } = useDataGrid()
const mergedHeaderGroups = getDataGridTableMergedHeaderGroups(table)
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
return (
<DataGridTableViewport>
<DataGridTableBase>
{renderHeader && (
<DataGridTableHead>
{mergedHeaderGroups.map((headerGroup) => {
return (
<DataGridTableHeadRow
key={headerGroup.id}
rowId={headerGroup.id}
>
{headerGroup.headers
.filter((header) => header.column.getIsPinned() !== "right")
.map((header) => {
const { column } = header
return (
<DataGridTableHeadRowCell
header={header}
key={header.id}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{props.tableLayout?.columnsResizable &&
column.getCanResize() && (
<DataGridTableHeadRowCellResize header={header} />
)}
</DataGridTableHeadRowCell>
)
})}
{props.tableLayout?.columnsResizable &&
hasRightPinnedColumns ? (
<DataGridTableFillHeadCell />
) : null}
{headerGroup.headers
.filter((header) => header.column.getIsPinned() === "right")
.map((header) => {
const { column } = header
return (
<DataGridTableHeadRowCell
header={header}
key={header.id}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
{props.tableLayout?.columnsResizable &&
column.getCanResize() && (
<DataGridTableHeadRowCellResize header={header} />
)}
</DataGridTableHeadRowCell>
)
})}
{props.tableLayout?.columnsResizable &&
!hasRightPinnedColumns ? (
<DataGridTableFillHeadCell />
) : null}
</DataGridTableHeadRow>
)
})}
</DataGridTableHead>
)}
{renderHeader &&
(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (
<DataGridTableRowSpacer />
)}
<DataGridTableBody>
<MemoizedDataGridTableBodyRows table={table} />
</DataGridTableBody>
{footerContent && (
<DataGridTableFoot>{footerContent}</DataGridTableFoot>
)}
</DataGridTableBase>
</DataGridTableViewport>
)
}
export {
DataGridTable,
DataGridTableBase,
DataGridTableBody,
DataGridTableBodyRow,
DataGridTableBodyRowCell,
DataGridTableBodyRowExpandded,
DataGridTableRenderedRow,
DataGridTableBodyRowSkeleton,
DataGridTableBodyRowSkeletonCell,
DataGridTableEmpty,
DataGridTableFillBodyCell,
DataGridTableFillHeadCell,
DataGridTableFoot,
DataGridTableFootRow,
DataGridTableFootRowCell,
DataGridTableHeader,
DataGridTableHead,
DataGridTableHeadRow,
DataGridTableHeadRowCell,
DataGridTableHeadRowCellResize,
DataGridTableLoader,
DataGridTableRowPin,
DataGridTableRowSelect,
DataGridTableRowSelectAll,
DataGridTableRowSpacer,
DataGridTableViewport,
getDataGridTableMergedHeaderGroups,
getPinningStyles,
getDataGridTableResolvedRows,
getDataGridTableRowSections,
hasDataGridTableRightPinnedColumns,
}
export type { DataGridTablePinnedBoundary }