feature(dashboard): customize event columns

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-05-09 20:59:24 +02:00
parent 584a6d21f1
commit dd39ff70a9
10 changed files with 408 additions and 230 deletions

View File

@@ -9,7 +9,9 @@ import { getProfileName } from '@/utils/getters';
import type { ColumnDef } from '@tanstack/react-table';
import { isToday } from 'date-fns';
import { ScrollArea } from '@/components/ui/scroll-area';
import type { IServiceEvent } from '@openpanel/db';
import { omit } from 'ramda';
export function useColumns() {
const number = useNumber();
@@ -101,20 +103,53 @@ export function useColumns() {
accessorKey: 'profileId',
header: 'Profile',
cell({ row }) {
const { profile } = row.original;
if (!profile) {
return null;
const { profile, profileId, deviceId } = row.original;
if (profile) {
return (
<ProjectLink
href={`/profiles/${profile.id}`}
className="whitespace-nowrap font-medium hover:underline"
>
{getProfileName(profile)}
</ProjectLink>
);
}
return (
<ProjectLink
href={`/profiles/${profile.id}`}
className="whitespace-nowrap font-medium hover:underline"
>
{getProfileName(profile)}
</ProjectLink>
);
if (profileId && profileId !== deviceId) {
return (
<ProjectLink
href={`/profiles/${profileId}`}
className="whitespace-nowrap font-medium hover:underline"
>
Unknown
</ProjectLink>
);
}
if (deviceId) {
return (
<ProjectLink
href={`/profiles/${deviceId}`}
className="whitespace-nowrap font-medium hover:underline"
>
Anonymous
</ProjectLink>
);
}
return null;
},
},
{
accessorKey: 'sessionId',
header: 'Session ID',
size: 320,
},
{
accessorKey: 'deviceId',
header: 'Device ID',
size: 320,
},
{
accessorKey: 'country',
header: 'Country',
@@ -157,6 +192,24 @@ export function useColumns() {
);
},
},
{
accessorKey: 'properties',
header: 'Properties',
size: 400,
cell({ row }) {
const { properties } = row.original;
const filteredProperties = Object.fromEntries(
Object.entries(properties || {}).filter(
([key]) => !key.startsWith('__'),
),
);
return (
<ScrollArea orientation="horizontal">
<pre>{JSON.stringify(filteredProperties)}</pre>
</ScrollArea>
);
},
},
];
return columns;

View File

@@ -8,9 +8,10 @@ import {
useReactTable,
} from '@tanstack/react-table';
import type { ColumnDef } from '@tanstack/react-table';
import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual';
import { useWindowVirtualizer } from '@tanstack/react-virtual';
import throttle from 'lodash.throttle';
import { useEffect, useRef, useState } from 'react';
import { useEventsTableColumns } from './events-table-columns';
interface DataTableProps<TData> {
columns: ColumnDef<TData, any>[];
@@ -21,6 +22,7 @@ export function EventsDataTable<TData>({
columns,
data,
}: DataTableProps<TData>) {
const [visibleColumns] = useEventsTableColumns();
const table = useReactTable({
data,
columns,
@@ -74,27 +76,29 @@ export function EventsDataTable<TData>({
>
{table.getHeaderGroups().map((headerGroup) => (
<div className="thead row h-12 sticky top-0" key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<GridCell
key={header.id}
isHeader
style={{
minWidth: header.column.getSize(),
flexShrink: 1,
overflow: 'hidden',
flex: 1,
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</GridCell>
);
})}
{headerGroup.headers
.filter((header) => visibleColumns.includes(header.id))
.map((header) => {
return (
<GridCell
key={header.id}
isHeader
style={{
minWidth: header.column.getSize(),
flexShrink: 1,
overflow: 'hidden',
flex: 1,
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</GridCell>
);
})}
</div>
))}
<div ref={parentRef} className="w-full">
@@ -122,24 +126,27 @@ export function EventsDataTable<TData>({
}px)`,
}}
>
{row.getVisibleCells().map((cell) => {
return (
<GridCell
key={cell.id}
style={{
minWidth: cell.column.getSize(),
flexShrink: 1,
overflow: 'hidden',
flex: 1,
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</GridCell>
);
})}
{row
.getVisibleCells()
.filter((cell) => visibleColumns.includes(cell.column.id))
.map((cell) => {
return (
<GridCell
key={cell.id}
style={{
minWidth: cell.column.getSize(),
flexShrink: 1,
overflow: 'hidden',
flex: 1,
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</GridCell>
);
})}
</div>
);
})}

View File

@@ -0,0 +1,70 @@
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { ColumnsIcon } from 'lucide-react';
import { useQueryState } from 'nuqs';
import { useLocalStorage } from 'usehooks-ts';
// Define available columns
const AVAILABLE_COLUMNS = [
{ id: 'name', label: 'Name' },
{ id: 'createdAt', label: 'Created at' },
{ id: 'profileId', label: 'Profile' },
{ id: 'country', label: 'Country' },
{ id: 'os', label: 'OS' },
{ id: 'browser', label: 'Browser' },
{ id: 'properties', label: 'Properties' },
{ id: 'sessionId', label: 'Session ID' },
{ id: 'deviceId', label: 'Device ID' },
] as const;
export function useEventsTableColumns() {
return useLocalStorage<string[]>('@op:events-table-columns', [
'name',
'createdAt',
'profileId',
'country',
'os',
'browser',
]);
}
export function EventsTableColumns() {
const [visibleColumns, setVisibleColumns] = useEventsTableColumns();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<ColumnsIcon className="h-4 w-4 mr-2" />
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{AVAILABLE_COLUMNS.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
checked={visibleColumns.includes(column.id)}
onCheckedChange={(checked) => {
setVisibleColumns(
checked
? [...visibleColumns, column.id]
: visibleColumns.filter((id) => id !== column.id),
);
}}
>
{column.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -39,14 +39,16 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full',
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] md:w-full p-2 sm:p-6',
className,
'max-h-screen overflow-y-auto', // Ensure the dialog is scrollable if it exceeds the screen height
'mt-auto', // Add margin-top: auto for all screen sizes
)}
{...props}
>
{children}
<div className="border bg-background p-6 shadow-lg sm:rounded-lg">
{children}
</div>
</DialogPrimitive.Content>
</DialogPortal>
));