Compare commits
2 Commits
main
...
new-table-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3476f3032d | ||
|
|
4b757b382f |
@@ -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",
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
69
frontend/src/periscope/components/Table/Table.styles.scss
Normal file
69
frontend/src/periscope/components/Table/Table.styles.scss
Normal 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;
|
||||
}
|
||||
295
frontend/src/periscope/components/Table/Table.tsx
Normal file
295
frontend/src/periscope/components/Table/Table.tsx
Normal 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;
|
||||
46
frontend/src/periscope/components/Table/makeData.ts
Normal file
46
frontend/src/periscope/components/Table/makeData.ts
Normal 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();
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user