multiple breakpoints
This commit is contained in:
@@ -1,19 +1,13 @@
|
||||
import type { HtmlProps } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { useChartContext } from './report/chart/ChartProvider';
|
||||
|
||||
type ColorSquareProps = HtmlProps<HTMLDivElement>;
|
||||
|
||||
export function ColorSquare({ children, className }: ColorSquareProps) {
|
||||
const { hideID } = useChartContext();
|
||||
if (hideID) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-purple-500 text-xs font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
|
||||
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-blue-600 text-xs font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { getPropertyLabel } from '@/translations/properties';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { X } from 'lucide-react';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
@@ -47,7 +48,7 @@ export function OverviewFiltersButtons({
|
||||
icon={X}
|
||||
onClick={() => setFilter(filter.name, filter.value[0], 'is')}
|
||||
>
|
||||
<span className="mr-1">{filter.name} is</span>
|
||||
<span className="mr-1">{getPropertyLabel(filter.name)} is</span>
|
||||
<strong>{filter.value[0]}</strong>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { ChartRoot } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
@@ -205,7 +205,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
setMetric(index);
|
||||
}}
|
||||
>
|
||||
<ChartSwitch hideID {...report} />
|
||||
<ChartRoot hideID {...report} />
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
@@ -217,7 +217,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card col-span-6 p-4">
|
||||
<ChartSwitch
|
||||
<ChartRoot
|
||||
key={selectedMetric.id}
|
||||
hideID
|
||||
{...selectedMetric}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
import { LazyChart } from '../report/chart/LazyChart';
|
||||
@@ -51,7 +52,7 @@ export default function OverviewTopDevices({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top devices',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -82,7 +83,7 @@ export default function OverviewTopDevices({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top browser',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -92,6 +93,9 @@ export default function OverviewTopDevices({
|
||||
title: 'Top Browser Version',
|
||||
btn: 'Browser Version',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return name[1] || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -107,13 +111,17 @@ export default function OverviewTopDevices({
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'browser',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'browser_version',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top Browser Version',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -144,7 +152,7 @@ export default function OverviewTopDevices({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top OS',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -154,6 +162,9 @@ export default function OverviewTopDevices({
|
||||
title: 'Top OS version',
|
||||
btn: 'OS Version',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return name[1] || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -169,13 +180,17 @@ export default function OverviewTopDevices({
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'os',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'os_version',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top OS version',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -211,19 +226,19 @@ export default function OverviewTopDevices({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'devices':
|
||||
setFilter('device', item.name);
|
||||
setFilter('device', item.names[0]);
|
||||
break;
|
||||
case 'browser':
|
||||
setFilter('browser', item.name);
|
||||
setFilter('browser', item.names[0]);
|
||||
break;
|
||||
case 'browser_version':
|
||||
setFilter('browser_version', item.name);
|
||||
setFilter('browser_version', item.names[1]);
|
||||
break;
|
||||
case 'os':
|
||||
setFilter('os', item.name);
|
||||
setFilter('os', item.names[0]);
|
||||
break;
|
||||
case 'os_version':
|
||||
setFilter('os_version', item.name);
|
||||
setFilter('os_version', item.names[1]);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function OverviewTopEvents({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Your top events',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -91,7 +91,7 @@ export default function OverviewTopEvents({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'All top events',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -131,7 +131,7 @@ export default function OverviewTopEvents({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Conversions',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
import { ChartRoot } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getCountry } from '@/translations/countries';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
import { LazyChart } from '../report/chart/LazyChart';
|
||||
@@ -29,6 +31,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
title: 'Top countries',
|
||||
btn: 'Countries',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return getCountry(name[0]) || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -50,7 +55,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top countries',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -60,6 +65,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
title: 'Top regions',
|
||||
btn: 'Regions',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return name[1] || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -75,13 +83,17 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'country',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'region',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top regions',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -91,6 +103,9 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
title: 'Top cities',
|
||||
btn: 'Cities',
|
||||
chart: {
|
||||
renderSerieName(name) {
|
||||
return name[1] || NOT_SET_VALUE;
|
||||
},
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -106,13 +121,17 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'country',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'city',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top cities',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -149,14 +168,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
switch (widget.key) {
|
||||
case 'countries':
|
||||
setWidget('regions');
|
||||
setFilter('country', item.name);
|
||||
setFilter('country', item.names[0]);
|
||||
break;
|
||||
case 'regions':
|
||||
setWidget('cities');
|
||||
setFilter('region', item.name);
|
||||
setFilter('region', item.names[1]);
|
||||
break;
|
||||
case 'cities':
|
||||
setFilter('city', item.name);
|
||||
setFilter('city', item.names[1]);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
@@ -169,7 +188,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
<div className="title">Map</div>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<ChartSwitch
|
||||
<ChartRoot
|
||||
hideID
|
||||
{...{
|
||||
projectId,
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ExternalLinkIcon, FilterIcon } from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
import { LazyChart } from '../report/chart/LazyChart';
|
||||
import { Tooltiper } from '../ui/tooltip';
|
||||
import { Widget, WidgetBody } from '../widget';
|
||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||
import OverviewDetailsButton from './overview-details-button';
|
||||
@@ -23,11 +26,19 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
useOverviewOptions();
|
||||
const [chartType, setChartType] = useState<IChartType>('bar');
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const renderSerieName = (names: string[]) => {
|
||||
return (
|
||||
<Tooltiper content={names.join('')} side="left" className="text-left">
|
||||
{names[1] || NOT_SET_VALUE}
|
||||
</Tooltiper>
|
||||
);
|
||||
};
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||
top: {
|
||||
title: 'Top pages',
|
||||
btn: 'Top pages',
|
||||
chart: {
|
||||
renderSerieName,
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -43,13 +54,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'origin',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top pages',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
@@ -59,6 +74,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
title: 'Entry Pages',
|
||||
btn: 'Entries',
|
||||
chart: {
|
||||
renderSerieName,
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -74,13 +90,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'origin',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
name: 'Entry Pages',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
@@ -90,6 +110,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
title: 'Exit Pages',
|
||||
btn: 'Exits',
|
||||
chart: {
|
||||
renderSerieName,
|
||||
limit: 10,
|
||||
projectId,
|
||||
startDate,
|
||||
@@ -105,13 +126,17 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'origin',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
name: 'Exit Pages',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
@@ -153,9 +178,22 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
hideID
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
setFilter('path', item.name);
|
||||
}}
|
||||
dropdownMenuContent={(serie) => [
|
||||
{
|
||||
title: 'Visit page',
|
||||
icon: ExternalLinkIcon,
|
||||
onClick: () => {
|
||||
window.open(serie.names.join(''), '_blank');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Set filter',
|
||||
icon: FilterIcon,
|
||||
onClick: () => {
|
||||
setFilter('path', serie.names[1]);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{widget.chart?.name && <OverviewDetailsButton chart={widget.chart} />}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top groups',
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -84,7 +84,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'Top urls',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -146,7 +146,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Source',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -177,7 +177,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Medium',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -208,7 +208,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Campaign',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -239,7 +239,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Term',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -270,7 +270,7 @@ export default function OverviewTopSources({
|
||||
chartType,
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
name: 'UTM Content',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
@@ -307,30 +307,30 @@ export default function OverviewTopSources({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'all':
|
||||
setFilter('referrer_name', item.name);
|
||||
setFilter('referrer_name', item.names[0]);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'domain':
|
||||
setFilter('referrer', item.name);
|
||||
setFilter('referrer', item.names[0]);
|
||||
break;
|
||||
case 'type':
|
||||
setFilter('referrer_type', item.name);
|
||||
setFilter('referrer_type', item.names[0]);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'utm_source':
|
||||
setFilter('properties.__query.utm_source', item.name);
|
||||
setFilter('properties.__query.utm_source', item.names[0]);
|
||||
break;
|
||||
case 'utm_medium':
|
||||
setFilter('properties.__query.utm_medium', item.name);
|
||||
setFilter('properties.__query.utm_medium', item.names[0]);
|
||||
break;
|
||||
case 'utm_campaign':
|
||||
setFilter('properties.__query.utm_campaign', item.name);
|
||||
setFilter('properties.__query.utm_campaign', item.names[0]);
|
||||
break;
|
||||
case 'utm_term':
|
||||
setFilter('properties.__query.utm_term', item.name);
|
||||
setFilter('properties.__query.utm_term', item.names[0]);
|
||||
break;
|
||||
case 'utm_content':
|
||||
setFilter('properties.__query.utm_content', item.name);
|
||||
setFilter('properties.__query.utm_content', item.names[0]);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
|
||||
import { mapKeys } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import type { IChartRoot } from '../report/chart';
|
||||
|
||||
export function useOverviewWidget<T extends string>(
|
||||
key: string,
|
||||
widgets: Record<
|
||||
T,
|
||||
{ title: string; btn: string; chart: IChartProps; hide?: boolean }
|
||||
{ title: string; btn: string; chart: IChartRoot; hide?: boolean }
|
||||
>
|
||||
) {
|
||||
const keys = Object.keys(widgets) as T[];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { api } from '@/trpc/client';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { ChartEmpty } from './ChartEmpty';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportAreaChart } from './ReportAreaChart';
|
||||
import { ReportBarChart } from './ReportBarChart';
|
||||
import { ReportHistogramChart } from './ReportHistogramChart';
|
||||
@@ -15,34 +16,22 @@ import { ReportPieChart } from './ReportPieChart';
|
||||
|
||||
export type ReportChartProps = IChartProps;
|
||||
|
||||
export function Chart({
|
||||
interval,
|
||||
events,
|
||||
breakdowns,
|
||||
chartType,
|
||||
range,
|
||||
lineType,
|
||||
previous,
|
||||
formula,
|
||||
metric,
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
limit,
|
||||
offset,
|
||||
}: ReportChartProps) {
|
||||
const [references] = api.reference.getChartReferences.useSuspenseQuery(
|
||||
{
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 5,
|
||||
}
|
||||
);
|
||||
|
||||
export function Chart() {
|
||||
const {
|
||||
interval,
|
||||
events,
|
||||
breakdowns,
|
||||
chartType,
|
||||
range,
|
||||
previous,
|
||||
formula,
|
||||
metric,
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
limit,
|
||||
offset,
|
||||
} = useChartContext();
|
||||
const [data] = api.chart.chart.useSuspenseQuery(
|
||||
{
|
||||
interval,
|
||||
@@ -73,7 +62,7 @@ export function Chart({
|
||||
}
|
||||
|
||||
if (chartType === 'histogram') {
|
||||
return <ReportHistogramChart interval={interval} data={data} />;
|
||||
return <ReportHistogramChart data={data} />;
|
||||
}
|
||||
|
||||
if (chartType === 'bar') {
|
||||
@@ -89,20 +78,11 @@ export function Chart({
|
||||
}
|
||||
|
||||
if (chartType === 'linear') {
|
||||
return (
|
||||
<ReportLineChart
|
||||
lineType={lineType}
|
||||
interval={interval}
|
||||
data={data}
|
||||
references={references}
|
||||
/>
|
||||
);
|
||||
return <ReportLineChart data={data} />;
|
||||
}
|
||||
|
||||
if (chartType === 'area') {
|
||||
return (
|
||||
<ReportAreaChart lineType={lineType} interval={interval} data={data} />
|
||||
);
|
||||
return <ReportAreaChart data={data} />;
|
||||
}
|
||||
|
||||
return <p>Unknown chart type</p>;
|
||||
|
||||
@@ -1,113 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
createContext,
|
||||
memo,
|
||||
Suspense,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartProps, IChartSerie } from '@openpanel/validation';
|
||||
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import { MetricCardLoading } from './MetricCard';
|
||||
|
||||
export interface ChartContextType extends IChartProps {
|
||||
export interface IChartContextType extends IChartProps {
|
||||
editMode?: boolean;
|
||||
hideID?: boolean;
|
||||
onClick?: (item: IChartSerie) => void;
|
||||
limit?: number;
|
||||
renderSerieName?: (names: string[]) => React.ReactNode;
|
||||
renderSerieIcon?: (serie: IChartSerie) => React.ReactNode;
|
||||
dropdownMenuContent?: (serie: IChartSerie) => {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}[];
|
||||
}
|
||||
|
||||
type ChartProviderProps = {
|
||||
type IChartProviderProps = {
|
||||
children: React.ReactNode;
|
||||
} & ChartContextType;
|
||||
} & IChartContextType;
|
||||
|
||||
const ChartContext = createContext<ChartContextType | null>({
|
||||
events: [],
|
||||
breakdowns: [],
|
||||
chartType: 'linear',
|
||||
lineType: 'monotone',
|
||||
interval: 'day',
|
||||
name: '',
|
||||
range: '7d',
|
||||
metric: 'sum',
|
||||
previous: false,
|
||||
projectId: '',
|
||||
limit: undefined,
|
||||
});
|
||||
const ChartContext = createContext<IChartContextType | null>(null);
|
||||
|
||||
export function ChartProvider({
|
||||
children,
|
||||
editMode,
|
||||
previous,
|
||||
hideID,
|
||||
limit,
|
||||
...props
|
||||
}: ChartProviderProps) {
|
||||
export function ChartProvider({ children, ...props }: IChartProviderProps) {
|
||||
return (
|
||||
<ChartContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
...props,
|
||||
editMode: editMode ?? false,
|
||||
previous: previous ?? false,
|
||||
hideID: hideID ?? false,
|
||||
limit,
|
||||
}),
|
||||
[editMode, previous, hideID, limit, props]
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ChartContext.Provider>
|
||||
<ChartContext.Provider value={props}>{children}</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function withChartProivder<ComponentProps>(
|
||||
WrappedComponent: React.FC<ComponentProps>
|
||||
) {
|
||||
const WithChartProvider = (props: ComponentProps & ChartContextType) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return props.chartType === 'metric' ? (
|
||||
<MetricCardLoading />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
props.chartType === 'metric' ? (
|
||||
<MetricCardLoading />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
)
|
||||
}
|
||||
>
|
||||
<ChartProvider {...props}>
|
||||
<WrappedComponent {...props} />
|
||||
</ChartProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
WithChartProvider.displayName = `WithChartProvider(${
|
||||
WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'
|
||||
})`;
|
||||
|
||||
return memo(WithChartProvider);
|
||||
}
|
||||
|
||||
export function useChartContext() {
|
||||
return useContext(ChartContext)!;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useInViewport } from 'react-in-viewport';
|
||||
|
||||
import { ChartSwitch } from '.';
|
||||
import type { IChartRoot } from '.';
|
||||
import { ChartRoot } from '.';
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import type { ChartContextType } from './ChartProvider';
|
||||
|
||||
export function LazyChart(props: ChartContextType) {
|
||||
export function LazyChart(props: IChartRoot) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const once = useRef(false);
|
||||
const { inViewport } = useInViewport(ref, undefined, {
|
||||
@@ -23,7 +23,7 @@ export function LazyChart(props: ChartContextType) {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{once.current || inViewport ? (
|
||||
<ChartSwitch {...props} editMode={false} />
|
||||
<ChartRoot {...props} editMode={false} />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { fancyMinutes, useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
PreviousDiffIndicatorText,
|
||||
} from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
interface MetricCardProps {
|
||||
serie: IChartData['series'][number];
|
||||
@@ -28,7 +28,7 @@ export function MetricCard({
|
||||
metric,
|
||||
unit,
|
||||
}: MetricCardProps) {
|
||||
const { previousIndicatorInverted } = useChartContext();
|
||||
const { previousIndicatorInverted, editMode } = useChartContext();
|
||||
const number = useNumber();
|
||||
|
||||
const renderValue = (value: number, unitClassName?: string) => {
|
||||
@@ -57,14 +57,15 @@ export function MetricCard({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative h-[70px] overflow-hidden'
|
||||
// '[#report-editor_&&]:card [#report-editor_&&]:px-4 [#report-editor_&&]:py-2'
|
||||
'group relative h-[70px] overflow-hidden',
|
||||
editMode && 'card h-[100px] px-4 py-2'
|
||||
)}
|
||||
key={serie.name}
|
||||
key={serie.id}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute -bottom-1 -left-1 -right-1 top-0 z-0 opacity-20 transition-opacity duration-300 group-hover:opacity-50'
|
||||
'pointer-events-none absolute -bottom-1 -left-1 -right-1 top-0 z-0 opacity-20 transition-opacity duration-300 group-hover:opacity-50',
|
||||
editMode && 'bottom-1'
|
||||
)}
|
||||
>
|
||||
<AutoSizer>
|
||||
@@ -91,9 +92,8 @@ export function MetricCard({
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2 text-left font-semibold">
|
||||
<ColorSquare>{serie.event.id}</ColorSquare>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
|
||||
{serie.name}
|
||||
<SerieName name={serie.names} />
|
||||
</span>
|
||||
</div>
|
||||
{/* <PreviousDiffIndicator {...serie.metrics.previous[metric]} /> */}
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import type { IChartLineType, IInterval } from '@openpanel/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
@@ -24,16 +22,10 @@ import { ResponsiveContainer } from './ResponsiveContainer';
|
||||
|
||||
interface ReportAreaChartProps {
|
||||
data: IChartData;
|
||||
interval: IInterval;
|
||||
lineType: IChartLineType;
|
||||
}
|
||||
|
||||
export function ReportAreaChart({
|
||||
lineType,
|
||||
interval,
|
||||
data,
|
||||
}: ReportAreaChartProps) {
|
||||
const { editMode } = useChartContext();
|
||||
export function ReportAreaChart({ data }: ReportAreaChartProps) {
|
||||
const { editMode, lineType, interval } = useChartContext();
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -65,7 +57,7 @@ export function ReportAreaChart({
|
||||
{series.map((serie) => {
|
||||
const color = getChartColor(serie.index);
|
||||
return (
|
||||
<React.Fragment key={serie.name}>
|
||||
<React.Fragment key={serie.id}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`color${color}`}
|
||||
@@ -87,7 +79,7 @@ export function ReportAreaChart({
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
key={serie.name}
|
||||
key={serie.id}
|
||||
type={lineType}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { DropdownMenuPortal } from '@radix-ui/react-dropdown-menu';
|
||||
import { ExternalLinkIcon, FilterIcon } from 'lucide-react';
|
||||
|
||||
import { round } from '@openpanel/common';
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
@@ -11,13 +20,15 @@ import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import { PreviousDiffIndicatorText } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { SerieIcon } from './SerieIcon';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
interface ReportBarChartProps {
|
||||
data: IChartData;
|
||||
}
|
||||
|
||||
export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||
const { editMode, metric, onClick, limit } = useChartContext();
|
||||
const { editMode, metric, onClick, limit, dropdownMenuContent } =
|
||||
useChartContext();
|
||||
const number = useNumber();
|
||||
const series = useMemo(
|
||||
() => (editMode ? data.series : data.series.slice(0, limit || 10)),
|
||||
@@ -33,42 +44,64 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
|
||||
)}
|
||||
>
|
||||
{series.map((serie) => {
|
||||
const isClickable = serie.name !== NOT_SET_VALUE && onClick;
|
||||
const isClickable = !serie.names.includes(NOT_SET_VALUE) && onClick;
|
||||
const isDropDownEnabled =
|
||||
!serie.names.includes(NOT_SET_VALUE) &&
|
||||
(dropdownMenuContent?.(serie) || []).length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={serie.name}
|
||||
className={cn('relative', isClickable && 'cursor-pointer')}
|
||||
{...(isClickable ? { onClick: () => onClick(serie) } : {})}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
|
||||
style={{
|
||||
width: `${(serie.metrics.sum / maxCount) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-10 flex w-full flex-1 items-center gap-4 overflow-hidden px-3 py-2">
|
||||
<div className="flex flex-1 items-center gap-2 break-all font-medium">
|
||||
<SerieIcon name={serie.name} />
|
||||
{serie.name}
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center justify-end gap-4">
|
||||
<PreviousDiffIndicatorText
|
||||
{...serie.metrics.previous?.[metric]}
|
||||
className="text-xs font-medium"
|
||||
<DropdownMenu key={serie.id}>
|
||||
<DropdownMenuTrigger asChild disabled={!isDropDownEnabled}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative',
|
||||
(isClickable || isDropDownEnabled) && 'cursor-pointer'
|
||||
)}
|
||||
{...(isClickable && !isDropDownEnabled
|
||||
? { onClick: () => onClick?.(serie) }
|
||||
: {})}
|
||||
>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 top-0 rounded bg-def-200"
|
||||
style={{
|
||||
width: `${(serie.metrics.sum / maxCount) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
{serie.metrics.previous?.[metric]?.value}
|
||||
<div className="text-muted-foreground">
|
||||
{number.format(
|
||||
round((serie.metrics.sum / data.metrics.sum) * 100, 2)
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
{number.format(serie.metrics.sum)}
|
||||
<div className="relative z-10 flex w-full flex-1 items-center gap-4 overflow-hidden px-3 py-2">
|
||||
<div className="flex flex-1 items-center gap-2 break-all font-medium">
|
||||
<SerieIcon name={serie.names[0]} />
|
||||
<SerieName name={serie.names} />
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center justify-end gap-4">
|
||||
<PreviousDiffIndicatorText
|
||||
{...serie.metrics.previous?.[metric]}
|
||||
className="text-xs font-medium"
|
||||
/>
|
||||
{serie.metrics.previous?.[metric]?.value}
|
||||
<div className="text-muted-foreground">
|
||||
{number.format(
|
||||
round((serie.metrics.sum / data.metrics.sum) * 100, 2)
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
{number.format(serie.metrics.sum)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
{dropdownMenuContent?.(serie).map((item) => (
|
||||
<DropdownMenuItem key={item.title} onClick={item.onClick}>
|
||||
{item.icon && <item.icon size={16} className="mr-2" />}
|
||||
{item.title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,8 @@ import type { IToolTipProps } from '@/types';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { SerieIcon } from './SerieIcon';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
type ReportLineChartTooltipProps = IToolTipProps<{
|
||||
value: number;
|
||||
@@ -53,7 +55,7 @@ export function ReportChartTooltip({
|
||||
) as IRechartPayloadItem;
|
||||
|
||||
return (
|
||||
<React.Fragment key={data.name}>
|
||||
<React.Fragment key={data.id}>
|
||||
{index === 0 && data.date && (
|
||||
<div className="flex justify-between gap-8">
|
||||
<div>{formatDate(new Date(data.date))}</div>
|
||||
@@ -65,8 +67,9 @@ export function ReportChartTooltip({
|
||||
style={{ background: data.color }}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="min-w-0 max-w-[200px] overflow-hidden text-ellipsis whitespace-nowrap font-medium">
|
||||
{getLabel(data.name)}
|
||||
<div className="flex items-center gap-1">
|
||||
<SerieIcon name={data.names} />
|
||||
<SerieName name={data.names} />
|
||||
</div>
|
||||
<div className="flex justify-between gap-8">
|
||||
<div>{number.formatWithUnit(data.count, unit)}</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ResponsiveContainer } from './ResponsiveContainer';
|
||||
|
||||
interface ReportHistogramChartProps {
|
||||
data: IChartData;
|
||||
interval: IInterval;
|
||||
}
|
||||
|
||||
function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
||||
@@ -32,11 +31,8 @@ function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportHistogramChart({
|
||||
interval,
|
||||
data,
|
||||
}: ReportHistogramChartProps) {
|
||||
const { editMode, previous } = useChartContext();
|
||||
export function ReportHistogramChart({ data }: ReportHistogramChartProps) {
|
||||
const { editMode, previous, interval } = useChartContext();
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -71,11 +67,11 @@ export function ReportHistogramChart({
|
||||
/>
|
||||
{series.map((serie) => {
|
||||
return (
|
||||
<React.Fragment key={serie.name}>
|
||||
<React.Fragment key={serie.id}>
|
||||
{previous && (
|
||||
<Bar
|
||||
key={`${serie.name}:prev`}
|
||||
name={`${serie.name}:prev`}
|
||||
key={`${serie.id}:prev`}
|
||||
name={`${serie.id}:prev`}
|
||||
dataKey={`${serie.id}:prev:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
fillOpacity={0.2}
|
||||
@@ -83,8 +79,8 @@ export function ReportHistogramChart({
|
||||
/>
|
||||
)}
|
||||
<Bar
|
||||
key={serie.name}
|
||||
name={serie.name}
|
||||
key={serie.id}
|
||||
name={serie.id}
|
||||
dataKey={`${serie.id}:count`}
|
||||
fill={getChartColor(serie.index)}
|
||||
radius={3}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import { api } from '@/trpc/client';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { isSameDay, isSameHour, isSameMonth } from 'date-fns';
|
||||
import { SplineIcon } from 'lucide-react';
|
||||
import { last, pathOr } from 'ramda';
|
||||
import {
|
||||
Area,
|
||||
CartesianGrid,
|
||||
ComposedChart,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
@@ -27,49 +32,35 @@ import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
import { ReportTable } from './ReportTable';
|
||||
import { ResponsiveContainer } from './ResponsiveContainer';
|
||||
import { SerieIcon } from './SerieIcon';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
interface ReportLineChartProps {
|
||||
data: IChartData;
|
||||
references: IServiceReference[];
|
||||
interval: IInterval;
|
||||
lineType: IChartLineType;
|
||||
}
|
||||
|
||||
function CustomLegend(props: {
|
||||
payload?: { value: string; payload: { fill: string } }[];
|
||||
}) {
|
||||
if (!props.payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1">
|
||||
{props.payload
|
||||
.filter((entry) => !entry.value.includes('noTooltip'))
|
||||
.filter((entry) => !entry.value.includes(':prev'))
|
||||
.map((entry) => (
|
||||
<div className="flex gap-1" key={entry.value}>
|
||||
<SplineIcon size={12} color={entry.payload.fill} />
|
||||
<div
|
||||
style={{
|
||||
color: entry.payload.fill,
|
||||
}}
|
||||
>
|
||||
{entry.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
export function ReportLineChart({ data }: ReportLineChartProps) {
|
||||
const {
|
||||
editMode,
|
||||
previous,
|
||||
interval,
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
lineType,
|
||||
} = useChartContext();
|
||||
const references = api.reference.getChartReferences.useQuery(
|
||||
{
|
||||
projectId,
|
||||
startDate,
|
||||
endDate,
|
||||
range,
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 5,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportLineChart({
|
||||
lineType,
|
||||
interval,
|
||||
data,
|
||||
references,
|
||||
}: ReportLineChartProps) {
|
||||
const { editMode, previous } = useChartContext();
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -96,20 +87,56 @@ export function ReportLineChart({
|
||||
</linearGradient>
|
||||
);
|
||||
|
||||
const useDashedLastLine = (series[0]?.data?.length || 0) > 2;
|
||||
const lastSerieDataItem = last(series[0]?.data || [])?.date || new Date();
|
||||
const useDashedLastLine = (() => {
|
||||
if (interval === 'hour') {
|
||||
return isSameHour(lastSerieDataItem, new Date());
|
||||
}
|
||||
|
||||
if (interval === 'day') {
|
||||
return isSameDay(lastSerieDataItem, new Date());
|
||||
}
|
||||
|
||||
if (interval === 'month') {
|
||||
return isSameMonth(lastSerieDataItem, new Date());
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
const CustomLegend = useCallback(() => {
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1">
|
||||
{series.map((serie) => (
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
key={serie.id}
|
||||
style={{
|
||||
color: getChartColor(serie.index),
|
||||
}}
|
||||
>
|
||||
<SerieIcon name={serie.names} />
|
||||
<SerieName name={serie.names} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, [series]);
|
||||
|
||||
const isAreaStyle = series.length === 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponsiveContainer>
|
||||
{({ width, height }) => (
|
||||
<LineChart width={width} height={height} data={rechartData}>
|
||||
<ComposedChart width={width} height={height} data={rechartData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
horizontal={true}
|
||||
vertical={false}
|
||||
className="stroke-def-200"
|
||||
/>
|
||||
{references.map((ref) => (
|
||||
{references.data?.map((ref) => (
|
||||
<ReferenceLine
|
||||
key={ref.id}
|
||||
x={ref.date.getTime()}
|
||||
@@ -150,18 +177,39 @@ export function ReportLineChart({
|
||||
tickLine={false}
|
||||
/>
|
||||
{series.map((serie) => {
|
||||
const color = getChartColor(serie.index);
|
||||
return (
|
||||
<React.Fragment key={serie.name}>
|
||||
<React.Fragment key={serie.id}>
|
||||
<defs>
|
||||
{isAreaStyle && (
|
||||
<linearGradient
|
||||
id={`color${color}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={color}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={color}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
</linearGradient>
|
||||
)}
|
||||
{gradientTwoColors(
|
||||
`hideAllButLastInterval_${serie.id}`,
|
||||
'rgba(0,0,0,0)',
|
||||
getChartColor(serie.index),
|
||||
color,
|
||||
lastIntervalPercent
|
||||
)}
|
||||
{gradientTwoColors(
|
||||
`hideJustLastInterval_${serie.id}`,
|
||||
getChartColor(serie.index),
|
||||
color,
|
||||
'rgba(0,0,0,0)',
|
||||
lastIntervalPercent
|
||||
)}
|
||||
@@ -169,24 +217,30 @@ export function ReportLineChart({
|
||||
<Line
|
||||
dot={false}
|
||||
type={lineType}
|
||||
name={serie.name}
|
||||
name={serie.id}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey={`${serie.id}:count`}
|
||||
stroke={
|
||||
useDashedLastLine
|
||||
? 'transparent'
|
||||
: getChartColor(serie.index)
|
||||
}
|
||||
stroke={useDashedLastLine ? 'transparent' : color}
|
||||
// Use for legend
|
||||
fill={getChartColor(serie.index)}
|
||||
fill={color}
|
||||
/>
|
||||
{isAreaStyle && (
|
||||
<Area
|
||||
name={`${serie.id}:area:noTooltip`}
|
||||
dataKey={`${serie.id}:count`}
|
||||
fill={`url(#color${color})`}
|
||||
type={lineType}
|
||||
isAnimationActive={false}
|
||||
fillOpacity={0.1}
|
||||
/>
|
||||
)}
|
||||
{useDashedLastLine && (
|
||||
<>
|
||||
<Line
|
||||
dot={false}
|
||||
type={lineType}
|
||||
name={`${serie.name}:dashed:noTooltip`}
|
||||
name={`${serie.id}:dashed:noTooltip`}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey={`${serie.id}:count`}
|
||||
@@ -197,7 +251,7 @@ export function ReportLineChart({
|
||||
<Line
|
||||
dot={false}
|
||||
type={lineType}
|
||||
name={`${serie.name}:solid:noTooltip`}
|
||||
name={`${serie.id}:solid:noTooltip`}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={2}
|
||||
dataKey={`${serie.id}:count`}
|
||||
@@ -208,22 +262,22 @@ export function ReportLineChart({
|
||||
{previous && (
|
||||
<Line
|
||||
type={lineType}
|
||||
name={`${serie.name}:prev`}
|
||||
name={`${serie.id}:prev`}
|
||||
isAnimationActive={false}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
strokeDasharray={'1 1'}
|
||||
strokeOpacity={0.5}
|
||||
dataKey={`${serie.id}:prev:count`}
|
||||
stroke={getChartColor(serie.index)}
|
||||
stroke={color}
|
||||
// Use for legend
|
||||
fill={getChartColor(serie.index)}
|
||||
fill={color}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
</ComposedChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
{editMode && (
|
||||
|
||||
@@ -18,7 +18,7 @@ export function ReportMapChart({ data }: ReportMapChartProps) {
|
||||
const mapData = useMemo(
|
||||
() =>
|
||||
series.map((s) => ({
|
||||
country: s.name.toLowerCase(),
|
||||
country: s.names[0]?.toLowerCase() ?? '',
|
||||
value: s.metrics[metric],
|
||||
})),
|
||||
[series, metric]
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ReportMetricChart({ data }: ReportMetricChartProps) {
|
||||
{series.map((serie) => {
|
||||
return (
|
||||
<MetricCard
|
||||
key={serie.name}
|
||||
key={serie.id}
|
||||
serie={serie}
|
||||
metric={metric}
|
||||
unit={unit}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ReportPieChart({ data }: ReportPieChartProps) {
|
||||
id: serie.id,
|
||||
color: getChartColor(serie.index),
|
||||
index: serie.index,
|
||||
name: serie.name,
|
||||
name: serie.names.join(' > '),
|
||||
count: serie.metrics.sum,
|
||||
percent: serie.metrics.sum / sum,
|
||||
}));
|
||||
|
||||
@@ -13,16 +13,19 @@ import {
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
Tooltiper,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { useSelector } from '@/redux';
|
||||
import { getPropertyLabel } from '@/translations/properties';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { SerieName } from './SerieName';
|
||||
|
||||
interface ReportTableProps {
|
||||
data: IChartData;
|
||||
@@ -40,8 +43,8 @@ export function ReportTable({
|
||||
const { setPage, paginate, page } = usePagination(ROWS_LIMIT);
|
||||
const number = useNumber();
|
||||
const interval = useSelector((state) => state.report.interval);
|
||||
const breakdowns = useSelector((state) => state.report.breakdowns);
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const getLabel = useMappings();
|
||||
|
||||
function handleChange(name: string, checked: boolean) {
|
||||
setVisibleSeries((prev) => {
|
||||
@@ -55,49 +58,61 @@ export function ReportTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-[200px_1fr] overflow-hidden rounded-md border border-border">
|
||||
<div className="grid grid-cols-[max(300px,30vw)_1fr] overflow-hidden rounded-md border border-border">
|
||||
<Table className="rounded-none border-b-0 border-l-0 border-t-0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
{breakdowns.length === 0 && <TableHead>Name</TableHead>}
|
||||
{breakdowns.map((breakdown) => (
|
||||
<TableHead key={breakdown.name}>
|
||||
{getPropertyLabel(breakdown.name)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableBody className="bg-def-100">
|
||||
{paginate(data.series).map((serie, index) => {
|
||||
const checked = !!visibleSeries.find(
|
||||
(item) => item.name === serie.name
|
||||
(item) => item.id === serie.id
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.name, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{getLabel(serie.name)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{getLabel(serie.name)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableRow key={serie.id}>
|
||||
{serie.names.map((name, nameIndex) => {
|
||||
return (
|
||||
<TableCell className="h-10" key={name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{nameIndex === 0 ? (
|
||||
<>
|
||||
<Checkbox
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange(serie.id, !!checked)
|
||||
}
|
||||
style={
|
||||
checked
|
||||
? {
|
||||
background: getChartColor(index),
|
||||
borderColor: getChartColor(index),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
checked={checked}
|
||||
/>
|
||||
<Tooltiper
|
||||
side="left"
|
||||
sideOffset={30}
|
||||
content={<SerieName name={serie.names} />}
|
||||
>
|
||||
{name}
|
||||
</Tooltiper>
|
||||
</>
|
||||
) : (
|
||||
<SerieName name={name} />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
@@ -122,7 +137,7 @@ export function ReportTable({
|
||||
<TableBody>
|
||||
{paginate(data.series).map((serie) => {
|
||||
return (
|
||||
<TableRow key={serie.name}>
|
||||
<TableRow key={serie.id}>
|
||||
<TableCell className="h-10">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{number.format(serie.metrics.sum)}
|
||||
|
||||
@@ -4,7 +4,7 @@ const createFlagIcon = (url: string) => {
|
||||
return function (_props: LucideProps) {
|
||||
return (
|
||||
<span
|
||||
className={`fi fis !block overflow-hidden rounded-full !leading-[1rem] fi-${url}`}
|
||||
className={`fi !block aspect-[1.33] overflow-hidden rounded-[2px] fi-${url}`}
|
||||
></span>
|
||||
);
|
||||
} as LucideIcon;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
SearchIcon,
|
||||
SmartphoneIcon,
|
||||
TabletIcon,
|
||||
TvIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
@@ -20,9 +21,9 @@ import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import flags from './SerieIcon.flags';
|
||||
import iconsWithUrls from './SerieIcon.urls';
|
||||
|
||||
interface SerieIconProps extends LucideProps {
|
||||
name?: string;
|
||||
}
|
||||
type SerieIconProps = Omit<LucideProps, 'name'> & {
|
||||
name?: string | string[];
|
||||
};
|
||||
|
||||
function getProxyImage(url: string) {
|
||||
return `${String(process.env.NEXT_PUBLIC_API_URL)}/misc/favicon?url=${encodeURIComponent(url)}`;
|
||||
@@ -30,7 +31,7 @@ function getProxyImage(url: string) {
|
||||
|
||||
const createImageIcon = (url: string) => {
|
||||
return function (_props: LucideProps) {
|
||||
return <img className="h-4 rounded-[2px] object-contain" src={url} />;
|
||||
return <img className="max-h-4 rounded-[2px] object-contain" src={url} />;
|
||||
} as LucideIcon;
|
||||
};
|
||||
|
||||
@@ -42,6 +43,7 @@ const mapper: Record<string, LucideIcon> = {
|
||||
link_out: ExternalLinkIcon,
|
||||
|
||||
// Misc
|
||||
smarttv: TvIcon,
|
||||
mobile: SmartphoneIcon,
|
||||
desktop: MonitorIcon,
|
||||
tablet: TabletIcon,
|
||||
@@ -64,7 +66,8 @@ const mapper: Record<string, LucideIcon> = {
|
||||
...flags,
|
||||
};
|
||||
|
||||
export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
export function SerieIcon({ name: names, ...props }: SerieIconProps) {
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
const Icon = useMemo(() => {
|
||||
if (!name) {
|
||||
return null;
|
||||
@@ -80,6 +83,7 @@ export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
return createImageIcon(getProxyImage(name));
|
||||
}
|
||||
|
||||
// Matching image file name
|
||||
if (name.match(/(.+)\.\w{2,3}$/)) {
|
||||
return createImageIcon(getProxyImage(`https://${name}`));
|
||||
}
|
||||
@@ -88,8 +92,8 @@ export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
}, [name]);
|
||||
|
||||
return Icon ? (
|
||||
<div className="[&_a]:![&_a]:!h-4 relative h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
||||
<Icon size={16} {...props} />
|
||||
<div className="relative max-h-4 flex-shrink-0 [&_svg]:!rounded-[2px]">
|
||||
<Icon size={16} {...props} name={name} />
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,18 @@ const data = {
|
||||
'vstat.info': 'https://vstat.info',
|
||||
'yahoo!': 'https://yahoo.com',
|
||||
android: 'https://image.similarpng.com/very-thumbnail/2020/08/Android-icon-on-transparent--background-PNG.png',
|
||||
'android browser': 'https://image.similarpng.com/very-thumbnail/2020/08/Android-icon-on-transparent--background-PNG.png',
|
||||
'silk': 'https://m.media-amazon.com/images/I/51VCjQCvF0L.png',
|
||||
'kakaotalk': 'https://www.kakaocorp.com/',
|
||||
bing: 'https://bing.com',
|
||||
'electron': 'https://www.electronjs.org',
|
||||
'whale': 'https://whale.naver.com',
|
||||
'wechat': 'https://wechat.com',
|
||||
chrome: 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'chrome webview': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'chrome headless': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
chromecast: 'https://upload.wikimedia.org/wikipedia/commons/archive/2/26/20161130022732%21Chromecast_cast_button_icon.svg',
|
||||
webkit: 'https://webkit.org',
|
||||
duckduckgo: 'https://duckduckgo.com',
|
||||
ecosia: 'https://ecosia.com',
|
||||
edge: 'https://upload.wikimedia.org/wikipedia/commons/7/7e/Microsoft_Edge_logo_%282019%29.png',
|
||||
@@ -19,6 +28,7 @@ const data = {
|
||||
github: 'https://github.com',
|
||||
gmail: 'https://mail.google.com',
|
||||
google: 'https://google.com',
|
||||
gsa: 'https://google.com', // Google Search App
|
||||
instagram: 'https://instagram.com',
|
||||
ios: 'https://cdn0.iconfinder.com/data/icons/flat-round-system/512/apple-1024.png',
|
||||
linkedin: 'https://linkedin.com',
|
||||
|
||||
40
apps/dashboard/src/components/report/chart/SerieName.tsx
Normal file
40
apps/dashboard/src/components/report/chart/SerieName.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
|
||||
import { useChartContext } from './ChartProvider';
|
||||
|
||||
interface SerieNameProps {
|
||||
name: string | string[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SerieName({ name, className }: SerieNameProps) {
|
||||
const chart = useChartContext();
|
||||
if (Array.isArray(name)) {
|
||||
if (chart.renderSerieName) {
|
||||
return chart.renderSerieName(name);
|
||||
}
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1', className)}>
|
||||
{name.map((n, index) => {
|
||||
return (
|
||||
<>
|
||||
<span>{n || NOT_SET_VALUE}</span>
|
||||
{name.length - 1 > index && (
|
||||
<ChevronRightIcon className="text-muted-foreground" size={12} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (chart.renderSerieName) {
|
||||
return chart.renderSerieName([name]);
|
||||
}
|
||||
|
||||
return <>{name}</>;
|
||||
}
|
||||
@@ -1,22 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { Funnel } from '../funnel';
|
||||
import { Chart } from './Chart';
|
||||
import { withChartProivder } from './ChartProvider';
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import type { IChartContextType } from './ChartProvider';
|
||||
import { ChartProvider } from './ChartProvider';
|
||||
import { MetricCardLoading } from './MetricCard';
|
||||
|
||||
export const ChartSwitch = withChartProivder(function ChartSwitch(
|
||||
props: IChartProps
|
||||
) {
|
||||
if (props.chartType === 'funnel') {
|
||||
return <Funnel {...props} />;
|
||||
export type IChartRoot = IChartContextType;
|
||||
|
||||
export function ChartRoot(props: IChartContextType) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return props.chartType === 'metric' ? (
|
||||
<MetricCardLoading />
|
||||
) : (
|
||||
<ChartLoading />
|
||||
);
|
||||
}
|
||||
|
||||
return <Chart {...props} />;
|
||||
});
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
props.chartType === 'metric' ? <MetricCardLoading /> : <ChartLoading />
|
||||
}
|
||||
>
|
||||
<ChartProvider {...props}>
|
||||
{props.chartType === 'funnel' ? <Funnel /> : <Chart />}
|
||||
</ChartProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChartSwitchShortcutProps {
|
||||
interface ChartRootShortcutProps {
|
||||
projectId: IChartProps['projectId'];
|
||||
range?: IChartProps['range'];
|
||||
previous?: IChartProps['previous'];
|
||||
@@ -25,16 +50,16 @@ interface ChartSwitchShortcutProps {
|
||||
events: IChartProps['events'];
|
||||
}
|
||||
|
||||
export const ChartSwitchShortcut = ({
|
||||
export const ChartRootShortcut = ({
|
||||
projectId,
|
||||
range = '7d',
|
||||
previous = false,
|
||||
chartType = 'linear',
|
||||
interval = 'day',
|
||||
events,
|
||||
}: ChartSwitchShortcutProps) => {
|
||||
}: ChartRootShortcutProps) => {
|
||||
return (
|
||||
<ChartSwitch
|
||||
<ChartRoot
|
||||
projectId={projectId}
|
||||
range={range}
|
||||
breakdowns={[]}
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
import { api } from '@/trpc/client';
|
||||
|
||||
import type { IChartInput, IChartProps } from '@openpanel/validation';
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
|
||||
import { ChartEmpty } from '../chart/ChartEmpty';
|
||||
import { withChartProivder } from '../chart/ChartProvider';
|
||||
import { useChartContext } from '../chart/ChartProvider';
|
||||
import { FunnelSteps } from './Funnel';
|
||||
|
||||
export type ReportChartProps = IChartProps;
|
||||
export function Funnel() {
|
||||
const { events, range, projectId } = useChartContext();
|
||||
|
||||
export const Funnel = withChartProivder(function Chart({
|
||||
events,
|
||||
range,
|
||||
projectId,
|
||||
}: ReportChartProps) {
|
||||
const input: IChartInput = {
|
||||
events,
|
||||
range,
|
||||
@@ -38,4 +34,4 @@ export const Funnel = withChartProivder(function Chart({
|
||||
<FunnelSteps {...data} input={input} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-def-200 h-10 border-b border-border px-4 text-left align-middle text-sm font-medium text-muted-foreground shadow-[0_0_0_0.5px] shadow-border [&:has([role=checkbox])]:pr-0',
|
||||
'h-10 border-b border-border bg-def-200 px-4 text-left align-middle text-sm font-medium text-muted-foreground shadow-[0_0_0_0.5px] shadow-border [&:has([role=checkbox])]:pr-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -90,7 +90,7 @@ const TableCell = React.forwardRef<
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-12 whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border [&:has([role=checkbox])]:pr-0',
|
||||
'h-12 whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -35,10 +35,13 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
interface TooltiperProps {
|
||||
asChild?: boolean;
|
||||
content: string;
|
||||
content: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
delayDuration?: number;
|
||||
sideOffset?: number;
|
||||
}
|
||||
export function Tooltiper({
|
||||
asChild,
|
||||
@@ -46,14 +49,19 @@ export function Tooltiper({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
side,
|
||||
delayDuration = 0,
|
||||
sideOffset = 10,
|
||||
}: TooltiperProps) {
|
||||
return (
|
||||
<Tooltip delayDuration={0}>
|
||||
<Tooltip delayDuration={delayDuration}>
|
||||
<TooltipTrigger asChild={asChild} className={className} onClick={onClick}>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent sideOffset={10}>{content}</TooltipContent>
|
||||
<TooltipContent sideOffset={sideOffset} side={side}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user