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 { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||||
|
|
||||||
import type { IChartEvent } from '@openpanel/validation';
|
import type { IChartEvent } from '@openpanel/validation';
|
||||||
@@ -26,7 +26,7 @@ export function EventsPerDayChart({ projectId, filters, events }: Props) {
|
|||||||
<span className="title">Events per day</span>
|
<span className="title">Events per day</span>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<ChartSwitchShortcut
|
<ChartRootShortcut
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
range="30d"
|
range="30d"
|
||||||
chartType="histogram"
|
chartType="histogram"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Dispatch, SetStateAction } 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 { Button } from '@/components/ui/button';
|
||||||
import { KeyValue } from '@/components/ui/key-value';
|
import { KeyValue } from '@/components/ui/key-value';
|
||||||
import {
|
import {
|
||||||
@@ -195,7 +195,7 @@ export function EventDetails({ event, open, setOpen }: Props) {
|
|||||||
Show all
|
Show all
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ChartSwitchShortcut
|
<ChartRootShortcut
|
||||||
projectId={event.projectId}
|
projectId={event.projectId}
|
||||||
chartType="histogram"
|
chartType="histogram"
|
||||||
events={[
|
events={[
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { ChartSwitch } from '@/components/report/chart';
|
import { ChartRoot } from '@/components/report/chart';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||||
|
|
||||||
import type { IChartProps } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
@@ -85,7 +85,7 @@ const ProfileCharts = ({ profileId, projectId }: Props) => {
|
|||||||
<span className="title">Page views</span>
|
<span className="title">Page views</span>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
<WidgetBody className="flex gap-2">
|
<WidgetBody className="flex gap-2">
|
||||||
<ChartSwitch {...pageViewsChart} />
|
<ChartRoot {...pageViewsChart} />
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
</Widget>
|
</Widget>
|
||||||
<Widget className="col-span-3 w-full">
|
<Widget className="col-span-3 w-full">
|
||||||
@@ -93,7 +93,7 @@ const ProfileCharts = ({ profileId, projectId }: Props) => {
|
|||||||
<span className="title">Events per day</span>
|
<span className="title">Events per day</span>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
<WidgetBody className="flex gap-2">
|
<WidgetBody className="flex gap-2">
|
||||||
<ChartSwitch {...eventsChart} />
|
<ChartRoot {...eventsChart} />
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
</Widget>
|
</Widget>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header';
|
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 { ReportChartType } from '@/components/report/ReportChartType';
|
||||||
import { ReportInterval } from '@/components/report/ReportInterval';
|
import { ReportInterval } from '@/components/report/ReportInterval';
|
||||||
import { ReportLineType } from '@/components/report/ReportLineType';
|
import { ReportLineType } from '@/components/report/ReportLineType';
|
||||||
@@ -99,7 +99,7 @@ export default function ReportEditor({
|
|||||||
</StickyBelowHeader>
|
</StickyBelowHeader>
|
||||||
<div className="flex flex-col gap-4 p-4" id="report-editor">
|
<div className="flex flex-col gap-4 p-4" id="report-editor">
|
||||||
{report.ready && (
|
{report.ready && (
|
||||||
<ChartSwitch {...report} projectId={projectId} editMode />
|
<ChartRoot {...report} projectId={projectId} editMode />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<SheetContent className="!max-w-lg" side="left">
|
<SheetContent className="!max-w-lg" side="left">
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import type { HtmlProps } from '@/types';
|
import type { HtmlProps } from '@/types';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { useChartContext } from './report/chart/ChartProvider';
|
|
||||||
|
|
||||||
type ColorSquareProps = HtmlProps<HTMLDivElement>;
|
type ColorSquareProps = HtmlProps<HTMLDivElement>;
|
||||||
|
|
||||||
export function ColorSquare({ children, className }: ColorSquareProps) {
|
export function ColorSquare({ children, className }: ColorSquareProps) {
|
||||||
const { hideID } = useChartContext();
|
|
||||||
if (hideID) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
useEventQueryFilters,
|
useEventQueryFilters,
|
||||||
useEventQueryNamesFilter,
|
useEventQueryNamesFilter,
|
||||||
} from '@/hooks/useEventQueryFilters';
|
} from '@/hooks/useEventQueryFilters';
|
||||||
|
import { getPropertyLabel } from '@/translations/properties';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import type { Options as NuqsOptions } from 'nuqs';
|
import type { Options as NuqsOptions } from 'nuqs';
|
||||||
@@ -47,7 +48,7 @@ export function OverviewFiltersButtons({
|
|||||||
icon={X}
|
icon={X}
|
||||||
onClick={() => setFilter(filter.name, filter.value[0], 'is')}
|
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>
|
<strong>{filter.value[0]}</strong>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { ChartSwitch } from '@/components/report/chart';
|
import { ChartRoot } from '@/components/report/chart';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
|||||||
setMetric(index);
|
setMetric(index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChartSwitch hideID {...report} />
|
<ChartRoot hideID {...report} />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<div
|
<div
|
||||||
@@ -217,7 +217,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card col-span-6 p-4">
|
<div className="card col-span-6 p-4">
|
||||||
<ChartSwitch
|
<ChartRoot
|
||||||
key={selectedMetric.id}
|
key={selectedMetric.id}
|
||||||
hideID
|
hideID
|
||||||
{...selectedMetric}
|
{...selectedMetric}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
|||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
|
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||||
import type { IChartType } from '@openpanel/validation';
|
import type { IChartType } from '@openpanel/validation';
|
||||||
|
|
||||||
import { LazyChart } from '../report/chart/LazyChart';
|
import { LazyChart } from '../report/chart/LazyChart';
|
||||||
@@ -51,7 +52,7 @@ export default function OverviewTopDevices({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top devices',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -82,7 +83,7 @@ export default function OverviewTopDevices({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top browser',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -92,6 +93,9 @@ export default function OverviewTopDevices({
|
|||||||
title: 'Top Browser Version',
|
title: 'Top Browser Version',
|
||||||
btn: 'Browser Version',
|
btn: 'Browser Version',
|
||||||
chart: {
|
chart: {
|
||||||
|
renderSerieName(name) {
|
||||||
|
return name[1] || NOT_SET_VALUE;
|
||||||
|
},
|
||||||
limit: 10,
|
limit: 10,
|
||||||
projectId,
|
projectId,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -107,13 +111,17 @@ export default function OverviewTopDevices({
|
|||||||
breakdowns: [
|
breakdowns: [
|
||||||
{
|
{
|
||||||
id: 'A',
|
id: 'A',
|
||||||
|
name: 'browser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
name: 'browser_version',
|
name: 'browser_version',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top Browser Version',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -144,7 +152,7 @@ export default function OverviewTopDevices({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top OS',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -154,6 +162,9 @@ export default function OverviewTopDevices({
|
|||||||
title: 'Top OS version',
|
title: 'Top OS version',
|
||||||
btn: 'OS Version',
|
btn: 'OS Version',
|
||||||
chart: {
|
chart: {
|
||||||
|
renderSerieName(name) {
|
||||||
|
return name[1] || NOT_SET_VALUE;
|
||||||
|
},
|
||||||
limit: 10,
|
limit: 10,
|
||||||
projectId,
|
projectId,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -169,13 +180,17 @@ export default function OverviewTopDevices({
|
|||||||
breakdowns: [
|
breakdowns: [
|
||||||
{
|
{
|
||||||
id: 'A',
|
id: 'A',
|
||||||
|
name: 'os',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
name: 'os_version',
|
name: 'os_version',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top OS version',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -211,19 +226,19 @@ export default function OverviewTopDevices({
|
|||||||
onClick={(item) => {
|
onClick={(item) => {
|
||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'devices':
|
case 'devices':
|
||||||
setFilter('device', item.name);
|
setFilter('device', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'browser':
|
case 'browser':
|
||||||
setFilter('browser', item.name);
|
setFilter('browser', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'browser_version':
|
case 'browser_version':
|
||||||
setFilter('browser_version', item.name);
|
setFilter('browser_version', item.names[1]);
|
||||||
break;
|
break;
|
||||||
case 'os':
|
case 'os':
|
||||||
setFilter('os', item.name);
|
setFilter('os', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'os_version':
|
case 'os_version':
|
||||||
setFilter('os_version', item.name);
|
setFilter('os_version', item.names[1]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default function OverviewTopEvents({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Your top events',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -91,7 +91,7 @@ export default function OverviewTopEvents({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'All top events',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -131,7 +131,7 @@ export default function OverviewTopEvents({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Conversions',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ChartSwitch } from '@/components/report/chart';
|
import { ChartRoot } from '@/components/report/chart';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
|
import { getCountry } from '@/translations/countries';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
|
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||||
import type { IChartType } from '@openpanel/validation';
|
import type { IChartType } from '@openpanel/validation';
|
||||||
|
|
||||||
import { LazyChart } from '../report/chart/LazyChart';
|
import { LazyChart } from '../report/chart/LazyChart';
|
||||||
@@ -29,6 +31,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
title: 'Top countries',
|
title: 'Top countries',
|
||||||
btn: 'Countries',
|
btn: 'Countries',
|
||||||
chart: {
|
chart: {
|
||||||
|
renderSerieName(name) {
|
||||||
|
return getCountry(name[0]) || NOT_SET_VALUE;
|
||||||
|
},
|
||||||
limit: 10,
|
limit: 10,
|
||||||
projectId,
|
projectId,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -50,7 +55,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top countries',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -60,6 +65,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
title: 'Top regions',
|
title: 'Top regions',
|
||||||
btn: 'Regions',
|
btn: 'Regions',
|
||||||
chart: {
|
chart: {
|
||||||
|
renderSerieName(name) {
|
||||||
|
return name[1] || NOT_SET_VALUE;
|
||||||
|
},
|
||||||
limit: 10,
|
limit: 10,
|
||||||
projectId,
|
projectId,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -75,13 +83,17 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
breakdowns: [
|
breakdowns: [
|
||||||
{
|
{
|
||||||
id: 'A',
|
id: 'A',
|
||||||
|
name: 'country',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
name: 'region',
|
name: 'region',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top regions',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -91,6 +103,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
title: 'Top cities',
|
title: 'Top cities',
|
||||||
btn: 'Cities',
|
btn: 'Cities',
|
||||||
chart: {
|
chart: {
|
||||||
|
renderSerieName(name) {
|
||||||
|
return name[1] || NOT_SET_VALUE;
|
||||||
|
},
|
||||||
limit: 10,
|
limit: 10,
|
||||||
projectId,
|
projectId,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -106,13 +121,17 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
breakdowns: [
|
breakdowns: [
|
||||||
{
|
{
|
||||||
id: 'A',
|
id: 'A',
|
||||||
|
name: 'country',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
name: 'city',
|
name: 'city',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top cities',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -149,14 +168,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'countries':
|
case 'countries':
|
||||||
setWidget('regions');
|
setWidget('regions');
|
||||||
setFilter('country', item.name);
|
setFilter('country', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'regions':
|
case 'regions':
|
||||||
setWidget('cities');
|
setWidget('cities');
|
||||||
setFilter('region', item.name);
|
setFilter('region', item.names[1]);
|
||||||
break;
|
break;
|
||||||
case 'cities':
|
case 'cities':
|
||||||
setFilter('city', item.name);
|
setFilter('city', item.names[1]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -169,7 +188,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
<div className="title">Map</div>
|
<div className="title">Map</div>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<ChartSwitch
|
<ChartRoot
|
||||||
hideID
|
hideID
|
||||||
{...{
|
{...{
|
||||||
projectId,
|
projectId,
|
||||||
|
|||||||
@@ -3,10 +3,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
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 type { IChartType } from '@openpanel/validation';
|
||||||
|
|
||||||
import { LazyChart } from '../report/chart/LazyChart';
|
import { LazyChart } from '../report/chart/LazyChart';
|
||||||
|
import { Tooltiper } from '../ui/tooltip';
|
||||||
import { Widget, WidgetBody } from '../widget';
|
import { Widget, WidgetBody } from '../widget';
|
||||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||||
import OverviewDetailsButton from './overview-details-button';
|
import OverviewDetailsButton from './overview-details-button';
|
||||||
@@ -23,11 +26,19 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
useOverviewOptions();
|
useOverviewOptions();
|
||||||
const [chartType, setChartType] = useState<IChartType>('bar');
|
const [chartType, setChartType] = useState<IChartType>('bar');
|
||||||
const [filters, setFilter] = useEventQueryFilters();
|
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', {
|
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||||
top: {
|
top: {
|
||||||
title: 'Top pages',
|
title: 'Top pages',
|
||||||
btn: 'Top pages',
|
btn: 'Top pages',
|
||||||
chart: {
|
chart: {
|
||||||
|
renderSerieName,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
projectId,
|
projectId,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -43,13 +54,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
breakdowns: [
|
breakdowns: [
|
||||||
{
|
{
|
||||||
id: 'A',
|
id: 'A',
|
||||||
|
name: 'origin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
name: 'path',
|
name: 'path',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval,
|
interval,
|
||||||
name: 'Top sources',
|
name: 'Top pages',
|
||||||
range,
|
range,
|
||||||
previous,
|
previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -59,6 +74,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
title: 'Entry Pages',
|
title: 'Entry Pages',
|
||||||
btn: 'Entries',
|
btn: 'Entries',
|
||||||
chart: {
|
chart: {
|
||||||
|
renderSerieName,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
projectId,
|
projectId,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -74,13 +90,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
breakdowns: [
|
breakdowns: [
|
||||||
{
|
{
|
||||||
id: 'A',
|
id: 'A',
|
||||||
|
name: 'origin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
name: 'path',
|
name: 'path',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval,
|
interval,
|
||||||
name: 'Top sources',
|
name: 'Entry Pages',
|
||||||
range,
|
range,
|
||||||
previous,
|
previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -90,6 +110,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
title: 'Exit Pages',
|
title: 'Exit Pages',
|
||||||
btn: 'Exits',
|
btn: 'Exits',
|
||||||
chart: {
|
chart: {
|
||||||
|
renderSerieName,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
projectId,
|
projectId,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -105,13 +126,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
breakdowns: [
|
breakdowns: [
|
||||||
{
|
{
|
||||||
id: 'A',
|
id: 'A',
|
||||||
|
name: 'origin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
name: 'path',
|
name: 'path',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval,
|
interval,
|
||||||
name: 'Top sources',
|
name: 'Exit Pages',
|
||||||
range,
|
range,
|
||||||
previous,
|
previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -153,9 +178,22 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
hideID
|
hideID
|
||||||
{...widget.chart}
|
{...widget.chart}
|
||||||
previous={false}
|
previous={false}
|
||||||
onClick={(item) => {
|
dropdownMenuContent={(serie) => [
|
||||||
setFilter('path', item.name);
|
{
|
||||||
}}
|
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} />}
|
{widget.chart?.name && <OverviewDetailsButton chart={widget.chart} />}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function OverviewTopSources({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top groups',
|
name: 'Top sources',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -84,7 +84,7 @@ export default function OverviewTopSources({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'Top urls',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -146,7 +146,7 @@ export default function OverviewTopSources({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'UTM Source',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -177,7 +177,7 @@ export default function OverviewTopSources({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'UTM Medium',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -208,7 +208,7 @@ export default function OverviewTopSources({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'UTM Campaign',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -239,7 +239,7 @@ export default function OverviewTopSources({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'UTM Term',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -270,7 +270,7 @@ export default function OverviewTopSources({
|
|||||||
chartType,
|
chartType,
|
||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: interval,
|
interval: interval,
|
||||||
name: 'Top sources',
|
name: 'UTM Content',
|
||||||
range: range,
|
range: range,
|
||||||
previous: previous,
|
previous: previous,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
@@ -307,30 +307,30 @@ export default function OverviewTopSources({
|
|||||||
onClick={(item) => {
|
onClick={(item) => {
|
||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'all':
|
case 'all':
|
||||||
setFilter('referrer_name', item.name);
|
setFilter('referrer_name', item.names[0]);
|
||||||
setWidget('domain');
|
setWidget('domain');
|
||||||
break;
|
break;
|
||||||
case 'domain':
|
case 'domain':
|
||||||
setFilter('referrer', item.name);
|
setFilter('referrer', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'type':
|
case 'type':
|
||||||
setFilter('referrer_type', item.name);
|
setFilter('referrer_type', item.names[0]);
|
||||||
setWidget('domain');
|
setWidget('domain');
|
||||||
break;
|
break;
|
||||||
case 'utm_source':
|
case 'utm_source':
|
||||||
setFilter('properties.__query.utm_source', item.name);
|
setFilter('properties.__query.utm_source', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'utm_medium':
|
case 'utm_medium':
|
||||||
setFilter('properties.__query.utm_medium', item.name);
|
setFilter('properties.__query.utm_medium', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'utm_campaign':
|
case 'utm_campaign':
|
||||||
setFilter('properties.__query.utm_campaign', item.name);
|
setFilter('properties.__query.utm_campaign', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'utm_term':
|
case 'utm_term':
|
||||||
setFilter('properties.__query.utm_term', item.name);
|
setFilter('properties.__query.utm_term', item.names[0]);
|
||||||
break;
|
break;
|
||||||
case 'utm_content':
|
case 'utm_content':
|
||||||
setFilter('properties.__query.utm_content', item.name);
|
setFilter('properties.__query.utm_content', item.names[0]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||||
|
|
||||||
import { mapKeys } from '@openpanel/validation';
|
import { mapKeys } from '@openpanel/validation';
|
||||||
import type { IChartProps } from '@openpanel/validation';
|
|
||||||
|
import type { IChartRoot } from '../report/chart';
|
||||||
|
|
||||||
export function useOverviewWidget<T extends string>(
|
export function useOverviewWidget<T extends string>(
|
||||||
key: string,
|
key: string,
|
||||||
widgets: Record<
|
widgets: Record<
|
||||||
T,
|
T,
|
||||||
{ title: string; btn: string; chart: IChartProps; hide?: boolean }
|
{ title: string; btn: string; chart: IChartRoot; hide?: boolean }
|
||||||
>
|
>
|
||||||
) {
|
) {
|
||||||
const keys = Object.keys(widgets) as T[];
|
const keys = Object.keys(widgets) as T[];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { api } from '@/trpc/client';
|
|||||||
import type { IChartProps } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { ChartEmpty } from './ChartEmpty';
|
import { ChartEmpty } from './ChartEmpty';
|
||||||
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportAreaChart } from './ReportAreaChart';
|
import { ReportAreaChart } from './ReportAreaChart';
|
||||||
import { ReportBarChart } from './ReportBarChart';
|
import { ReportBarChart } from './ReportBarChart';
|
||||||
import { ReportHistogramChart } from './ReportHistogramChart';
|
import { ReportHistogramChart } from './ReportHistogramChart';
|
||||||
@@ -15,13 +16,13 @@ import { ReportPieChart } from './ReportPieChart';
|
|||||||
|
|
||||||
export type ReportChartProps = IChartProps;
|
export type ReportChartProps = IChartProps;
|
||||||
|
|
||||||
export function Chart({
|
export function Chart() {
|
||||||
|
const {
|
||||||
interval,
|
interval,
|
||||||
events,
|
events,
|
||||||
breakdowns,
|
breakdowns,
|
||||||
chartType,
|
chartType,
|
||||||
range,
|
range,
|
||||||
lineType,
|
|
||||||
previous,
|
previous,
|
||||||
formula,
|
formula,
|
||||||
metric,
|
metric,
|
||||||
@@ -30,19 +31,7 @@ export function Chart({
|
|||||||
endDate,
|
endDate,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
}: ReportChartProps) {
|
} = useChartContext();
|
||||||
const [references] = api.reference.getChartReferences.useSuspenseQuery(
|
|
||||||
{
|
|
||||||
projectId,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [data] = api.chart.chart.useSuspenseQuery(
|
const [data] = api.chart.chart.useSuspenseQuery(
|
||||||
{
|
{
|
||||||
interval,
|
interval,
|
||||||
@@ -73,7 +62,7 @@ export function Chart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (chartType === 'histogram') {
|
if (chartType === 'histogram') {
|
||||||
return <ReportHistogramChart interval={interval} data={data} />;
|
return <ReportHistogramChart data={data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chartType === 'bar') {
|
if (chartType === 'bar') {
|
||||||
@@ -89,20 +78,11 @@ export function Chart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (chartType === 'linear') {
|
if (chartType === 'linear') {
|
||||||
return (
|
return <ReportLineChart data={data} />;
|
||||||
<ReportLineChart
|
|
||||||
lineType={lineType}
|
|
||||||
interval={interval}
|
|
||||||
data={data}
|
|
||||||
references={references}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chartType === 'area') {
|
if (chartType === 'area') {
|
||||||
return (
|
return <ReportAreaChart data={data} />;
|
||||||
<ReportAreaChart lineType={lineType} interval={interval} data={data} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <p>Unknown chart type</p>;
|
return <p>Unknown chart type</p>;
|
||||||
|
|||||||
@@ -1,113 +1,35 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { createContext, useContext } from 'react';
|
||||||
createContext,
|
import type { LucideIcon } from 'lucide-react';
|
||||||
memo,
|
|
||||||
Suspense,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import type { IChartProps, IChartSerie } from '@openpanel/validation';
|
import type { IChartProps, IChartSerie } from '@openpanel/validation';
|
||||||
|
|
||||||
import { ChartLoading } from './ChartLoading';
|
export interface IChartContextType extends IChartProps {
|
||||||
import { MetricCardLoading } from './MetricCard';
|
|
||||||
|
|
||||||
export interface ChartContextType extends IChartProps {
|
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
hideID?: boolean;
|
hideID?: boolean;
|
||||||
onClick?: (item: IChartSerie) => void;
|
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;
|
children: React.ReactNode;
|
||||||
} & ChartContextType;
|
} & IChartContextType;
|
||||||
|
|
||||||
const ChartContext = createContext<ChartContextType | null>({
|
const ChartContext = createContext<IChartContextType | null>(null);
|
||||||
events: [],
|
|
||||||
breakdowns: [],
|
|
||||||
chartType: 'linear',
|
|
||||||
lineType: 'monotone',
|
|
||||||
interval: 'day',
|
|
||||||
name: '',
|
|
||||||
range: '7d',
|
|
||||||
metric: 'sum',
|
|
||||||
previous: false,
|
|
||||||
projectId: '',
|
|
||||||
limit: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ChartProvider({
|
export function ChartProvider({ children, ...props }: IChartProviderProps) {
|
||||||
children,
|
|
||||||
editMode,
|
|
||||||
previous,
|
|
||||||
hideID,
|
|
||||||
limit,
|
|
||||||
...props
|
|
||||||
}: ChartProviderProps) {
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider
|
<ChartContext.Provider value={props}>{children}</ChartContext.Provider>
|
||||||
value={useMemo(
|
|
||||||
() => ({
|
|
||||||
...props,
|
|
||||||
editMode: editMode ?? false,
|
|
||||||
previous: previous ?? false,
|
|
||||||
hideID: hideID ?? false,
|
|
||||||
limit,
|
|
||||||
}),
|
|
||||||
[editMode, previous, hideID, limit, 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() {
|
export function useChartContext() {
|
||||||
return useContext(ChartContext)!;
|
return useContext(ChartContext)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useInViewport } from 'react-in-viewport';
|
import { useInViewport } from 'react-in-viewport';
|
||||||
|
|
||||||
import { ChartSwitch } from '.';
|
import type { IChartRoot } from '.';
|
||||||
|
import { ChartRoot } from '.';
|
||||||
import { ChartLoading } from './ChartLoading';
|
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 ref = useRef<HTMLDivElement>(null);
|
||||||
const once = useRef(false);
|
const once = useRef(false);
|
||||||
const { inViewport } = useInViewport(ref, undefined, {
|
const { inViewport } = useInViewport(ref, undefined, {
|
||||||
@@ -23,7 +23,7 @@ export function LazyChart(props: ChartContextType) {
|
|||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
{once.current || inViewport ? (
|
{once.current || inViewport ? (
|
||||||
<ChartSwitch {...props} editMode={false} />
|
<ChartRoot {...props} editMode={false} />
|
||||||
) : (
|
) : (
|
||||||
<ChartLoading />
|
<ChartLoading />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ColorSquare } from '@/components/color-square';
|
|
||||||
import { fancyMinutes, useNumber } from '@/hooks/useNumerFormatter';
|
import { fancyMinutes, useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import type { IChartData } from '@/trpc/client';
|
import type { IChartData } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
PreviousDiffIndicatorText,
|
PreviousDiffIndicatorText,
|
||||||
} from '../PreviousDiffIndicator';
|
} from '../PreviousDiffIndicator';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
|
import { SerieName } from './SerieName';
|
||||||
|
|
||||||
interface MetricCardProps {
|
interface MetricCardProps {
|
||||||
serie: IChartData['series'][number];
|
serie: IChartData['series'][number];
|
||||||
@@ -28,7 +28,7 @@ export function MetricCard({
|
|||||||
metric,
|
metric,
|
||||||
unit,
|
unit,
|
||||||
}: MetricCardProps) {
|
}: MetricCardProps) {
|
||||||
const { previousIndicatorInverted } = useChartContext();
|
const { previousIndicatorInverted, editMode } = useChartContext();
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
|
|
||||||
const renderValue = (value: number, unitClassName?: string) => {
|
const renderValue = (value: number, unitClassName?: string) => {
|
||||||
@@ -57,14 +57,15 @@ export function MetricCard({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative h-[70px] overflow-hidden'
|
'group relative h-[70px] overflow-hidden',
|
||||||
// '[#report-editor_&&]:card [#report-editor_&&]:px-4 [#report-editor_&&]:py-2'
|
editMode && 'card h-[100px] px-4 py-2'
|
||||||
)}
|
)}
|
||||||
key={serie.name}
|
key={serie.id}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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>
|
<AutoSizer>
|
||||||
@@ -91,9 +92,8 @@ export function MetricCard({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex min-w-0 items-center gap-2 text-left font-semibold">
|
<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">
|
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
|
||||||
{serie.name}
|
<SerieName name={serie.names} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* <PreviousDiffIndicator {...serie.metrics.previous[metric]} /> */}
|
{/* <PreviousDiffIndicator {...serie.metrics.previous[metric]} /> */}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
import type { IChartLineType, IInterval } from '@openpanel/validation';
|
|
||||||
|
|
||||||
import { getYAxisWidth } from './chart-utils';
|
import { getYAxisWidth } from './chart-utils';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
@@ -24,16 +22,10 @@ import { ResponsiveContainer } from './ResponsiveContainer';
|
|||||||
|
|
||||||
interface ReportAreaChartProps {
|
interface ReportAreaChartProps {
|
||||||
data: IChartData;
|
data: IChartData;
|
||||||
interval: IInterval;
|
|
||||||
lineType: IChartLineType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReportAreaChart({
|
export function ReportAreaChart({ data }: ReportAreaChartProps) {
|
||||||
lineType,
|
const { editMode, lineType, interval } = useChartContext();
|
||||||
interval,
|
|
||||||
data,
|
|
||||||
}: ReportAreaChartProps) {
|
|
||||||
const { editMode } = useChartContext();
|
|
||||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||||
const formatDate = useFormatDateInterval(interval);
|
const formatDate = useFormatDateInterval(interval);
|
||||||
const rechartData = useRechartDataModel(series);
|
const rechartData = useRechartDataModel(series);
|
||||||
@@ -65,7 +57,7 @@ export function ReportAreaChart({
|
|||||||
{series.map((serie) => {
|
{series.map((serie) => {
|
||||||
const color = getChartColor(serie.index);
|
const color = getChartColor(serie.index);
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={serie.name}>
|
<React.Fragment key={serie.id}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id={`color${color}`}
|
id={`color${color}`}
|
||||||
@@ -87,7 +79,7 @@ export function ReportAreaChart({
|
|||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<Area
|
<Area
|
||||||
key={serie.name}
|
key={serie.id}
|
||||||
type={lineType}
|
type={lineType}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import type { IChartData } from '@/trpc/client';
|
import type { IChartData } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
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 { round } from '@openpanel/common';
|
||||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||||
@@ -11,13 +20,15 @@ import { NOT_SET_VALUE } from '@openpanel/constants';
|
|||||||
import { PreviousDiffIndicatorText } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicatorText } from '../PreviousDiffIndicator';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { SerieIcon } from './SerieIcon';
|
import { SerieIcon } from './SerieIcon';
|
||||||
|
import { SerieName } from './SerieName';
|
||||||
|
|
||||||
interface ReportBarChartProps {
|
interface ReportBarChartProps {
|
||||||
data: IChartData;
|
data: IChartData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReportBarChart({ data }: ReportBarChartProps) {
|
export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||||
const { editMode, metric, onClick, limit } = useChartContext();
|
const { editMode, metric, onClick, limit, dropdownMenuContent } =
|
||||||
|
useChartContext();
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
const series = useMemo(
|
const series = useMemo(
|
||||||
() => (editMode ? data.series : data.series.slice(0, limit || 10)),
|
() => (editMode ? data.series : data.series.slice(0, limit || 10)),
|
||||||
@@ -33,12 +44,22 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{series.map((serie) => {
|
{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 (
|
return (
|
||||||
|
<DropdownMenu key={serie.id}>
|
||||||
|
<DropdownMenuTrigger asChild disabled={!isDropDownEnabled}>
|
||||||
<div
|
<div
|
||||||
key={serie.name}
|
className={cn(
|
||||||
className={cn('relative', isClickable && 'cursor-pointer')}
|
'relative',
|
||||||
{...(isClickable ? { onClick: () => onClick(serie) } : {})}
|
(isClickable || isDropDownEnabled) && 'cursor-pointer'
|
||||||
|
)}
|
||||||
|
{...(isClickable && !isDropDownEnabled
|
||||||
|
? { onClick: () => onClick?.(serie) }
|
||||||
|
: {})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
|
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
|
||||||
@@ -48,8 +69,8 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
|||||||
/>
|
/>
|
||||||
<div className="relative z-10 flex w-full flex-1 items-center gap-4 overflow-hidden px-3 py-2">
|
<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">
|
<div className="flex flex-1 items-center gap-2 break-all font-medium">
|
||||||
<SerieIcon name={serie.name} />
|
<SerieIcon name={serie.names[0]} />
|
||||||
{serie.name}
|
<SerieName name={serie.names} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 items-center justify-end gap-4">
|
<div className="flex flex-shrink-0 items-center justify-end gap-4">
|
||||||
<PreviousDiffIndicatorText
|
<PreviousDiffIndicatorText
|
||||||
@@ -69,6 +90,18 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
|||||||
</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>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type { IToolTipProps } from '@/types';
|
|||||||
|
|
||||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
|
import { SerieIcon } from './SerieIcon';
|
||||||
|
import { SerieName } from './SerieName';
|
||||||
|
|
||||||
type ReportLineChartTooltipProps = IToolTipProps<{
|
type ReportLineChartTooltipProps = IToolTipProps<{
|
||||||
value: number;
|
value: number;
|
||||||
@@ -53,7 +55,7 @@ export function ReportChartTooltip({
|
|||||||
) as IRechartPayloadItem;
|
) as IRechartPayloadItem;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={data.name}>
|
<React.Fragment key={data.id}>
|
||||||
{index === 0 && data.date && (
|
{index === 0 && data.date && (
|
||||||
<div className="flex justify-between gap-8">
|
<div className="flex justify-between gap-8">
|
||||||
<div>{formatDate(new Date(data.date))}</div>
|
<div>{formatDate(new Date(data.date))}</div>
|
||||||
@@ -65,8 +67,9 @@ export function ReportChartTooltip({
|
|||||||
style={{ background: data.color }}
|
style={{ background: data.color }}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="min-w-0 max-w-[200px] overflow-hidden text-ellipsis whitespace-nowrap font-medium">
|
<div className="flex items-center gap-1">
|
||||||
{getLabel(data.name)}
|
<SerieIcon name={data.names} />
|
||||||
|
<SerieName name={data.names} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-8">
|
<div className="flex justify-between gap-8">
|
||||||
<div>{number.formatWithUnit(data.count, unit)}</div>
|
<div>{number.formatWithUnit(data.count, unit)}</div>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { ResponsiveContainer } from './ResponsiveContainer';
|
|||||||
|
|
||||||
interface ReportHistogramChartProps {
|
interface ReportHistogramChartProps {
|
||||||
data: IChartData;
|
data: IChartData;
|
||||||
interval: IInterval;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
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({
|
export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
||||||
interval,
|
const { editMode, previous, interval } = useChartContext();
|
||||||
data,
|
|
||||||
}: ReportHistogramChartProps) {
|
|
||||||
const { editMode, previous } = useChartContext();
|
|
||||||
const formatDate = useFormatDateInterval(interval);
|
const formatDate = useFormatDateInterval(interval);
|
||||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||||
const rechartData = useRechartDataModel(series);
|
const rechartData = useRechartDataModel(series);
|
||||||
@@ -71,11 +67,11 @@ export function ReportHistogramChart({
|
|||||||
/>
|
/>
|
||||||
{series.map((serie) => {
|
{series.map((serie) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={serie.name}>
|
<React.Fragment key={serie.id}>
|
||||||
{previous && (
|
{previous && (
|
||||||
<Bar
|
<Bar
|
||||||
key={`${serie.name}:prev`}
|
key={`${serie.id}:prev`}
|
||||||
name={`${serie.name}:prev`}
|
name={`${serie.id}:prev`}
|
||||||
dataKey={`${serie.id}:prev:count`}
|
dataKey={`${serie.id}:prev:count`}
|
||||||
fill={getChartColor(serie.index)}
|
fill={getChartColor(serie.index)}
|
||||||
fillOpacity={0.2}
|
fillOpacity={0.2}
|
||||||
@@ -83,8 +79,8 @@ export function ReportHistogramChart({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Bar
|
<Bar
|
||||||
key={serie.name}
|
key={serie.id}
|
||||||
name={serie.name}
|
name={serie.id}
|
||||||
dataKey={`${serie.id}:count`}
|
dataKey={`${serie.id}:count`}
|
||||||
fill={getChartColor(serie.index)}
|
fill={getChartColor(serie.index)}
|
||||||
radius={3}
|
radius={3}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||||
|
import { api } from '@/trpc/client';
|
||||||
import type { IChartData } from '@/trpc/client';
|
import type { IChartData } from '@/trpc/client';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
|
import { isSameDay, isSameHour, isSameMonth } from 'date-fns';
|
||||||
import { SplineIcon } from 'lucide-react';
|
import { SplineIcon } from 'lucide-react';
|
||||||
|
import { last, pathOr } from 'ramda';
|
||||||
import {
|
import {
|
||||||
|
Area,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
|
ComposedChart,
|
||||||
Legend,
|
Legend,
|
||||||
Line,
|
Line,
|
||||||
LineChart,
|
LineChart,
|
||||||
@@ -27,49 +32,35 @@ import { useChartContext } from './ChartProvider';
|
|||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
import { ReportTable } from './ReportTable';
|
import { ReportTable } from './ReportTable';
|
||||||
import { ResponsiveContainer } from './ResponsiveContainer';
|
import { ResponsiveContainer } from './ResponsiveContainer';
|
||||||
|
import { SerieIcon } from './SerieIcon';
|
||||||
|
import { SerieName } from './SerieName';
|
||||||
|
|
||||||
interface ReportLineChartProps {
|
interface ReportLineChartProps {
|
||||||
data: IChartData;
|
data: IChartData;
|
||||||
references: IServiceReference[];
|
|
||||||
interval: IInterval;
|
|
||||||
lineType: IChartLineType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomLegend(props: {
|
export function ReportLineChart({ data }: ReportLineChartProps) {
|
||||||
payload?: { value: string; payload: { fill: string } }[];
|
const {
|
||||||
}) {
|
editMode,
|
||||||
if (!props.payload) {
|
previous,
|
||||||
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({
|
|
||||||
lineType,
|
|
||||||
interval,
|
interval,
|
||||||
data,
|
projectId,
|
||||||
references,
|
startDate,
|
||||||
}: ReportLineChartProps) {
|
endDate,
|
||||||
const { editMode, previous } = useChartContext();
|
range,
|
||||||
|
lineType,
|
||||||
|
} = useChartContext();
|
||||||
|
const references = api.reference.getChartReferences.useQuery(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
range,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
}
|
||||||
|
);
|
||||||
const formatDate = useFormatDateInterval(interval);
|
const formatDate = useFormatDateInterval(interval);
|
||||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||||
const rechartData = useRechartDataModel(series);
|
const rechartData = useRechartDataModel(series);
|
||||||
@@ -96,20 +87,56 @@ export function ReportLineChart({
|
|||||||
</linearGradient>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResponsiveContainer>
|
<ResponsiveContainer>
|
||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
<LineChart width={width} height={height} data={rechartData}>
|
<ComposedChart width={width} height={height} data={rechartData}>
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
vertical={false}
|
vertical={false}
|
||||||
className="stroke-def-200"
|
className="stroke-def-200"
|
||||||
/>
|
/>
|
||||||
{references.map((ref) => (
|
{references.data?.map((ref) => (
|
||||||
<ReferenceLine
|
<ReferenceLine
|
||||||
key={ref.id}
|
key={ref.id}
|
||||||
x={ref.date.getTime()}
|
x={ref.date.getTime()}
|
||||||
@@ -150,18 +177,39 @@ export function ReportLineChart({
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
/>
|
/>
|
||||||
{series.map((serie) => {
|
{series.map((serie) => {
|
||||||
|
const color = getChartColor(serie.index);
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={serie.name}>
|
<React.Fragment key={serie.id}>
|
||||||
<defs>
|
<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(
|
{gradientTwoColors(
|
||||||
`hideAllButLastInterval_${serie.id}`,
|
`hideAllButLastInterval_${serie.id}`,
|
||||||
'rgba(0,0,0,0)',
|
'rgba(0,0,0,0)',
|
||||||
getChartColor(serie.index),
|
color,
|
||||||
lastIntervalPercent
|
lastIntervalPercent
|
||||||
)}
|
)}
|
||||||
{gradientTwoColors(
|
{gradientTwoColors(
|
||||||
`hideJustLastInterval_${serie.id}`,
|
`hideJustLastInterval_${serie.id}`,
|
||||||
getChartColor(serie.index),
|
color,
|
||||||
'rgba(0,0,0,0)',
|
'rgba(0,0,0,0)',
|
||||||
lastIntervalPercent
|
lastIntervalPercent
|
||||||
)}
|
)}
|
||||||
@@ -169,24 +217,30 @@ export function ReportLineChart({
|
|||||||
<Line
|
<Line
|
||||||
dot={false}
|
dot={false}
|
||||||
type={lineType}
|
type={lineType}
|
||||||
name={serie.name}
|
name={serie.id}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dataKey={`${serie.id}:count`}
|
dataKey={`${serie.id}:count`}
|
||||||
stroke={
|
stroke={useDashedLastLine ? 'transparent' : color}
|
||||||
useDashedLastLine
|
|
||||||
? 'transparent'
|
|
||||||
: getChartColor(serie.index)
|
|
||||||
}
|
|
||||||
// Use for legend
|
// 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 && (
|
{useDashedLastLine && (
|
||||||
<>
|
<>
|
||||||
<Line
|
<Line
|
||||||
dot={false}
|
dot={false}
|
||||||
type={lineType}
|
type={lineType}
|
||||||
name={`${serie.name}:dashed:noTooltip`}
|
name={`${serie.id}:dashed:noTooltip`}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dataKey={`${serie.id}:count`}
|
dataKey={`${serie.id}:count`}
|
||||||
@@ -197,7 +251,7 @@ export function ReportLineChart({
|
|||||||
<Line
|
<Line
|
||||||
dot={false}
|
dot={false}
|
||||||
type={lineType}
|
type={lineType}
|
||||||
name={`${serie.name}:solid:noTooltip`}
|
name={`${serie.id}:solid:noTooltip`}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dataKey={`${serie.id}:count`}
|
dataKey={`${serie.id}:count`}
|
||||||
@@ -208,22 +262,22 @@ export function ReportLineChart({
|
|||||||
{previous && (
|
{previous && (
|
||||||
<Line
|
<Line
|
||||||
type={lineType}
|
type={lineType}
|
||||||
name={`${serie.name}:prev`}
|
name={`${serie.id}:prev`}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
dot={false}
|
dot={false}
|
||||||
strokeDasharray={'1 1'}
|
strokeDasharray={'1 1'}
|
||||||
strokeOpacity={0.5}
|
strokeOpacity={0.5}
|
||||||
dataKey={`${serie.id}:prev:count`}
|
dataKey={`${serie.id}:prev:count`}
|
||||||
stroke={getChartColor(serie.index)}
|
stroke={color}
|
||||||
// Use for legend
|
// Use for legend
|
||||||
fill={getChartColor(serie.index)}
|
fill={color}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</LineChart>
|
</ComposedChart>
|
||||||
)}
|
)}
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
{editMode && (
|
{editMode && (
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function ReportMapChart({ data }: ReportMapChartProps) {
|
|||||||
const mapData = useMemo(
|
const mapData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
series.map((s) => ({
|
series.map((s) => ({
|
||||||
country: s.name.toLowerCase(),
|
country: s.names[0]?.toLowerCase() ?? '',
|
||||||
value: s.metrics[metric],
|
value: s.metrics[metric],
|
||||||
})),
|
})),
|
||||||
[series, metric]
|
[series, metric]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function ReportMetricChart({ data }: ReportMetricChartProps) {
|
|||||||
{series.map((serie) => {
|
{series.map((serie) => {
|
||||||
return (
|
return (
|
||||||
<MetricCard
|
<MetricCard
|
||||||
key={serie.name}
|
key={serie.id}
|
||||||
serie={serie}
|
serie={serie}
|
||||||
metric={metric}
|
metric={metric}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function ReportPieChart({ data }: ReportPieChartProps) {
|
|||||||
id: serie.id,
|
id: serie.id,
|
||||||
color: getChartColor(serie.index),
|
color: getChartColor(serie.index),
|
||||||
index: serie.index,
|
index: serie.index,
|
||||||
name: serie.name,
|
name: serie.names.join(' > '),
|
||||||
count: serie.metrics.sum,
|
count: serie.metrics.sum,
|
||||||
percent: serie.metrics.sum / sum,
|
percent: serie.metrics.sum / sum,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -13,16 +13,19 @@ import {
|
|||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
|
Tooltiper,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||||
import { useMappings } from '@/hooks/useMappings';
|
import { useMappings } from '@/hooks/useMappings';
|
||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import { useSelector } from '@/redux';
|
import { useSelector } from '@/redux';
|
||||||
|
import { getPropertyLabel } from '@/translations/properties';
|
||||||
import type { IChartData } from '@/trpc/client';
|
import type { IChartData } from '@/trpc/client';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
|
|
||||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||||
|
import { SerieName } from './SerieName';
|
||||||
|
|
||||||
interface ReportTableProps {
|
interface ReportTableProps {
|
||||||
data: IChartData;
|
data: IChartData;
|
||||||
@@ -40,8 +43,8 @@ export function ReportTable({
|
|||||||
const { setPage, paginate, page } = usePagination(ROWS_LIMIT);
|
const { setPage, paginate, page } = usePagination(ROWS_LIMIT);
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
const interval = useSelector((state) => state.report.interval);
|
const interval = useSelector((state) => state.report.interval);
|
||||||
|
const breakdowns = useSelector((state) => state.report.breakdowns);
|
||||||
const formatDate = useFormatDateInterval(interval);
|
const formatDate = useFormatDateInterval(interval);
|
||||||
const getLabel = useMappings();
|
|
||||||
|
|
||||||
function handleChange(name: string, checked: boolean) {
|
function handleChange(name: string, checked: boolean) {
|
||||||
setVisibleSeries((prev) => {
|
setVisibleSeries((prev) => {
|
||||||
@@ -55,26 +58,35 @@ export function ReportTable({
|
|||||||
|
|
||||||
return (
|
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">
|
<Table className="rounded-none border-b-0 border-l-0 border-t-0">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Name</TableHead>
|
{breakdowns.length === 0 && <TableHead>Name</TableHead>}
|
||||||
|
{breakdowns.map((breakdown) => (
|
||||||
|
<TableHead key={breakdown.name}>
|
||||||
|
{getPropertyLabel(breakdown.name)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody className="bg-def-100">
|
||||||
{paginate(data.series).map((serie, index) => {
|
{paginate(data.series).map((serie, index) => {
|
||||||
const checked = !!visibleSeries.find(
|
const checked = !!visibleSeries.find(
|
||||||
(item) => item.name === serie.name
|
(item) => item.id === serie.id
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={serie.name}>
|
<TableRow key={serie.id}>
|
||||||
<TableCell className="h-10">
|
{serie.names.map((name, nameIndex) => {
|
||||||
|
return (
|
||||||
|
<TableCell className="h-10" key={name}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{nameIndex === 0 ? (
|
||||||
|
<>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleChange(serie.name, !!checked)
|
handleChange(serie.id, !!checked)
|
||||||
}
|
}
|
||||||
style={
|
style={
|
||||||
checked
|
checked
|
||||||
@@ -86,18 +98,21 @@ export function ReportTable({
|
|||||||
}
|
}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
/>
|
/>
|
||||||
<Tooltip delayDuration={200}>
|
<Tooltiper
|
||||||
<TooltipTrigger asChild>
|
side="left"
|
||||||
<div className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
sideOffset={30}
|
||||||
{getLabel(serie.name)}
|
content={<SerieName name={serie.names} />}
|
||||||
</div>
|
>
|
||||||
</TooltipTrigger>
|
{name}
|
||||||
<TooltipContent>
|
</Tooltiper>
|
||||||
<p>{getLabel(serie.name)}</p>
|
</>
|
||||||
</TooltipContent>
|
) : (
|
||||||
</Tooltip>
|
<SerieName name={name} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -122,7 +137,7 @@ export function ReportTable({
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{paginate(data.series).map((serie) => {
|
{paginate(data.series).map((serie) => {
|
||||||
return (
|
return (
|
||||||
<TableRow key={serie.name}>
|
<TableRow key={serie.id}>
|
||||||
<TableCell className="h-10">
|
<TableCell className="h-10">
|
||||||
<div className="flex items-center gap-2 font-medium">
|
<div className="flex items-center gap-2 font-medium">
|
||||||
{number.format(serie.metrics.sum)}
|
{number.format(serie.metrics.sum)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const createFlagIcon = (url: string) => {
|
|||||||
return function (_props: LucideProps) {
|
return function (_props: LucideProps) {
|
||||||
return (
|
return (
|
||||||
<span
|
<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>
|
></span>
|
||||||
);
|
);
|
||||||
} as LucideIcon;
|
} as LucideIcon;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
SearchIcon,
|
SearchIcon,
|
||||||
SmartphoneIcon,
|
SmartphoneIcon,
|
||||||
TabletIcon,
|
TabletIcon,
|
||||||
|
TvIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||||
@@ -20,9 +21,9 @@ import { NOT_SET_VALUE } from '@openpanel/constants';
|
|||||||
import flags from './SerieIcon.flags';
|
import flags from './SerieIcon.flags';
|
||||||
import iconsWithUrls from './SerieIcon.urls';
|
import iconsWithUrls from './SerieIcon.urls';
|
||||||
|
|
||||||
interface SerieIconProps extends LucideProps {
|
type SerieIconProps = Omit<LucideProps, 'name'> & {
|
||||||
name?: string;
|
name?: string | string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
function getProxyImage(url: string) {
|
function getProxyImage(url: string) {
|
||||||
return `${String(process.env.NEXT_PUBLIC_API_URL)}/misc/favicon?url=${encodeURIComponent(url)}`;
|
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) => {
|
const createImageIcon = (url: string) => {
|
||||||
return function (_props: LucideProps) {
|
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;
|
} as LucideIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ const mapper: Record<string, LucideIcon> = {
|
|||||||
link_out: ExternalLinkIcon,
|
link_out: ExternalLinkIcon,
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
|
smarttv: TvIcon,
|
||||||
mobile: SmartphoneIcon,
|
mobile: SmartphoneIcon,
|
||||||
desktop: MonitorIcon,
|
desktop: MonitorIcon,
|
||||||
tablet: TabletIcon,
|
tablet: TabletIcon,
|
||||||
@@ -64,7 +66,8 @@ const mapper: Record<string, LucideIcon> = {
|
|||||||
...flags,
|
...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(() => {
|
const Icon = useMemo(() => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return null;
|
return null;
|
||||||
@@ -80,6 +83,7 @@ export function SerieIcon({ name, ...props }: SerieIconProps) {
|
|||||||
return createImageIcon(getProxyImage(name));
|
return createImageIcon(getProxyImage(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matching image file name
|
||||||
if (name.match(/(.+)\.\w{2,3}$/)) {
|
if (name.match(/(.+)\.\w{2,3}$/)) {
|
||||||
return createImageIcon(getProxyImage(`https://${name}`));
|
return createImageIcon(getProxyImage(`https://${name}`));
|
||||||
}
|
}
|
||||||
@@ -88,8 +92,8 @@ export function SerieIcon({ name, ...props }: SerieIconProps) {
|
|||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
return Icon ? (
|
return Icon ? (
|
||||||
<div className="[&_a]:![&_a]:!h-4 relative h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
<div className="relative max-h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
||||||
<Icon size={16} {...props} />
|
<Icon size={16} {...props} name={name} />
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,18 @@ const data = {
|
|||||||
'vstat.info': 'https://vstat.info',
|
'vstat.info': 'https://vstat.info',
|
||||||
'yahoo!': 'https://yahoo.com',
|
'yahoo!': 'https://yahoo.com',
|
||||||
android: 'https://image.similarpng.com/very-thumbnail/2020/08/Android-icon-on-transparent--background-PNG.png',
|
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',
|
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: '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',
|
chromecast: 'https://upload.wikimedia.org/wikipedia/commons/archive/2/26/20161130022732%21Chromecast_cast_button_icon.svg',
|
||||||
|
webkit: 'https://webkit.org',
|
||||||
duckduckgo: 'https://duckduckgo.com',
|
duckduckgo: 'https://duckduckgo.com',
|
||||||
ecosia: 'https://ecosia.com',
|
ecosia: 'https://ecosia.com',
|
||||||
edge: 'https://upload.wikimedia.org/wikipedia/commons/7/7e/Microsoft_Edge_logo_%282019%29.png',
|
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',
|
github: 'https://github.com',
|
||||||
gmail: 'https://mail.google.com',
|
gmail: 'https://mail.google.com',
|
||||||
google: 'https://google.com',
|
google: 'https://google.com',
|
||||||
|
gsa: 'https://google.com', // Google Search App
|
||||||
instagram: 'https://instagram.com',
|
instagram: 'https://instagram.com',
|
||||||
ios: 'https://cdn0.iconfinder.com/data/icons/flat-round-system/512/apple-1024.png',
|
ios: 'https://cdn0.iconfinder.com/data/icons/flat-round-system/512/apple-1024.png',
|
||||||
linkedin: 'https://linkedin.com',
|
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';
|
'use client';
|
||||||
|
|
||||||
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import type { IChartProps } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { Funnel } from '../funnel';
|
import { Funnel } from '../funnel';
|
||||||
import { Chart } from './Chart';
|
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(
|
export type IChartRoot = IChartContextType;
|
||||||
props: IChartProps
|
|
||||||
) {
|
export function ChartRoot(props: IChartContextType) {
|
||||||
if (props.chartType === 'funnel') {
|
const [mounted, setMounted] = useState(false);
|
||||||
return <Funnel {...props} />;
|
|
||||||
|
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'];
|
projectId: IChartProps['projectId'];
|
||||||
range?: IChartProps['range'];
|
range?: IChartProps['range'];
|
||||||
previous?: IChartProps['previous'];
|
previous?: IChartProps['previous'];
|
||||||
@@ -25,16 +50,16 @@ interface ChartSwitchShortcutProps {
|
|||||||
events: IChartProps['events'];
|
events: IChartProps['events'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChartSwitchShortcut = ({
|
export const ChartRootShortcut = ({
|
||||||
projectId,
|
projectId,
|
||||||
range = '7d',
|
range = '7d',
|
||||||
previous = false,
|
previous = false,
|
||||||
chartType = 'linear',
|
chartType = 'linear',
|
||||||
interval = 'day',
|
interval = 'day',
|
||||||
events,
|
events,
|
||||||
}: ChartSwitchShortcutProps) => {
|
}: ChartRootShortcutProps) => {
|
||||||
return (
|
return (
|
||||||
<ChartSwitch
|
<ChartRoot
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
range={range}
|
range={range}
|
||||||
breakdowns={[]}
|
breakdowns={[]}
|
||||||
|
|||||||
@@ -2,19 +2,15 @@
|
|||||||
|
|
||||||
import { api } from '@/trpc/client';
|
import { api } from '@/trpc/client';
|
||||||
|
|
||||||
import type { IChartInput, IChartProps } from '@openpanel/validation';
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
|
|
||||||
import { ChartEmpty } from '../chart/ChartEmpty';
|
import { ChartEmpty } from '../chart/ChartEmpty';
|
||||||
import { withChartProivder } from '../chart/ChartProvider';
|
import { useChartContext } from '../chart/ChartProvider';
|
||||||
import { FunnelSteps } from './Funnel';
|
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 = {
|
const input: IChartInput = {
|
||||||
events,
|
events,
|
||||||
range,
|
range,
|
||||||
@@ -38,4 +34,4 @@ export const Funnel = withChartProivder(function Chart({
|
|||||||
<FunnelSteps {...data} input={input} />
|
<FunnelSteps {...data} input={input} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const TableHead = React.forwardRef<
|
|||||||
<th
|
<th
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -90,7 +90,7 @@ const TableCell = React.forwardRef<
|
|||||||
<td
|
<td
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -35,10 +35,13 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|||||||
|
|
||||||
interface TooltiperProps {
|
interface TooltiperProps {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
content: string;
|
content: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||||
|
delayDuration?: number;
|
||||||
|
sideOffset?: number;
|
||||||
}
|
}
|
||||||
export function Tooltiper({
|
export function Tooltiper({
|
||||||
asChild,
|
asChild,
|
||||||
@@ -46,14 +49,19 @@ export function Tooltiper({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
|
side,
|
||||||
|
delayDuration = 0,
|
||||||
|
sideOffset = 10,
|
||||||
}: TooltiperProps) {
|
}: TooltiperProps) {
|
||||||
return (
|
return (
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={delayDuration}>
|
||||||
<TooltipTrigger asChild={asChild} className={className} onClick={onClick}>
|
<TooltipTrigger asChild={asChild} className={className} onClick={onClick}>
|
||||||
{children}
|
{children}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent sideOffset={10}>{content}</TooltipContent>
|
<TooltipContent sideOffset={sideOffset} side={side}>
|
||||||
|
{content}
|
||||||
|
</TooltipContent>
|
||||||
</TooltipPortal>
|
</TooltipPortal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import mappings from '@/mappings.json';
|
import mappings from '@/mappings.json';
|
||||||
|
|
||||||
export function useMappings() {
|
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;
|
return mappings.find((item) => item.id === val)?.name ?? val;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getChartColor } from '@/utils/theme';
|
|||||||
|
|
||||||
export type IRechartPayloadItem = {
|
export type IRechartPayloadItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
names: string[];
|
||||||
color: string;
|
color: string;
|
||||||
event: { id: string; name: string };
|
event: { id: string; name: string };
|
||||||
count: number;
|
count: number;
|
||||||
@@ -39,7 +39,7 @@ export function useRechartDataModel(series: IChartData['series']) {
|
|||||||
...item,
|
...item,
|
||||||
id: serie.id,
|
id: serie.id,
|
||||||
event: serie.event,
|
event: serie.event,
|
||||||
name: serie.name,
|
names: serie.names,
|
||||||
color: getChartColor(idx),
|
color: getChartColor(idx),
|
||||||
} satisfies IRechartPayloadItem;
|
} satisfies IRechartPayloadItem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ export type IVisibleSeries = ReturnType<typeof useVisibleSeries>['series'];
|
|||||||
export function useVisibleSeries(data: IChartData, limit?: number | undefined) {
|
export function useVisibleSeries(data: IChartData, limit?: number | undefined) {
|
||||||
const max = limit ?? 5;
|
const max = limit ?? 5;
|
||||||
const [visibleSeries, setVisibleSeries] = useState<string[]>(
|
const [visibleSeries, setVisibleSeries] = useState<string[]>(
|
||||||
data?.series?.slice(0, max).map((serie) => serie.name) ?? []
|
data?.series?.slice(0, max).map((serie) => serie.id) ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisibleSeries(
|
setVisibleSeries(
|
||||||
data?.series?.slice(0, max).map((serie) => serie.name) ?? []
|
data?.series?.slice(0, max).map((serie) => serie.id) ?? []
|
||||||
);
|
);
|
||||||
}, [data, max]);
|
}, [data, max]);
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export function useVisibleSeries(data: IChartData, limit?: number | undefined) {
|
|||||||
...serie,
|
...serie,
|
||||||
index,
|
index,
|
||||||
}))
|
}))
|
||||||
.filter((serie) => visibleSeries.includes(serie.name)),
|
.filter((serie) => visibleSeries.includes(serie.id)),
|
||||||
setVisibleSeries,
|
setVisibleSeries,
|
||||||
} as const;
|
} as const;
|
||||||
}, [visibleSeries, data.series]);
|
}, [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 { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
||||||
import type { IChartProps } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
@@ -15,7 +15,7 @@ const OverviewChartDetails = (props: Props) => {
|
|||||||
<ModalHeader title={props.chart.name} />
|
<ModalHeader title={props.chart.name} />
|
||||||
<ScrollArea className="-m-6 max-h-[calc(100vh-200px)]">
|
<ScrollArea className="-m-6 max-h-[calc(100vh-200px)]">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<ChartSwitch {...props.chart} limit={999} chartType="bar" />
|
<ChartRoot {...props.chart} limit={999} chartType="bar" />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</ModalContent>
|
</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 ?? '',
|
model: uaInfo?.model ?? '',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
path: path,
|
path: path,
|
||||||
origin: origin,
|
origin: origin || sessionStartEvent?.origin || '',
|
||||||
referrer: referrer?.url,
|
referrer: referrer?.url,
|
||||||
referrerName: referrer?.name || utmReferrer?.name || '',
|
referrerName: referrer?.name || utmReferrer?.name || '',
|
||||||
referrerType: referrer?.type || utmReferrer?.type || '',
|
referrerType: referrer?.type || utmReferrer?.type || '',
|
||||||
|
|||||||
@@ -16,7 +16,16 @@ import type { IInterval } from '@openpanel/validation';
|
|||||||
|
|
||||||
// Define the data structure
|
// Define the data structure
|
||||||
export interface ISerieDataItem {
|
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;
|
count: number;
|
||||||
date: string;
|
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
|
// Function to complete the timeline for each label
|
||||||
export function completeSerie(
|
export function completeSerie(
|
||||||
data: ISerieDataItem[],
|
data: ISerieDataItem[],
|
||||||
@@ -51,23 +93,23 @@ export function completeSerie(
|
|||||||
data.forEach((entry) => {
|
data.forEach((entry) => {
|
||||||
const roundedDate = roundDate(parseISO(entry.date), interval);
|
const roundedDate = roundDate(parseISO(entry.date), interval);
|
||||||
const dateKey = format(roundedDate, 'yyyy-MM-dd HH:mm:ss');
|
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)) {
|
if (!labelsMap.has(label)) {
|
||||||
labelsMap.set(label, new Map());
|
labelsMap.set(label, new Map());
|
||||||
}
|
}
|
||||||
const labelData = labelsMap.get(label);
|
const labelData = labelsMap.get(label)!;
|
||||||
labelData?.set(dateKey, (labelData.get(dateKey) || 0) + (entry.count || 0));
|
labelData.set(dateKey, (labelData.get(dateKey) || 0) + (entry.count || 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Complete the timeline for each label
|
// Complete the timeline for each label
|
||||||
const result: Record<string, ISerieDataItem[]> = {};
|
const result: Record<string, ISerieDataItemComplete[]> = {};
|
||||||
labelsMap.forEach((counts, label) => {
|
labelsMap.forEach((counts, label) => {
|
||||||
let currentDate = roundDate(startDate, interval);
|
let currentDate = roundDate(startDate, interval);
|
||||||
result[label] = [];
|
result[label] = [];
|
||||||
while (currentDate <= endDate) {
|
while (currentDate <= endDate) {
|
||||||
const dateKey = format(currentDate, 'yyyy-MM-dd HH:mm:ss');
|
const dateKey = format(currentDate, 'yyyy-MM-dd HH:mm:ss');
|
||||||
result[label]!.push({
|
result[label]!.push({
|
||||||
label: label,
|
labels: label.split(':::'),
|
||||||
date: dateKey,
|
date: dateKey,
|
||||||
count: counts.get(dateKey) || 0,
|
count: counts.get(dateKey) || 0,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function toDots(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...acc,
|
...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)}`;
|
sb.where.projectId = `project_id = ${escape(projectId)}`;
|
||||||
|
|
||||||
if (event.name !== '*') {
|
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)}`;
|
sb.where.eventName = `name = ${escape(event.name)}`;
|
||||||
} else {
|
} else {
|
||||||
sb.select.label = `'*' as label`;
|
sb.select.label_0 = `'*' as label_0`;
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.select.count = `count(*) as count`;
|
sb.select.count = `count(*) as count`;
|
||||||
@@ -60,11 +60,11 @@ export function getChartSql({
|
|||||||
}
|
}
|
||||||
|
|
||||||
breakdowns.forEach((breakdown, index) => {
|
breakdowns.forEach((breakdown, index) => {
|
||||||
const key = index === 0 ? 'label' : `label_${index}`;
|
const key = `label_${index}`;
|
||||||
const value = breakdown.name.startsWith('properties.')
|
const value = breakdown.name.startsWith('properties.')
|
||||||
? `mapValues(mapExtractKeyLike(properties, ${escape(
|
? `arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||||
breakdown.name.replace(/^properties\./, '').replace('.*.', '.%.')
|
breakdown.name.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||||
)}))`
|
)})))`
|
||||||
: escape(breakdown.name);
|
: escape(breakdown.name);
|
||||||
sb.select[key] = breakdown.name.startsWith('properties.')
|
sb.select[key] = breakdown.name.startsWith('properties.')
|
||||||
? `arrayElement(${value}, 1) as ${key}`
|
? `arrayElement(${value}, 1) as ${key}`
|
||||||
@@ -125,9 +125,9 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name.startsWith('properties.')) {
|
if (name.startsWith('properties.')) {
|
||||||
const whereFrom = `mapValues(mapExtractKeyLike(properties, ${escape(
|
const whereFrom = `arrayMap(x -> trim(x), mapValues(mapExtractKeyLike(properties, ${escape(
|
||||||
name.replace(/^properties\./, '').replace('.*.', '.%.')
|
name.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||||
)}))`;
|
)})))`;
|
||||||
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case 'is': {
|
case 'is': {
|
||||||
|
|||||||
@@ -458,14 +458,17 @@ export async function getChartSerie(payload: IGetChartDataInput) {
|
|||||||
completeSerie(data, payload.startDate, payload.endDate, payload.interval)
|
completeSerie(data, payload.startDate, payload.endDate, payload.interval)
|
||||||
)
|
)
|
||||||
.then((series) => {
|
.then((series) => {
|
||||||
return Object.keys(series).map((label) => {
|
return Object.keys(series).map((key) => {
|
||||||
|
const firstDataItem = series[key]![0]!;
|
||||||
const isBreakdown =
|
const isBreakdown =
|
||||||
payload.breakdowns.length && !alphabetIds.includes(label as 'A');
|
payload.breakdowns.length && firstDataItem.labels.length;
|
||||||
const serieLabel = isBreakdown ? label : getEventLegend(payload.event);
|
const serieLabel = isBreakdown
|
||||||
|
? firstDataItem.labels
|
||||||
|
: [getEventLegend(payload.event)];
|
||||||
return {
|
return {
|
||||||
name: serieLabel,
|
name: serieLabel,
|
||||||
event: payload.event,
|
event: payload.event,
|
||||||
data: series[label]!.map((item) => ({
|
data: series[key]!.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
date: toDynamicISODateWithTZ(
|
date: toDynamicISODateWithTZ(
|
||||||
item.date,
|
item.date,
|
||||||
@@ -523,7 +526,7 @@ export async function getChart(input: IChartInput) {
|
|||||||
const final: FinalChart = {
|
const final: FinalChart = {
|
||||||
series: series.map((serie) => {
|
series: series.map((serie) => {
|
||||||
const previousSerie = previousSeries?.find(
|
const previousSerie = previousSeries?.find(
|
||||||
(item) => item.name === serie.name
|
(item) => item.name.join('-') === serie.name.join('-')
|
||||||
);
|
);
|
||||||
const metrics = {
|
const metrics = {
|
||||||
sum: sum(serie.data.map((item) => item.count)),
|
sum: sum(serie.data.map((item) => item.count)),
|
||||||
@@ -533,8 +536,8 @@ export async function getChart(input: IChartInput) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: slug(serie.name),
|
id: slug(serie.name.join('-')),
|
||||||
name: serie.name,
|
names: serie.name,
|
||||||
event: {
|
event: {
|
||||||
id: serie.event.id!,
|
id: serie.event.id!,
|
||||||
name: serie.event.displayName ?? serie.event.name,
|
name: serie.event.displayName ?? serie.event.name,
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ export const chartRouter = createTRPCRouter({
|
|||||||
sb.where.event = `name = ${escape(event)}`;
|
sb.where.event = `name = ${escape(event)}`;
|
||||||
}
|
}
|
||||||
if (property.startsWith('properties.')) {
|
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('.*.', '.%.')
|
property.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||||
)})) as values`;
|
)}))) as values`;
|
||||||
} else {
|
} else {
|
||||||
sb.select.values = `distinct ${property} as values`;
|
sb.select.values = `distinct ${property} as values`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ export const profileRouter = createTRPCRouter({
|
|||||||
sb.from = 'profiles';
|
sb.from = 'profiles';
|
||||||
sb.where.project_id = `project_id = ${escape(projectId)}`;
|
sb.where.project_id = `project_id = ${escape(projectId)}`;
|
||||||
if (property.startsWith('properties.')) {
|
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('.*.', '.%.')
|
property.replace(/^properties\./, '').replace('.*.', '.%.')
|
||||||
)})) as values`;
|
)}))) as values`;
|
||||||
} else {
|
} else {
|
||||||
sb.select.values = `${property} as values`;
|
sb.select.values = `${property} as values`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export type Metrics = {
|
|||||||
|
|
||||||
export type IChartSerie = {
|
export type IChartSerie = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
names: string[];
|
||||||
event: {
|
event: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user