import { ProjectLink } from '@/components/links'; import { ProfileAvatar } from '@/components/profiles/profile-avatar'; import { SerieIcon } from '@/components/report-chart/common/serie-icon'; import { DropdownMenuShortcut } from '@/components/ui/dropdown-menu'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { useTRPC } from '@/integrations/trpc/react'; import type { IChartData } from '@/trpc/client'; import { cn } from '@/utils/cn'; import { getProfileName } from '@/utils/getters'; import type { IChartInput } from '@openpanel/validation'; import { useQuery } from '@tanstack/react-query'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useEffect, useMemo, useState } from 'react'; import { popModal } from '.'; import { ModalHeader } from './Modal/Container'; import { ScrollableModal, useScrollableModal } from './Modal/scrollable-modal'; const ProfileItem = ({ profile }: { profile: any }) => { console.log('ProfileItem', profile.id); return ( { if (e.metaKey || e.ctrlKey || e.shiftKey) { return; } popModal(); }} >
{getProfileName(profile)}
{profile.properties.country && (
{profile.properties.country} {profile.properties.city && ` / ${profile.properties.city}`}
)} {profile.properties.os && (
{profile.properties.os}
)} {profile.properties.browser && (
{profile.properties.browser}
)}
); }; // Shared profile list component function ProfileList({ profiles }: { profiles: any[] }) { const ITEM_HEIGHT = 74; const CONTAINER_PADDING = 20; const ITEM_GAP = 5; const { scrollAreaRef } = useScrollableModal(); const [isScrollReady, setIsScrollReady] = useState(false); // Check if scroll container is ready useEffect(() => { if (scrollAreaRef.current) { setIsScrollReady(true); } else { setIsScrollReady(false); } }, [scrollAreaRef]); const virtualizer = useVirtualizer({ count: profiles.length, getScrollElement: () => scrollAreaRef.current, estimateSize: () => ITEM_HEIGHT + ITEM_GAP, overscan: 5, paddingStart: CONTAINER_PADDING, paddingEnd: CONTAINER_PADDING, }); // Re-measure when scroll container becomes available or profiles change useEffect(() => { if (isScrollReady && scrollAreaRef.current) { // Small delay to ensure DOM is ready const timeoutId = setTimeout(() => { virtualizer.measure(); }, 0); return () => clearTimeout(timeoutId); } }, [isScrollReady, profiles.length, virtualizer]); if (profiles.length === 0) { return (
No users found
); } const virtualItems = virtualizer.getVirtualItems(); return (
{/* Only the visible items in the virtualizer, manually positioned to be in view */} {virtualItems.map((virtualItem) => { const profile = profiles[virtualItem.index]; return (
); })}
); } // Chart-specific props and component interface ChartUsersViewProps { chartData: IChartData; report: IChartInput; date: string; } function ChartUsersView({ chartData, report, date }: ChartUsersViewProps) { const trpc = useTRPC(); const [selectedSerieId, setSelectedSerieId] = useState( report.series[0]?.id || null, ); const [selectedBreakdownId, setSelectedBreakdownId] = useState( null, ); const selectedReportSerie = useMemo( () => report.series.find((s) => s.id === selectedSerieId), [report.series, selectedSerieId], ); // Get all chart series that match the selected report serie const matchingChartSeries = useMemo(() => { if (!selectedSerieId || !chartData) return []; return chartData.series.filter((s) => s.event.id === selectedSerieId); }, [chartData?.series, selectedSerieId]); const selectedBreakdown = useMemo(() => { if (!selectedBreakdownId) return null; return matchingChartSeries.find((s) => s.id === selectedBreakdownId); }, [matchingChartSeries, selectedBreakdownId]); // Reset breakdown selection when serie changes const handleSerieChange = (value: string) => { setSelectedSerieId(value); setSelectedBreakdownId(null); }; const profilesQuery = useQuery( trpc.chart.getProfiles.queryOptions( { projectId: report.projectId, date: date, series: selectedReportSerie && selectedReportSerie.type === 'event' ? [selectedReportSerie] : [], breakdowns: selectedBreakdown?.event.breakdowns, interval: report.interval, }, { enabled: !!selectedReportSerie && selectedReportSerie.type === 'event', }, ), ); const profiles = profilesQuery.data ?? []; return ( {report.series.length > 0 && (
{matchingChartSeries.length > 1 && ( )}
)} } >
{profilesQuery.isLoading ? (
Loading users...
) : ( )}
); } // Funnel-specific props and component interface FunnelUsersViewProps { report: IChartInput; stepIndex: number; } function FunnelUsersView({ report, stepIndex }: FunnelUsersViewProps) { const trpc = useTRPC(); const [showDropoffs, setShowDropoffs] = useState(false); const profilesQuery = useQuery( trpc.chart.getFunnelProfiles.queryOptions( { projectId: report.projectId, startDate: report.startDate!, endDate: report.endDate!, range: report.range, series: report.series, stepIndex: stepIndex, showDropoffs: showDropoffs, funnelWindow: report.funnelWindow, funnelGroup: report.funnelGroup, breakdowns: report.breakdowns, }, { enabled: stepIndex !== undefined, }, ), ); const profiles = profilesQuery.data ?? []; const isLastStep = stepIndex === report.series.length - 1; return ( {!isLastStep && (
)} } >
{profilesQuery.isLoading ? (
Loading users...
) : ( )}
); } // Union type for props type ViewChartUsersProps = | { type: 'chart'; chartData: IChartData; report: IChartInput; date: string; } | { type: 'funnel'; report: IChartInput; stepIndex: number; }; // Main component that routes to the appropriate view export default function ViewChartUsers(props: ViewChartUsersProps) { if (props.type === 'funnel') { return ( ); } return ( ); }