feature(dashboard): refactor overview

fix(lint)
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-03-20 09:28:54 +01:00
committed by Carl-Gerhard Lindesvärd
parent b035c0d586
commit a1eb4a296f
83 changed files with 59167 additions and 32403 deletions

View File

@@ -15,6 +15,7 @@ export function useColumns() {
const number = useNumber();
const columns: ColumnDef<IServiceEvent>[] = [
{
size: 300,
accessorKey: 'name',
header: 'Name',
cell({ row }) {
@@ -50,29 +51,29 @@ export function useColumns() {
return (
<div className="flex items-center gap-2">
<TooltipComplete content="Click to edit" side="left">
<button
type="button"
className="transition-transform hover:scale-105"
onClick={() => {
pushModal('EditEvent', {
id: row.original.id,
});
}}
>
<EventIcon
size="sm"
name={row.original.name}
meta={row.original.meta}
/>
</button>
</TooltipComplete>
<button
type="button"
className="transition-transform hover:scale-105"
onClick={() => {
pushModal('EditEvent', {
id: row.original.id,
});
}}
>
<EventIcon
size="sm"
name={row.original.name}
meta={row.original.meta}
/>
</button>
<span className="flex gap-2">
<button
type="button"
onClick={() => {
pushModal('EventDetails', {
id: row.original.id,
createdAt: row.original.createdAt,
projectId: row.original.projectId,
});
}}
className="font-medium"
@@ -86,41 +87,13 @@ export function useColumns() {
},
},
{
accessorKey: 'country',
header: 'Country',
accessorKey: 'createdAt',
header: 'Created at',
size: 170,
cell({ row }) {
const { country, city } = row.original;
const date = row.original.createdAt;
return (
<div className="inline-flex min-w-full flex-none items-center gap-2">
<SerieIcon name={country} />
<span>{city}</span>
</div>
);
},
},
{
accessorKey: 'os',
header: 'OS',
cell({ row }) {
const { os } = row.original;
return (
<div className="flex min-w-full items-center gap-2">
<SerieIcon name={os} />
<span>{os}</span>
</div>
);
},
},
{
accessorKey: 'browser',
header: 'Browser',
cell({ row }) {
const { browser } = row.original;
return (
<div className="inline-flex min-w-full flex-none items-center gap-2">
<SerieIcon name={browser} />
<span>{browser}</span>
</div>
<div>{isToday(date) ? formatTime(date) : formatDateTime(date)}</div>
);
},
},
@@ -134,8 +107,8 @@ export function useColumns() {
}
return (
<ProjectLink
href={`/profiles/${profile?.id}`}
className="max-w-[80px] overflow-hidden text-ellipsis whitespace-nowrap font-medium hover:underline"
href={`/profiles/${profile.id}`}
className="whitespace-nowrap font-medium hover:underline"
>
{getProfileName(profile)}
</ProjectLink>
@@ -143,12 +116,44 @@ export function useColumns() {
},
},
{
accessorKey: 'createdAt',
header: 'Created at',
accessorKey: 'country',
header: 'Country',
size: 150,
cell({ row }) {
const date = row.original.createdAt;
const { country, city } = row.original;
return (
<div>{isToday(date) ? formatTime(date) : formatDateTime(date)}</div>
<div className="row items-center gap-2 min-w-0">
<SerieIcon name={country} />
<span className="truncate">{city}</span>
</div>
);
},
},
{
accessorKey: 'os',
header: 'OS',
size: 130,
cell({ row }) {
const { os } = row.original;
return (
<div className="row items-center gap-2 min-w-0">
<SerieIcon name={os} />
<span className="truncate">{os}</span>
</div>
);
},
},
{
accessorKey: 'browser',
header: 'Browser',
size: 110,
cell({ row }) {
const { browser } = row.original;
return (
<div className="row items-center gap-2 min-w-0">
<SerieIcon name={browser} />
<span className="truncate">{browser}</span>
</div>
);
},
},

View File

@@ -0,0 +1,152 @@
'use client';
import { GridCell } from '@/components/grid-table';
import { cn } from '@/utils/cn';
import {
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import type { ColumnDef } from '@tanstack/react-table';
import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual';
import throttle from 'lodash.throttle';
import { useEffect, useRef, useState } from 'react';
interface DataTableProps<TData> {
columns: ColumnDef<TData, any>[];
data: TData[];
}
export function EventsDataTable<TData>({
columns,
data,
}: DataTableProps<TData>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
const parentRef = useRef<HTMLDivElement>(null);
const [scrollMargin, setScrollMargin] = useState(0);
const { rows } = table.getRowModel();
const virtualizer = useWindowVirtualizer({
count: rows.length,
estimateSize: () => 48,
scrollMargin,
overscan: 10,
});
useEffect(() => {
const updateScrollMargin = throttle(() => {
if (parentRef.current) {
setScrollMargin(
parentRef.current.getBoundingClientRect().top + window.scrollY,
);
}
}, 500);
// Initial calculation
updateScrollMargin();
// Listen for scroll and resize events
window.addEventListener('scroll', updateScrollMargin);
window.addEventListener('resize', updateScrollMargin);
return () => {
window.removeEventListener('scroll', updateScrollMargin);
window.removeEventListener('resize', updateScrollMargin);
};
}, []); // Empty dependency array since we're setting up listeners
const visibleRows = virtualizer.getVirtualItems();
return (
<div className="card">
<div className="relative w-full overflow-auto rounded-md">
<div
className="w-full"
style={{
width: 'max-content',
minWidth: '100%',
}}
>
{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>
);
})}
</div>
))}
<div ref={parentRef} className="w-full">
<div
className="tbody [&>*:last-child]:border-0"
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{visibleRows.map((virtualRow, index) => {
const row = rows[virtualRow.index]!;
if (!row) {
return null;
}
return (
<div
key={row.id}
className={cn('absolute top-0 left-0 w-full h-12 row')}
style={{
transform: `translateY(${
virtualRow.start - virtualizer.options.scrollMargin
}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>
);
})}
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,64 +1,82 @@
import { DataTable } from '@/components/data-table';
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { Pagination } from '@/components/pagination';
import { Button } from '@/components/ui/button';
import { TableSkeleton } from '@/components/ui/table';
import type { UseQueryResult } from '@tanstack/react-query';
import { GanttChartIcon } from 'lucide-react';
import { column } from 'mathjs';
import type { Dispatch, SetStateAction } from 'react';
import type { IServiceEvent } from '@openpanel/db';
import type {
UseInfiniteQueryResult,
UseQueryResult,
} from '@tanstack/react-query';
import { GanttChartIcon, Loader2Icon } from 'lucide-react';
import { useEffect, useRef } from 'react';
import type { RouterOutputs } from '@/trpc/client';
import { cn } from '@/utils/cn';
import { useInViewport } from 'react-in-viewport';
import { useColumns } from './columns';
import { EventsDataTable } from './events-data-table';
type Props =
| {
query: UseQueryResult<IServiceEvent[]>;
query: UseInfiniteQueryResult<RouterOutputs['event']['events']>;
}
| {
query: UseQueryResult<IServiceEvent[]>;
cursor: number;
setCursor: Dispatch<SetStateAction<number>>;
query: UseQueryResult<RouterOutputs['event']['events']>;
};
export const EventsTable = ({ query, ...props }: Props) => {
const columns = useColumns();
const { data, isFetching, isLoading } = query;
const { isLoading } = query;
const ref = useRef<HTMLDivElement>(null);
const { inViewport, enterCount } = useInViewport(ref, undefined, {
disconnectOnLeave: true,
});
const isInfiniteQuery = 'fetchNextPage' in query;
const data =
(isInfiniteQuery
? query.data?.pages?.flatMap((p) => p.items)
: query.data?.items) ?? [];
const hasNextPage = isInfiniteQuery
? query.data?.pages[query.data.pages.length - 1]?.meta.next
: query.data?.meta.next;
useEffect(() => {
if (
hasNextPage &&
isInfiniteQuery &&
data.length > 0 &&
inViewport &&
enterCount > 0 &&
query.isFetchingNextPage === false
) {
query.fetchNextPage();
}
}, [inViewport, enterCount, hasNextPage]);
if (isLoading) {
return <TableSkeleton cols={columns.length} />;
}
if (data?.length === 0) {
if (data.length === 0) {
return (
<FullPageEmptyState title="No events here" icon={GanttChartIcon}>
<p>Could not find any events</p>
{'cursor' in props && props.cursor !== 0 && (
<Button
className="mt-8"
variant="outline"
onClick={() => props.setCursor((p) => p - 1)}
>
Go to previous page
</Button>
)}
</FullPageEmptyState>
);
}
return (
<>
<DataTable data={data ?? []} columns={columns} />
{'cursor' in props && (
<Pagination
className="mt-2"
setCursor={props.setCursor}
cursor={props.cursor}
count={Number.POSITIVE_INFINITY}
take={50}
loading={isFetching}
/>
<EventsDataTable data={data} columns={columns} />
{isInfiniteQuery && (
<div className="w-full h-10 center-center pt-10" ref={ref}>
<div
className={cn(
'size-8 bg-background rounded-full center-center border opacity-0 transition-opacity',
isInfiniteQuery && query.isFetchingNextPage && 'opacity-100',
)}
>
<Loader2Icon className="size-4 animate-spin" />
</div>
</div>
)}
</>
);