wip improve event list

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-03-08 10:44:25 +01:00
parent f3bbada880
commit cb53072bf7
11 changed files with 480 additions and 94 deletions

View File

@@ -21,11 +21,11 @@ import { toast } from 'sonner';
import type { EventMeta } from '@mixan/db';
const variants = cva('flex items-center justify-center shrink-0', {
const variants = cva('flex items-center justify-center shrink-0 rounded-full', {
variants: {
size: {
sm: 'w-6 h-6 rounded',
default: 'w-12 h-12 rounded-xl',
sm: 'w-6 h-6',
default: 'w-10 h-10',
},
},
defaultVariants: {

View File

@@ -0,0 +1,275 @@
'use client';
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
import { SerieIcon } from '@/components/report/chart/SerieIcon';
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
import { useAppParams } from '@/hooks/useAppParams';
import {
useEventQueryFilters,
useEventQueryNamesFilter,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn';
import { getProfileName } from '@/utils/getters';
import { round } from '@/utils/math';
import Link from 'next/link';
import { uniq } from 'ramda';
import type { IServiceCreateEventPayload } from '@mixan/db';
import { EventIcon } from './event-icon';
type EventListItemProps = IServiceCreateEventPayload;
export function EventListItem(props: EventListItemProps) {
const {
profile,
createdAt,
name,
properties,
path,
duration,
referrer,
referrerName,
referrerType,
brand,
model,
browser,
browserVersion,
os,
osVersion,
city,
region,
country,
continent,
device,
projectId,
meta,
} = props;
const params = useAppParams();
const [, setEvents] = useEventQueryNamesFilter({ shallow: false });
const [, setFilter] = useEventQueryFilters({ shallow: false });
const keyValueList = [
{
name: 'Duration',
value: duration ? round(duration / 1000, 1) : undefined,
},
{
name: 'Referrer',
value: referrer,
onClick() {
setFilter('referrer', referrer ?? '');
},
},
{
name: 'Referrer name',
value: referrerName,
onClick() {
setFilter('referrer_name', referrerName ?? '');
},
},
{
name: 'Referrer type',
value: referrerType,
onClick() {
setFilter('referrer_type', referrerType ?? '');
},
},
{
name: 'Brand',
value: brand,
onClick() {
setFilter('brand', brand ?? '');
},
},
{
name: 'Model',
value: model,
onClick() {
setFilter('model', model ?? '');
},
},
{
name: 'Browser',
value: browser,
onClick() {
setFilter('browser', browser ?? '');
},
},
{
name: 'Browser version',
value: browserVersion,
onClick() {
setFilter('browser_version', browserVersion ?? '');
},
},
{
name: 'OS',
value: os,
onClick() {
setFilter('os', os ?? '');
},
},
{
name: 'OS version',
value: osVersion,
onClick() {
setFilter('os_version', osVersion ?? '');
},
},
{
name: 'City',
value: city,
onClick() {
setFilter('city', city ?? '');
},
},
{
name: 'Region',
value: region,
onClick() {
setFilter('region', region ?? '');
},
},
{
name: 'Country',
value: country,
onClick() {
setFilter('country', country ?? '');
},
},
{
name: 'Continent',
value: continent,
onClick() {
setFilter('continent', continent ?? '');
},
},
{
name: 'Device',
value: device,
onClick() {
setFilter('device', device ?? '');
},
},
].filter((item) => typeof item.value === 'string' && item.value);
const propertiesList = Object.entries(properties)
.map(([name, value]) => ({
name,
value: value as string | number | undefined,
}))
.filter((item) => typeof item.value === 'string' && item.value);
return (
<div className="p-4 flex gap-4">
<EventIcon name={name} meta={meta} projectId={projectId} />
<div>
{!!profile && (
<div className="flex gap-2 items-center mb-1">
<ProfileAvatar size="xs" {...profile} />
<Link
className="font-medium text-sm text-muted-foreground"
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
>
{getProfileName(profile)}
</Link>
</div>
)}
<div className="[&>span]:inline text-slate-700">
<span
className={cn(
'font-medium bg-muted p-0.5 px-1 leading-none rounded-md',
meta?.conversion && `bg-${meta.color}-100 text-${meta.color}-800`
)}
>
{meta?.conversion && '⭐️ '}
{name}
</span>
<span className="text-muted-foreground">{' at '}</span>
<span>{path}</span>
<span className="text-muted-foreground">{' from '}</span>
<span>
{city || 'Unknown'}, {country}
</span>
<span className="text-muted-foreground">{' using '}</span>
<span className="!inline-flex items-center gap-1">
{brand || device} <SerieIcon name={device} />
</span>
</div>
</div>
</div>
);
// return (
// <ExpandableListItem
// className={cn(meta?.conversion && `bg-${meta.color}-50`)}
// title={
// <button onClick={() => setEvents((p) => uniq([...p, name]))}>
// {name.split('_').join(' ')}
// </button>
// }
// content={
// <>
// <KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
// {profile?.id === props.deviceId && (
// <KeyValueSubtle name="Anonymous" value={'Yes'} />
// )}
// {profile && (
// <KeyValueSubtle
// name="Profile"
// value={getProfileName(profile)}
// href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
// />
// )}
// {path && (
// <KeyValueSubtle
// name="Path"
// value={path}
// onClick={() => {
// setFilter('path', path);
// }}
// />
// )}
// </>
// }
// image={<EventIcon name={name} meta={meta} projectId={projectId} />}
// >
// <div className="bg-white p-4">
// {propertiesList.length > 0 && (
// <div className="flex flex-col gap-4 mb-6">
// <div className="font-medium">Your properties</div>
// <div className="flex flex-wrap gap-x-4 gap-y-2">
// {propertiesList.map((item) => (
// <KeyValue
// key={item.name}
// name={item.name}
// value={item.value}
// onClick={() => {
// setFilter(
// `properties.${item.name}`,
// item.value ? String(item.value) : '',
// 'is'
// );
// }}
// />
// ))}
// </div>
// </div>
// )}
// <div className="flex flex-col gap-4">
// <div className="font-medium">Properties</div>
// <div className="flex flex-wrap gap-x-4 gap-y-2">
// {keyValueList.map((item) => (
// <KeyValue
// onClick={() => item.onClick?.()}
// key={item.name}
// name={item.name}
// value={item.value}
// />
// ))}
// </div>
// </div>
// </div>
// </ExpandableListItem>
// );
}

View File

@@ -1,6 +1,8 @@
'use client';
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
import { SerieIcon } from '@/components/report/chart/SerieIcon';
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
import { useAppParams } from '@/hooks/useAppParams';
import {
@@ -10,6 +12,7 @@ import {
import { cn } from '@/utils/cn';
import { getProfileName } from '@/utils/getters';
import { round } from '@/utils/math';
import Link from 'next/link';
import { uniq } from 'ramda';
import type { IServiceCreateEventPayload } from '@mixan/db';
@@ -159,75 +162,164 @@ export function EventListItem(props: EventListItemProps) {
.filter((item) => typeof item.value === 'string' && item.value);
return (
<ExpandableListItem
className={cn(meta?.conversion && `bg-${meta.color}-50`)}
title={
<button onClick={() => setEvents((p) => uniq([...p, name]))}>
{name.split('_').join(' ')}
</button>
}
content={
<>
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
{profile?.id === props.deviceId && (
<KeyValueSubtle name="Anonymous" value={'Yes'} />
)}
{profile && (
<KeyValueSubtle
name="Profile"
value={getProfileName(profile)}
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
/>
)}
{path && (
<KeyValueSubtle
name="Path"
value={path}
onClick={() => {
setFilter('path', path);
}}
/>
)}
</>
}
image={<EventIcon name={name} meta={meta} projectId={projectId} />}
>
<div className="bg-white p-4">
{propertiesList.length > 0 && (
<div className="flex flex-col gap-4 mb-6">
<div className="font-medium">Your properties</div>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{propertiesList.map((item) => (
<KeyValue
key={item.name}
name={item.name}
value={item.value}
onClick={() => {
setFilter(
`properties.${item.name}`,
item.value ? String(item.value) : '',
'is'
);
}}
/>
))}
</div>
</div>
)}
<div className="flex flex-col gap-4">
<div className="font-medium">Properties</div>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{keyValueList.map((item) => (
<KeyValue
onClick={() => item.onClick?.()}
key={item.name}
name={item.name}
value={item.value}
/>
))}
</div>
<div className="p-4 flex flex-col gap-2 hover:bg-slate-50 rounded-lg transition-colors">
<div className="flex justify-between items-center">
<div className="flex gap-4 items-center">
<EventIcon name={name} meta={meta} projectId={projectId} />
<div className="font-semibold">{name.replace(/_/g, ' ')}</div>
</div>
<div className="text-muted-foreground">
{createdAt.toLocaleTimeString()}
</div>
</div>
</ExpandableListItem>
<div className="flex flex-wrap gap-2">
{path && <KeyValueSubtle name={'Path'} value={path} />}
{profile && (
<KeyValueSubtle
name={'Profile'}
value={
<>
{profile.avatar && <ProfileAvatar size="xs" {...profile} />}
{getProfileName(profile)}
</>
}
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
/>
)}
<KeyValueSubtle
name={'From'}
value={
<>
<SerieIcon name={country} />
{city}
</>
}
/>
<KeyValueSubtle
name={'Device'}
value={
<>
<SerieIcon name={device} />
{brand}
</>
}
/>
{browser !== 'WebKit' && browser !== '' && (
<KeyValueSubtle
name={'Browser'}
value={
<>
<SerieIcon name={browser} />
{browser}
</>
}
/>
)}
{/* {!!profile && (
<div className="flex gap-2 items-center mb-1">
<ProfileAvatar size="xs" {...profile} />
<Link
className="font-medium text-sm text-muted-foreground"
href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
>
{getProfileName(profile)}
</Link>
</div>
)}
<div className="[&>span]:inline text-slate-700">
<span
className={cn(
'font-medium bg-muted p-0.5 px-1 leading-none rounded-md',
meta?.conversion && `bg-${meta.color}-100 text-${meta.color}-800`
)}
>
{meta?.conversion && '⭐️ '}
{name}
</span>
<span className="text-muted-foreground">{' at '}</span>
<span>{path}</span>
<span className="text-muted-foreground">{' from '}</span>
<span>
{city || 'Unknown'}, {country}
</span>
<span className="text-muted-foreground">{' using '}</span>
<span className="!inline-flex items-center gap-1">
{brand || device} <SerieIcon name={device} />
</span>
</div> */}
</div>
</div>
);
// return (
// <ExpandableListItem
// className={cn(meta?.conversion && `bg-${meta.color}-50`)}
// title={
// <button onClick={() => setEvents((p) => uniq([...p, name]))}>
// {name.split('_').join(' ')}
// </button>
// }
// content={
// <>
// <KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
// {profile?.id === props.deviceId && (
// <KeyValueSubtle name="Anonymous" value={'Yes'} />
// )}
// {profile && (
// <KeyValueSubtle
// name="Profile"
// value={getProfileName(profile)}
// href={`/${params.organizationId}/${params.projectId}/profiles/${profile.id}`}
// />
// )}
// {path && (
// <KeyValueSubtle
// name="Path"
// value={path}
// onClick={() => {
// setFilter('path', path);
// }}
// />
// )}
// </>
// }
// image={<EventIcon name={name} meta={meta} projectId={projectId} />}
// >
// <div className="bg-white p-4">
// {propertiesList.length > 0 && (
// <div className="flex flex-col gap-4 mb-6">
// <div className="font-medium">Your properties</div>
// <div className="flex flex-wrap gap-x-4 gap-y-2">
// {propertiesList.map((item) => (
// <KeyValue
// key={item.name}
// name={item.name}
// value={item.value}
// onClick={() => {
// setFilter(
// `properties.${item.name}`,
// item.value ? String(item.value) : '',
// 'is'
// );
// }}
// />
// ))}
// </div>
// </div>
// )}
// <div className="flex flex-col gap-4">
// <div className="font-medium">Properties</div>
// <div className="flex flex-wrap gap-x-4 gap-y-2">
// {keyValueList.map((item) => (
// <KeyValue
// onClick={() => item.onClick?.()}
// key={item.name}
// name={item.name}
// value={item.value}
// />
// ))}
// </div>
// </div>
// </div>
// </ExpandableListItem>
// );
}

View File

@@ -60,14 +60,14 @@ export function EventList({ data, count }: EventListProps) {
count={count}
take={50}
/>
<div className="flex flex-col gap-4 my-4">
<div className="flex flex-col my-4 card p-4">
{data.map((item, index, list) => (
<Fragment key={item.id}>
{showDateHeader(
item.createdAt,
list[index - 1]?.createdAt
) && (
<div className="font-medium text-xs [&:not(:first-child)]:mt-12">
<div className="text-muted-foreground font-medium text-sm [&:not(:first-child)]:mt-12 text-center">
{item.createdAt.toLocaleDateString()}
</div>
)}

View File

@@ -19,6 +19,7 @@ export default function LayoutProjectSelector({
return (
<div>
<Combobox
portal
align="end"
className="w-auto min-w-0 max-sm:max-w-[100px]"
placeholder={'Select project'}

View File

@@ -18,7 +18,7 @@ interface ProfileAvatarProps
const variants = cva('', {
variants: {
size: {
default: 'h-12 w-12 rounded-xl [&>span]:rounded-xl',
default: 'h-12 w-12 rounded-full [&>span]:rounded-full',
sm: 'h-6 w-6 rounded [&>span]:rounded',
xs: 'h-4 w-4 rounded [&>span]:rounded',
},

View File

@@ -32,7 +32,9 @@ const createImageIcon = (url: string) => {
const createFlagIcon = (url: string) => {
return function (props: LucideProps) {
return <span className={`rounded fi fi-${url}`}></span>;
return (
<span className={`rounded !block !leading-[1rem] fi fi-${url}`}></span>
);
} as LucideIcon;
};

View File

@@ -36,6 +36,7 @@ export interface ComboboxProps<T> {
size?: ButtonProps['size'];
label?: string;
align?: 'start' | 'end' | 'center';
portal?: boolean;
}
export type ExtendedComboboxProps<T> = Omit<
@@ -57,6 +58,7 @@ export function Combobox<T extends string>({
icon: Icon,
size,
align = 'start',
portal,
}: ComboboxProps<T>) {
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState('');
@@ -87,7 +89,11 @@ export function Combobox<T extends string>({
</Button>
)}
</PopoverTrigger>
<PopoverContent className="w-full max-w-md p-0" align={align}>
<PopoverContent
className="w-full max-w-md p-0"
align={align}
portal={portal}
>
<Command>
{searchable === true && (
<CommandInput

View File

@@ -45,7 +45,7 @@ export function KeyValueSubtle({ href, onClick, name, value }: KeyValueProps) {
<div className="text-gray-400">{name}</div>
<div
className={cn(
'bg-black/5 rounded p-0.5 px-1 sm:p-1 sm:px-2 text-gray-600 whitespace-nowrap overflow-hidden text-ellipsis',
'bg-black/5 rounded p-0.5 px-1 sm:p-1 sm:px-2 text-gray-600 whitespace-nowrap text-ellipsis flex items-center gap-1',
clickable && 'group-hover:underline'
)}
>

View File

@@ -8,22 +8,32 @@ const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
style={{
boxShadow: '0 0 0 9000px rgba(0, 0, 0, 0.05)',
}}
{...props}
/>
));
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
portal?: boolean;
}
>(({ className, align = 'center', sideOffset = 4, portal, ...props }, ref) => {
const node = (
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
style={{
boxShadow: '0 0 0 9000px rgba(0, 0, 0, 0.05)',
}}
{...props}
/>
);
if (portal) {
return <PopoverPrimitive.Portal>{node}</PopoverPrimitive.Portal>;
}
return node;
});
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View File

@@ -38,7 +38,7 @@ const twColors = [
'grey',
'slate',
];
const twColorVariants = ['50', '100', '200', '700'];
const twColorVariants = ['50', '100', '200', '700', '800', '900'];
/** @type {import('tailwindcss').Config} */
const config = {