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:
committed by
GitHub
parent
436e81ecc9
commit
81a7e5d62e
346
apps/start/src/components/overview/overview-widget-table.tsx
Normal file
346
apps/start/src/components/overview/overview-widget-table.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
|
||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||
import type { RouterOutputs } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ExternalLinkIcon } from 'lucide-react';
|
||||
import { SerieIcon } from '../report-chart/common/serie-icon';
|
||||
import { Skeleton } from '../skeleton';
|
||||
import { Tooltiper } from '../ui/tooltip';
|
||||
import { WidgetTable, type Props as WidgetTableProps } from '../widget-table';
|
||||
|
||||
type Props<T> = WidgetTableProps<T> & {
|
||||
getColumnPercentage: (item: T) => number;
|
||||
};
|
||||
|
||||
export const OverviewWidgetTable = <T,>({
|
||||
data,
|
||||
keyExtractor,
|
||||
columns,
|
||||
getColumnPercentage,
|
||||
className,
|
||||
}: Props<T>) => {
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<WidgetTable
|
||||
data={data ?? []}
|
||||
keyExtractor={keyExtractor}
|
||||
className={'text-sm min-h-[358px] @container [&_.head]:pt-3'}
|
||||
columnClassName="[&_.cell:first-child]:pl-4 [&_.cell:last-child]:pr-4"
|
||||
eachRow={(item) => {
|
||||
return (
|
||||
<div className="absolute top-0 left-0 !p-0 w-full h-full">
|
||||
<div
|
||||
className="h-full bg-def-200 group-hover/row:bg-blue-200 dark:group-hover/row:bg-blue-900 transition-colors relative"
|
||||
style={{
|
||||
width: `${getColumnPercentage(item) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
columns={columns.map((column, index) => {
|
||||
return {
|
||||
...column,
|
||||
className: cn(
|
||||
index === 0
|
||||
? 'text-left w-full font-medium min-w-0'
|
||||
: 'text-right font-mono',
|
||||
index !== 0 &&
|
||||
index !== columns.length - 1 &&
|
||||
'hidden @[310px]:table-cell',
|
||||
column.className,
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function OverviewWidgetTableLoading({
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<OverviewWidgetTable
|
||||
className={className}
|
||||
data={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
|
||||
keyExtractor={(item) => item.toString()}
|
||||
getColumnPercentage={() => 0}
|
||||
columns={[
|
||||
{
|
||||
name: 'Path',
|
||||
render: () => <Skeleton className="h-4 w-1/3" />,
|
||||
width: 'w-full',
|
||||
},
|
||||
{
|
||||
name: 'BR',
|
||||
render: () => <Skeleton className="h-4 w-[30px]" />,
|
||||
width: '60px',
|
||||
},
|
||||
// {
|
||||
// name: 'Duration',
|
||||
// render: () => <Skeleton className="h-4 w-[30px]" />,
|
||||
// },
|
||||
{
|
||||
name: 'Sessions',
|
||||
render: () => <Skeleton className="h-4 w-[30px]" />,
|
||||
width: '84px',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getPath(path: string, showDomain = false) {
|
||||
try {
|
||||
const url = new URL(path);
|
||||
if (showDomain) {
|
||||
return url.hostname + url.pathname;
|
||||
}
|
||||
return url.pathname;
|
||||
} catch {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
export function OverviewWidgetTablePages({
|
||||
data,
|
||||
lastColumnName,
|
||||
className,
|
||||
showDomain = false,
|
||||
}: {
|
||||
className?: string;
|
||||
lastColumnName: string;
|
||||
data: {
|
||||
origin: string;
|
||||
path: string;
|
||||
avg_duration: number;
|
||||
bounce_rate: number;
|
||||
sessions: number;
|
||||
}[];
|
||||
showDomain?: boolean;
|
||||
}) {
|
||||
const [_filters, setFilter] = useEventQueryFilters();
|
||||
const number = useNumber();
|
||||
const maxSessions = Math.max(...data.map((item) => item.sessions));
|
||||
return (
|
||||
<OverviewWidgetTable
|
||||
className={className}
|
||||
data={data ?? []}
|
||||
keyExtractor={(item) => item.path + item.origin}
|
||||
getColumnPercentage={(item) => item.sessions / maxSessions}
|
||||
columns={[
|
||||
{
|
||||
name: 'Path',
|
||||
width: 'w-full',
|
||||
render(item) {
|
||||
return (
|
||||
<Tooltiper asChild content={item.origin + item.path} side="left">
|
||||
<div className="row items-center gap-2 min-w-0 relative">
|
||||
<SerieIcon name={item.origin} />
|
||||
<button
|
||||
type="button"
|
||||
className="truncate"
|
||||
onClick={() => {
|
||||
setFilter('path', item.path);
|
||||
setFilter('origin', item.origin);
|
||||
}}
|
||||
>
|
||||
{item.path ? (
|
||||
<>
|
||||
{showDomain ? (
|
||||
<>
|
||||
<span className="opacity-40">{item.origin}</span>
|
||||
<span>{item.path}</span>
|
||||
</>
|
||||
) : (
|
||||
item.path
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="opacity-40">Not set</span>
|
||||
)}
|
||||
</button>
|
||||
<a
|
||||
href={item.origin + item.path}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ExternalLinkIcon className="size-3 group-hover/row:opacity-100 opacity-0 transition-opacity" />
|
||||
</a>
|
||||
</div>
|
||||
</Tooltiper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BR',
|
||||
width: '60px',
|
||||
render(item) {
|
||||
return number.shortWithUnit(item.bounce_rate, '%');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Duration',
|
||||
width: '75px',
|
||||
render(item) {
|
||||
return number.shortWithUnit(item.avg_duration, 'min');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: lastColumnName,
|
||||
width: '84px',
|
||||
render(item) {
|
||||
return (
|
||||
<div className="row gap-2 justify-end">
|
||||
<span className="font-semibold">
|
||||
{number.short(item.sessions)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverviewWidgetTableBots({
|
||||
data,
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
data: {
|
||||
total_sessions: number;
|
||||
origin: string;
|
||||
path: string;
|
||||
sessions: number;
|
||||
avg_duration: number;
|
||||
bounce_rate: number;
|
||||
}[];
|
||||
}) {
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const number = useNumber();
|
||||
const maxSessions = Math.max(...data.map((item) => item.sessions));
|
||||
return (
|
||||
<OverviewWidgetTable
|
||||
className={className}
|
||||
data={data ?? []}
|
||||
keyExtractor={(item) => item.path + item.origin}
|
||||
getColumnPercentage={(item) => item.sessions / maxSessions}
|
||||
columns={[
|
||||
{
|
||||
name: 'Path',
|
||||
width: 'w-full',
|
||||
render(item) {
|
||||
return (
|
||||
<Tooltiper asChild content={item.origin + item.path} side="left">
|
||||
<div className="row items-center gap-2 min-w-0 relative">
|
||||
<SerieIcon name={item.origin} />
|
||||
<button
|
||||
type="button"
|
||||
className="truncate"
|
||||
onClick={() => {
|
||||
setFilter('path', item.path);
|
||||
}}
|
||||
>
|
||||
{getPath(item.path)}
|
||||
</button>
|
||||
<a
|
||||
href={item.origin + item.path}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ExternalLinkIcon className="size-3 group-hover/row:opacity-100 opacity-0 transition-opacity" />
|
||||
</a>
|
||||
</div>
|
||||
</Tooltiper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Bot',
|
||||
width: '60px',
|
||||
render(item) {
|
||||
return (
|
||||
<div className="row gap-2 justify-end">
|
||||
<span className="font-semibold">Google bot</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Date',
|
||||
width: '60px',
|
||||
render(item) {
|
||||
return (
|
||||
<div className="row gap-2 justify-end">
|
||||
<span className="font-semibold">Google bot</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverviewWidgetTableGeneric({
|
||||
data,
|
||||
column,
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
data: RouterOutputs['overview']['topGeneric'];
|
||||
column: {
|
||||
name: string;
|
||||
render: (
|
||||
item: RouterOutputs['overview']['topGeneric'][number],
|
||||
) => React.ReactNode;
|
||||
};
|
||||
}) {
|
||||
const number = useNumber();
|
||||
const maxSessions = Math.max(...data.map((item) => item.sessions));
|
||||
return (
|
||||
<OverviewWidgetTable
|
||||
className={className}
|
||||
data={data ?? []}
|
||||
keyExtractor={(item) => item.name}
|
||||
getColumnPercentage={(item) => item.sessions / maxSessions}
|
||||
columns={[
|
||||
{
|
||||
...column,
|
||||
width: 'w-full',
|
||||
},
|
||||
{
|
||||
name: 'BR',
|
||||
width: '60px',
|
||||
render(item) {
|
||||
return number.shortWithUnit(item.bounce_rate, '%');
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'Duration',
|
||||
// render(item) {
|
||||
// return number.shortWithUnit(item.avg_session_duration, 'min');
|
||||
// },
|
||||
// },
|
||||
{
|
||||
name: 'Sessions',
|
||||
width: '84px',
|
||||
render(item) {
|
||||
return (
|
||||
<div className="row gap-2 justify-end">
|
||||
<span className="font-semibold">
|
||||
{number.short(item.sessions)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user