fix(dashboard): share overview (all widgets didnt work)

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-02-05 21:24:37 +00:00
parent 2d0478d626
commit 7a88b262c0
15 changed files with 820 additions and 136 deletions

View File

@@ -10,11 +10,12 @@ import { AnimatedNumber } from '../animated-number';
export interface LiveCounterProps {
projectId: string;
shareId?: string;
}
const FIFTEEN_SECONDS = 1000 * 30;
export function LiveCounter({ projectId }: LiveCounterProps) {
export function LiveCounter({ projectId, shareId }: LiveCounterProps) {
const trpc = useTRPC();
const client = useQueryClient();
const counter = useDebounceState(0, 1000);
@@ -22,6 +23,7 @@ export function LiveCounter({ projectId }: LiveCounterProps) {
const query = useQuery(
trpc.overview.liveVisitors.queryOptions({
projectId,
shareId,
}),
);

View File

@@ -18,16 +18,18 @@ import {
import { SerieIcon } from '../report-chart/common/serie-icon';
interface OverviewLiveHistogramProps {
projectId: string;
shareId?: string;
}
export function OverviewLiveHistogram({
projectId,
shareId,
}: OverviewLiveHistogramProps) {
const trpc = useTRPC();
// Use the new liveData endpoint instead of chart props
const { data: liveData, isLoading } = useQuery(
trpc.overview.liveData.queryOptions({ projectId }),
trpc.overview.liveData.queryOptions({ projectId, shareId }),
);
const totalSessions = liveData?.totalSessions ?? 0;

View File

@@ -0,0 +1,90 @@
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
import { useTRPC } from '@/integrations/trpc/react';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import WorldMap from 'react-svg-worldmap';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useOverviewOptions } from './useOverviewOptions';
interface OverviewMapProps {
projectId: string;
shareId?: string;
}
export function OverviewMap({ projectId, shareId }: OverviewMapProps) {
const { range, startDate, endDate } = useOverviewOptions();
const [filters, setFilter] = useEventQueryFilters();
const trpc = useTRPC();
const query = useQuery(
trpc.overview.map.queryOptions({
projectId,
shareId,
range,
filters,
startDate,
endDate,
}),
);
const mapData = useMemo(() => {
if (!query.data) return [];
// Aggregate by country (sum counts for same country)
const countryMap = new Map<string, number>();
query.data.forEach((item) => {
const country = item.country.toLowerCase();
const current = countryMap.get(country) ?? 0;
countryMap.set(country, current + item.count);
});
return Array.from(countryMap.entries()).map(([country, value]) => ({
country,
value,
}));
}, [query.data]);
if (query.isLoading) {
return (
<div className="h-full w-full flex items-center justify-center">
<div className="text-muted-foreground">Loading map...</div>
</div>
);
}
if (query.isError) {
return (
<div className="h-full w-full flex items-center justify-center">
<div className="text-muted-foreground">Error loading map</div>
</div>
);
}
if (!query.data || mapData.length === 0) {
return (
<div className="h-full w-full flex items-center justify-center">
<div className="text-muted-foreground">No data available</div>
</div>
);
}
return (
<div className="h-full w-full" style={{ minHeight: 300 }}>
<AutoSizer disableHeight>
{({ width }) => (
<WorldMap
onClickFunction={(event) => {
if (event.countryCode) {
setFilter('country', event.countryCode);
}
}}
size={width}
data={mapData}
color={'var(--chart-0)'}
borderColor={'var(--foreground)'}
/>
)}
</AutoSizer>
</div>
);
}

View File

@@ -36,6 +36,7 @@ import { OverviewMetricCard } from './overview-metric-card';
interface OverviewMetricsProps {
projectId: string;
shareId?: string;
}
const TITLES = [
@@ -83,7 +84,10 @@ const TITLES = [
},
] as const;
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
export default function OverviewMetrics({
projectId,
shareId,
}: OverviewMetricsProps) {
const { range, interval, metric, setMetric, startDate, endDate } =
useOverviewOptions();
const [filters] = useEventQueryFilters();
@@ -93,6 +97,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
const overviewQuery = useQuery(
trpc.overview.stats.queryOptions({
projectId,
shareId,
range,
interval,
filters,
@@ -138,7 +143,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
'col-span-4 min-h-16 flex-1 p-4 pb-0 shadow-[0_0_0_0.5px] shadow-border max-md:row-start-1 md:col-span-1',
)}
>
<OverviewLiveHistogram projectId={projectId} />
<OverviewLiveHistogram projectId={projectId} shareId={shareId} />
</div>
</div>
@@ -344,7 +349,7 @@ function Chart({
onAnimationEnd={handleAnimationEnd}
/>
<Tooltip />
<YAxis {...yAxisProps} domain={[0, 'dataMax']} width={25} />
<YAxis {...yAxisProps} width={25} />
<XAxis {...xAxisProps} />
<CartesianGrid
strokeDasharray="3 3"
@@ -471,7 +476,12 @@ function Chart({
<Tooltip />
<YAxis
{...yAxisProps}
domain={[0, activeMetric.key === 'bounce_rate' ? 100 : 'dataMax']}
domain={[
0,
activeMetric.key === 'bounce_rate'
? 100
: (dataMax: number) => Math.max(dataMax, 1),
]}
width={25}
/>
<YAxis
@@ -480,14 +490,18 @@ function Chart({
orientation="right"
domain={[
0,
data.reduce(
(max, item) => Math.max(max, item.total_revenue ?? 0),
0,
) * 2,
Math.max(
data.reduce(
(max, item) => Math.max(max, item.total_revenue ?? 0),
0,
) * 1.2,
1,
),
]}
width={30}
allowDataOverflow={false}
/>
<XAxis {...xAxisProps} />
<XAxis {...xAxisProps} padding={{ left: 10, right: 10 }} />
<CartesianGrid
strokeDasharray="3 3"
@@ -523,19 +537,11 @@ function Chart({
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
strokeWidth={2}
isAnimationActive={false}
dot={
data.length > 90
? false
: {
stroke: 'oklch(from var(--foreground) l c h / 0.1)',
fill: 'transparent',
strokeWidth: 1.5,
r: 2,
}
}
dot={false}
activeDot={{
stroke: 'oklch(from var(--foreground) l c h / 0.2)',
fill: 'transparent',
fill: 'var(--def-100)',
fillOpacity: 1,
strokeWidth: 1.5,
r: 3,
}}
@@ -581,7 +587,8 @@ function Chart({
? false
: {
stroke: getChartColor(0),
fill: 'transparent',
fill: 'var(--def-100)',
fillOpacity: 1,
strokeWidth: 1.5,
r: 3,
}

View File

@@ -26,9 +26,11 @@ import { useOverviewWidget } from './useOverviewWidget';
interface OverviewTopDevicesProps {
projectId: string;
shareId?: string;
}
export default function OverviewTopDevices({
projectId,
shareId,
}: OverviewTopDevicesProps) {
const { interval, range, previous, startDate, endDate } =
useOverviewOptions();
@@ -325,6 +327,7 @@ export default function OverviewTopDevices({
const query = useQuery(
trpc.overview.topGeneric.queryOptions({
projectId,
shareId,
range,
filters,
column: widget.key,
@@ -337,6 +340,7 @@ export default function OverviewTopDevices({
trpc.overview.topGenericSeries.queryOptions(
{
projectId,
shareId,
range,
filters,
column: widget.key,

View File

@@ -1,8 +1,6 @@
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
import { useMemo, useState } from 'react';
import type { IReportInput } from '@openpanel/validation';
import { useTRPC } from '@/integrations/trpc/react';
import { useQuery } from '@tanstack/react-query';
import { Widget, WidgetBody } from '../widget';
@@ -17,17 +15,18 @@ import { useOverviewWidgetV2 } from './useOverviewWidget';
export interface OverviewTopEventsProps {
projectId: string;
shareId?: string;
}
export default function OverviewTopEvents({
projectId,
shareId,
}: OverviewTopEventsProps) {
const { interval, range, previous, startDate, endDate } =
useOverviewOptions();
const { range, startDate, endDate } = useOverviewOptions();
const [filters, setFilter] = useEventQueryFilters();
const trpc = useTRPC();
const { data: conversions } = useQuery(
trpc.event.conversionNames.queryOptions({ projectId }),
trpc.overview.topConversions.queryOptions({ projectId, shareId }),
);
const [searchQuery, setSearchQuery] = useState('');
@@ -36,15 +35,7 @@ export default function OverviewTopEvents({
title: 'Events',
btn: 'Events',
meta: {
filters: [
{
id: 'ex_session',
name: 'name',
operator: 'isNot',
value: ['session_start', 'session_end', 'screen_view'],
},
],
eventName: '*',
type: 'events' as const,
},
},
conversions: {
@@ -52,69 +43,84 @@ export default function OverviewTopEvents({
btn: 'Conversions',
hide: !conversions || conversions.length === 0,
meta: {
filters: [
{
id: 'conversion',
name: 'name',
operator: 'is',
value: conversions?.map((c) => c.name) ?? [],
},
],
eventName: '*',
type: 'conversions' as const,
},
},
link_out: {
title: 'Link out',
btn: 'Link out',
meta: {
filters: [],
eventName: 'link_out',
breakdownProperty: 'properties.href',
type: 'linkOut' as const,
},
},
});
const report: IReportInput = useMemo(
() => ({
limit: 1000,
// Use different endpoints based on widget type
const eventsQuery = useQuery(
trpc.overview.topEvents.queryOptions({
projectId,
shareId,
range,
startDate,
endDate,
series: [
{
type: 'event' as const,
segment: 'event' as const,
filters: [...filters, ...(widget.meta?.filters ?? [])],
id: 'A',
name: widget.meta?.eventName ?? '*',
},
],
breakdowns: [
{
id: 'A',
name: widget.meta?.breakdownProperty ?? 'name',
},
],
chartType: 'bar' as const,
interval,
range,
previous,
metric: 'sum' as const,
filters,
excludeEvents:
widget.meta?.type === 'events'
? ['session_start', 'session_end', 'screen_view']
: undefined,
}),
[projectId, startDate, endDate, filters, widget, interval, range, previous],
);
const query = useQuery(trpc.chart.aggregate.queryOptions(report));
const linkOutQuery = useQuery(
trpc.overview.topLinkOut.queryOptions({
projectId,
shareId,
range,
startDate,
endDate,
filters,
}),
);
const tableData: EventTableItem[] = useMemo(() => {
if (!query.data?.series) return [];
// For link out, use href as name
if (widget.meta?.type === 'linkOut') {
if (!linkOutQuery.data) return [];
return linkOutQuery.data.map((item) => ({
id: item.href,
name: item.href,
count: item.count,
}));
}
return query.data.series.map((serie) => ({
id: serie.id,
name: serie.names[serie.names.length - 1] ?? serie.names[0] ?? '',
count: serie.metrics.sum,
// For events and conversions
if (!eventsQuery.data) return [];
// For conversions, filter events by conversion names (client-side filtering)
if (widget.meta?.type === 'conversions' && conversions) {
const conversionNames = new Set(conversions.map((c) => c.name));
return eventsQuery.data
.filter((item) => conversionNames.has(item.name))
.map((item) => ({
id: item.name,
name: item.name,
count: item.count,
}));
}
// For regular events
return eventsQuery.data.map((item) => ({
id: item.name,
name: item.name,
count: item.count,
}));
}, [query.data]);
}, [eventsQuery.data, linkOutQuery.data, widget.meta?.type, conversions]);
// Determine which query's loading state to use
const isLoading =
widget.meta?.type === 'linkOut'
? linkOutQuery.isLoading
: eventsQuery.isLoading;
const filteredData = useMemo(() => {
if (!searchQuery.trim()) {
@@ -150,14 +156,14 @@ export default function OverviewTopEvents({
className="border-b-0 pb-2"
/>
<WidgetBody className="p-0">
{query.isLoading ? (
{isLoading ? (
<OverviewWidgetTableLoading />
) : (
<OverviewWidgetTableEvents
data={filteredData}
onItemClick={(name) => {
if (widget.meta?.breakdownProperty) {
setFilter(widget.meta.breakdownProperty, name);
if (widget.meta?.type === 'linkOut') {
setFilter('properties.href', name);
} else {
setFilter('name', name);
}

View File

@@ -9,9 +9,7 @@ import { countries } from '@/translations/countries';
import { NOT_SET_VALUE } from '@openpanel/constants';
import { useQuery } from '@tanstack/react-query';
import { ChevronRightIcon } from 'lucide-react';
import { ReportChart } from '../report-chart';
import { SerieIcon } from '../report-chart/common/serie-icon';
import { ReportChartShortcut } from '../report-chart/shortcut';
import { Widget, WidgetBody } from '../widget';
import { OVERVIEW_COLUMNS_NAME } from './overview-constants';
import OverviewDetailsButton from './overview-details-button';
@@ -19,6 +17,7 @@ import {
OverviewLineChart,
OverviewLineChartLoading,
} from './overview-line-chart';
import { OverviewMap } from './overview-map';
import { OverviewViewToggle, useOverviewView } from './overview-view-toggle';
import {
WidgetFooter,
@@ -34,8 +33,12 @@ import { useOverviewWidgetV2 } from './useOverviewWidget';
interface OverviewTopGeoProps {
projectId: string;
shareId?: string;
}
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
export default function OverviewTopGeo({
projectId,
shareId,
}: OverviewTopGeoProps) {
const { interval, range, previous, startDate, endDate } =
useOverviewOptions();
const [chartType, setChartType] = useState<IChartType>('bar');
@@ -63,6 +66,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
const query = useQuery(
trpc.overview.topGeneric.queryOptions({
projectId,
shareId,
range,
filters,
column: widget.key,
@@ -75,6 +79,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
trpc.overview.topGenericSeries.queryOptions(
{
projectId,
shareId,
range,
filters,
column: widget.key,
@@ -211,32 +216,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
<div className="title">Map</div>
</WidgetHead>
<WidgetBody>
<ReportChartShortcut
{...{
projectId,
startDate,
endDate,
series: [
{
type: 'event',
segment: 'event',
filters,
id: 'A',
name: isPageFilter ? 'screen_view' : 'session_start',
},
],
breakdowns: [
{
id: 'A',
name: 'country',
},
],
chartType: 'map',
interval: interval,
range: range,
previous: previous,
}}
/>
<OverviewMap projectId={projectId} shareId={shareId} />
</WidgetBody>
</Widget>
</>

View File

@@ -20,9 +20,13 @@ import { useOverviewWidgetV2 } from './useOverviewWidget';
interface OverviewTopPagesProps {
projectId: string;
shareId?: string;
}
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
export default function OverviewTopPages({
projectId,
shareId,
}: OverviewTopPagesProps) {
const { interval, range, startDate, endDate } = useOverviewOptions();
const [filters] = useEventQueryFilters();
const [domain, setDomain] = useQueryState('d', parseAsBoolean);
@@ -56,6 +60,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
const query = useQuery(
trpc.overview.topPages.queryOptions({
projectId,
shareId,
filters,
startDate,
endDate,

View File

@@ -24,9 +24,11 @@ import { useOverviewWidgetV2 } from './useOverviewWidget';
interface OverviewTopSourcesProps {
projectId: string;
shareId?: string;
}
export default function OverviewTopSources({
projectId,
shareId,
}: OverviewTopSourcesProps) {
const { interval, range, startDate, endDate } = useOverviewOptions();
const [filters, setFilter] = useEventQueryFilters();
@@ -71,6 +73,7 @@ export default function OverviewTopSources({
const query = useQuery(
trpc.overview.topGeneric.queryOptions({
projectId,
shareId,
range,
filters,
column: widget.key,
@@ -83,6 +86,7 @@ export default function OverviewTopSources({
trpc.overview.topGenericSeries.queryOptions(
{
projectId,
shareId,
range,
filters,
column: widget.key,

View File

@@ -29,6 +29,7 @@ import { useOverviewOptions } from './useOverviewOptions';
interface OverviewUserJourneyProps {
projectId: string;
shareId?: string;
}
type PortalTooltipPosition = { left: number; top: number; ready: boolean };
@@ -159,6 +160,7 @@ function SankeyPortalTooltip({
export default function OverviewUserJourney({
projectId,
shareId,
}: OverviewUserJourneyProps) {
const { range, startDate, endDate } = useOverviewOptions();
const [filters] = useEventQueryFilters();
@@ -177,6 +179,7 @@ export default function OverviewUserJourney({
endDate,
range,
steps: steps ?? 5,
shareId,
}),
);