redesign overview
This commit is contained in:
@@ -58,10 +58,7 @@ export function ListReports({ reports }: ListReportsProps) {
|
|||||||
{reports.map((report) => {
|
{reports.map((report) => {
|
||||||
const chartRange = report.range; // timeRanges[report.range];
|
const chartRange = report.range; // timeRanges[report.range];
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="card" key={report.id}>
|
||||||
className="rounded-md border border-border bg-white shadow"
|
|
||||||
key={report.id}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href={`/${params.organizationId}/${params.projectId}/reports/${report.id}`}
|
href={`/${params.organizationId}/${params.projectId}/reports/${report.id}`}
|
||||||
className="flex border-b border-border p-4 leading-none [&_svg]:hover:opacity-100 items-center justify-between"
|
className="flex border-b border-border p-4 leading-none [&_svg]:hover:opacity-100 items-center justify-between"
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import { getExists } from '@/server/pageExists';
|
|||||||
|
|
||||||
import { db } from '@mixan/db';
|
import { db } from '@mixan/db';
|
||||||
|
|
||||||
|
import OverviewMetrics from '../../../../components/overview/overview-metrics';
|
||||||
import { CreateClient } from './create-client';
|
import { CreateClient } from './create-client';
|
||||||
import { StickyBelowHeader } from './layout-sticky-below-header';
|
import { StickyBelowHeader } from './layout-sticky-below-header';
|
||||||
import OverviewMetrics from './overview-metrics';
|
|
||||||
import { OverviewReportRange } from './overview-sticky-header';
|
import { OverviewReportRange } from './overview-sticky-header';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||||
import OverviewMetrics from '@/app/(app)/[organizationId]/[projectId]/overview-metrics';
|
|
||||||
import { OverviewReportRange } from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
import { OverviewReportRange } from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/Logo';
|
||||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||||
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
||||||
import ServerLiveCounter from '@/components/overview/live-counter';
|
import ServerLiveCounter from '@/components/overview/live-counter';
|
||||||
import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram';
|
import { OverviewLiveHistogram } from '@/components/overview/overview-live-histogram';
|
||||||
|
import OverviewMetrics from '@/components/overview/overview-metrics';
|
||||||
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
||||||
import OverviewTopEvents from '@/components/overview/overview-top-events/overview-top-events';
|
import OverviewTopEvents from '@/components/overview/overview-top-events';
|
||||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||||
@@ -43,7 +43,7 @@ export default async function Page({ params: { id } }: PageProps) {
|
|||||||
<Logo className="text-white" />
|
<Logo className="text-white" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-lg shadow ring-8 ring-blue-600/50">
|
<div className="bg-slate-100 rounded-lg shadow ring-8 ring-blue-600/50">
|
||||||
<StickyBelowHeader>
|
<StickyBelowHeader>
|
||||||
<div className="p-4 flex gap-2 justify-between">
|
<div className="p-4 flex gap-2 justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -65,9 +65,7 @@ export default async function Page({ params: { id } }: PageProps) {
|
|||||||
<OverviewTopPages projectId={projectId} />
|
<OverviewTopPages projectId={projectId} />
|
||||||
<OverviewTopDevices projectId={projectId} />
|
<OverviewTopDevices projectId={projectId} />
|
||||||
<OverviewTopEvents projectId={projectId} />
|
<OverviewTopEvents projectId={projectId} />
|
||||||
<div className="col-span-6">
|
<OverviewTopGeo projectId={projectId} />
|
||||||
<OverviewTopGeo projectId={projectId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" className="light">
|
<html lang="en" className="light">
|
||||||
<body
|
<body
|
||||||
className={cn('min-h-screen font-sans antialiased grainy bg-slate-50')}
|
className={cn('min-h-screen font-sans antialiased grainy bg-slate-100')}
|
||||||
>
|
>
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export function Card({ children, hover, className }: CardProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'border border-border rounded relative bg-white',
|
'card relative',
|
||||||
hover && 'transition-all hover:shadow hover:border-black',
|
hover && 'transition-all hover:border-black',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -30,14 +30,5 @@ export interface WidgetProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
export function Widget({ children, className }: WidgetProps) {
|
export function Widget({ children, className }: WidgetProps) {
|
||||||
return (
|
return <div className={cn('card self-start', className)}>{children}</div>;
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'border border-border rounded-md bg-white self-start',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ export function ExpandableListItem({
|
|||||||
}: ExpandableListItemProps) {
|
}: ExpandableListItemProps) {
|
||||||
const [open, setOpen] = useState(initialOpen ?? false);
|
const [open, setOpen] = useState(initialOpen ?? false);
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('card overflow-hidden', className)}>
|
||||||
className={cn('bg-white shadow rounded-xl overflow-hidden', className)}
|
|
||||||
>
|
|
||||||
<div className="p-2 sm:p-4 flex gap-4">
|
<div className="p-2 sm:p-4 flex gap-4">
|
||||||
<div className="flex gap-1">{image}</div>
|
<div className="flex gap-1">{image}</div>
|
||||||
<div className="flex flex-col flex-1 gap-1 min-w-0">
|
<div className="flex flex-col flex-1 gap-1 min-w-0">
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { getConversionEventNames } from '@mixan/db';
|
||||||
|
|
||||||
|
import type { OverviewLatestEventsProps } from './overview-latest-events';
|
||||||
|
import OverviewLatestEvents from './overview-latest-events';
|
||||||
|
|
||||||
|
export default async function OverviewLatestEventsServer({
|
||||||
|
projectId,
|
||||||
|
}: Omit<OverviewLatestEventsProps, 'conversions'>) {
|
||||||
|
const eventNames = await getConversionEventNames(projectId);
|
||||||
|
return (
|
||||||
|
<OverviewLatestEvents
|
||||||
|
projectId={projectId}
|
||||||
|
conversions={eventNames.map((item) => item.name)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChartSwitch } from '@/components/report/chart';
|
||||||
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
|
import { Widget, WidgetBody } from '../../Widget';
|
||||||
|
import { WidgetButtons, WidgetHead } from '../overview-widget';
|
||||||
|
import { useOverviewOptions } from '../useOverviewOptions';
|
||||||
|
import { useOverviewWidget } from '../useOverviewWidget';
|
||||||
|
|
||||||
|
export interface OverviewLatestEventsProps {
|
||||||
|
projectId: string;
|
||||||
|
conversions: string[];
|
||||||
|
}
|
||||||
|
export default function OverviewLatestEvents({
|
||||||
|
projectId,
|
||||||
|
conversions,
|
||||||
|
}: OverviewLatestEventsProps) {
|
||||||
|
const { interval, range, previous, startDate, endDate } =
|
||||||
|
useOverviewOptions();
|
||||||
|
const [filters] = useEventQueryFilters();
|
||||||
|
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||||
|
all: {
|
||||||
|
title: 'Top events',
|
||||||
|
btn: 'All',
|
||||||
|
chart: {
|
||||||
|
projectId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
segment: 'event',
|
||||||
|
filters: [
|
||||||
|
...filters,
|
||||||
|
{
|
||||||
|
id: 'ex_session',
|
||||||
|
name: 'name',
|
||||||
|
operator: 'isNot',
|
||||||
|
value: ['session_start', 'session_end'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'A',
|
||||||
|
name: '*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
breakdowns: [
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
chartType: 'bar',
|
||||||
|
lineType: 'monotone',
|
||||||
|
interval: interval,
|
||||||
|
name: 'Top sources',
|
||||||
|
range: range,
|
||||||
|
previous: previous,
|
||||||
|
metric: 'sum',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conversions: {
|
||||||
|
title: 'Conversions',
|
||||||
|
btn: 'Conversions',
|
||||||
|
hide: conversions.length === 0,
|
||||||
|
chart: {
|
||||||
|
projectId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
segment: 'event',
|
||||||
|
filters: [
|
||||||
|
...filters,
|
||||||
|
{
|
||||||
|
id: 'conversion',
|
||||||
|
name: 'name',
|
||||||
|
operator: 'is',
|
||||||
|
value: conversions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'A',
|
||||||
|
name: '*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
breakdowns: [
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
chartType: 'bar',
|
||||||
|
lineType: 'monotone',
|
||||||
|
interval: interval,
|
||||||
|
name: 'Top sources',
|
||||||
|
range: range,
|
||||||
|
previous: previous,
|
||||||
|
metric: 'sum',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Widget className="col-span-6 md:col-span-3">
|
||||||
|
<WidgetHead>
|
||||||
|
<div className="title">{widget.title}</div>
|
||||||
|
<WidgetButtons>
|
||||||
|
{widgets
|
||||||
|
.filter((item) => item.hide !== true)
|
||||||
|
.map((w) => (
|
||||||
|
<button
|
||||||
|
key={w.key}
|
||||||
|
onClick={() => setWidget(w.key)}
|
||||||
|
className={cn(w.key === widget.key && 'active')}
|
||||||
|
>
|
||||||
|
{w.btn}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</WidgetButtons>
|
||||||
|
</WidgetHead>
|
||||||
|
<WidgetBody>
|
||||||
|
<ChartSwitch hideID {...widget.chart} previous={false} />
|
||||||
|
</WidgetBody>
|
||||||
|
</Widget>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { Fragment } from 'react';
|
||||||
|
import { api } from '@/app/_trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||||
import AnimateHeight from 'react-animate-height';
|
import AnimateHeight from 'react-animate-height';
|
||||||
|
|
||||||
import type { IChartInput } from '@mixan/validation';
|
import type { IChartInput } from '@mixan/validation';
|
||||||
|
|
||||||
import { ChartSwitch } from '../report/chart';
|
import { redisSub } from '../../../../../packages/redis';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '../Widget';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||||
import { useOverviewOptions } from './useOverviewOptions';
|
import { useOverviewOptions } from './useOverviewOptions';
|
||||||
|
|
||||||
interface OverviewLiveHistogramProps {
|
interface OverviewLiveHistogramProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverviewLiveHistogram({
|
export function OverviewLiveHistogram({
|
||||||
projectId,
|
projectId,
|
||||||
}: OverviewLiveHistogramProps) {
|
}: OverviewLiveHistogramProps) {
|
||||||
const { liveHistogram, setLiveHistogram } = useOverviewOptions();
|
const { liveHistogram } = useOverviewOptions();
|
||||||
const report: IChartInput = {
|
const report: IChartInput = {
|
||||||
projectId,
|
projectId,
|
||||||
events: [
|
events: [
|
||||||
@@ -44,26 +47,112 @@ export function OverviewLiveHistogram({
|
|||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
previous: false,
|
previous: false,
|
||||||
};
|
};
|
||||||
|
const countReport: IChartInput = {
|
||||||
|
name: '',
|
||||||
|
projectId,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
segment: 'user',
|
||||||
|
filters: [],
|
||||||
|
id: 'A',
|
||||||
|
name: 'session_start',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
breakdowns: [],
|
||||||
|
chartType: 'metric',
|
||||||
|
lineType: 'monotone',
|
||||||
|
interval: 'minute',
|
||||||
|
range: '30min',
|
||||||
|
previous: false,
|
||||||
|
metric: 'sum',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = api.chart.chart.useQuery(report);
|
||||||
|
const countRes = api.chart.chart.useQuery(countReport);
|
||||||
|
|
||||||
|
const metrics = res.data?.series[0]?.metrics;
|
||||||
|
const minutes = (res.data?.series[0]?.data || []).slice(-30);
|
||||||
|
const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0;
|
||||||
|
|
||||||
|
if (res.isInitialLoading || countRes.isInitialLoading) {
|
||||||
|
// prettier-ignore
|
||||||
|
const staticArray = [
|
||||||
|
10, 25, 30, 45, 20, 5, 55, 18, 40, 12,
|
||||||
|
50, 35, 8, 22, 38, 42, 15, 28, 52, 5,
|
||||||
|
48, 14, 32, 58, 7, 19, 33, 56, 24, 5
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper count={0} open={liveHistogram}>
|
||||||
|
{staticArray.map((percent, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex-1 rounded-md bg-slate-200 animate-pulse"
|
||||||
|
style={{ height: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.isSuccess && !countRes.isSuccess) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Wrapper open={liveHistogram} count={liveCount}>
|
||||||
<button onClick={() => setLiveHistogram((p) => !p)} className="w-full">
|
{minutes.map((minute) => {
|
||||||
<WidgetHead
|
return (
|
||||||
className={cn(
|
<Tooltip key={minute.date}>
|
||||||
'flex justify-between items-center',
|
<TooltipTrigger asChild>
|
||||||
!liveHistogram && 'border-b-0'
|
<div
|
||||||
)}
|
className={cn(
|
||||||
>
|
'flex-1 rounded-md hover:scale-110 transition-all ease-in-out',
|
||||||
<div className="title">Active users last 30 minutes</div>
|
minute.count === 0 ? 'bg-slate-200' : 'bg-blue-600'
|
||||||
<ChevronsUpDownIcon size={16} />
|
)}
|
||||||
</WidgetHead>
|
style={{
|
||||||
</button>
|
height:
|
||||||
|
minute.count === 0
|
||||||
<AnimateHeight duration={500} height={liveHistogram ? 'auto' : 0}>
|
? '5%'
|
||||||
<WidgetBody>
|
: `${(minute.count / metrics!.max) * 100}%`,
|
||||||
<ChartSwitch {...report} />
|
}}
|
||||||
</WidgetBody>
|
/>
|
||||||
</AnimateHeight>
|
</TooltipTrigger>
|
||||||
</Widget>
|
<TooltipContent side="top">
|
||||||
|
<div>{minute.count} active users</div>
|
||||||
|
<div>@ {new Date(minute.date).toLocaleTimeString()}</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WrapperProps {
|
||||||
|
open: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wrapper({ open, children, count }: WrapperProps) {
|
||||||
|
return (
|
||||||
|
<AnimateHeight duration={500} height={open ? 'auto' : 0}>
|
||||||
|
<div className="flex items-end flex-col md:flex-row">
|
||||||
|
<div className="md:mr-2 flex md:flex-col max-md:justify-between items-end max-md:w-full max-md:mb-2 md:card md:p-4">
|
||||||
|
<div className="text-sm max-md:mb-1">Last 30 minutes</div>
|
||||||
|
<div className="text-2xl font-bold text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
|
{count}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[150px] aspect-[5/1] flex flex-1 gap-0.5 md:gap-2 items-end w-full relative">
|
||||||
|
<div className="absolute top-0 right-0 text-xs text-muted-foreground">
|
||||||
|
NOW
|
||||||
|
</div>
|
||||||
|
{/* <div className="md:absolute top-0 left-0 md:card md:p-4 mr-2 md:bg-white/90 z-50"> */}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateHeight>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,12 +202,6 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChartSwitch hideID {...report} />
|
<ChartSwitch hideID {...report} />
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'transition-opacity top-0 left-0 right-0 bottom-0 absolute rounded-md w-full h-full border ring-1 border-black ring-black',
|
|
||||||
metric === index ? 'opacity-100' : 'opacity-0'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{/* add active border */}
|
{/* add active border */}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -18,6 +18,7 @@ export default function OverviewTopDevices({
|
|||||||
const { interval, range, previous, startDate, endDate } =
|
const { interval, range, previous, startDate, endDate } =
|
||||||
useOverviewOptions();
|
useOverviewOptions();
|
||||||
const [filters, setFilter] = useEventQueryFilters();
|
const [filters, setFilter] = useEventQueryFilters();
|
||||||
|
const isPageFilter = filters.find((filter) => filter.name === 'path');
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
||||||
devices: {
|
devices: {
|
||||||
title: 'Top devices',
|
title: 'Top devices',
|
||||||
@@ -31,7 +32,7 @@ export default function OverviewTopDevices({
|
|||||||
segment: 'user',
|
segment: 'user',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
@@ -61,7 +62,7 @@ export default function OverviewTopDevices({
|
|||||||
segment: 'user',
|
segment: 'user',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
@@ -91,7 +92,7 @@ export default function OverviewTopDevices({
|
|||||||
segment: 'user',
|
segment: 'user',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
@@ -121,7 +122,7 @@ export default function OverviewTopDevices({
|
|||||||
segment: 'user',
|
segment: 'user',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
@@ -151,7 +152,7 @@ export default function OverviewTopDevices({
|
|||||||
segment: 'user',
|
segment: 'user',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
const { interval, range, previous, startDate, endDate } =
|
const { interval, range, previous, startDate, endDate } =
|
||||||
useOverviewOptions();
|
useOverviewOptions();
|
||||||
const [filters, setFilter] = useEventQueryFilters();
|
const [filters, setFilter] = useEventQueryFilters();
|
||||||
|
const isPageFilter = filters.find((filter) => filter.name === 'path');
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
||||||
countries: {
|
countries: {
|
||||||
title: 'Top countries',
|
title: 'Top countries',
|
||||||
@@ -29,7 +30,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
segment: 'event',
|
segment: 'event',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
@@ -59,7 +60,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
segment: 'event',
|
segment: 'event',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
@@ -89,7 +90,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
segment: 'event',
|
segment: 'event',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
@@ -165,7 +166,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
segment: 'event',
|
segment: 'event',
|
||||||
filters,
|
filters,
|
||||||
id: 'A',
|
id: 'A',
|
||||||
name: '*',
|
name: isPageFilter ? 'screen_view' : 'session_start',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
breakdowns: [
|
breakdowns: [
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ import { WidgetHead as WidgetHeadBase } from '../Widget';
|
|||||||
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
||||||
return (
|
return (
|
||||||
<WidgetHeadBase
|
<WidgetHeadBase
|
||||||
className={cn(
|
className={cn('flex flex-col p-0 [&_.title]:p-4', className)}
|
||||||
'flex flex-col p-0 [&_.title]:text-sm [&_.title]:px-4 [&_.title]:py-2',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import {
|
import {
|
||||||
parseAsBoolean,
|
parseAsBoolean,
|
||||||
parseAsInteger,
|
parseAsInteger,
|
||||||
@@ -48,7 +47,7 @@ export function useOverviewOptions() {
|
|||||||
// Toggles
|
// Toggles
|
||||||
const [liveHistogram, setLiveHistogram] = useQueryState(
|
const [liveHistogram, setLiveHistogram] = useQueryState(
|
||||||
'live',
|
'live',
|
||||||
parseAsBoolean.withDefault(false).withOptions(nuqsOptions)
|
parseAsBoolean.withDefault(true).withOptions(nuqsOptions)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export function PreviousDiffIndicatorText({
|
|||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
{number.format(diff)}%
|
{number.short(diff)}%
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function MetricCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group relative border border-border p-2 rounded-md bg-white overflow-hidden h-24"
|
className="group relative card p-4 overflow-hidden h-24"
|
||||||
key={serie.name}
|
key={serie.name}
|
||||||
>
|
>
|
||||||
<div className="absolute -top-2 -left-2 -right-2 -bottom-2 z-0 opacity-20 transition-opacity duration-300 group-hover:opacity-50 rounded-md">
|
<div className="absolute -top-2 -left-2 -right-2 -bottom-2 z-0 opacity-20 transition-opacity duration-300 group-hover:opacity-50 rounded-md">
|
||||||
@@ -64,28 +64,14 @@ export function MetricCard({
|
|||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
<AreaChart
|
<AreaChart
|
||||||
width={width}
|
width={width}
|
||||||
height={height / 3}
|
height={height / 4}
|
||||||
data={serie.data}
|
data={serie.data}
|
||||||
style={{ marginTop: (height / 3) * 2 }}
|
style={{ marginTop: (height / 4) * 3 }}
|
||||||
>
|
>
|
||||||
<defs>
|
|
||||||
<linearGradient id="red" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor={'red'} stopOpacity={0.5} />
|
|
||||||
<stop offset="95%" stopColor={'red'} stopOpacity={0.2} />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="green" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor={'green'} stopOpacity={0.5} />
|
|
||||||
<stop offset="95%" stopColor={'green'} stopOpacity={0.2} />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="blue" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor={'blue'} stopOpacity={0.5} />
|
|
||||||
<stop offset="95%" stopColor={'blue'} stopOpacity={0.2} />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<Area
|
<Area
|
||||||
dataKey="count"
|
dataKey="count"
|
||||||
type="monotone"
|
type="monotone"
|
||||||
fill={`url(#${graphColors})`}
|
fill={`transparent`}
|
||||||
fillOpacity={1}
|
fillOpacity={1}
|
||||||
stroke={graphColors}
|
stroke={graphColors}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
@@ -120,7 +106,7 @@ export function MetricCard({
|
|||||||
|
|
||||||
export function MetricCardEmpty() {
|
export function MetricCardEmpty() {
|
||||||
return (
|
return (
|
||||||
<div className="border border-border p-4 rounded-md bg-white h-24">
|
<div className="card p-4 h-24">
|
||||||
<div className="flex items-center justify-center h-full text-slate-600">
|
<div className="flex items-center justify-center h-full text-slate-600">
|
||||||
No data
|
No data
|
||||||
</div>
|
</div>
|
||||||
@@ -130,7 +116,7 @@ export function MetricCardEmpty() {
|
|||||||
|
|
||||||
export function MetricCardLoading() {
|
export function MetricCardLoading() {
|
||||||
return (
|
return (
|
||||||
<div className="h-24 p-4 py-5 flex flex-col bg-white border border-border rounded-md">
|
<div className="h-24 p-4 py-5 flex flex-col card">
|
||||||
<div className="bg-slate-200 rounded animate-pulse h-4 w-1/2"></div>
|
<div className="bg-slate-200 rounded animate-pulse h-4 w-1/2"></div>
|
||||||
<div className="bg-slate-200 rounded animate-pulse h-6 w-1/5 mt-auto"></div>
|
<div className="bg-slate-200 rounded animate-pulse h-6 w-1/5 mt-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,16 +30,17 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col w-full text-xs -mx-2',
|
'flex flex-col w-full text-xs -mx-2',
|
||||||
editMode && 'text-base bg-white border border-border rounded-md p-4'
|
editMode && 'text-base card p-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{series.map((serie) => {
|
{series.map((serie, index) => {
|
||||||
const isClickable = serie.name !== NOT_SET_VALUE && onClick;
|
const isClickable = serie.name !== NOT_SET_VALUE && onClick;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={serie.name}
|
key={serie.name}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative py-3 px-2 flex flex-1 w-full gap-4 items-center even:bg-slate-50 [&_[role=progressbar]]:even:bg-white [&_[role=progressbar]]:shadow-sm rounded overflow-hidden',
|
'relative py-3 px-2 flex flex-1 w-full gap-4 items-center even:bg-slate-50 rounded overflow-hidden',
|
||||||
|
'[&_[role=progressbar]]:even:bg-white [&_[role=progressbar]]:shadow-sm',
|
||||||
isClickable && 'cursor-pointer hover:!bg-slate-100'
|
isClickable && 'cursor-pointer hover:!bg-slate-100'
|
||||||
)}
|
)}
|
||||||
{...(isClickable ? { onClick: () => onClick(serie) } : {})}
|
{...(isClickable ? { onClick: () => onClick(serie) } : {})}
|
||||||
@@ -57,14 +58,12 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
|||||||
<div className="font-bold">
|
<div className="font-bold">
|
||||||
{number.format(serie.metrics.sum)}
|
{number.format(serie.metrics.sum)}
|
||||||
</div>
|
</div>
|
||||||
|
<Progress
|
||||||
|
color={getChartColor(index)}
|
||||||
|
className={cn('w-1/2', editMode ? 'h-5' : 'h-2')}
|
||||||
|
value={(serie.metrics.sum / maxCount) * 100}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
className="absolute left-0 bottom-0 h-0.5 rounded-full min-w-2 z-10 bg-blue-600"
|
|
||||||
style={{
|
|
||||||
width: `${(serie.metrics.sum / maxCount) * 100}%`,
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -31,12 +31,7 @@ export function ReportPieChart({ data }: ReportPieChartProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className={cn('max-sm:-mx-3', editMode && 'card p-4')}>
|
||||||
className={cn(
|
|
||||||
'max-sm:-mx-3',
|
|
||||||
editMode && 'border border-border bg-white rounded-md p-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AutoSizer disableHeight>
|
<AutoSizer disableHeight>
|
||||||
{({ width }) => {
|
{({ width }) => {
|
||||||
const height = Math.min(Math.max(width * 0.5625, 250), 400);
|
const height = Math.min(Math.max(width * 0.5625, 250), 400);
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ export function ResponsiveContainer({ children }: ResponsiveContainerProps) {
|
|||||||
maxHeight,
|
maxHeight,
|
||||||
minHeight,
|
minHeight,
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn('max-sm:-mx-3 aspect-video w-full', editMode && 'card p-4')}
|
||||||
'max-sm:-mx-3 aspect-video w-full',
|
|
||||||
editMode && 'border border-border bg-white rounded-md p-4'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<AutoSizer disableHeight>
|
<AutoSizer disableHeight>
|
||||||
{({ width }) =>
|
{({ width }) =>
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ const mapper: Record<string, LucideIcon> = {
|
|||||||
link_out: ExternalLinkIcon,
|
link_out: ExternalLinkIcon,
|
||||||
|
|
||||||
// Websites
|
// Websites
|
||||||
|
linkedin: createImageIcon(getProxyImage('https://linkedin.com')),
|
||||||
|
slack: createImageIcon(getProxyImage('https://slack.com')),
|
||||||
|
pinterest: createImageIcon(getProxyImage('https://www.pinterest.se')),
|
||||||
|
ecosia: createImageIcon(getProxyImage('https://ecosia.com')),
|
||||||
|
yandex: createImageIcon(getProxyImage('https://yandex.com')),
|
||||||
google: createImageIcon(getProxyImage('https://google.com')),
|
google: createImageIcon(getProxyImage('https://google.com')),
|
||||||
facebook: createImageIcon(getProxyImage('https://facebook.com')),
|
facebook: createImageIcon(getProxyImage('https://facebook.com')),
|
||||||
bing: createImageIcon(getProxyImage('https://bing.com')),
|
bing: createImageIcon(getProxyImage('https://bing.com')),
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function FunnelSteps({
|
|||||||
)}
|
)}
|
||||||
key={step.event.id}
|
key={step.event.id}
|
||||||
>
|
>
|
||||||
<div className="border border-border divide-y divide-border bg-white">
|
<div className="card divide-y divide-border bg-white">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<p className="text-muted-foreground">Step {index + 1}</p>
|
<p className="text-muted-foreground">Step {index + 1}</p>
|
||||||
<h3 className="font-bold">
|
<h3 className="font-bold">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const Table = React.forwardRef<
|
|||||||
overflow?: boolean;
|
overflow?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, wrapper, overflow = true, ...props }, ref) => (
|
>(({ className, wrapper, overflow = true, ...props }, ref) => (
|
||||||
<div className={cn('border border-border rounded-md bg-white', className)}>
|
<div className={cn('card', className)}>
|
||||||
<div className={cn('relative w-full', overflow && 'overflow-auto')}>
|
<div className={cn('relative w-full', overflow && 'overflow-auto')}>
|
||||||
<table
|
<table
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -118,6 +118,12 @@
|
|||||||
0 0;
|
0 0;
|
||||||
transition-duration: 0.5s;
|
transition-duration: 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 0 !important;
|
||||||
|
@apply bg-white rounded-xl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.resizer {
|
.resizer {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"js:codegen": "pnpm -r --filter sdk-web run build-for-openpanel",
|
"js:codegen": "pnpm -r --filter sdk-web run build-for-openpanel",
|
||||||
"migrate": "pnpm -r --filter db run migrate",
|
"migrate": "pnpm -r --filter db run migrate",
|
||||||
"migrate:deploy": "pnpm -r --filter db run migrate:deploy",
|
"migrate:deploy": "pnpm -r --filter db run migrate:deploy",
|
||||||
"dev": "pnpm -r dev",
|
"dev": "pnpm -r --parallel testing",
|
||||||
"format": "pnpm -r format --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
"format": "pnpm -r format --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
||||||
"format:fix": "pnpm -r format --write --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
"format:fix": "pnpm -r format --write --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
||||||
"lint": "pnpm -r lint",
|
"lint": "pnpm -r lint",
|
||||||
|
|||||||
Reference in New Issue
Block a user