Compare commits

...

2 Commits

Author SHA1 Message Date
YounixM
3476f3032d format 2024-06-22 14:45:47 +05:30
YounixM
4b757b382f feat: resizable table 2024-06-22 14:04:55 +05:30
6 changed files with 433 additions and 1 deletions

View File

@@ -34,6 +34,8 @@
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@faker-js/faker": "8.4.1",
"@grafana/data": "^9.5.2",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
@@ -43,6 +45,7 @@
"@sentry/react": "7.102.1",
"@sentry/webpack-plugin": "2.16.0",
"@signozhq/design-tokens": "0.0.8",
"@tanstack/react-table": "8.17.3",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/shape": "3.5.0",

View File

@@ -16,6 +16,7 @@ import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import PeriscopeTable from 'periscope/components/Table/Table';
import { memo, useCallback, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
// interfaces
@@ -157,6 +158,7 @@ function LogsExplorerList({
return (
<div className="logs-list-view-container">
<PeriscopeTable />
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}
{!isLoading &&

View File

@@ -0,0 +1,69 @@
table,
.divTable {
border: 1px solid lightgray;
width: fit-content;
}
.tr {
display: flex;
}
tr,
.tr {
width: fit-content;
height: 30px;
}
th,
.th,
td,
.td {
box-shadow: inset 0 0 0 1px lightgray;
padding: 0.25rem;
}
th,
.th {
padding: 2px 4px;
position: relative;
font-weight: bold;
text-align: center;
height: 30px;
}
td,
.td {
height: 30px;
}
.resizer {
position: absolute;
top: 0;
height: 100%;
right: 0;
width: 5px;
background: rgba(0, 0, 0, 0.5);
cursor: col-resize;
user-select: none;
touch-action: none;
}
.resizer.isResizing {
background: blue;
opacity: 1;
}
@media (hover: hover) {
.resizer {
opacity: 0;
}
*:hover > .resizer {
opacity: 1;
}
}
.container {
border: 1px solid lightgray;
margin: 1rem auto;
}

View File

@@ -0,0 +1,295 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/no-unstable-nested-components */
import './Table.styles.scss';
// needed for table body level scope DnD setup
import {
closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
import {
arrayMove,
horizontalListSortingStrategy,
SortableContext,
useSortable,
} from '@dnd-kit/sortable';
// needed for row & cell level scope DnD setup
import { CSS } from '@dnd-kit/utilities';
import {
Cell,
ColumnDef,
flexRender,
getCoreRowModel,
Header,
Table,
useReactTable,
} from '@tanstack/react-table';
import React, { CSSProperties } from 'react';
import { makeData, Person } from './makeData';
function DraggableTableHeader({
header,
}: {
header: Header<Person, unknown>;
}): JSX.Element {
const {
attributes,
isDragging,
listeners,
setNodeRef,
transform,
} = useSortable({
id: header.column.id,
});
const style: CSSProperties = {
opacity: isDragging ? 0.8 : 1,
position: 'relative',
transform: CSS.Translate.toString(transform), // translate instead of transform to avoid squishing
transition: 'width transform 0.2s ease-in-out',
whiteSpace: 'nowrap',
width: header.column.getSize(),
zIndex: isDragging ? 1 : 0,
};
return (
<th colSpan={header.colSpan} ref={setNodeRef} style={style}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
<button type="button" {...attributes} {...listeners}>
🟰
</button>
<div
{...{
onDoubleClick: (): void => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
className: `resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`,
}}
/>
</th>
);
}
function DragAlongCell({ cell }: { cell: Cell<Person, unknown> }): JSX.Element {
const { isDragging, setNodeRef, transform } = useSortable({
id: cell.column.id,
});
const style: CSSProperties = {
opacity: isDragging ? 0.8 : 1,
position: 'relative',
transform: CSS.Translate.toString(transform), // translate instead of transform to avoid squishing
transition: 'width transform 0.2s ease-in-out',
width: cell.column.getSize(),
zIndex: isDragging ? 1 : 0,
};
return (
<td style={style} ref={setNodeRef}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
}
// un-memoized normal table body component - see memoized version below
function TableBody({ table }: { table: Table<Person> }): JSX.Element {
const { columnOrder } = table.getState();
console.log('columnOrder', columnOrder);
return (
<div
{...{
className: 'tbody',
}}
>
{table.getRowModel().rows.map((row) => (
<div key={row.id} className="tr">
{row.getVisibleCells().map((cell) => (
<SortableContext
key={cell.id}
items={columnOrder}
strategy={horizontalListSortingStrategy}
>
<DragAlongCell key={cell.id} cell={cell} />
</SortableContext>
))}
</div>
))}
</div>
);
}
// special memoized wrapper for our table body that we will use during column resizing
export const MemoizedTableBody = React.memo(
TableBody,
(prev, next) => prev.table.options.data === next.table.options.data,
) as typeof TableBody;
function PeriscopeTable(): JSX.Element {
const columns = React.useMemo<ColumnDef<Person>[]>(
() => [
{
accessorKey: 'firstName',
cell: (info): any => info.getValue(),
id: 'firstName',
size: 150,
},
{
accessorFn: (row): any => row.lastName,
cell: (info): any => info.getValue(),
header: (): JSX.Element => <span>Last Name</span>,
id: 'lastName',
size: 150,
},
{
accessorKey: 'age',
header: (): any => 'Age',
id: 'age',
size: 120,
},
{
accessorKey: 'visits',
header: (): JSX.Element => <span>Visits</span>,
id: 'visits',
size: 120,
},
{
accessorKey: 'status',
header: 'Status',
id: 'status',
size: 150,
},
{
accessorKey: 'progress',
header: 'Profile Progress',
id: 'progress',
size: 180,
},
],
[],
);
const [data, setData] = React.useState(() => makeData(20));
const [columnOrder, setColumnOrder] = React.useState<string[]>(() =>
columns.map((c) => c.id!),
);
const [columnVisibility, setColumnVisibility] = React.useState({});
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
state: {
columnOrder,
columnVisibility,
},
defaultColumn: {
minSize: 60,
maxSize: 800,
},
columnResizeMode: 'onChange',
onColumnOrderChange: setColumnOrder,
onColumnVisibilityChange: setColumnVisibility,
debugTable: true,
debugHeaders: true,
debugColumns: true,
});
/**
* Instead of calling `column.getSize()` on every render for every header
* and especially every data cell (very expensive),
* we will calculate all column sizes at once at the root table level in a useMemo
* and pass the column sizes down as CSS variables to the <table> element.
*/
const columnSizeVars = React.useMemo(() => {
const headers = table.getFlatHeaders();
const colSizes: { [key: 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
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
// reorder columns after drag & drop
function handleDragEnd(event: DragEndEvent): void {
const { active, over } = event;
console.log('active', active, over);
if (active && over && active.id !== over.id) {
setColumnOrder((columnOrder) => {
const oldIndex = columnOrder.indexOf(active.id as string);
const newIndex = columnOrder.indexOf(over.id as string);
return arrayMove(columnOrder, oldIndex, newIndex); // this is just a splice util
});
}
}
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {}),
);
return (
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToHorizontalAxis]}
// eslint-disable-next-line react/jsx-no-bind
onDragEnd={handleDragEnd}
sensors={sensors}
>
<div className="p-2">
<div className="overflow-x-auto">
<div
className="divTable"
style={{
...columnSizeVars, // Define column sizes on the <table> element
width: table.getTotalSize(),
}}
>
<div className="thead">
{table.getHeaderGroups().map((headerGroup) => (
<div key={headerGroup.id} className="tr">
<SortableContext
items={columnOrder}
strategy={horizontalListSortingStrategy}
>
{headerGroup.headers.map((header) => (
<div
key={header.id}
className="th"
style={{
width: `calc(var(--header-${header?.id}-size) * 1px)`,
}}
>
<DraggableTableHeader key={header.id} header={header} />
</div>
))}
</SortableContext>
</div>
))}
</div>
<TableBody table={table} />
</div>
</div>
</div>
</DndContext>
);
}
export default PeriscopeTable;

View File

@@ -0,0 +1,46 @@
import { faker } from '@faker-js/faker';
export type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
progress: number;
status: 'relationship' | 'complicated' | 'single';
subRows?: Person[];
};
const range = (len: number) => {
const arr: number[] = [];
for (let i = 0; i < len; i++) {
arr.push(i);
}
return arr;
};
const newPerson = (): Person => ({
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
});
export function makeData(...lens: number[]): any {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!;
return range(len).map(
(d): Person => ({
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}),
);
};
return makeDataLevel();
}

View File

@@ -2541,7 +2541,7 @@
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/utilities@^3.2.2":
"@dnd-kit/utilities@3.2.2", "@dnd-kit/utilities@^3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b"
integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
@@ -2597,6 +2597,11 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@faker-js/faker@8.4.1":
version "8.4.1"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.4.1.tgz#5d5e8aee8fce48f5e189bf730ebd1f758f491451"
integrity sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==
"@floating-ui/core@^1.4.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071"
@@ -3800,6 +3805,18 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@tanstack/react-table@8.17.3":
version "8.17.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.17.3.tgz#4e10b4cf5355a40d6d72a83d3f4b3ecd32f56bf4"
integrity sha512-5gwg5SvPD3lNAXPuJJz1fOCEZYk9/GeBFH3w/hCgnfyszOIzwkwgp5I7Q4MJtn0WECp84b5STQUDdmvGi8m3nA==
dependencies:
"@tanstack/table-core" "8.17.3"
"@tanstack/table-core@8.17.3":
version "8.17.3"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.17.3.tgz#d7a9830abb29cd369b52b2a7159dc0360af646fd"
integrity sha512-mPBodDGVL+fl6d90wUREepHa/7lhsghg2A3vFpakEhrhtbIlgNAZiMr7ccTgak5qbHqF14Fwy+W1yFWQt+WmYQ==
"@testing-library/dom@^8.5.0":
version "8.20.0"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"