diff --git a/apps/start/package.json b/apps/start/package.json
index b9de5ab5..4ab99a63 100644
--- a/apps/start/package.json
+++ b/apps/start/package.json
@@ -3,7 +3,6 @@
"private": true,
"type": "module",
"scripts": {
- "testing": "pnpm dev",
"dev": "pnpm with-env vite dev --port 3000",
"start_deprecated": "pnpm with-env node .output/server/index.mjs",
"preview": "vite preview",
diff --git a/apps/start/src/components/charts/common-bar.tsx b/apps/start/src/components/charts/common-bar.tsx
index dc74bb83..12ff7224 100644
--- a/apps/start/src/components/charts/common-bar.tsx
+++ b/apps/start/src/components/charts/common-bar.tsx
@@ -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 (
{
width={width}
height={height}
stroke="none"
- fill={isActive ? options.active.fill : options.fill}
+ fill={withActive(fill)}
+ rx={3}
/>
{value > 0 && (
)}
@@ -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),
+ },
+ }),
+);
diff --git a/apps/start/src/components/clients/table/columns.tsx b/apps/start/src/components/clients/table/columns.tsx
index 6a2c22d4..adc707c3 100644
--- a/apps/start/src/components/clients/table/columns.tsx
+++ b/apps/start/src/components/clients/table/columns.tsx
@@ -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 (
-
{isToday(date) ? formatTime(date) : formatDateTime(date)}
- );
+ size: ColumnCreatedAt.size,
+ cell: ({ row }) => {
+ const item = row.original;
+ return {item.createdAt};
},
},
createActionColumn(({ row }) => {
diff --git a/apps/start/src/components/column-created-at.tsx b/apps/start/src/components/column-created-at.tsx
new file mode 100644
index 00000000..92fd22cb
--- /dev/null
+++ b/apps/start/src/components/column-created-at.tsx
@@ -0,0 +1,18 @@
+import { formatDateTime, timeAgo } from '@/utils/date';
+
+export function ColumnCreatedAt({ children }: { children: Date | string }) {
+ return (
+
+
+ {formatDateTime(
+ typeof children === 'string' ? new Date(children) : children,
+ )}
+
+
+ {timeAgo(typeof children === 'string' ? new Date(children) : children)}
+
+
+ );
+}
+
+ColumnCreatedAt.size = 150;
diff --git a/apps/start/src/components/events/event-listener.tsx b/apps/start/src/components/events/event-listener.tsx
index de5e5ed9..defabb7d 100644
--- a/apps/start/src/components/events/event-listener.tsx
+++ b/apps/start/src/components/events/event-listener.tsx
@@ -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(
+ useWS(
`/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);
}
},
diff --git a/apps/start/src/components/events/table/columns.tsx b/apps/start/src/components/events/table/columns.tsx
index a1725ae4..f801d221 100644
--- a/apps/start/src/components/events/table/columns.tsx
+++ b/apps/start/src/components/events/table/columns.tsx
@@ -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 (
-
-
- {formatDateTime(session.createdAt)}
-
-
- {formatTimeAgoOrDateTime(session.createdAt)}
-
-
- );
+ return {session.createdAt};
},
},
{
diff --git a/apps/start/src/components/events/table/index.tsx b/apps/start/src/components/events/table/index.tsx
index ac4e1742..af06e9f7 100644
--- a/apps/start/src/components/events/table/index.tsx
+++ b/apps/start/src/components/events/table/index.tsx
@@ -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'}
-
+
diff --git a/apps/start/src/components/notifications/table/columns.tsx b/apps/start/src/components/notifications/table/columns.tsx
index 00129e33..ac97ed26 100644
--- a/apps/start/src/components/notifications/table/columns.tsx
+++ b/apps/start/src/components/notifications/table/columns.tsx
@@ -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 (
- {isToday(date) ? formatTime(date) : formatDateTime(date)}
- );
+ size: ColumnCreatedAt.size,
+ cell: ({ row }) => {
+ const item = row.original;
+ return {item.createdAt};
},
filterFn: 'isWithinRange',
meta: {
diff --git a/apps/start/src/components/overview/filters/overview-filters-buttons.tsx b/apps/start/src/components/overview/filters/overview-filters-buttons.tsx
index cbffb171..27a27741 100644
--- a/apps/start/src/components/overview/filters/overview-filters-buttons.tsx
+++ b/apps/start/src/components/overview/filters/overview-filters-buttons.tsx
@@ -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 (
+
+ );
+}
+
export function OverviewFiltersButtons({
className,
nuqsOptions,
diff --git a/apps/start/src/components/overview/filters/overview-filters-drawer-content.tsx b/apps/start/src/components/overview/filters/overview-filters-drawer-content.tsx
deleted file mode 100644
index 1c9f03e1..00000000
--- a/apps/start/src/components/overview/filters/overview-filters-drawer-content.tsx
+++ /dev/null
@@ -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 (
-
-
- Overview filters
-
-
-
-
-
- {enableEventsFilter && (
- !excludePropertyFilter(item.name),
- )}
- placeholder="Select event"
- maxDisplayItems={2}
- />
- )}
- {
- 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"
- />
-
- {filters
- .filter((filter) => filter.value[0] !== null)
- .map((filter) => {
- return mode === 'events' ? (
-
{
- 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;
- })}
-
-
- );
-}
-
-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 (
-
-
{filter.name}
-
setFilter(filter.name, value, filter.operator)}
- placeholder={'Select a value'}
- items={values.map((value) => ({
- value,
- label: value,
- }))}
- value={String(filter.value[0] ?? '')}
- />
-
-
- );
-}
-
-export function FilterOptionProfile({
- setFilter,
- projectId,
- ...filter
-}: IChartEventFilter & {
- projectId: string;
- setFilter: (
- name: string,
- value: IChartEventFilterValue,
- operator: IChartEventFilterOperator,
- ) => void;
-}) {
- const values = useProfileValues(projectId, filter.name);
-
- return (
-
-
{filter.name}
-
setFilter(filter.name, value, filter.operator)}
- placeholder={'Select a value'}
- items={values.map((value) => ({
- value,
- label: value,
- }))}
- value={String(filter.value[0] ?? '')}
- />
-
-
- );
-}
diff --git a/apps/start/src/components/overview/filters/overview-filters-drawer.tsx b/apps/start/src/components/overview/filters/overview-filters-drawer.tsx
deleted file mode 100644
index 8f94f9bb..00000000
--- a/apps/start/src/components/overview/filters/overview-filters-drawer.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/start/src/components/overview/overview-interval.tsx b/apps/start/src/components/overview/overview-interval.tsx
index ba4984cc..aa0b7cf1 100644
--- a/apps/start/src/components/overview/overview-interval.tsx
+++ b/apps/start/src/components/overview/overview-interval.tsx
@@ -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 (
- {
- 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',
- },
- ]}
+
);
}
diff --git a/apps/start/src/components/profiles/table/columns.tsx b/apps/start/src/components/profiles/table/columns.tsx
index cef783d8..40cbf424 100644
--- a/apps/start/src/components/profiles/table/columns.tsx
+++ b/apps/start/src/components/profiles/table/columns.tsx
@@ -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 (
-
-
- {isToday(profile.createdAt)
- ? formatTime(profile.createdAt)
- : formatDateTime(profile.createdAt)}
-
-
- );
+ const item = row.original;
+ return {item.createdAt};
},
},
];
diff --git a/apps/start/src/components/report-chart/area/chart.tsx b/apps/start/src/components/report-chart/area/chart.tsx
index cbe20c4e..155d28b8 100644
--- a/apps/start/src/components/report-chart/area/chart.tsx
+++ b/apps/start/src/components/report-chart/area/chart.tsx
@@ -129,7 +129,7 @@ export function Chart({ data }: Props) {
-
+ }
+ >
{rechartData.map((item, index) => (
- |
+ |
))}
@@ -340,22 +348,30 @@ export function Chart({ data }: { data: RouterOutputs['chart']['funnel'] }) {
);
}
+type Hej = RouterOutputs['chart']['funnel']['current'];
+
const { Tooltip, TooltipProvider } = createChartTooltip<
RechartData,
- Record
->(({ 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 (
<>
- {variants.map((key, index) => {
+ {variants.map((key) => {
const variant = data[key];
const prevVariant = data[`prev_${key}`];
if (!variant?.step) {
diff --git a/apps/start/src/components/report-chart/line/chart.tsx b/apps/start/src/components/report-chart/line/chart.tsx
index a2fae48a..0e4a25c3 100644
--- a/apps/start/src/components/report-chart/line/chart.tsx
+++ b/apps/start/src/components/report-chart/line/chart.tsx
@@ -149,7 +149,7 @@ export function Chart({ data }: Props) {
dispatch(changeInterval(newInterval))}
range={report.range}
chartType={report.chartType}
+ startDate={report.startDate}
+ endDate={report.endDate}
/>
diff --git a/apps/start/src/components/report/ReportInterval.tsx b/apps/start/src/components/report/ReportInterval.tsx
index b3920842..216892df 100644
--- a/apps/start/src/components/report/ReportInterval.tsx
+++ b/apps/start/src/components/report/ReportInterval.tsx
@@ -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',
diff --git a/apps/start/src/components/report/sidebar/PropertiesCombobox.tsx b/apps/start/src/components/report/sidebar/PropertiesCombobox.tsx
index 20fa2c15..99637fcf 100644
--- a/apps/start/src/components/report/sidebar/PropertiesCombobox.tsx
+++ b/apps/start/src/components/report/sidebar/PropertiesCombobox.tsx
@@ -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 (
handleStateChange('index')}
+ onBack={
+ mode === undefined ? () => handleStateChange('index') : undefined
+ }
onSearch={setSearch}
value={search}
/>
diff --git a/apps/start/src/components/sessions/table/columns.tsx b/apps/start/src/components/sessions/table/columns.tsx
index 53302198..33f11bc7 100644
--- a/apps/start/src/components/sessions/table/columns.tsx
+++ b/apps/start/src/components/sessions/table/columns.tsx
@@ -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 (
-
-
- {formatDateTime(session.createdAt)}
-
-
- {formatTimeAgoOrDateTime(session.createdAt)}
-
-
- );
+ const item = row.original;
+ return {item.createdAt};
},
},
{
diff --git a/apps/start/src/components/settings/invites/columns.tsx b/apps/start/src/components/settings/invites/columns.tsx
index 36264ead..d335a1cf 100644
--- a/apps/start/src/components/settings/invites/columns.tsx
+++ b/apps/start/src/components/settings/invites/columns.tsx
@@ -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 }) => (
-
- {new Date(row.original.createdAt).toLocaleDateString()}
-
- ),
+ size: ColumnCreatedAt.size,
+ cell: ({ row }) => {
+ const item = row.original;
+ return {item.createdAt};
+ },
meta: {
label: 'Created',
},
diff --git a/apps/start/src/components/settings/members/columns.tsx b/apps/start/src/components/settings/members/columns.tsx
index 57ed5fda..91572566 100644
--- a/apps/start/src/components/settings/members/columns.tsx
+++ b/apps/start/src/components/settings/members/columns.tsx
@@ -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 }) => (
-
- {new Date(row.original.createdAt).toLocaleDateString()}
-
- ),
+ size: ColumnCreatedAt.size,
+ cell: ({ row }) => {
+ const item = row.original;
+ return {item.createdAt};
+ },
meta: {
label: 'Created',
},
diff --git a/apps/start/src/components/ui/table.tsx b/apps/start/src/components/ui/table.tsx
index 04e90c0d..cbd7b7d6 100644
--- a/apps/start/src/components/ui/table.tsx
+++ b/apps/start/src/components/ui/table.tsx
@@ -69,7 +69,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
filter.value[0] !== null);
+ return (
+
+
+
+
+ {enableEventsFilter && (
+
+ )}
+
+
+
+ {selectedFilters.map((filter) => {
+ return (
+
{
+ setFilter(filter.name, [], filter.operator);
+ }}
+ onChangeValue={(value) => {
+ setFilter(filter.name, value, filter.operator);
+ }}
+ onChangeOperator={(operator) => {
+ setFilter(filter.name, filter.value, operator);
+ }}
+ />
+ );
+ })}
+
+
{
+ setFilter(action.value, [], 'is');
+ }}
+ >
+ {(setOpen) => (
+
+ )}
+
+
+
+ );
+}
+
+export function FilterOptionProfile({
+ setFilter,
+ projectId,
+ ...filter
+}: IChartEventFilter & {
+ projectId: string;
+ setFilter: (
+ name: string,
+ value: IChartEventFilterValue,
+ operator: IChartEventFilterOperator,
+ ) => void;
+}) {
+ const values = useProfileValues(projectId, filter.name);
+
+ return (
+
+
{filter.name}
+
setFilter(filter.name, value, filter.operator)}
+ placeholder={'Select a value'}
+ items={values.map((value) => ({
+ value,
+ label: value,
+ }))}
+ value={String(filter.value[0] ?? '')}
+ />
+
+
+ );
+}
diff --git a/apps/start/src/modals/share-overview-modal.tsx b/apps/start/src/modals/share-overview-modal.tsx
index c705035b..5ee503d2 100644
--- a/apps/start/src/modals/share-overview-modal.tsx
+++ b/apps/start/src/modals/share-overview-modal.tsx
@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/use-app-params';
import { handleError } from '@/integrations/trpc/react';
import { zodResolver } from '@hookform/resolvers/zod';
-import { useRouter } from '@tanstack/react-router';
+import { useNavigate } from '@tanstack/react-router';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import type { z } from 'zod';
@@ -22,6 +22,7 @@ type IForm = z.infer;
export default function ShareOverviewModal() {
const { projectId, organizationId } = useAppParams();
+ const navigate = useNavigate();
const { register, handleSubmit } = useForm({
resolver: zodResolver(validator),
@@ -44,6 +45,16 @@ export default function ShareOverviewModal() {
description: `Your overview is now ${
res.public ? 'public' : 'private'
}`,
+ action: {
+ label: 'View',
+ onClick: () =>
+ navigate({
+ to: '/share/overview/$shareId',
+ params: {
+ shareId: res.id,
+ },
+ }),
+ },
});
popModal();
},
diff --git a/apps/start/src/routes/_app.$organizationId.$projectId.tsx b/apps/start/src/routes/_app.$organizationId.$projectId.tsx
index fa9e40dc..25837b31 100644
--- a/apps/start/src/routes/_app.$organizationId.$projectId.tsx
+++ b/apps/start/src/routes/_app.$organizationId.$projectId.tsx
@@ -1,5 +1,7 @@
-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 { LiveCounter } from '@/components/overview/live-counter';
import { OverviewInterval } from '@/components/overview/overview-interval';
import OverviewMetrics from '@/components/overview/overview-metrics';
@@ -35,7 +37,7 @@ function ProjectDashboard() {
-
+
diff --git a/apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.conversions.tsx b/apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.conversions.tsx
index 5125067e..49781aa4 100644
--- a/apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.conversions.tsx
+++ b/apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.conversions.tsx
@@ -1,4 +1,5 @@
import { EventsTable } from '@/components/events/table';
+import { useEventQueryNamesFilter } from '@/hooks/use-event-query-filters';
import { useTRPC } from '@/integrations/trpc/react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
@@ -18,12 +19,14 @@ function Component() {
parseAsIsoDateTime,
);
const [endDate, setEndDate] = useQueryState('endDate', parseAsIsoDateTime);
+ const [eventNames] = useEventQueryNamesFilter();
const query = useInfiniteQuery(
trpc.event.conversions.infiniteQueryOptions(
{
projectId,
startDate: startDate || undefined,
endDate: endDate || undefined,
+ events: eventNames,
},
{
getNextPageParam: (lastPage) => lastPage.meta.next,
diff --git a/apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.stats.tsx b/apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.stats.tsx
index 867271cc..928e52c5 100644
--- a/apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.stats.tsx
+++ b/apps/start/src/routes/_app.$organizationId.$projectId_.events._tabs.stats.tsx
@@ -1,5 +1,7 @@
-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 { ReportChartShortcut } from '@/components/report-chart/shortcut';
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import {
@@ -34,11 +36,7 @@ function Component() {
return (
-
+
diff --git a/apps/start/src/routes/_app.$organizationId.$projectId_.pages.tsx b/apps/start/src/routes/_app.$organizationId.$projectId_.pages.tsx
index 0b2f6ad3..11b6e3e7 100644
--- a/apps/start/src/routes/_app.$organizationId.$projectId_.pages.tsx
+++ b/apps/start/src/routes/_app.$organizationId.$projectId_.pages.tsx
@@ -1,5 +1,5 @@
import { FullPageEmptyState } from '@/components/full-page-empty-state';
-import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
+import { OverviewFilterButton } from '@/components/overview/filters/overview-filters-buttons';
import { OverviewInterval } from '@/components/overview/overview-interval';
import { OverviewRange } from '@/components/overview/overview-range';
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
@@ -79,7 +79,7 @@ function Component() {
-
+
[] = [
{
accessorKey: 'createdAt',
header: createHeaderColumn('Created at'),
- cell({ row }) {
- const date = row.original.createdAt;
- return formatDate(date);
+ size: ColumnCreatedAt.size,
+ cell: ({ row }) => {
+ const item = row.original;
+ return {item.createdAt};
},
filterFn: 'isWithinRange',
sortingFn: 'datetime',
diff --git a/apps/start/src/utils/date.ts b/apps/start/src/utils/date.ts
index 36163b07..46f6d638 100644
--- a/apps/start/src/utils/date.ts
+++ b/apps/start/src/utils/date.ts
@@ -32,6 +32,8 @@ export function formatDateTime(date: Date) {
hour: '2-digit',
minute: '2-digit',
hour12: false,
+ year:
+ date.getFullYear() === new Date().getFullYear() ? undefined : 'numeric',
}).format(date);
return `${datePart}, ${timePart}`;
diff --git a/apps/start/src/utils/theme.ts b/apps/start/src/utils/theme.ts
index 419bedd1..c70f4931 100644
--- a/apps/start/src/utils/theme.ts
+++ b/apps/start/src/utils/theme.ts
@@ -7,27 +7,25 @@
// export const theme = resolvedTailwindConfig.theme as Record;
const chartColors = [
- '#2563EB',
- '#ff7557',
- '#7fe1d8',
- '#f8bc3c',
- '#b3596e',
- '#72bef4',
- '#ffb27a',
- '#0f7ea0',
- '#3ba974',
- '#febbb2',
- '#cb80dc',
- '#5cb7af',
- '#7856ff',
+ { main: '#2563EB', translucent: 'rgba(37, 99, 235, 0.1)' },
+ { main: '#ff7557', translucent: 'rgba(255, 117, 87, 0.1)' },
+ { main: '#7fe1d8', translucent: 'rgba(127, 225, 216, 0.1)' },
+ { main: '#f8bc3c', translucent: 'rgba(248, 188, 60, 0.1)' },
+ { main: '#b3596e', translucent: 'rgba(179, 89, 110, 0.1)' },
+ { main: '#72bef4', translucent: 'rgba(114, 190, 244, 0.1)' },
+ { main: '#ffb27a', translucent: 'rgba(255, 178, 122, 0.1)' },
+ { main: '#0f7ea0', translucent: 'rgba(15, 126, 160, 0.1)' },
+ { main: '#3ba974', translucent: 'rgba(59, 169, 116, 0.1)' },
+ { main: '#febbb2', translucent: 'rgba(254, 187, 178, 0.1)' },
+ { main: '#cb80dc', translucent: 'rgba(203, 128, 220, 0.1)' },
+ { main: '#5cb7af', translucent: 'rgba(92, 183, 175, 0.1)' },
+ { main: '#7856ff', translucent: 'rgba(120, 86, 255, 0.1)' },
];
export function getChartColor(index: number): string {
- // const colors = theme?.colors ?? {};
- // const chartColors: string[] = Object.keys(colors)
- // .filter((key) => key.startsWith('chart-'))
- // .map((key) => colors[key])
- // .filter((item): item is string => typeof item === 'string');
-
- return chartColors[index % chartColors.length]!;
+ return chartColors[index % chartColors.length]!.main;
+}
+
+export function getChartTranslucentColor(index: number): string {
+ return chartColors[index % chartColors.length]!.translucent;
}
diff --git a/packages/db/src/clickhouse/query-builder.ts b/packages/db/src/clickhouse/query-builder.ts
index fd2afa2d..351ef6b1 100644
--- a/packages/db/src/clickhouse/query-builder.ts
+++ b/packages/db/src/clickhouse/query-builder.ts
@@ -680,12 +680,10 @@ clix.toStartOf = (node: string, interval: IInterval, timezone?: string) => {
return `toStartOfDay(${node})`;
}
case 'week': {
- // Does not respect timezone settings (session_timezone) so we need to pass it manually
- return `toStartOfWeek(${node}${timezone ? `, 1, '${timezone}'` : ''})`;
+ return `toStartOfWeek(toDateTime(${node}))`;
}
case 'month': {
- // Does not respect timezone settings (session_timezone) so we need to pass it manually
- return `toStartOfMonth(${node}${timezone ? `, '${timezone}'` : ''})`;
+ return `toStartOfMonth(toDateTime(${node}))`;
}
}
};
diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts
index 195fc887..a182b69b 100644
--- a/packages/db/src/services/event.service.ts
+++ b/packages/db/src/services/event.service.ts
@@ -1,4 +1,4 @@
-import { path, assocPath, last, mergeDeepRight } from 'ramda';
+import { path, assocPath, last, mergeDeepRight, uniq } from 'ramda';
import sqlstring from 'sqlstring';
import { v4 as uuid } from 'uuid';
@@ -561,6 +561,15 @@ export async function getEventList(options: GetEventListOptions) {
...sb.where,
...getEventFiltersWhereClause(filters),
};
+
+ // Join profiles table if any filter uses profile fields
+ const profileFilters = filters
+ .filter((f) => f.name.startsWith('profile.'))
+ .map((f) => f.name.replace('profile.', ''));
+
+ if (profileFilters.length > 0) {
+ sb.joins.profiles = `LEFT ANY JOIN (SELECT id, ${uniq(profileFilters.map((f) => f.split('.')[0])).join(', ')} FROM ${TABLE_NAMES.profiles} FINAL WHERE project_id = ${sqlstring.escape(projectId)}) as profile on profile.id = profile_id`;
+ }
}
sb.orderBy.created_at =
@@ -622,6 +631,15 @@ export async function getEventsCount({
...sb.where,
...getEventFiltersWhereClause(filters),
};
+
+ // Join profiles table if any filter uses profile fields
+ const profileFilters = filters
+ .filter((f) => f.name.startsWith('profile.'))
+ .map((f) => f.name.replace('profile.', ''));
+
+ if (profileFilters.length > 0) {
+ sb.joins.profiles = `LEFT ANY JOIN (SELECT id, ${uniq(profileFilters.map((f) => f.split('.')[0])).join(', ')} FROM ${TABLE_NAMES.profiles} FINAL WHERE project_id = ${sqlstring.escape(projectId)}) as profile on profile.id = profile_id`;
+ }
}
const res = await chQuery<{ count: number }>(
@@ -701,6 +719,7 @@ class EventService {
select,
limit,
orderBy,
+ filters,
}: {
projectId: string;
profileId?: string;
@@ -715,7 +734,14 @@ class EventService {
};
limit?: number;
orderBy?: keyof IClickhouseEvent;
+ filters?: IChartEventFilter[];
}) {
+ // Extract profile filters if any
+ const profileFilters =
+ filters
+ ?.filter((f) => f.name.startsWith('profile.'))
+ .map((f) => f.name.replace('profile.', '')) ?? [];
+
const events = clix(this.client)
.select<
Partial & {
@@ -744,6 +770,12 @@ class EventService {
])
.from('events e')
.where('project_id', '=', projectId)
+ .when(profileFilters.length > 0, (q) => {
+ q.leftJoin(
+ `(SELECT id, ${uniq(profileFilters.map((f) => f.split('.')[0])).join(', ')} FROM ${TABLE_NAMES.profiles} FINAL WHERE project_id = ${sqlstring.escape(projectId)}) as profile`,
+ 'profile.id = e.profile_id',
+ );
+ })
.when(!!where?.event, where?.event)
// Do not limit if profileId, we will limit later since we need the "correct" profileId
.when(!!limit && !profileId, (q) => q.limit(limit!))
@@ -941,6 +973,7 @@ class EventService {
profileId,
limit,
orderBy: 'created_at',
+ filters,
select: {
event: {
deviceId: true,
diff --git a/packages/db/src/services/overview.service.ts b/packages/db/src/services/overview.service.ts
index e0897c17..38ef8cc7 100644
--- a/packages/db/src/services/overview.service.ts
+++ b/packages/db/src/services/overview.service.ts
@@ -200,6 +200,12 @@ export class OverviewService {
])
.rawWhere(this.getRawWhereClause('events', filters));
+ // Use toDate for month/week intervals, toDateTime for others
+ const rollupDate =
+ interval === 'month' || interval === 'week'
+ ? clix.date('1970-01-01')
+ : clix.datetime('1970-01-01 00:00:00');
+
return clix(this.client, timezone)
.with('session_agg', sessionAggQuery)
.with(
@@ -207,14 +213,14 @@ export class OverviewService {
clix(this.client, timezone)
.select(['bounce_rate'])
.from('session_agg')
- .where('date', '=', clix.datetime('1970-01-01 00:00:00')),
+ .where('date', '=', rollupDate),
)
.with(
'daily_stats',
clix(this.client, timezone)
.select(['date', 'bounce_rate'])
.from('session_agg')
- .where('date', '!=', clix.datetime('1970-01-01 00:00:00')),
+ .where('date', '!=', rollupDate),
)
.with('overall_unique_visitors', overallUniqueVisitorsQuery)
.select<{
diff --git a/packages/trpc/src/routers/event.ts b/packages/trpc/src/routers/event.ts
index 0506fe6c..5b5b2d52 100644
--- a/packages/trpc/src/routers/event.ts
+++ b/packages/trpc/src/routers/event.ts
@@ -172,7 +172,7 @@ export const eventRouter = createTRPCRouter({
data: items,
meta: {
next:
- items.length === 50 && lastItem
+ items.length > 0 && lastItem
? lastItem.createdAt.toISOString()
: null,
},
@@ -190,12 +190,19 @@ export const eventRouter = createTRPCRouter({
cursor: z.string().optional(),
startDate: z.date().optional(),
endDate: z.date().optional(),
+ events: z.array(z.string()).optional(),
}),
)
.query(async ({ input }) => {
const conversions = await getConversionEventNames(input.projectId);
+ const filteredConversions = conversions.filter((event) => {
+ if (input.events && input.events.length > 0) {
+ return input.events.includes(event.name);
+ }
+ return true;
+ });
- if (conversions.length === 0) {
+ if (filteredConversions.length === 0) {
return {
data: [],
meta: {
@@ -220,7 +227,7 @@ export const eventRouter = createTRPCRouter({
origin: true,
},
custom: (sb) => {
- sb.where.name = `name IN (${conversions.map((event) => sqlstring.escape(event.name)).join(',')})`;
+ sb.where.name = `name IN (${filteredConversions.map((event) => sqlstring.escape(event.name)).join(',')})`;
},
});
@@ -249,7 +256,7 @@ export const eventRouter = createTRPCRouter({
data: items,
meta: {
next:
- items.length === 50 && lastItem
+ items.length > 0 && lastItem
? lastItem.createdAt.toISOString()
: null,
},
@@ -354,15 +361,11 @@ export const eventRouter = createTRPCRouter({
)
.query(async ({ input }) => {
const res = await chQuery<{ origin: string }>(
- `SELECT DISTINCT origin FROM ${TABLE_NAMES.events} WHERE project_id = ${sqlstring.escape(
+ `SELECT DISTINCT origin, count(id) as count FROM ${TABLE_NAMES.events} WHERE project_id = ${sqlstring.escape(
input.projectId,
- )} AND origin IS NOT NULL AND origin != '' AND toDate(created_at) > now() - INTERVAL 30 DAY ORDER BY origin ASC`,
+ )} AND origin IS NOT NULL AND origin != '' AND toDate(created_at) > now() - INTERVAL 30 DAY GROUP BY origin ORDER BY count DESC LIMIT 3`,
);
- return res.sort((a, b) =>
- a.origin
- .replace(/https?:\/\//, '')
- .localeCompare(b.origin.replace(/https?:\/\//, '')),
- );
+ return res;
}),
});