fix: improvements for frontend
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { getChartColor, getChartTranslucentColor } from '@/utils/theme';
|
||||
import { Bar } from 'recharts';
|
||||
|
||||
type Options = {
|
||||
@@ -11,6 +12,26 @@ export const BarWithBorder = (options: Options) => {
|
||||
return (props: any) => {
|
||||
const { x, y, width, height, value, isActive } = props;
|
||||
|
||||
const fill =
|
||||
options.fill === 'props'
|
||||
? props.fill
|
||||
: isActive
|
||||
? options.active.fill
|
||||
: options.fill;
|
||||
const border =
|
||||
options.border === 'props'
|
||||
? props.stroke
|
||||
: isActive
|
||||
? options.active.border
|
||||
: options.border;
|
||||
|
||||
const withActive = (color: string) => {
|
||||
if (color.startsWith('rgba')) {
|
||||
return isActive ? color.replace(/, 0.\d+\)$/, ', 0.4)') : color;
|
||||
}
|
||||
return color;
|
||||
};
|
||||
|
||||
return (
|
||||
<g>
|
||||
<rect
|
||||
@@ -19,16 +40,18 @@ export const BarWithBorder = (options: Options) => {
|
||||
width={width}
|
||||
height={height}
|
||||
stroke="none"
|
||||
fill={isActive ? options.active.fill : options.fill}
|
||||
fill={withActive(fill)}
|
||||
rx={3}
|
||||
/>
|
||||
{value > 0 && (
|
||||
<rect
|
||||
x={x}
|
||||
y={y - options.borderHeight - 2}
|
||||
y={y - options.borderHeight - 1}
|
||||
width={width}
|
||||
height={options.borderHeight}
|
||||
stroke="none"
|
||||
fill={isActive ? options.active.border : options.border}
|
||||
fill={withActive(border)}
|
||||
rx={2}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
@@ -54,3 +77,24 @@ export const BarShapeBlue = BarWithBorder({
|
||||
fill: 'rgba(59, 121, 255, 0.4)',
|
||||
},
|
||||
});
|
||||
export const BarShapeProps = BarWithBorder({
|
||||
borderHeight: 2,
|
||||
border: 'props',
|
||||
fill: 'props',
|
||||
active: {
|
||||
border: 'props',
|
||||
fill: 'props',
|
||||
},
|
||||
});
|
||||
|
||||
const BarShapes = [...new Array(13)].map((_, index) =>
|
||||
BarWithBorder({
|
||||
borderHeight: 2,
|
||||
border: getChartColor(index),
|
||||
fill: getChartTranslucentColor(index),
|
||||
active: {
|
||||
border: getChartColor(index),
|
||||
fill: getChartTranslucentColor(index),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { formatDateTime, formatTime } from '@/utils/date';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { isToday } from 'date-fns';
|
||||
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import CopyInput from '@/components/forms/copy-input';
|
||||
import { createActionColumn } from '@/components/ui/data-table/data-table-helpers';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
@@ -30,11 +31,10 @@ export function useColumns() {
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Created at',
|
||||
cell({ row }) {
|
||||
const date = row.original.createdAt;
|
||||
return (
|
||||
<div>{isToday(date) ? formatTime(date) : formatDateTime(date)}</div>
|
||||
);
|
||||
size: ColumnCreatedAt.size,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
return <ColumnCreatedAt>{item.createdAt}</ColumnCreatedAt>;
|
||||
},
|
||||
},
|
||||
createActionColumn(({ row }) => {
|
||||
|
||||
18
apps/start/src/components/column-created-at.tsx
Normal file
18
apps/start/src/components/column-created-at.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { formatDateTime, timeAgo } from '@/utils/date';
|
||||
|
||||
export function ColumnCreatedAt({ children }: { children: Date | string }) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 opacity-0 group-hover/row:opacity-100 transition-opacity duration-100">
|
||||
{formatDateTime(
|
||||
typeof children === 'string' ? new Date(children) : children,
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground group-hover/row:opacity-0 transition-opacity duration-100">
|
||||
{timeAgo(typeof children === 'string' ? new Date(children) : children)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ColumnCreatedAt.size = 150;
|
||||
@@ -8,7 +8,8 @@ import { useDebounceState } from '@/hooks/use-debounce-state';
|
||||
import useWS from '@/hooks/use-ws';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import type { IServiceEventMinimal } from '@openpanel/db';
|
||||
import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db';
|
||||
import { useParams } from '@tanstack/react-router';
|
||||
import { AnimatedNumber } from '../animated-number';
|
||||
|
||||
export default function EventListener({
|
||||
@@ -16,13 +17,24 @@ export default function EventListener({
|
||||
}: {
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const params = useParams({
|
||||
strict: false,
|
||||
});
|
||||
const { projectId } = useAppParams();
|
||||
const counter = useDebounceState(0, 1000);
|
||||
|
||||
useWS<IServiceEventMinimal>(
|
||||
useWS<IServiceEventMinimal | IServiceEvent>(
|
||||
`/live/events/${projectId}`,
|
||||
(event) => {
|
||||
if (event?.name) {
|
||||
if (event) {
|
||||
const isProfilePage = !!params?.profileId;
|
||||
if (isProfilePage) {
|
||||
const profile = 'profile' in event ? event.profile : null;
|
||||
if (profile?.id === params?.profileId) {
|
||||
counter.set((prev) => prev + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
counter.set((prev) => prev + 1);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,10 +3,11 @@ 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 { formatDateTime, formatTimeAgoOrDateTime } from '@/utils/date';
|
||||
import { formatDateTime, formatTimeAgoOrDateTime, timeAgo } from '@/utils/date';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import { KeyValueGrid } from '@/components/ui/key-value-grid';
|
||||
import type { IServiceEvent } from '@openpanel/db';
|
||||
|
||||
@@ -16,19 +17,10 @@ export function useColumns() {
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Created at',
|
||||
size: 140,
|
||||
size: ColumnCreatedAt.size,
|
||||
cell: ({ row }) => {
|
||||
const session = row.original;
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 opacity-0 group-hover/row:opacity-100 transition-opacity duration-100">
|
||||
{formatDateTime(session.createdAt)}
|
||||
</div>
|
||||
<div className="text-muted-foreground group-hover/row:opacity-0 transition-opacity duration-100">
|
||||
{formatTimeAgoOrDateTime(session.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <ColumnCreatedAt>{session.createdAt}</ColumnCreatedAt>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
||||
import {
|
||||
OverviewFilterButton,
|
||||
OverviewFiltersButtons,
|
||||
} from '@/components/overview/filters/overview-filters-buttons';
|
||||
import { Skeleton } from '@/components/skeleton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useDataTableColumnVisibility } from '@/components/ui/data-table/data-table-hooks';
|
||||
@@ -9,24 +11,19 @@ import { DataTableViewOptions } from '@/components/ui/data-table/data-table-view
|
||||
import { useAppParams } from '@/hooks/use-app-params';
|
||||
import { pushModal } from '@/modals';
|
||||
import type { RouterInputs, RouterOutputs } from '@/trpc/client';
|
||||
import { arePropsEqual } from '@/utils/are-props-equal';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { IServiceEvent } from '@openpanel/db';
|
||||
import type { UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
import type { Table } from '@tanstack/react-table';
|
||||
import { getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import { Updater } from '@tanstack/react-table';
|
||||
import { ColumnOrderState } from '@tanstack/react-table';
|
||||
import { useWindowVirtualizer } from '@tanstack/react-virtual';
|
||||
import type { TRPCInfiniteData } from '@trpc/tanstack-react-query';
|
||||
import { format } from 'date-fns';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { CalendarIcon, Loader2Icon } from 'lucide-react';
|
||||
import { CalendarIcon, FilterIcon, Loader2Icon } from 'lucide-react';
|
||||
import { parseAsIsoDateTime, useQueryState } from 'nuqs';
|
||||
import { last } from 'ramda';
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { useInViewport } from 'react-in-viewport';
|
||||
import { useLocalStorage } from 'usehooks-ts';
|
||||
import EventListener from '../event-listener';
|
||||
import { useColumns } from './columns';
|
||||
|
||||
@@ -328,11 +325,7 @@ function EventsTableToolbar({
|
||||
? `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}`
|
||||
: 'Date range'}
|
||||
</Button>
|
||||
<OverviewFiltersDrawer
|
||||
mode="events"
|
||||
projectId={projectId}
|
||||
enableEventsFilter
|
||||
/>
|
||||
<OverviewFilterButton enableEventsFilter />
|
||||
<OverviewFiltersButtons className="justify-end p-0" />
|
||||
</div>
|
||||
<DataTableViewOptions table={table} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { formatDateTime, formatTime } from '@/utils/date';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { isToday } from 'date-fns';
|
||||
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import { ProjectLink } from '@/components/links';
|
||||
import { SerieIcon } from '@/components/report-chart/common/serie-icon';
|
||||
import { createHeaderColumn } from '@/components/ui/data-table/data-table-helpers';
|
||||
@@ -162,14 +163,10 @@ export function useColumns() {
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Created at',
|
||||
cell({ row }) {
|
||||
const date = row.original.createdAt;
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>{isToday(date) ? formatTime(date) : formatDateTime(date)}</div>
|
||||
);
|
||||
size: ColumnCreatedAt.size,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
return <ColumnCreatedAt>{item.createdAt}</ColumnCreatedAt>;
|
||||
},
|
||||
filterFn: 'isWithinRange',
|
||||
meta: {
|
||||
|
||||
@@ -3,10 +3,12 @@ import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/use-event-query-filters';
|
||||
import { pushModal } from '@/modals';
|
||||
import type { OverviewFiltersProps } from '@/modals/overview-filters';
|
||||
import { getPropertyLabel } from '@/translations/properties';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { operators } from '@openpanel/constants';
|
||||
import { X } from 'lucide-react';
|
||||
import { FilterIcon, X } from 'lucide-react';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
interface OverviewFiltersButtonsProps {
|
||||
@@ -14,6 +16,23 @@ interface OverviewFiltersButtonsProps {
|
||||
nuqsOptions?: NuqsOptions;
|
||||
}
|
||||
|
||||
export function OverviewFilterButton(props: OverviewFiltersProps) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
responsive
|
||||
icon={FilterIcon}
|
||||
onClick={() =>
|
||||
pushModal('OverviewFilters', {
|
||||
...props,
|
||||
})
|
||||
}
|
||||
>
|
||||
Filters
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverviewFiltersButtons({
|
||||
className,
|
||||
nuqsOptions,
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { useEventNames } from '@/hooks/use-event-names';
|
||||
import { useEventProperties } from '@/hooks/use-event-properties';
|
||||
import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/use-event-query-filters';
|
||||
import { useProfileProperties } from '@/hooks/use-profile-properties';
|
||||
import { useProfileValues } from '@/hooks/use-profile-values';
|
||||
import { usePropertyValues } from '@/hooks/use-property-values';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
import type {
|
||||
IChartEventFilter,
|
||||
IChartEventFilterOperator,
|
||||
IChartEventFilterValue,
|
||||
} from '@openpanel/validation';
|
||||
|
||||
import { ComboboxEvents } from '@/components/ui/combobox-events';
|
||||
import { OriginFilter } from './origin-filter';
|
||||
|
||||
export interface OverviewFiltersDrawerContentProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
enableEventsFilter?: boolean;
|
||||
mode: 'profiles' | 'events';
|
||||
}
|
||||
|
||||
const excludePropertyFilter = (name: string) => {
|
||||
return ['*', 'duration', 'created_at', 'has_profile'].includes(name);
|
||||
};
|
||||
|
||||
export function OverviewFiltersDrawerContent({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
enableEventsFilter,
|
||||
mode,
|
||||
}: OverviewFiltersDrawerContentProps) {
|
||||
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||
const [event, setEvent] = useEventQueryNamesFilter(nuqsOptions);
|
||||
const eventNames = useEventNames({ projectId });
|
||||
const eventProperties = useEventProperties({ projectId, event: event[0] });
|
||||
const profileProperties = useProfileProperties(projectId);
|
||||
const properties = mode === 'events' ? eventProperties : profileProperties;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SheetHeader className="mb-8">
|
||||
<SheetTitle>Overview filters</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="mt-8 flex flex-col rounded-md border bg-def-100">
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<OriginFilter />
|
||||
{enableEventsFilter && (
|
||||
<ComboboxEvents
|
||||
className="w-full"
|
||||
value={event}
|
||||
onChange={setEvent}
|
||||
multiple
|
||||
items={eventNames.filter(
|
||||
(item) => !excludePropertyFilter(item.name),
|
||||
)}
|
||||
placeholder="Select event"
|
||||
maxDisplayItems={2}
|
||||
/>
|
||||
)}
|
||||
<Combobox
|
||||
className="w-full"
|
||||
onChange={(value) => {
|
||||
setFilter(value, [], 'is');
|
||||
}}
|
||||
value=""
|
||||
placeholder="Filter by property"
|
||||
label="What do you want to filter by?"
|
||||
items={properties
|
||||
.filter((item) => item !== 'name')
|
||||
.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))}
|
||||
searchable
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
{filters
|
||||
.filter((filter) => filter.value[0] !== null)
|
||||
.map((filter) => {
|
||||
return mode === 'events' ? (
|
||||
<PureFilterItem
|
||||
className="border-t p-4 first:border-0"
|
||||
eventName="screen_view"
|
||||
key={filter.name}
|
||||
filter={filter}
|
||||
onRemove={() => {
|
||||
setFilter(filter.name, [], filter.operator);
|
||||
}}
|
||||
onChangeValue={(value) => {
|
||||
setFilter(filter.name, value, filter.operator);
|
||||
}}
|
||||
onChangeOperator={(operator) => {
|
||||
setFilter(filter.name, filter.value, operator);
|
||||
}}
|
||||
/>
|
||||
) : /* TODO: Implement profile filters */
|
||||
null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterOptionEvent({
|
||||
setFilter,
|
||||
projectId,
|
||||
...filter
|
||||
}: IChartEventFilter & {
|
||||
projectId: string;
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator,
|
||||
) => void;
|
||||
}) {
|
||||
const values = usePropertyValues({
|
||||
projectId,
|
||||
event: filter.name === 'path' ? 'screen_view' : 'session_start',
|
||||
property: filter.name,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div>{filter.name}</div>
|
||||
<Combobox
|
||||
className="flex-1"
|
||||
onChange={(value) => setFilter(filter.name, value, filter.operator)}
|
||||
placeholder={'Select a value'}
|
||||
items={values.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
}))}
|
||||
value={String(filter.value[0] ?? '')}
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
setFilter(filter.name, filter.value[0] ?? '', filter.operator)
|
||||
}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterOptionProfile({
|
||||
setFilter,
|
||||
projectId,
|
||||
...filter
|
||||
}: IChartEventFilter & {
|
||||
projectId: string;
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator,
|
||||
) => void;
|
||||
}) {
|
||||
const values = useProfileValues(projectId, filter.name);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div>{filter.name}</div>
|
||||
<Combobox
|
||||
className="flex-1"
|
||||
onChange={(value) => setFilter(filter.name, value, filter.operator)}
|
||||
placeholder={'Select a value'}
|
||||
items={values.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
}))}
|
||||
value={String(filter.value[0] ?? '')}
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
setFilter(filter.name, filter.value[0] ?? '', filter.operator)
|
||||
}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { FilterIcon } from 'lucide-react';
|
||||
|
||||
import type { OverviewFiltersDrawerContentProps } from './overview-filters-drawer-content';
|
||||
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
|
||||
|
||||
export function OverviewFiltersDrawer(
|
||||
props: OverviewFiltersDrawerContentProps,
|
||||
) {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" responsive icon={FilterIcon}>
|
||||
Filters
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full !max-w-lg" side="right">
|
||||
<OverviewFiltersDrawerContent {...props} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -4,46 +4,21 @@ import {
|
||||
isMinuteIntervalEnabledByRange,
|
||||
} from '@openpanel/constants';
|
||||
import { ClockIcon } from 'lucide-react';
|
||||
import { ReportInterval } from '../report/ReportInterval';
|
||||
import { Combobox } from '../ui/combobox';
|
||||
|
||||
export function OverviewInterval() {
|
||||
const { interval, setInterval, range } = useOverviewOptions();
|
||||
const { interval, setInterval, range, startDate, endDate } =
|
||||
useOverviewOptions();
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
className="hidden md:flex"
|
||||
icon={ClockIcon}
|
||||
placeholder="Interval"
|
||||
onChange={(value) => {
|
||||
setInterval(value);
|
||||
}}
|
||||
value={interval}
|
||||
items={[
|
||||
{
|
||||
value: 'minute',
|
||||
label: 'Minute',
|
||||
disabled: !isMinuteIntervalEnabledByRange(range),
|
||||
},
|
||||
{
|
||||
value: 'hour',
|
||||
label: 'Hour',
|
||||
disabled: !isHourIntervalEnabledByRange(range),
|
||||
},
|
||||
{
|
||||
value: 'day',
|
||||
label: 'Day',
|
||||
},
|
||||
{
|
||||
value: 'week',
|
||||
label: 'Week',
|
||||
},
|
||||
{
|
||||
value: 'month',
|
||||
label: 'Month',
|
||||
disabled:
|
||||
range === 'today' || range === 'lastHour' || range === '30min',
|
||||
},
|
||||
]}
|
||||
<ReportInterval
|
||||
interval={interval}
|
||||
onChange={setInterval}
|
||||
range={range}
|
||||
chartType="linear"
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { isToday } from 'date-fns';
|
||||
|
||||
import type { IServiceProfile } from '@openpanel/db';
|
||||
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import { ProfileAvatar } from '../profile-avatar';
|
||||
|
||||
export function useColumns(type: 'profiles' | 'power-users') {
|
||||
@@ -100,17 +101,10 @@ export function useColumns(type: 'profiles' | 'power-users') {
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Last seen',
|
||||
size: ColumnCreatedAt.size,
|
||||
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>
|
||||
);
|
||||
const item = row.original;
|
||||
return <ColumnCreatedAt>{item.createdAt}</ColumnCreatedAt>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -129,7 +129,7 @@ export function Chart({ data }: Props) {
|
||||
<ReferenceLine
|
||||
key={ref.id}
|
||||
x={ref.date.getTime()}
|
||||
stroke={'#94a3b8'}
|
||||
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
|
||||
strokeDasharray={'3 3'}
|
||||
label={{
|
||||
value: ref.title,
|
||||
|
||||
@@ -90,7 +90,7 @@ export function Chart({ data }: Props) {
|
||||
<ReferenceLine
|
||||
key={ref.id}
|
||||
x={ref.date.getTime()}
|
||||
stroke={'#94a3b8'}
|
||||
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
|
||||
strokeDasharray={'3 3'}
|
||||
label={{
|
||||
value: ref.title,
|
||||
@@ -114,7 +114,6 @@ export function Chart({ data }: Props) {
|
||||
strokeOpacity={0.5}
|
||||
/>
|
||||
<Line
|
||||
dot={false}
|
||||
dataKey="rate"
|
||||
stroke={getChartColor(0)}
|
||||
type={lineType}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { ChevronRightIcon, InfoIcon } from 'lucide-react';
|
||||
import { alphabetIds } from '@openpanel/constants';
|
||||
|
||||
import { createChartTooltip } from '@/components/charts/chart-tooltip';
|
||||
import { BarShapeBlue } from '@/components/charts/common-bar';
|
||||
import { BarShapeBlue, BarShapeProps } from '@/components/charts/common-bar';
|
||||
import { Tooltiper } from '@/components/ui/tooltip';
|
||||
import { WidgetTable } from '@/components/widget-table';
|
||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { getChartColor, getChartTranslucentColor } from '@/utils/theme';
|
||||
import { getPreviousMetric } from '@openpanel/common';
|
||||
import {
|
||||
Bar,
|
||||
@@ -327,9 +327,17 @@ export function Chart({ data }: { data: RouterOutputs['chart']['funnel'] }) {
|
||||
}
|
||||
/>
|
||||
<YAxis {...yAxisProps} />
|
||||
<Bar data={rechartData} dataKey="step:percent:0">
|
||||
<Bar
|
||||
data={rechartData}
|
||||
dataKey="step:percent:0"
|
||||
shape={<BarShapeProps />}
|
||||
>
|
||||
{rechartData.map((item, index) => (
|
||||
<Cell key={item.name} fill={getChartColor(index)} />
|
||||
<Cell
|
||||
key={item.name}
|
||||
fill={getChartTranslucentColor(index)}
|
||||
stroke={getChartColor(index)}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
<Tooltip />
|
||||
@@ -340,22 +348,30 @@ export function Chart({ data }: { data: RouterOutputs['chart']['funnel'] }) {
|
||||
);
|
||||
}
|
||||
|
||||
type Hej = RouterOutputs['chart']['funnel']['current'];
|
||||
|
||||
const { Tooltip, TooltipProvider } = createChartTooltip<
|
||||
RechartData,
|
||||
Record<string, unknown>
|
||||
>(({ data: dataArray }) => {
|
||||
{
|
||||
data: RouterOutputs['chart']['funnel']['current'];
|
||||
}
|
||||
>(({ data: dataArray, context, ...props }) => {
|
||||
const data = dataArray[0]!;
|
||||
const number = useNumber();
|
||||
const variants = Object.keys(data).filter((key) =>
|
||||
key.startsWith('step:data:'),
|
||||
) as `step:data:${number}`[];
|
||||
|
||||
const index = context.data[0].steps.findIndex(
|
||||
(step) => step.event.id === (data as any).id,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between gap-8 text-muted-foreground">
|
||||
<div>{data.name}</div>
|
||||
</div>
|
||||
{variants.map((key, index) => {
|
||||
{variants.map((key) => {
|
||||
const variant = data[key];
|
||||
const prevVariant = data[`prev_${key}`];
|
||||
if (!variant?.step) {
|
||||
|
||||
@@ -149,7 +149,7 @@ export function Chart({ data }: Props) {
|
||||
<ReferenceLine
|
||||
key={ref.id}
|
||||
x={ref.date.getTime()}
|
||||
stroke={'#94a3b8'}
|
||||
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
|
||||
strokeDasharray={'3 3'}
|
||||
label={{
|
||||
value: ref.title,
|
||||
|
||||
@@ -96,6 +96,8 @@ export default function ReportEditor({
|
||||
onChange={(newInterval) => dispatch(changeInterval(newInterval))}
|
||||
range={report.range}
|
||||
chartType={report.chartType}
|
||||
startDate={report.startDate}
|
||||
endDate={report.endDate}
|
||||
/>
|
||||
<ReportLineType className="min-w-0 flex-1" />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import { ClockIcon } from 'lucide-react';
|
||||
|
||||
import {
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { IChartRange, IChartType, IInterval } from '@openpanel/validation';
|
||||
import { differenceInDays, isSameDay } from 'date-fns';
|
||||
import { Button } from '../ui/button';
|
||||
import { CommandShortcut } from '../ui/command';
|
||||
import {
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
import { changeInterval } from './reportSlice';
|
||||
|
||||
interface ReportIntervalProps {
|
||||
className?: string;
|
||||
@@ -28,6 +27,8 @@ interface ReportIntervalProps {
|
||||
onChange: (range: IInterval) => void;
|
||||
chartType: IChartType;
|
||||
range: IChartRange;
|
||||
startDate?: string | null;
|
||||
endDate?: string | null;
|
||||
}
|
||||
export function ReportInterval({
|
||||
className,
|
||||
@@ -35,6 +36,8 @@ export function ReportInterval({
|
||||
onChange,
|
||||
chartType,
|
||||
range,
|
||||
startDate,
|
||||
endDate,
|
||||
}: ReportIntervalProps) {
|
||||
if (
|
||||
chartType !== 'linear' &&
|
||||
@@ -47,6 +50,11 @@ export function ReportInterval({
|
||||
return null;
|
||||
}
|
||||
|
||||
let isHourIntervalEnabled = isHourIntervalEnabledByRange(range);
|
||||
if (startDate && endDate && range === 'custom') {
|
||||
isHourIntervalEnabled = differenceInDays(endDate, startDate) <= 4;
|
||||
}
|
||||
|
||||
const items = [
|
||||
{
|
||||
value: 'minute',
|
||||
@@ -56,7 +64,7 @@ export function ReportInterval({
|
||||
{
|
||||
value: 'hour',
|
||||
label: 'Hour',
|
||||
disabled: !isHourIntervalEnabledByRange(range),
|
||||
disabled: !isHourIntervalEnabled,
|
||||
},
|
||||
{
|
||||
value: 'day',
|
||||
|
||||
@@ -24,6 +24,8 @@ interface PropertiesComboboxProps {
|
||||
label: string;
|
||||
description: string;
|
||||
}) => void;
|
||||
exclude?: string[];
|
||||
mode?: 'events' | 'profile';
|
||||
}
|
||||
|
||||
function SearchHeader({
|
||||
@@ -56,6 +58,8 @@ export function PropertiesCombobox({
|
||||
event,
|
||||
children,
|
||||
onSelect,
|
||||
mode,
|
||||
exclude = [],
|
||||
}: PropertiesComboboxProps) {
|
||||
const { projectId } = useAppParams();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -69,20 +73,35 @@ export function PropertiesCombobox({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setState('index');
|
||||
setState(!mode ? 'index' : mode === 'events' ? 'event' : 'profile');
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, mode]);
|
||||
|
||||
const shouldShowProperty = (property: string) => {
|
||||
return !exclude.find((ex) => {
|
||||
if (ex.endsWith('*')) {
|
||||
return property.startsWith(ex.slice(0, -1));
|
||||
}
|
||||
return property === ex;
|
||||
});
|
||||
};
|
||||
|
||||
// Mock data for the lists
|
||||
const profileActions = properties
|
||||
.filter((property) => property.startsWith('profile'))
|
||||
.filter(
|
||||
(property) =>
|
||||
property.startsWith('profile') && shouldShowProperty(property),
|
||||
)
|
||||
.map((property) => ({
|
||||
value: property,
|
||||
label: property.split('.').pop() ?? property,
|
||||
description: property.split('.').slice(0, -1).join('.'),
|
||||
}));
|
||||
const eventActions = properties
|
||||
.filter((property) => !property.startsWith('profile'))
|
||||
.filter(
|
||||
(property) =>
|
||||
!property.startsWith('profile') && shouldShowProperty(property),
|
||||
)
|
||||
.map((property) => ({
|
||||
value: property,
|
||||
label: property.split('.').pop() ?? property,
|
||||
@@ -142,7 +161,9 @@ export function PropertiesCombobox({
|
||||
return (
|
||||
<div className="col">
|
||||
<SearchHeader
|
||||
onBack={() => handleStateChange('index')}
|
||||
onBack={
|
||||
mode === undefined ? () => handleStateChange('index') : undefined
|
||||
}
|
||||
onSearch={setSearch}
|
||||
value={search}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SerieIcon } from '@/components/report-chart/common/serie-icon';
|
||||
import { formatDateTime, formatTimeAgoOrDateTime } from '@/utils/date';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import { round } from '@openpanel/common';
|
||||
import type { IServiceSession } from '@openpanel/db';
|
||||
@@ -29,19 +30,10 @@ export function useColumns() {
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Started',
|
||||
size: 140,
|
||||
size: ColumnCreatedAt.size,
|
||||
cell: ({ row }) => {
|
||||
const session = row.original;
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 opacity-0 group-hover/row:opacity-100 transition-opacity duration-100">
|
||||
{formatDateTime(session.createdAt)}
|
||||
</div>
|
||||
<div className="text-muted-foreground group-hover/row:opacity-0 transition-opacity duration-100">
|
||||
{formatTimeAgoOrDateTime(session.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const item = row.original;
|
||||
return <ColumnCreatedAt>{item.createdAt}</ColumnCreatedAt>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ColumnDef, Row } from '@tanstack/react-table';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import { createActionColumn } from '@/components/ui/data-table/data-table-helpers';
|
||||
import type { RouterOutputs } from '@/trpc/client';
|
||||
import { clipboard } from '@/utils/clipboard';
|
||||
@@ -39,13 +40,11 @@ export function useColumns(): ColumnDef<
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Created',
|
||||
cell: ({ row }) => (
|
||||
<TooltipComplete
|
||||
content={new Date(row.original.createdAt).toLocaleString()}
|
||||
>
|
||||
{new Date(row.original.createdAt).toLocaleDateString()}
|
||||
</TooltipComplete>
|
||||
),
|
||||
size: ColumnCreatedAt.size,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
return <ColumnCreatedAt>{item.createdAt}</ColumnCreatedAt>;
|
||||
},
|
||||
meta: {
|
||||
label: 'Created',
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { ColumnCreatedAt } from '@/components/column-created-at';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { createActionColumn } from '@/components/ui/data-table/data-table-helpers';
|
||||
import { pushModal } from '@/modals';
|
||||
@@ -52,13 +53,11 @@ export function useColumns() {
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Created',
|
||||
cell: ({ row }) => (
|
||||
<TooltipComplete
|
||||
content={new Date(row.original.createdAt).toLocaleString()}
|
||||
>
|
||||
{new Date(row.original.createdAt).toLocaleDateString()}
|
||||
</TooltipComplete>
|
||||
),
|
||||
size: ColumnCreatedAt.size,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
return <ColumnCreatedAt>{item.createdAt}</ColumnCreatedAt>;
|
||||
},
|
||||
meta: {
|
||||
label: 'Created',
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors group/row',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user