Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New table component #6749

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/container/LogsExplorerList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -157,6 +158,7 @@ function LogsExplorerList({

return (
<div className="logs-list-view-container">
<PeriscopeTable />
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}

{!isLoading &&
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/periscope/components/Table/Table.styles.scss
Original file line number Diff line number Diff line change
@@ -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;
}
295 changes: 295 additions & 0 deletions frontend/src/periscope/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -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;
Loading