multiple breakpoints
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ChartSwitchShortcut } from '@/components/report/chart';
|
||||
import { ChartRootShortcut } from '@/components/report/chart';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
|
||||
import type { IChartEvent } from '@openpanel/validation';
|
||||
@@ -26,7 +26,7 @@ export function EventsPerDayChart({ projectId, filters, events }: Props) {
|
||||
<span className="title">Events per day</span>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<ChartSwitchShortcut
|
||||
<ChartRootShortcut
|
||||
projectId={projectId}
|
||||
range="30d"
|
||||
chartType="histogram"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { ChartSwitchShortcut } from '@/components/report/chart';
|
||||
import { ChartRootShortcut } from '@/components/report/chart';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { KeyValue } from '@/components/ui/key-value';
|
||||
import {
|
||||
@@ -195,7 +195,7 @@ export function EventDetails({ event, open, setOpen }: Props) {
|
||||
Show all
|
||||
</button>
|
||||
</div>
|
||||
<ChartSwitchShortcut
|
||||
<ChartRootShortcut
|
||||
projectId={event.projectId}
|
||||
chartType="histogram"
|
||||
events={[
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { ChartRoot } from '@/components/report/chart';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
@@ -85,7 +85,7 @@ const ProfileCharts = ({ profileId, projectId }: Props) => {
|
||||
<span className="title">Page views</span>
|
||||
</WidgetHead>
|
||||
<WidgetBody className="flex gap-2">
|
||||
<ChartSwitch {...pageViewsChart} />
|
||||
<ChartRoot {...pageViewsChart} />
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
<Widget className="col-span-3 w-full">
|
||||
@@ -93,7 +93,7 @@ const ProfileCharts = ({ profileId, projectId }: Props) => {
|
||||
<span className="title">Events per day</span>
|
||||
</WidgetHead>
|
||||
<WidgetBody className="flex gap-2">
|
||||
<ChartSwitch {...eventsChart} />
|
||||
<ChartRoot {...eventsChart} />
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { ChartRoot } from '@/components/report/chart';
|
||||
import { ReportChartType } from '@/components/report/ReportChartType';
|
||||
import { ReportInterval } from '@/components/report/ReportInterval';
|
||||
import { ReportLineType } from '@/components/report/ReportLineType';
|
||||
@@ -99,7 +99,7 @@ export default function ReportEditor({
|
||||
</StickyBelowHeader>
|
||||
<div className="flex flex-col gap-4 p-4" id="report-editor">
|
||||
{report.ready && (
|
||||
<ChartSwitch {...report} projectId={projectId} editMode />
|
||||
<ChartRoot {...report} projectId={projectId} editMode />
|
||||
)}
|
||||
</div>
|
||||
<SheetContent className="!max-w-lg" side="left">
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import type { HtmlProps } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { useChartContext } from './report/chart/ChartProvider';
|
||||
|
||||
type ColorSquareProps = HtmlProps<HTMLDivElement>;
|
||||
|
||||
export function ColorSquare({ children, className }: ColorSquareProps) {
|
||||
const { hideID } = useChartContext();
|
||||
if (hideID) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-purple-500 text-xs font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
|
||||
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-blue-600 text-xs font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { getPropertyLabel } from '@/translations/properties';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { X } from 'lucide-react';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
@@ -47,7 +48,7 @@ export function OverviewFiltersButtons({
|
||||
icon={X}
|
||||
onClick={() => setFilter(filter.name, filter.value[0], 'is')}
|
||||
>
|
||||
<span className="mr-1">{filter.name} is</span>
|
||||
<span className="mr-1">{getPropertyLabel(filter.name)} is</span>
|
||||
<strong>{filter.value[0]}</strong>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { ChartRoot } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
@@ -205,7 +205,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
setMetric(index);
|
||||
}}
|
||||
>
|
||||
<ChartSwitch hideID {...report} />
|
||||
<ChartRoot hideID {...report} />
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
@@ -217,7 +217,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card col-span-6 p-4">
|
||||
<ChartSwitch
|
||||
<ChartRoot
|
||||
key={selectedMetric.id}
|
||||
hideID
|
||||
{...selectedMetric}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
import { LazyChart } from '../report/chart/LazyChart';
|
||||
@@ -51,7 +52,7 @@ export default function OverviewTopDevices({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top devices',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -82,7 +83,7 @@ export default function OverviewTopDevices({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top browser',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -92,6 +93,9 @@ export default function OverviewTopDevices({
|
||||
title: 'Top Browser Version',
|
||||
btn: 'Browser Version',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return name[1] || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -107,13 +111,17 @@ export default function OverviewTopDevices({
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'browser',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'browser_version',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top Browser Version',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -144,7 +152,7 @@ export default function OverviewTopDevices({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top OS',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -154,6 +162,9 @@ export default function OverviewTopDevices({
|
||||
title: 'Top OS version',
|
||||
btn: 'OS Version',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return name[1] || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -169,13 +180,17 @@ export default function OverviewTopDevices({
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'os',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'os_version',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top OS version',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -211,19 +226,19 @@ export default function OverviewTopDevices({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'devices':
|
||||
setFilter('device', item.name);
|
||||
setFilter('device', item.names[0]);
|
||||
break;
|
||||
case 'browser':
|
||||
setFilter('browser', item.name);
|
||||
setFilter('browser', item.names[0]);
|
||||
break;
|
||||
case 'browser_version':
|
||||
setFilter('browser_version', item.name);
|
||||
setFilter('browser_version', item.names[1]);
|
||||
break;
|
||||
case 'os':
|
||||
setFilter('os', item.name);
|
||||
setFilter('os', item.names[0]);
|
||||
break;
|
||||
case 'os_version':
|
||||
setFilter('os_version', item.name);
|
||||
setFilter('os_version', item.names[1]);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function OverviewTopEvents({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Your top events',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -91,7 +91,7 @@ export default function OverviewTopEvents({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'All top events',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -131,7 +131,7 @@ export default function OverviewTopEvents({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Conversions',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { ChartRoot } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getCountry } from '@/translations/countries';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
import { LazyChart } from '../report/chart/LazyChart';
|
||||
@@ -29,6 +31,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
title: 'Top countries',
|
||||
btn: 'Countries',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return getCountry(name[0]) || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -50,7 +55,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top countries',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -60,6 +65,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
title: 'Top regions',
|
||||
btn: 'Regions',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return name[1] || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -75,13 +83,17 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'country',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'region',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top regions',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -91,6 +103,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
title: 'Top cities',
|
||||
btn: 'Cities',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return name[1] || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -106,13 +121,17 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'country',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'city',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top cities',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -149,14 +168,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
switch (widget.key) {
|
||||
case 'countries':
|
||||
setWidget('regions');
|
||||
setFilter('country', item.name);
|
||||
setFilter('country', item.names[0]);
|
||||
break;
|
||||
case 'regions':
|
||||
setWidget('cities');
|
||||
setFilter('region', item.name);
|
||||
setFilter('region', item.names[1]);
|
||||
break;
|
||||
case 'cities':
|
||||
setFilter('city', item.name);
|
||||
setFilter('city', item.names[1]);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
@@ -169,7 +188,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
<div className="title">Map</div>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<ChartSwitch
|
||||
<ChartRoot
|
||||
hideID
|
||||
{...{
|
||||
projectId,
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ExternalLinkIcon, FilterIcon } from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
import { LazyChart } from '../report/chart/LazyChart';
|
||||
import { Tooltiper } from '../ui/tooltip';
|
||||
import { Widget, WidgetBody } from '../widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import OverviewDetailsButton from './overview-details-button';
|
||||
@@ -23,11 +26,19 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
useOverviewOptions();
|
||||
const [chartType, setChartType] = useState<IChartType>('bar');
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const renderSerieName = (names: string[]) => {
|
||||
return (
|
||||
<Tooltiper content={names.join('')} side="left" className="text-left">
|
||||
{names[1] || NOT_SET_VALUE}
|
||||
</Tooltiper>
|
||||
);
|
||||
};
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||
top: {
|
||||
title: 'Top pages',
|
||||
btn: 'Top pages',
|
||||
chart: {
|
||||
renderSerieName,
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -43,13 +54,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'origin',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top pages',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
@@ -59,6 +74,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
title: 'Entry Pages',
|
||||
btn: 'Entries',
|
||||
chart: {
|
||||
renderSerieName,
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -74,13 +90,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'origin',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
name: 'Entry Pages',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
@@ -90,6 +110,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
title: 'Exit Pages',
|
||||
btn: 'Exits',
|
||||
chart: {
|
||||
renderSerieName,
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -105,13 +126,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'origin',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
name: 'Exit Pages',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
@@ -153,9 +178,22 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
hideID
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
setFilter('path', item.name);
|
||||
}}
|
||||
dropdownMenuContent={(serie) => [
|
||||
{
|
||||
title: 'Visit page',
|
||||
icon: ExternalLinkIcon,
|
||||
onClick: () => {
|
||||
window.open(serie.names.join(''), '_blank');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Set filter',
|
||||
icon: FilterIcon,
|
||||
onClick: () => {
|
||||
setFilter('path', serie.names[1]);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{widget.chart?.name && <OverviewDetailsButton chart={widget.chart} />}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top groups',
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -84,7 +84,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top urls',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -146,7 +146,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Source',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -177,7 +177,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Medium',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -208,7 +208,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Campaign',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -239,7 +239,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Term',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -270,7 +270,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Content',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -307,30 +307,30 @@ export default function OverviewTopSources({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'all':
|
||||
setFilter('referrer_name', item.name);
|
||||
setFilter('referrer_name', item.names[0]);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'domain':
|
||||
setFilter('referrer', item.name);
|
||||
setFilter('referrer', item.names[0]);
|
||||
break;
|
||||
case 'type':
|
||||
setFilter('referrer_type', item.name);
|
||||
setFilter('referrer_type', item.names[0]);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'utm_source':
|
||||
setFilter('properties.__query.utm_source', item.name);
|
||||
setFilter('properties.__query.utm_source', item.names[0]);
|
||||
break;
|
||||
case 'utm_medium':
|
||||
setFilter('properties.__query.utm_medium', item.name);
|
||||
setFilter('properties.__query.utm_medium', item.names[0]);
|
||||
break;
|
||||
case 'utm_campaign':
|
||||
setFilter('properties.__query.utm_campaign', item.name);
|
||||
setFilter('properties.__query.utm_campaign', item.names[0]);
|
||||
break;
|
||||
case 'utm_term':
|
||||
setFilter('properties.__query.utm_term', item.name);
|
||||
setFilter('properties.__query.utm_term', item.names[0]);
|
||||
break;
|
||||
case 'utm_content':
|
||||
setFilter('properties.__query.utm_content', item.name);
|
||||
setFilter('properties.__query.utm_content', item.names[0]);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
|
||||
import { mapKeys } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import type { IChartRoot } from '../report/chart';
|
||||
|
||||
export function useOverviewWidget<T extends string>(
|
||||
key: string,
|
||||
widgets: Record<
|
||||
T,
|
||||
{ title: string; btn: string; chart: IChartProps; hide?: boolean }
|
||||
{ title: string; btn: string; chart: IChartRoot; hide?: boolean }
|
||||
>
|
||||
) {
|
||||
const keys = Object.keys(widgets) as T[];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { api } from '@/trpc/client';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { ChartEmpty } from './ChartEmpty';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportAreaChart } from './ReportAreaChart';
|
||||
import { ReportBarChart } from './ReportBarChart';
|
||||
import { ReportHistogramChart } from './ReportHistogramChart';
|
||||
@@ -15,34 +16,22 @@ import { ReportPieChart } from './ReportPieChart';
|
||||
|
||||
export type ReportChartProps = IChartProps;
|
||||
|
||||
export function Chart({
|
||||
interval,
|
||||
events,
|
||||
breakdowns,
|
||||
chartType,
|
||||
range,
|
||||
lineType,
|
||||
previous,
|
||||
formula,
|
||||
metric,
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
limit,
|
||||
offset,
|
||||
}: ReportChartProps) {
|
||||
const [references] = api.reference.getChartReferences.useSuspenseQuery(
|
||||
{
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 5,
|
||||
}
|
||||
);
|
||||
|
||||
export function Chart() {
|
||||
const {
|
||||
interval,
|
||||
events,
|
||||
breakdowns,
|
||||
chartType,
|
||||
range,
|
||||
previous,
|
||||
formula,
|
||||
metric,
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
limit,
|
||||
offset,
|
||||
} = useChartContext();
|
||||
const [data] = api.chart.chart.useSuspenseQuery(
|
||||
{
|
||||
interval,
|
||||
@@ -73,7 +62,7 @@ export function Chart({
|
||||
}
|
||||
|
||||
if (chartType === 'histogram') {
|
||||
return <ReportHistogramChart interval={interval} data={data} />;
|
||||
return <ReportHistogramChart data={data} />;
|
||||
}
|
||||
|
||||
if (chartType === 'bar') {
|
||||
@@ -89,20 +78,11 @@ export function Chart({
|
||||
}
|
||||
|
||||
if (chartType === 'linear') {
|
||||
return (
|
||||
<ReportLineChart
|
||||
lineType={lineType}
|
||||
interval={interval}
|
||||
data={data}
|
||||
references={references}
|
||||
/>
|
||||
);
|
||||
return <ReportLineChart data={data} />;
|
||||
}
|
||||
|
||||
if (chartType === 'area') {
|
||||
return (
|
||||
<ReportAreaChart lineType={lineType} interval={interval} data={data} />
|
||||
);
|
||||
return <ReportAreaChart data={data} />;
|
||||
}
|
||||
|
||||
return <p>Unknown chart type</p>;
|
||||
|
||||
@@ -1,113 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
createContext,
|
||||
memo,
|
||||
Suspense,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartProps, IChartSerie } from '@openpanel/validation';
|
||||
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import { MetricCardLoading } from './MetricCard';
|
||||
|
||||
export interface ChartContextType extends IChartProps {
|
||||
export interface IChartContextType extends IChartProps {
|
||||
editMode?: boolean;
|
||||
hideID?: boolean;
|
||||
onClick?: (item: IChartSerie) => void;
|
||||
limit?: number;
|
||||
renderSerieName?: (names: string[]) => React.ReactNode;
|
||||
renderSerieIcon?: (serie: IChartSerie) => React.ReactNode;
|
||||
dropdownMenuContent?: (serie: IChartSerie) => {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}[];
|
||||
}
|
||||
|
||||
type ChartProviderProps = {
|
||||
type IChartProviderProps = {
|
||||
children: React.ReactNode;
|
||||
} & ChartContextType;
|
||||
} & IChartContextType;
|
||||
|
||||
const ChartContext = createContext<ChartContextType | null>({
|
||||
events: [],
|
||||
breakdowns: [],
|
||||
chartType: 'linear',
|
||||
lineType: 'monotone',
|
||||
interval: 'day',
|
||||
name: '',
|
||||
range: '7d',
|
||||
metric: 'sum',
|
||||
previous: false,
|
||||
projectId: '',
|
||||
limit: undefined,
|
||||
});
|
||||
const ChartContext = createContext<IChartContextType | null>(null);
|
||||
|
||||
export function ChartProvider({
|
||||
children,
|
||||
editMode,
|
||||
previous,
|
||||
hideID,
|
||||
limit,
|
||||
...props
|
||||
}: ChartProviderProps) {
|
||||
export function ChartProvider({ children, ...props }: IChartProviderProps) {
|
||||
return (
|
||||
<ChartContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
...props,
|
||||
editMode: editMode ?? false,
|
||||
previous: previous ?? false,
|
||||
hideID: hideID ?? false,
|
||||
limit,
|
||||
}),
|
||||
[editMode, previous, hideID, limit, props]
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ChartContext.Provider>
|
||||
<ChartContext.Provider value={props}>{children}</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function withChartProivder<ComponentProps>(
|
||||
WrappedComponent: React.FC<ComponentProps>
|
||||
) {
|
||||
const WithChartProvider = (props: ComponentProps & ChartContextType) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return props.chartType === 'metric' ? (
|
||||
<MetricCardLoading />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
props.chartType === 'metric' ? (
|
||||
<MetricCardLoading />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
)
|
||||
}
|
||||
>
|
||||
<ChartProvider {...props}>
|
||||
<WrappedComponent {...props} />
|
||||
</ChartProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
WithChartProvider.displayName = `WithChartProvider(${
|
||||
WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'
|
||||
})`;
|
||||
|
||||
return memo(WithChartProvider);
|
||||
}
|
||||
|
||||
export function useChartContext() {
|
||||
return useContext(ChartContext)!;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useInViewport } from 'react-in-viewport';
|
||||
|
||||
import { ChartSwitch } from '.';
|
||||
import type { IChartRoot } from '.';
|
||||
import { ChartRoot } from '.';
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import type { ChartContextType } from './ChartProvider';
|
||||
|
||||
export function LazyChart(props: ChartContextType) {
|
||||
export function LazyChart(props: IChartRoot) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const once = useRef(false);
|
||||
const { inViewport } = useInViewport(ref, undefined, {
|
||||
@@ -23,7 +23,7 @@ export function LazyChart(props: ChartContextType) {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{once.current || inViewport ? (
|
||||
<ChartSwitch {...props} editMode={false} />
|
||||
<ChartRoot {...props} editMode={false} />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { fancyMinutes, useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
PreviousDiffIndicatorText,
|
||||
} from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
interface MetricCardProps {
|
||||
serie: IChartData['series'][number];
|
||||
@@ -28,7 +28,7 @@ export function MetricCard({
|
||||
metric,
|
||||
unit,
|
||||
}: MetricCardProps) {
|
||||
const { previousIndicatorInverted } = useChartContext();
|
||||
const { previousIndicatorInverted, editMode } = useChartContext();
|
||||
const number = useNumber();
|
||||
|
||||
const renderValue = (value: number, unitClassName?: string) => {
|
||||
@@ -57,14 +57,15 @@ export function MetricCard({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative h-[70px] overflow-hidden'
|
||||
// '[#report-editor_&&]:card [#report-editor_&&]:px-4 [#report-editor_&&]:py-2'
|
||||
'group relative h-[70px] overflow-hidden',
|
||||
editMode && 'card h-[100px] px-4 py-2'
|
||||
)}
|
||||
key={serie.name}
|
||||
key={serie.id}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute -bottom-1 -left-1 -right-1 top-0 z-0 opacity-20 transition-opacity duration-300 group-hover:opacity-50'
|
||||
'pointer-events-none absolute -bottom-1 -left-1 -right-1 top-0 z-0 opacity-20 transition-opacity duration-300 group-hover:opacity-50',
|
||||
editMode && 'bottom-1'
|
||||
)}
|
||||
>
|
||||
<AutoSizer>
|
||||
@@ -91,9 +92,8 @@ export function MetricCard({
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2 text-left font-semibold">
|
||||
<ColorSquare>{serie.event.id}</ColorSquare>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
|
||||
{serie.name}
|
||||
<SerieName name={serie.names} />
|
||||
</span>
|
||||
</div>
|
||||
{/* <PreviousDiffIndicator {...serie.metrics.previous[metric]} /> */}
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import type { IChartLineType, IInterval } from '@openpanel/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
@@ -24,16 +22,10 @@ import { ResponsiveContainer } from './ResponsiveContainer';
|
||||
|
||||
interface ReportAreaChartProps {
|
||||
data: IChartData;
|
||||
interval: IInterval;
|
||||
lineType: IChartLineType;
|
||||
}
|
||||
|
||||
export function ReportAreaChart({
|
||||
lineType,
|
||||
interval,
|
||||
data,
|
||||
}: ReportAreaChartProps) {
|
||||
const { editMode } = useChartContext();
|
||||
export function ReportAreaChart({ data }: ReportAreaChartProps) {
|
||||
const { editMode, lineType, interval } = useChartContext();
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -65,7 +57,7 @@ export function ReportAreaChart({
|
||||
{series.map((serie) => {
|
||||
const color = getChartColor(serie.index);
|
||||
return (
|
||||
<React.Fragment key={serie.name}>
|
||||
<React.Fragment key={serie.id}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`color${color}`}
|
||||
@@ -87,7 +79,7 @@ export function ReportAreaChart({
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
key={serie.name}
|
||||
key={serie.id}
|
||||
type={lineType}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { DropdownMenuPortal } from '@radix-ui/react-dropdown-menu';
|
||||
import { ExternalLinkIcon, FilterIcon } from 'lucide-react';
|
||||
|
||||
import { round } from '@openpanel/common';
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
@@ -11,13 +20,15 @@ import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import { PreviousDiffIndicatorText } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { SerieIcon } from './SerieIcon';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
interface ReportBarChartProps {
|
||||
data: IChartData;
|
||||
}
|
||||
|
||||
export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||
const { editMode, metric, onClick, limit } = useChartContext();
|
||||
const { editMode, metric, onClick, limit, dropdownMenuContent } =
|
||||
useChartContext();
|
||||
const number = useNumber();
|
||||
const series = useMemo(
|
||||
() => (editMode ? data.series : data.series.slice(0, limit || 10)),
|
||||
@@ -33,42 +44,64 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||
)}
|
||||
>
|
||||
{series.map((serie) => {
|
||||
const isClickable = serie.name !== NOT_SET_VALUE && onClick;
|
||||
const isClickable = !serie.names.includes(NOT_SET_VALUE) && onClick;
|
||||
const isDropDownEnabled =
|
||||
!serie.names.includes(NOT_SET_VALUE) &&
|
||||
(dropdownMenuContent?.(serie) || []).length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={serie.name}
|
||||
className={cn('relative', isClickable && 'cursor-pointer')}
|
||||
{...(isClickable ? { onClick: () => onClick(serie) } : {})}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
|
||||
style={{
|
||||
width: `${(serie.metrics.sum / maxCount) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-10 flex w-full flex-1 items-center gap-4 overflow-hidden px-3 py-2">
|
||||
<div className="flex flex-1 items-center gap-2 break-all font-medium">
|
||||
<SerieIcon name={serie.name} />
|
||||
{serie.name}
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center justify-end gap-4">
|
||||
<PreviousDiffIndicatorText
|
||||
{...serie.metrics.previous?.[metric]}
|
||||
className="text-xs font-medium"
|
||||
<DropdownMenu key={serie.id}>
|
||||
<DropdownMenuTrigger asChild disabled={!isDropDownEnabled}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative',
|
||||
(isClickable || isDropDownEnabled) && 'cursor-pointer'
|
||||
)}
|
||||
{...(isClickable && !isDropDownEnabled
|
||||
? { onClick: () => onClick?.(serie) }
|
||||
: {})}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
|
||||
style={{
|
||||
width: `${(serie.metrics.sum / maxCount) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
{serie.metrics.previous?.[metric]?.value}
|
||||
<div className="text-muted-foreground">
|
||||
{number.format(
|
||||
round((serie.metrics.sum / data.metrics.sum) * 100, 2)
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
{number.format(serie.metrics.sum)}
|
||||
<div className="relative z-10 flex w-full flex-1 items-center gap-4 overflow-hidden px-3 py-2">
|
||||
<div className="flex flex-1 items-center gap-2 break-all font-medium">
|
||||
<SerieIcon name={serie.names[0]} />
|
||||
<SerieName name={serie.names} />
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center justify-end gap-4">
|
||||
<PreviousDiffIndicatorText
|
||||
{...serie.metrics.previous?.[metric]}
|
||||
className="text-xs font-medium"
|
||||
/>
|
||||
{serie.metrics.previous?.[metric]?.value}
|
||||
<div className="text-muted-foreground">
|
||||
{number.format(
|
||||
round((serie.metrics.sum / data.metrics.sum) * 100, 2)
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
{number.format(serie.metrics.sum)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
{dropdownMenuContent?.(serie).map((item) => (
|
||||
<DropdownMenuItem key={item.title} onClick={item.onClick}>
|
||||
{item.icon && <item.icon size={16} className="mr-2" />}
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,8 @@ import type { IToolTipProps } from '@/types';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { SerieIcon } from './SerieIcon';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
type ReportLineChartTooltipProps = IToolTipProps<{
|
||||
value: number;
|
||||
@@ -53,7 +55,7 @@ export function ReportChartTooltip({
|
||||
) as IRechartPayloadItem;
|
||||
|
||||
return (
|
||||
<React.Fragment key={data.name}>
|
||||
<React.Fragment key={data.id}>
|
||||
{index === 0 && data.date && (
|
||||
<div className="flex justify-between gap-8">
|
||||
<div>{formatDate(new Date(data.date))}</div>
|
||||
@@ -65,8 +67,9 @@ export function ReportChartTooltip({
|
||||
style={{ background: data.color }}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="min-w-0 max-w-[200px] overflow-hidden text-ellipsis whitespace-nowrap font-medium">
|
||||
{getLabel(data.name)}
|
||||
<div className="flex items-center gap-1">
|
||||
<SerieIcon name={data.names} />
|
||||
<SerieName name={data.names} />
|
||||
</div>
|
||||
<div className="flex justify-between gap-8">
|
||||
<div>{number.formatWithUnit(data.count, unit)}</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ResponsiveContainer } from './ResponsiveContainer';
|
||||
|
||||
interface ReportHistogramChartProps {
|
||||
data: IChartData;
|
||||
interval: IInterval;
|
||||
}
|
||||
|
||||
function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
||||
@@ -32,11 +31,8 @@ function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportHistogramChart({
|
||||
interval,
|
||||
data,
|
||||
}: ReportHistogramChartProps) {
|
||||
const { editMode, previous } = useChartContext();
|
||||
export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
||||
const { editMode, previous, interval } = useChartContext();
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -71,11 +67,11 @@ export function ReportHistogramChart({
|
||||
/>
|
||||
{series.map((serie) => {
|
||||
return (
|
||||
<React.Fragment key={serie.name}>
|
||||
<React.Fragment key={serie.id}>
|
||||
{previous && (
|
||||
<Bar
|
||||
key={`${serie.name}:prev`}
|
||||
name={`${serie.name}:prev`}
|
||||
key={`${serie.id}:prev`}
|
||||
name={`${serie.id}:prev`}
|
||||
dataKey={`${serie.id}:prev:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
fillOpacity={0.2}
|
||||
@@ -83,8 +79,8 @@ export function ReportHistogramChart({
|
||||
/>
|
||||
)}
|
||||
<Bar
|
||||
key={serie.name}
|
||||
name={serie.name}
|
||||
key={serie.id}
|
||||
name={serie.id}
|
||||
dataKey={`${serie.id}:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
radius={3}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import { api } from '@/trpc/client';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { isSameDay, isSameHour, isSameMonth } from 'date-fns';
|
||||
import { SplineIcon } from 'lucide-react';
|
||||
import { last, pathOr } from 'ramda';
|
||||
import {
|
||||
Area,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
@@ -27,49 +32,35 @@ import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
import { ReportTable } from './ReportTable';
|
||||
import { ResponsiveContainer } from './ResponsiveContainer';
|
||||
import { SerieIcon } from './SerieIcon';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
interface ReportLineChartProps {
|
||||
data: IChartData;
|
||||
references: IServiceReference[];
|
||||
interval: IInterval;
|
||||
lineType: IChartLineType;
|
||||
}
|
||||
|
||||
function CustomLegend(props: {
|
||||
payload?: { value: string; payload: { fill: string } }[];
|
||||
}) {
|
||||
if (!props.payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1">
|
||||
{props.payload
|
||||
.filter((entry) => !entry.value.includes('noTooltip'))
|
||||
.filter((entry) => !entry.value.includes(':prev'))
|
||||
.map((entry) => (
|
||||
<div className="flex gap-1" key={entry.value}>
|
||||
<SplineIcon size={12} color={entry.payload.fill} />
|
||||
<div
|
||||
style={{
|
||||
color: entry.payload.fill,
|
||||
}}
|
||||
>
|
||||
{entry.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
export function ReportLineChart({ data }: ReportLineChartProps) {
|
||||
const {
|
||||
editMode,
|
||||
previous,
|
||||
interval,
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
lineType,
|
||||
} = useChartContext();
|
||||
const references = api.reference.getChartReferences.useQuery(
|
||||
{
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 5,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportLineChart({
|
||||
lineType,
|
||||
interval,
|
||||
data,
|
||||
references,
|
||||
}: ReportLineChartProps) {
|
||||
const { editMode, previous } = useChartContext();
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -96,20 +87,56 @@ export function ReportLineChart({
|
||||
</linearGradient>
|
||||
);
|
||||
|
||||
const useDashedLastLine = (series[0]?.data?.length || 0) > 2;
|
||||
const lastSerieDataItem = last(series[0]?.data || [])?.date || new Date();
|
||||
const useDashedLastLine = (() => {
|
||||
if (interval === 'hour') {
|
||||
return isSameHour(lastSerieDataItem, new Date());
|
||||
}
|
||||
|
||||
if (interval === 'day') {
|
||||
return isSameDay(lastSerieDataItem, new Date());
|
||||
}
|
||||
|
||||
if (interval === 'month') {
|
||||
return isSameMonth(lastSerieDataItem, new Date());
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
const CustomLegend = useCallback(() => {
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1">
|
||||
{series.map((serie) => (
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
key={serie.id}
|
||||
style={{
|
||||
color: getChartColor(serie.index),
|
||||
}}
|
||||
>
|
||||
<SerieIcon name={serie.names} />
|
||||
<SerieName name={serie.names} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, [series]);
|
||||
|
||||
const isAreaStyle = series.length === 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponsiveContainer>
|
||||
{({ width, height }) => (
|
||||
<LineChart width={width} height={height} data={rechartData}>
|
||||
<ComposedChart width={width} height={height} data={rechartData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
horizontal={true}
|
||||
vertical={false}
|
||||
className="stroke-def-200"
|
||||
/>
|
||||
{references.map((ref) => (
|
||||
{references.data?.map((ref) => (
|
||||
<ReferenceLine
|
||||
key={ref.id}
|
||||
x={ref.date.getTime()}
|
||||
@@ -150,18 +177,39 @@ export function ReportLineChart({
|
||||
tickLine={false}
|
||||
/>
|
||||
{series.map((serie) => {
|
||||
const color = getChartColor(serie.index);
|
||||
return (
|
||||
<React.Fragment key={serie.name}>
|
||||
<React.Fragment key={serie.id}>
|
||||
<defs>
|
||||
{isAreaStyle && (
|
||||
<linearGradient
|
||||
id={`color${color}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={color}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={color}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
</linearGradient>
|
||||
)}
|
||||
{gradientTwoColors(
|
||||
`hideAllButLastInterval_${serie.id}`,
|
||||
'rgba(0,0,0,0)',
|
||||
getChartColor(serie.index),
|
||||
color,
|
||||
lastIntervalPercent
|
||||
)}
|
||||
{gradientTwoColors(
|
||||
`hideJustLastInterval_${serie.id}`,
|
||||
getChartColor(serie.index),
|
||||
color,
|
||||
'rgba(0,0,0,0)',
|
||||
lastIntervalPercent
|
||||
)}
|
||||
@@ -169,24 +217,30 @@ export function ReportLineChart({
|
||||
<Line
|
||||
dot={false}
|
||||
type={lineType}
|
||||
name={serie.name}
|
||||
name={serie.id}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey={`${serie.id}:count`}
|
||||
stroke={
|
||||
useDashedLastLine
|
||||
? 'transparent'
|
||||
: getChartColor(serie.index)
|
||||
}
|
||||
stroke={useDashedLastLine ? 'transparent' : color}
|
||||
// Use for legend
|
||||
fill={getChartColor(serie.index)}
|
||||
fill={color}
|
||||
/>
|
||||
{isAreaStyle && (
|
||||
<Area
|
||||
name={`${serie.id}:area:noTooltip`}
|
||||
dataKey={`${serie.id}:count`}
|
||||
fill={`url(#color${color})`}
|
||||
type={lineType}
|
||||
isAnimationActive={false}
|
||||
fillOpacity={0.1}
|
||||
/>
|
||||
)}
|
||||
{useDashedLastLine && (
|
||||
<>
|
||||
<Line
|
||||
dot={false}
|
||||
type={lineType}
|
||||
name={`${serie.name}:dashed:noTooltip`}
|
||||
name={`${serie.id}:dashed:noTooltip`}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey={`${serie.id}:count`}
|
||||
@@ -197,7 +251,7 @@ export function ReportLineChart({
|
||||
<Line
|
||||
dot={false}
|
||||
type={lineType}
|
||||
name={`${serie.name}:solid:noTooltip`}
|
||||
name={`${serie.id}:solid:noTooltip`}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey={`${serie.id}:count`}
|
||||
@@ -208,22 +262,22 @@ export function ReportLineChart({
|
||||
{previous && (
|
||||
<Line
|
||||
type={lineType}
|
||||
name={`${serie.name}:prev`}
|
||||
name={`${serie.id}:prev`}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
strokeDasharray={'1 1'}
|
||||
strokeOpacity={0.5}
|
||||
dataKey={`${serie.id}:prev:count`}
|
||||
stroke={getChartColor(serie.index)}
|
||||
stroke={color}
|
||||
// Use for legend
|
||||
fill={getChartColor(serie.index)}
|
||||
fill={color}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
</ComposedChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
{editMode && (
|
||||
|
||||
@@ -18,7 +18,7 @@ export function ReportMapChart({ data }: ReportMapChartProps) {
|
||||
const mapData = useMemo(
|
||||
() =>
|
||||
series.map((s) => ({
|
||||
country: s.name.toLowerCase(),
|
||||
country: s.names[0]?.toLowerCase() ?? '',
|
||||
value: s.metrics[metric],
|
||||
})),
|
||||
[series, metric]
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ReportMetricChart({ data }: ReportMetricChartProps) {
|
||||
{series.map((serie) => {
|
||||
return (
|
||||
<MetricCard
|
||||
key={serie.name}
|
||||
key={serie.id}
|
||||
serie={serie}
|
||||
metric={metric}
|
||||
unit={unit}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ReportPieChart({ data }: ReportPieChartProps) {
|
||||
id: serie.id,
|
||||
color: getChartColor(serie.index),
|
||||
index: serie.index,
|
||||
name: serie.name,
|
||||
name: serie.names.join(' > '),
|
||||
count: serie.metrics.sum,
|
||||
percent: serie.metrics.sum / sum,
|
||||
}));
|
||||
|
||||
@@ -13,16 +13,19 @@ import {
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
Tooltiper,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { useSelector } from '@/redux';
|
||||
import { getPropertyLabel } from '@/translations/properties';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
interface ReportTableProps {
|
||||
data: IChartData;
|
||||
@@ -40,8 +43,8 @@ export function ReportTable({
|
||||
const { setPage, paginate, page } = usePagination(ROWS_LIMIT);
|
||||
const number = useNumber();
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const breakdowns = useSelector((state) => state.report.breakdowns);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const getLabel = useMappings();
|
||||
|
||||
function handleChange(name: string, checked: boolean) {
|
||||
setVisibleSeries((prev) => {
|
||||
@@ -55,49 +58,61 @@ export function ReportTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-[200px_1fr] overflow-hidden rounded-md border border-border">
|
||||
<div className="grid grid-cols-[max(300px,30vw)_1fr] overflow-hidden rounded-md border border-border">
|
||||
<Table className="rounded-none border-b-0 border-l-0 border-t-0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
{breakdowns.length === 0 && <TableHead>Name</TableHead>}
|
||||
{breakdowns.map((breakdown) => (
|
||||
<TableHead key={breakdown.name}>
|
||||
{getPropertyLabel(breakdown.name)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableBody className="bg-def-100">
|
||||
{paginate(data.series).map((serie, index) => {
|
||||
const checked = !!visibleSeries.find(
|
||||
(item) => item.name === serie.name
|
||||
(item) => item.id === serie.id
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.name, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{getLabel(serie.name)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{getLabel(serie.name)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableRow key={serie.id}>
|
||||
{serie.names.map((name, nameIndex) => {
|
||||
return (
|
||||
<TableCell className="h-10" key={name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{nameIndex === 0 ? (
|
||||
<>
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.id, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
<Tooltiper
|
||||
side="left"
|
||||
sideOffset={30}
|
||||
content={<SerieName name={serie.names} />}
|
||||
>
|
||||
{name}
|
||||
</Tooltiper>
|
||||
</>
|
||||
) : (
|
||||
<SerieName name={name} />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
@@ -122,7 +137,7 @@ export function ReportTable({
|
||||
<TableBody>
|
||||
{paginate(data.series).map((serie) => {
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableRow key={serie.id}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{number.format(serie.metrics.sum)}
|
||||
|
||||
@@ -4,7 +4,7 @@ const createFlagIcon = (url: string) => {
|
||||
return function (_props: LucideProps) {
|
||||
return (
|
||||
<span
|
||||
className={`fi fis !block overflow-hidden rounded-full !leading-[1rem] fi-${url}`}
|
||||
className={`fi !block aspect-[1.33] overflow-hidden rounded-[2px] fi-${url}`}
|
||||
></span>
|
||||
);
|
||||
} as LucideIcon;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
SearchIcon,
|
||||
SmartphoneIcon,
|
||||
TabletIcon,
|
||||
TvIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
@@ -20,9 +21,9 @@ import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import flags from './SerieIcon.flags';
|
||||
import iconsWithUrls from './SerieIcon.urls';
|
||||
|
||||
interface SerieIconProps extends LucideProps {
|
||||
name?: string;
|
||||
}
|
||||
type SerieIconProps = Omit<LucideProps, 'name'> & {
|
||||
name?: string | string[];
|
||||
};
|
||||
|
||||
function getProxyImage(url: string) {
|
||||
return `${String(process.env.NEXT_PUBLIC_API_URL)}/misc/favicon?url=${encodeURIComponent(url)}`;
|
||||
@@ -30,7 +31,7 @@ function getProxyImage(url: string) {
|
||||
|
||||
const createImageIcon = (url: string) => {
|
||||
return function (_props: LucideProps) {
|
||||
return <img className="h-4 rounded-[2px] object-contain" src={url} />;
|
||||
return <img className="max-h-4 rounded-[2px] object-contain" src={url} />;
|
||||
} as LucideIcon;
|
||||
};
|
||||
|
||||
@@ -42,6 +43,7 @@ const mapper: Record<string, LucideIcon> = {
|
||||
link_out: ExternalLinkIcon,
|
||||
|
||||
// Misc
|
||||
smarttv: TvIcon,
|
||||
mobile: SmartphoneIcon,
|
||||
desktop: MonitorIcon,
|
||||
tablet: TabletIcon,
|
||||
@@ -64,7 +66,8 @@ const mapper: Record<string, LucideIcon> = {
|
||||
...flags,
|
||||
};
|
||||
|
||||
export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
export function SerieIcon({ name: names, ...props }: SerieIconProps) {
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
const Icon = useMemo(() => {
|
||||
if (!name) {
|
||||
return null;
|
||||
@@ -80,6 +83,7 @@ export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
return createImageIcon(getProxyImage(name));
|
||||
}
|
||||
|
||||
// Matching image file name
|
||||
if (name.match(/(.+)\.\w{2,3}$/)) {
|
||||
return createImageIcon(getProxyImage(`https://${name}`));
|
||||
}
|
||||
@@ -88,8 +92,8 @@ export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
}, [name]);
|
||||
|
||||
return Icon ? (
|
||||
<div className="[&_a]:![&_a]:!h-4 relative h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
||||
<Icon size={16} {...props} />
|
||||
<div className="relative max-h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
||||
<Icon size={16} {...props} name={name} />
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,18 @@ const data = {
|
||||
'vstat.info': 'https://vstat.info',
|
||||
'yahoo!': 'https://yahoo.com',
|
||||
android: 'https://image.similarpng.com/very-thumbnail/2020/08/Android-icon-on-transparent--background-PNG.png',
|
||||
'android browser': 'https://image.similarpng.com/very-thumbnail/2020/08/Android-icon-on-transparent--background-PNG.png',
|
||||
'silk': 'https://m.media-amazon.com/images/I/51VCjQCvF0L.png',
|
||||
'kakaotalk': 'https://www.kakaocorp.com/',
|
||||
bing: 'https://bing.com',
|
||||
'electron': 'https://www.electronjs.org',
|
||||
'whale': 'https://whale.naver.com',
|
||||
'wechat': 'https://wechat.com',
|
||||
chrome: 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'chrome webview': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'chrome headless': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
chromecast: 'https://upload.wikimedia.org/wikipedia/commons/archive/2/26/20161130022732%21Chromecast_cast_button_icon.svg',
|
||||
webkit: 'https://webkit.org',
|
||||
duckduckgo: 'https://duckduckgo.com',
|
||||
ecosia: 'https://ecosia.com',
|
||||
edge: 'https://upload.wikimedia.org/wikipedia/commons/7/7e/Microsoft_Edge_logo_%282019%29.png',
|
||||
@@ -19,6 +28,7 @@ const data = {
|
||||
github: 'https://github.com',
|
||||
gmail: 'https://mail.google.com',
|
||||
google: 'https://google.com',
|
||||
gsa: 'https://google.com', // Google Search App
|
||||
instagram: 'https://instagram.com',
|
||||
ios: 'https://cdn0.iconfinder.com/data/icons/flat-round-system/512/apple-1024.png',
|
||||
linkedin: 'https://linkedin.com',
|
||||
|
||||
40
apps/dashboard/src/components/report/chart/SerieName.tsx
Normal file
40
apps/dashboard/src/components/report/chart/SerieName.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
|
||||
import { useChartContext } from './ChartProvider';
|
||||
|
||||
interface SerieNameProps {
|
||||
name: string | string[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SerieName({ name, className }: SerieNameProps) {
|
||||
const chart = useChartContext();
|
||||
if (Array.isArray(name)) {
|
||||
if (chart.renderSerieName) {
|
||||
return chart.renderSerieName(name);
|
||||
}
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1', className)}>
|
||||
{name.map((n, index) => {
|
||||
return (
|
||||
<>
|
||||
<span>{n || NOT_SET_VALUE}</span>
|
||||
{name.length - 1 > index && (
|
||||
<ChevronRightIcon className="text-muted-foreground" size={12} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (chart.renderSerieName) {
|
||||
return chart.renderSerieName([name]);
|
||||
}
|
||||
|
||||
return <>{name}</>;
|
||||
}
|
||||
@@ -1,22 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { Funnel } from '../funnel';
|
||||
import { Chart } from './Chart';
|
||||
import { withChartProivder } from './ChartProvider';
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import type { IChartContextType } from './ChartProvider';
|
||||
import { ChartProvider } from './ChartProvider';
|
||||
import { MetricCardLoading } from './MetricCard';
|
||||
|
||||
export const ChartSwitch = withChartProivder(function ChartSwitch(
|
||||
props: IChartProps
|
||||
) {
|
||||
if (props.chartType === 'funnel') {
|
||||
return <Funnel {...props} />;
|
||||
export type IChartRoot = IChartContextType;
|
||||
|
||||
export function ChartRoot(props: IChartContextType) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return props.chartType === 'metric' ? (
|
||||
<MetricCardLoading />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
);
|
||||
}
|
||||
|
||||
return <Chart {...props} />;
|
||||
});
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
props.chartType === 'metric' ? <MetricCardLoading /> : <ChartLoading />
|
||||
}
|
||||
>
|
||||
<ChartProvider {...props}>
|
||||
{props.chartType === 'funnel' ? <Funnel /> : <Chart />}
|
||||
</ChartProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChartSwitchShortcutProps {
|
||||
interface ChartRootShortcutProps {
|
||||
projectId: IChartProps['projectId'];
|
||||
range?: IChartProps['range'];
|
||||
previous?: IChartProps['previous'];
|
||||
@@ -25,16 +50,16 @@ interface ChartSwitchShortcutProps {
|
||||
events: IChartProps['events'];
|
||||
}
|
||||
|
||||
export const ChartSwitchShortcut = ({
|
||||
export const ChartRootShortcut = ({
|
||||
projectId,
|
||||
range = '7d',
|
||||
previous = false,
|
||||
chartType = 'linear',
|
||||
interval = 'day',
|
||||
events,
|
||||
}: ChartSwitchShortcutProps) => {
|
||||
}: ChartRootShortcutProps) => {
|
||||
return (
|
||||
<ChartSwitch
|
||||
<ChartRoot
|
||||
projectId={projectId}
|
||||
range={range}
|
||||
breakdowns={[]}
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
import { api } from '@/trpc/client';
|
||||
|
||||
import type { IChartInput, IChartProps } from '@openpanel/validation';
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
|
||||
import { ChartEmpty } from '../chart/ChartEmpty';
|
||||
import { withChartProivder } from '../chart/ChartProvider';
|
||||
import { useChartContext } from '../chart/ChartProvider';
|
||||
import { FunnelSteps } from './Funnel';
|
||||
|
||||
export type ReportChartProps = IChartProps;
|
||||
export function Funnel() {
|
||||
const { events, range, projectId } = useChartContext();
|
||||
|
||||
export const Funnel = withChartProivder(function Chart({
|
||||
events,
|
||||
range,
|
||||
projectId,
|
||||
}: ReportChartProps) {
|
||||
const input: IChartInput = {
|
||||
events,
|
||||
range,
|
||||
@@ -38,4 +34,4 @@ export const Funnel = withChartProivder(function Chart({
|
||||
<FunnelSteps {...data} input={input} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-def-200 h-10 border-b border-border px-4 text-left align-middle text-sm font-medium text-muted-foreground shadow-[0_0_0_0.5px] shadow-border [&:has([role=checkbox])]:pr-0',
|
||||
'h-10 border-b border-border bg-def-200 px-4 text-left align-middle text-sm font-medium text-muted-foreground shadow-[0_0_0_0.5px] shadow-border [&:has([role=checkbox])]:pr-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -90,7 +90,7 @@ const TableCell = React.forwardRef<
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-12 whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border [&:has([role=checkbox])]:pr-0',
|
||||
'h-12 whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -35,10 +35,13 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
interface TooltiperProps {
|
||||
asChild?: boolean;
|
||||
content: string;
|
||||
content: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
delayDuration?: number;
|
||||
sideOffset?: number;
|
||||
}
|
||||
export function Tooltiper({
|
||||
asChild,
|
||||
@@ -46,14 +49,19 @@ export function Tooltiper({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
side,
|
||||
delayDuration = 0,
|
||||
sideOffset = 10,
|
||||
}: TooltiperProps) {
|
||||
return (
|
||||
<Tooltip delayDuration={0}>
|
||||
<Tooltip delayDuration={delayDuration}>
|
||||
<TooltipTrigger asChild={asChild} className={className} onClick={onClick}>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent sideOffset={10}>{content}</TooltipContent>
|
||||
<TooltipContent sideOffset={sideOffset} side={side}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import mappings from '@/mappings.json';
|
||||
|
||||
export function useMappings() {
|
||||
return (val: string | null) => {
|
||||
return (val: string | string[]): string => {
|
||||
if (Array.isArray(val)) {
|
||||
return val
|
||||
.map((v) => mappings.find((item) => item.id === v)?.name ?? v)
|
||||
.join('');
|
||||
}
|
||||
|
||||
return mappings.find((item) => item.id === val)?.name ?? val;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getChartColor } from '@/utils/theme';
|
||||
|
||||
export type IRechartPayloadItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
names: string[];
|
||||
color: string;
|
||||
event: { id: string; name: string };
|
||||
count: number;
|
||||
@@ -39,7 +39,7 @@ export function useRechartDataModel(series: IChartData['series']) {
|
||||
...item,
|
||||
id: serie.id,
|
||||
event: serie.event,
|
||||
name: serie.name,
|
||||
names: serie.names,
|
||||
color: getChartColor(idx),
|
||||
} satisfies IRechartPayloadItem;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ export type IVisibleSeries = ReturnType<typeof useVisibleSeries>['series'];
|
||||
export function useVisibleSeries(data: IChartData, limit?: number | undefined) {
|
||||
const max = limit ?? 5;
|
||||
const [visibleSeries, setVisibleSeries] = useState<string[]>(
|
||||
data?.series?.slice(0, max).map((serie) => serie.name) ?? []
|
||||
data?.series?.slice(0, max).map((serie) => serie.id) ?? []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleSeries(
|
||||
data?.series?.slice(0, max).map((serie) => serie.name) ?? []
|
||||
data?.series?.slice(0, max).map((serie) => serie.id) ?? []
|
||||
);
|
||||
}, [data, max]);
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useVisibleSeries(data: IChartData, limit?: number | undefined) {
|
||||
...serie,
|
||||
index,
|
||||
}))
|
||||
.filter((serie) => visibleSeries.includes(serie.name)),
|
||||
.filter((serie) => visibleSeries.includes(serie.id)),
|
||||
setVisibleSeries,
|
||||
} as const;
|
||||
}, [visibleSeries, data.series]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { ChartRoot } from '@/components/report/chart';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
@@ -15,7 +15,7 @@ const OverviewChartDetails = (props: Props) => {
|
||||
<ModalHeader title={props.chart.name} />
|
||||
<ScrollArea className="-m-6 max-h-[calc(100vh-200px)]">
|
||||
<div className="p-6">
|
||||
<ChartSwitch {...props.chart} limit={999} chartType="bar" />
|
||||
<ChartRoot {...props.chart} limit={999} chartType="bar" />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</ModalContent>
|
||||
|
||||
255
apps/dashboard/src/translations/countries.ts
Normal file
255
apps/dashboard/src/translations/countries.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
export const countries = {
|
||||
AF: 'Afghanistan',
|
||||
AL: 'Albania',
|
||||
DZ: 'Algeria',
|
||||
AS: 'American Samoa',
|
||||
AD: 'Andorra',
|
||||
AO: 'Angola',
|
||||
AI: 'Anguilla',
|
||||
AQ: 'Antarctica',
|
||||
AG: 'Antigua and Barbuda',
|
||||
AR: 'Argentina',
|
||||
AM: 'Armenia',
|
||||
AW: 'Aruba',
|
||||
AU: 'Australia',
|
||||
AT: 'Austria',
|
||||
AZ: 'Azerbaijan',
|
||||
BS: 'Bahamas',
|
||||
BH: 'Bahrain',
|
||||
BD: 'Bangladesh',
|
||||
BB: 'Barbados',
|
||||
BY: 'Belarus',
|
||||
BE: 'Belgium',
|
||||
BZ: 'Belize',
|
||||
BJ: 'Benin',
|
||||
BM: 'Bermuda',
|
||||
BT: 'Bhutan',
|
||||
BO: 'Bolivia',
|
||||
BQ: 'Bonaire, Sint Eustatius and Saba',
|
||||
BA: 'Bosnia and Herzegovina',
|
||||
BW: 'Botswana',
|
||||
BV: 'Bouvet Island',
|
||||
BR: 'Brazil',
|
||||
IO: 'British Indian Ocean Territory',
|
||||
BN: 'Brunei Darussalam',
|
||||
BG: 'Bulgaria',
|
||||
BF: 'Burkina Faso',
|
||||
BI: 'Burundi',
|
||||
CV: 'Cabo Verde',
|
||||
KH: 'Cambodia',
|
||||
CM: 'Cameroon',
|
||||
CA: 'Canada',
|
||||
KY: 'Cayman Islands',
|
||||
CF: 'Central African Republic',
|
||||
TD: 'Chad',
|
||||
CL: 'Chile',
|
||||
CN: 'China',
|
||||
CX: 'Christmas Island',
|
||||
CC: 'Cocos (Keeling) Islands',
|
||||
CO: 'Colombia',
|
||||
KM: 'Comoros',
|
||||
CD: 'Congo (Democratic Republic)',
|
||||
CG: 'Congo',
|
||||
CK: 'Cook Islands',
|
||||
CR: 'Costa Rica',
|
||||
HR: 'Croatia',
|
||||
CU: 'Cuba',
|
||||
CW: 'Curaçao',
|
||||
CY: 'Cyprus',
|
||||
CZ: 'Czechia',
|
||||
CI: "Côte d'Ivoire",
|
||||
DK: 'Denmark',
|
||||
DJ: 'Djibouti',
|
||||
DM: 'Dominica',
|
||||
DO: 'Dominican Republic',
|
||||
EC: 'Ecuador',
|
||||
EG: 'Egypt',
|
||||
SV: 'El Salvador',
|
||||
GQ: 'Equatorial Guinea',
|
||||
ER: 'Eritrea',
|
||||
EE: 'Estonia',
|
||||
SZ: 'Eswatina',
|
||||
ET: 'Ethiopia',
|
||||
FK: 'Falkland Islands',
|
||||
FO: 'Faroe Islands',
|
||||
FJ: 'Fiji',
|
||||
FI: 'Finland',
|
||||
FR: 'France',
|
||||
GF: 'French Guiana',
|
||||
PF: 'French Polynesia',
|
||||
TF: 'French Southern Territories',
|
||||
GA: 'Gabon',
|
||||
GM: 'Gambia',
|
||||
GE: 'Georgia',
|
||||
DE: 'Germany',
|
||||
GH: 'Ghana',
|
||||
GI: 'Gibraltar',
|
||||
GR: 'Greece',
|
||||
GL: 'Greenland',
|
||||
GD: 'Grenada',
|
||||
GP: 'Guadeloupe',
|
||||
GU: 'Guam',
|
||||
GT: 'Guatemala',
|
||||
GG: 'Guernsey',
|
||||
GN: 'Guinea',
|
||||
GW: 'Guinea-Bissau',
|
||||
GY: 'Guyana',
|
||||
HT: 'Haiti',
|
||||
HM: 'Heard Island and McDonald Islands',
|
||||
VA: 'Holy See',
|
||||
HN: 'Honduras',
|
||||
HK: 'Hong Kong',
|
||||
HU: 'Hungary',
|
||||
IS: 'Iceland',
|
||||
IN: 'India',
|
||||
ID: 'Indonesia',
|
||||
IR: 'Iran',
|
||||
IQ: 'Iraq',
|
||||
IE: 'Ireland',
|
||||
IM: 'Isle of Man',
|
||||
IL: 'Israel',
|
||||
IT: 'Italy',
|
||||
JM: 'Jamaica',
|
||||
JP: 'Japan',
|
||||
JE: 'Jersey',
|
||||
JO: 'Jordan',
|
||||
KZ: 'Kazakhstan',
|
||||
KE: 'Kenya',
|
||||
KI: 'Kiribati',
|
||||
KP: "Korea (Democratic People's Republic)",
|
||||
KR: 'Korea (Republic)',
|
||||
KW: 'Kuwait',
|
||||
KG: 'Kyrgyzstan',
|
||||
LA: "Lao People's Democratic Republic",
|
||||
LV: 'Latvia',
|
||||
LB: 'Lebanon',
|
||||
LS: 'Lesotho',
|
||||
LR: 'Liberia',
|
||||
LY: 'Libya',
|
||||
LI: 'Liechtenstein',
|
||||
LT: 'Lithuania',
|
||||
LU: 'Luxembourg',
|
||||
MO: 'Macao',
|
||||
MG: 'Madagascar',
|
||||
MW: 'Malawi',
|
||||
MY: 'Malaysia',
|
||||
MV: 'Maldives',
|
||||
ML: 'Mali',
|
||||
MT: 'Malta',
|
||||
MH: 'Marshall Islands',
|
||||
MQ: 'Martinique',
|
||||
MR: 'Mauritania',
|
||||
MU: 'Mauritius',
|
||||
YT: 'Mayotte',
|
||||
MX: 'Mexico',
|
||||
FM: 'Micronesia',
|
||||
MD: 'Moldova',
|
||||
MC: 'Monaco',
|
||||
MN: 'Mongolia',
|
||||
ME: 'Montenegro',
|
||||
MS: 'Montserrat',
|
||||
MA: 'Morocco',
|
||||
MZ: 'Mozambique',
|
||||
MM: 'Myanmar',
|
||||
NA: 'Namibia',
|
||||
NR: 'Nauru',
|
||||
NP: 'Nepal',
|
||||
NL: 'Netherlands',
|
||||
NC: 'New Caledonia',
|
||||
NZ: 'New Zealand',
|
||||
NI: 'Nicaragua',
|
||||
NE: 'Niger',
|
||||
NG: 'Nigeria',
|
||||
NU: 'Niue',
|
||||
NF: 'Norfolk Island',
|
||||
MP: 'Northern Mariana Islands',
|
||||
NO: 'Norway',
|
||||
OM: 'Oman',
|
||||
PK: 'Pakistan',
|
||||
PW: 'Palau',
|
||||
PS: 'Palestine, State of',
|
||||
PA: 'Panama',
|
||||
PG: 'Papua New Guinea',
|
||||
PY: 'Paraguay',
|
||||
PE: 'Peru',
|
||||
PH: 'Philippines',
|
||||
PN: 'Pitcairn',
|
||||
PL: 'Poland',
|
||||
PT: 'Portugal',
|
||||
PR: 'Puerto Rico',
|
||||
QA: 'Qatar',
|
||||
MK: 'Republic of North Macedonia',
|
||||
RO: 'Romania',
|
||||
RU: 'Russian Federation',
|
||||
RW: 'Rwanda',
|
||||
RE: 'Réunion',
|
||||
BL: 'Saint Barthélemy',
|
||||
SH: 'Saint Helena, Ascension and Tristan da Cunha',
|
||||
KN: 'Saint Kitts and Nevis',
|
||||
LC: 'Saint Lucia',
|
||||
MF: 'Saint Martin (French part)',
|
||||
PM: 'Saint Pierre and Miquelon',
|
||||
VC: 'Saint Vincent and the Grenadines',
|
||||
WS: 'Samoa',
|
||||
SM: 'San Marino',
|
||||
ST: 'Sao Tome and Principe',
|
||||
SA: 'Saudi Arabia',
|
||||
SN: 'Senegal',
|
||||
RS: 'Serbia',
|
||||
SC: 'Seychelles',
|
||||
SL: 'Sierra Leone',
|
||||
SG: 'Singapore',
|
||||
SX: 'Sint Maarten (Dutch part)',
|
||||
SK: 'Slovakia',
|
||||
SI: 'Slovenia',
|
||||
SB: 'Solomon Islands',
|
||||
SO: 'Somalia',
|
||||
ZA: 'South Africa',
|
||||
GS: 'South Georgia and the South Sandwich Islands',
|
||||
SS: 'South Sudan',
|
||||
ES: 'Spain',
|
||||
LK: 'Sri Lanka',
|
||||
SD: 'Sudan',
|
||||
SR: 'Suriname',
|
||||
SJ: 'Svalbard and Jan Mayen',
|
||||
SE: 'Sweden',
|
||||
CH: 'Switzerland',
|
||||
SY: 'Syrian Arab Republic',
|
||||
TW: 'Taiwan',
|
||||
TJ: 'Tajikistan',
|
||||
TZ: 'Tanzania, United Republic of',
|
||||
TH: 'Thailand',
|
||||
TL: 'Timor-Leste',
|
||||
TG: 'Togo',
|
||||
TK: 'Tokelau',
|
||||
TO: 'Tonga',
|
||||
TT: 'Trinidad and Tobago',
|
||||
TN: 'Tunisia',
|
||||
TR: 'Turkey',
|
||||
TM: 'Turkmenistan',
|
||||
TC: 'Turks and Caicos Islands',
|
||||
TV: 'Tuvalu',
|
||||
UG: 'Uganda',
|
||||
UA: 'Ukraine',
|
||||
AE: 'United Arab Emirates',
|
||||
GB: 'United Kingdom',
|
||||
US: 'United States',
|
||||
UM: 'United States Minor Outlying Islands',
|
||||
UY: 'Uruguay',
|
||||
UZ: 'Uzbekistan',
|
||||
VU: 'Vanuatu',
|
||||
VE: 'Venezuela',
|
||||
VN: 'Viet Nam',
|
||||
VG: 'Virgin Islands (British)',
|
||||
VI: 'Virgin Islands (U.S.)',
|
||||
WF: 'Wallis and Futuna',
|
||||
EH: 'Western Sahara',
|
||||
YE: 'Yemen',
|
||||
ZM: 'Zambia',
|
||||
ZW: 'Zimbabwe',
|
||||
AX: 'Åland Islands',
|
||||
} as const;
|
||||
|
||||
export function getCountry(code?: string) {
|
||||
return countries[code as keyof typeof countries];
|
||||
}
|
||||
24
apps/dashboard/src/translations/properties.ts
Normal file
24
apps/dashboard/src/translations/properties.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
const properties = {
|
||||
has_profile: 'Has a profile',
|
||||
name: 'Name',
|
||||
path: 'Path',
|
||||
origin: 'Origin',
|
||||
referrer: 'Referrer',
|
||||
referrer_name: 'Referrer name',
|
||||
duration: 'Duration',
|
||||
created_at: 'Created at',
|
||||
country: 'Country',
|
||||
city: 'City',
|
||||
region: 'Region',
|
||||
os: 'OS',
|
||||
os_version: 'OS version',
|
||||
browser: 'Browser',
|
||||
browser_version: 'Browser version',
|
||||
device: 'Device',
|
||||
brand: 'Brand',
|
||||
model: 'Model',
|
||||
};
|
||||
|
||||
export function getPropertyLabel(property: string) {
|
||||
return properties[property as keyof typeof properties] || property;
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export async function incomingEvent(job: Job<EventsQueuePayloadIncomingEvent>) {
|
||||
model: uaInfo?.model ?? '',
|
||||
duration: 0,
|
||||
path: path,
|
||||
origin: origin,
|
||||
origin: origin || sessionStartEvent?.origin || '',
|
||||
referrer: referrer?.url,
|
||||
referrerName: referrer?.name || utmReferrer?.name || '',
|
||||
referrerType: referrer?.type || utmReferrer?.type || '',
|
||||
|
||||
@@ -16,7 +16,16 @@ import type { IInterval } from '@openpanel/validation';
|
||||
|
||||
// Define the data structure
|
||||
export interface ISerieDataItem {
|
||||
label: string | null | undefined;
|
||||
label_0: string | null | undefined;
|
||||
label_1?: string | null | undefined;
|
||||
label_2?: string | null | undefined;
|
||||
label_3?: string | null | undefined;
|
||||
count: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface ISerieDataItemComplete {
|
||||
labels: string[];
|
||||
count: number;
|
||||
date: string;
|
||||
}
|
||||
@@ -37,6 +46,39 @@ function roundDate(date: Date, interval: IInterval): Date {
|
||||
}
|
||||
}
|
||||
|
||||
function filterFalsyAfterTruthy(array: (string | undefined | null)[]) {
|
||||
let foundTruthy = false;
|
||||
const filtered = array.filter((item) => {
|
||||
if (foundTruthy) {
|
||||
// After a truthy, filter out falsy values
|
||||
return !!item;
|
||||
}
|
||||
if (item) {
|
||||
// Mark when the first truthy is encountered
|
||||
foundTruthy = true;
|
||||
}
|
||||
// Return all elements until the first truthy is found
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filtered.some((item) => !!item)) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
return [null];
|
||||
}
|
||||
|
||||
function concatLabels(entry: ISerieDataItem): string {
|
||||
return filterFalsyAfterTruthy([
|
||||
entry.label_0,
|
||||
entry.label_1,
|
||||
entry.label_2,
|
||||
entry.label_3,
|
||||
])
|
||||
.map((label) => label || NOT_SET_VALUE)
|
||||
.join(':::');
|
||||
}
|
||||
|
||||
// Function to complete the timeline for each label
|
||||
export function completeSerie(
|
||||
data: ISerieDataItem[],
|
||||
@@ -51,23 +93,23 @@ export function completeSerie(
|
||||
data.forEach((entry) => {
|
||||
const roundedDate = roundDate(parseISO(entry.date), interval);
|
||||
const dateKey = format(roundedDate, 'yyyy-MM-dd HH:mm:ss');
|
||||
const label = entry.label || NOT_SET_VALUE;
|
||||
const label = concatLabels(entry) || NOT_SET_VALUE;
|
||||
if (!labelsMap.has(label)) {
|
||||
labelsMap.set(label, new Map());
|
||||
}
|
||||
const labelData = labelsMap.get(label);
|
||||
labelData?.set(dateKey, (labelData.get(dateKey) || 0) + (entry.count || 0));
|
||||
const labelData = labelsMap.get(label)!;
|
||||
labelData.set(dateKey, (labelData.get(dateKey) || 0) + (entry.count || 0));
|
||||
});
|
||||
|
||||
// Complete the timeline for each label
|
||||
const result: Record<string, ISerieDataItem[]> = {};
|
||||
const result: Record<string, ISerieDataItemComplete[]> = {};
|
||||
labelsMap.forEach((counts, label) => {
|
||||
let currentDate = roundDate(startDate, interval);
|
||||
result[label] = [];
|
||||
while (currentDate <= endDate) {
|
||||
const dateKey = format(currentDate, 'yyyy-MM-dd HH:mm:ss');
|
||||
result[label]!.push({
|
||||
label: label,
|
||||
labels: label.split(':::'),
|
||||
date: dateKey,
|
||||
count: counts.get(dateKey) || 0,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export function toDots(
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[`${path}${key}`]: value,
|
||||
[`${path}${key}`]: typeof value === 'string' ? value.trim() : value,
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ export function getChartSql({
|
||||
sb.where.projectId = `project_id = ${escape(projectId)}`;
|
||||
|
||||
if (event.name !== '*') {
|
||||
sb.select.label = `${escape(event.name)} as label`;
|
||||
sb.select.label_0 = `${escape(event.name)} as label_0`;
|
||||
sb.where.eventName = `name = ${escape(event.name)}`;
|
||||
} else {
|
||||
sb.select.label = `'*' as label`;
|
||||
sb.select.label_0 = `'*' as label_0`;
|
||||
}
|
||||
|
||||
sb.select.count = `count(*) as count`;
|
||||
@@ -60,11 +60,11 @@ export function getChartSql({
|
||||
}
|
||||
|
||||
breakdowns.forEach((breakdown, index) => {
|
||||
const key = index === 0 ? 'label' : `label_${index}`;
|
||||
const key = `label_${index}`;
|
||||
const value = breakdown.name.startsWith('properties.')
|
||||
? `mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
? `arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
breakdown.name.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
)}))`
|
||||
)})))`
|
||||
: escape(breakdown.name);
|
||||
sb.select[key] = breakdown.name.startsWith('properties.')
|
||||
? `arrayElement(${value}, 1) as ${key}`
|
||||
@@ -125,9 +125,9 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||
}
|
||||
|
||||
if (name.startsWith('properties.')) {
|
||||
const whereFrom = `mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
const whereFrom = `arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
name.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
)}))`;
|
||||
)})))`;
|
||||
|
||||
switch (operator) {
|
||||
case 'is': {
|
||||
|
||||
@@ -458,14 +458,17 @@ export async function getChartSerie(payload: IGetChartDataInput) {
|
||||
completeSerie(data, payload.startDate, payload.endDate, payload.interval)
|
||||
)
|
||||
.then((series) => {
|
||||
return Object.keys(series).map((label) => {
|
||||
return Object.keys(series).map((key) => {
|
||||
const firstDataItem = series[key]![0]!;
|
||||
const isBreakdown =
|
||||
payload.breakdowns.length && !alphabetIds.includes(label as 'A');
|
||||
const serieLabel = isBreakdown ? label : getEventLegend(payload.event);
|
||||
payload.breakdowns.length && firstDataItem.labels.length;
|
||||
const serieLabel = isBreakdown
|
||||
? firstDataItem.labels
|
||||
: [getEventLegend(payload.event)];
|
||||
return {
|
||||
name: serieLabel,
|
||||
event: payload.event,
|
||||
data: series[label]!.map((item) => ({
|
||||
data: series[key]!.map((item) => ({
|
||||
...item,
|
||||
date: toDynamicISODateWithTZ(
|
||||
item.date,
|
||||
@@ -523,7 +526,7 @@ export async function getChart(input: IChartInput) {
|
||||
const final: FinalChart = {
|
||||
series: series.map((serie) => {
|
||||
const previousSerie = previousSeries?.find(
|
||||
(item) => item.name === serie.name
|
||||
(item) => item.name.join('-') === serie.name.join('-')
|
||||
);
|
||||
const metrics = {
|
||||
sum: sum(serie.data.map((item) => item.count)),
|
||||
@@ -533,8 +536,8 @@ export async function getChart(input: IChartInput) {
|
||||
};
|
||||
|
||||
return {
|
||||
id: slug(serie.name),
|
||||
name: serie.name,
|
||||
id: slug(serie.name.join('-')),
|
||||
names: serie.name,
|
||||
event: {
|
||||
id: serie.event.id!,
|
||||
name: serie.event.displayName ?? serie.event.name,
|
||||
|
||||
@@ -102,9 +102,9 @@ export const chartRouter = createTRPCRouter({
|
||||
sb.where.event = `name = ${escape(event)}`;
|
||||
}
|
||||
if (property.startsWith('properties.')) {
|
||||
sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
sb.select.values = `distinct arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
property.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
)})) as values`;
|
||||
)}))) as values`;
|
||||
} else {
|
||||
sb.select.values = `distinct ${property} as values`;
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ export const profileRouter = createTRPCRouter({
|
||||
sb.from = 'profiles';
|
||||
sb.where.project_id = `project_id = ${escape(projectId)}`;
|
||||
if (property.startsWith('properties.')) {
|
||||
sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
sb.select.values = `distinct arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||
property.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||
)})) as values`;
|
||||
)}))) as values`;
|
||||
} else {
|
||||
sb.select.values = `${property} as values`;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export type Metrics = {
|
||||
|
||||
export type IChartSerie = {
|
||||
id: string;
|
||||
name: string;
|
||||
names: string[];
|
||||
event: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user