feat: dashboard v2, esm, upgrades (#211)

* esm

* wip

* wip

* wip

* wip

* wip

* wip

* subscription notice

* wip

* wip

* wip

* fix envs

* fix: update docker build

* fix

* esm/types

* delete dashboard :D

* add patches to dockerfiles

* update packages + catalogs + ts

* wip

* remove native libs

* ts

* improvements

* fix redirects and fetching session

* try fix favicon

* fixes

* fix

* order and resize reportds within a dashboard

* improvements

* wip

* added userjot to dashboard

* fix

* add op

* wip

* different cache key

* improve date picker

* fix table

* event details loading

* redo onboarding completely

* fix login

* fix

* fix

* extend session, billing and improve bars

* fix

* reduce price on 10M
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-10-16 12:27:44 +02:00
committed by GitHub
parent 436e81ecc9
commit 81a7e5d62e
741 changed files with 32695 additions and 16996 deletions

View File

@@ -0,0 +1,131 @@
import { ProjectLink } from '@/components/links';
import { SerieIcon } from '@/components/report-chart/common/serie-icon';
import { Tooltiper } from '@/components/ui/tooltip';
import { formatDateTime, formatTime } from '@/utils/date';
import { getProfileName } from '@/utils/getters';
import type { ColumnDef } from '@tanstack/react-table';
import { isToday } from 'date-fns';
import type { IServiceProfile } from '@openpanel/db';
import { ProfileAvatar } from '../profile-avatar';
export function useColumns(type: 'profiles' | 'power-users') {
const columns: ColumnDef<IServiceProfile>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => {
const profile = row.original;
return (
<ProjectLink
href={`/profiles/${profile.id}`}
className="flex items-center gap-2 font-medium"
title={getProfileName(profile, false)}
>
<ProfileAvatar size="sm" {...profile} />
{getProfileName(profile)}
</ProjectLink>
);
},
},
{
accessorKey: 'referrer',
header: 'Referrer',
cell({ row }) {
const { referrer, referrer_name } = row.original.properties;
const ref = referrer_name || referrer;
return (
<div className="flex min-w-0 items-center gap-2">
<SerieIcon name={ref} />
<span className="truncate">{ref}</span>
</div>
);
},
},
{
accessorKey: 'country',
header: 'Country',
cell({ row }) {
const { country, city } = row.original.properties;
return (
<div className="flex min-w-0 items-center gap-2">
<SerieIcon name={country} />
<span className="truncate">{city}</span>
</div>
);
},
},
{
accessorKey: 'os',
header: 'OS',
cell({ row }) {
const { os } = row.original.properties;
return (
<div className="flex min-w-0 items-center gap-2">
<SerieIcon name={os} />
<span className="truncate">{os}</span>
</div>
);
},
},
{
accessorKey: 'browser',
header: 'Browser',
cell({ row }) {
const { browser } = row.original.properties;
return (
<div className="flex min-w-0 items-center gap-2">
<SerieIcon name={browser} />
<span className="truncate">{browser}</span>
</div>
);
},
},
{
accessorKey: 'model',
header: 'Model',
cell({ row }) {
const { model, brand } = row.original.properties;
return (
<div className="flex min-w-0 items-center gap-2">
<SerieIcon name={brand} />
<span className="truncate">
{brand} / {model}
</span>
</div>
);
},
},
{
accessorKey: 'createdAt',
header: 'Last seen',
cell: ({ row }) => {
const profile = row.original;
return (
<Tooltiper asChild content={formatDateTime(profile.createdAt)}>
<div className="text-muted-foreground">
{isToday(profile.createdAt)
? formatTime(profile.createdAt)
: formatDateTime(profile.createdAt)}
</div>
</Tooltiper>
);
},
},
];
if (type === 'power-users') {
columns.unshift({
accessorKey: 'count',
header: 'Events',
cell: ({ row }) => {
const profile = row.original;
// @ts-expect-error
return <div>{profile.count}</div>;
},
});
}
return columns;
}

View File

@@ -0,0 +1,94 @@
import type { UseQueryResult } from '@tanstack/react-query';
import { useDataTableColumnVisibility } from '@/components/ui/data-table/data-table-hooks';
import type { RouterOutputs } from '@/trpc/client';
import { useColumns } from './columns';
import { DataTable } from '@/components/ui/data-table/data-table';
import { useDataTablePagination } from '@/components/ui/data-table/data-table-hooks';
import {
AnimatedSearchInput,
DataTableToolbarContainer,
} from '@/components/ui/data-table/data-table-toolbar';
import { DataTableViewOptions } from '@/components/ui/data-table/data-table-view-options';
import { useSearchQueryState } from '@/hooks/use-search-query-state';
import { arePropsEqual } from '@/utils/are-props-equal';
import type { IServiceProfile } from '@openpanel/db';
import type { PaginationState, Table, Updater } from '@tanstack/react-table';
import { getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { memo } from 'react';
type Props = {
query: UseQueryResult<RouterOutputs['profile']['list'], unknown>;
type: 'profiles' | 'power-users';
};
const LOADING_DATA = [{}, {}, {}, {}, {}, {}, {}, {}, {}] as IServiceProfile[];
export const ProfilesTable = memo(
({ type, query }: Props) => {
const { data, isLoading } = query;
const columns = useColumns(type);
const { setPage, state: pagination } = useDataTablePagination();
const { columnVisibility, setColumnVisibility } =
useDataTableColumnVisibility(columns);
const table = useReactTable({
data: isLoading ? LOADING_DATA : (data?.data ?? []),
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
manualFiltering: true,
manualSorting: true,
columns,
rowCount: data?.meta.count,
pageCount: Math.ceil(
(data?.meta.count || 0) / (pagination.pageSize || 1),
),
filterFns: {
isWithinRange: () => true,
},
state: {
pagination,
columnVisibility,
},
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: (updaterOrValue: Updater<PaginationState>) => {
const nextPagination =
typeof updaterOrValue === 'function'
? updaterOrValue(pagination)
: updaterOrValue;
setPage(nextPagination.pageIndex + 1);
},
});
return (
<>
<ProfileTableToolbar table={table} />
<DataTable
table={table}
loading={isLoading}
empty={{
title: 'No profiles',
description: "Looks like you haven't identified any profiles yet.",
}}
/>
</>
);
},
arePropsEqual(['query.isLoading', 'query.data', 'type']),
);
function ProfileTableToolbar({ table }: { table: Table<IServiceProfile> }) {
const { search, setSearch } = useSearchQueryState();
return (
<DataTableToolbarContainer>
<AnimatedSearchInput
placeholder="Search profiles"
value={search}
onChange={setSearch}
/>
<DataTableViewOptions table={table} />
</DataTableToolbarContainer>
);
}