240 lines
6.5 KiB
TypeScript
240 lines
6.5 KiB
TypeScript
import { EventIcon } from '@/components/events/event-icon';
|
|
import { ProjectLink } from '@/components/links';
|
|
import { SerieIcon } from '@/components/report-chart/common/serie-icon';
|
|
import { useNumber } from '@/hooks/use-numer-formatter';
|
|
import { pushModal } from '@/modals';
|
|
import { getProfileName } from '@/utils/getters';
|
|
import type { ColumnDef } from '@tanstack/react-table';
|
|
|
|
import { ColumnCreatedAt } from '@/components/column-created-at';
|
|
import { ProfileAvatar } from '@/components/profiles/profile-avatar';
|
|
import { KeyValueGrid } from '@/components/ui/key-value-grid';
|
|
import type { IServiceEvent } from '@openpanel/db';
|
|
|
|
export function useColumns() {
|
|
const number = useNumber();
|
|
const columns: ColumnDef<IServiceEvent>[] = [
|
|
{
|
|
accessorKey: 'createdAt',
|
|
header: 'Created at',
|
|
size: ColumnCreatedAt.size,
|
|
cell: ({ row }) => {
|
|
const session = row.original;
|
|
return <ColumnCreatedAt>{session.createdAt}</ColumnCreatedAt>;
|
|
},
|
|
},
|
|
{
|
|
size: 300,
|
|
accessorKey: 'name',
|
|
header: 'Name',
|
|
cell({ row }) {
|
|
const { name, path, duration, properties, revenue } = row.original;
|
|
const renderName = () => {
|
|
if (name === 'screen_view') {
|
|
if (path.includes('/')) {
|
|
return <span className="max-w-md truncate">{path}</span>;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<span className="text-muted-foreground">Screen: </span>
|
|
<span className="max-w-md truncate">{path}</span>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (name === 'revenue' && revenue) {
|
|
return `${name} (${number.currency(revenue / 100)})`;
|
|
}
|
|
|
|
return name.replace(/_/g, ' ');
|
|
};
|
|
|
|
const renderDuration = () => {
|
|
if (name === 'screen_view') {
|
|
return (
|
|
<span className="text-muted-foreground">
|
|
{number.shortWithUnit(duration / 1000, 'min')}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<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"
|
|
>
|
|
{renderName()}
|
|
</button>
|
|
{renderDuration()}
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'profileId',
|
|
header: 'Profile',
|
|
cell({ row }) {
|
|
const { profile, profileId, deviceId } = row.original;
|
|
if (profile) {
|
|
return (
|
|
<ProjectLink
|
|
href={`/profiles/${encodeURIComponent(profile.id)}`}
|
|
className="group whitespace-nowrap font-medium hover:underline row items-center gap-2"
|
|
>
|
|
<ProfileAvatar size="sm" {...profile} />
|
|
{getProfileName(profile)}
|
|
</ProjectLink>
|
|
);
|
|
}
|
|
|
|
if (profileId && profileId !== deviceId) {
|
|
return (
|
|
<ProjectLink
|
|
href={`/profiles/${encodeURIComponent(profileId)}`}
|
|
className="whitespace-nowrap font-medium hover:underline"
|
|
>
|
|
Unknown
|
|
</ProjectLink>
|
|
);
|
|
}
|
|
|
|
if (deviceId) {
|
|
return (
|
|
<ProjectLink
|
|
href={`/profiles/${encodeURIComponent(deviceId)}`}
|
|
className="whitespace-nowrap font-medium hover:underline"
|
|
>
|
|
Anonymous
|
|
</ProjectLink>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'sessionId',
|
|
header: 'Session ID',
|
|
size: 320,
|
|
meta: {
|
|
hidden: true,
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'deviceId',
|
|
header: 'Device ID',
|
|
size: 320,
|
|
meta: {
|
|
hidden: true,
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'country',
|
|
header: 'Country',
|
|
size: 150,
|
|
cell({ row }) {
|
|
const { country, city } = row.original;
|
|
return (
|
|
<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>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'properties',
|
|
header: 'Properties',
|
|
size: 400,
|
|
meta: {
|
|
hidden: true,
|
|
},
|
|
cell({ row }) {
|
|
const { properties } = row.original;
|
|
const filteredProperties = Object.fromEntries(
|
|
Object.entries(properties || {}).filter(
|
|
([key]) => !key.startsWith('__'),
|
|
),
|
|
);
|
|
const items = Object.entries(filteredProperties);
|
|
const limit = 2;
|
|
const data = items.slice(0, limit).map(([key, value]) => ({
|
|
name: key,
|
|
value: value,
|
|
}));
|
|
if (items.length > limit) {
|
|
data.push({
|
|
name: '',
|
|
value: `${items.length - limit} more item${items.length - limit === 1 ? '' : 's'}`,
|
|
});
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return <KeyValueGrid className="w-full" data={data} />;
|
|
},
|
|
},
|
|
];
|
|
|
|
return columns;
|
|
}
|