add 30 min active user histogram

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-13 23:18:24 +01:00
parent 2572da3456
commit f32dc4711a
34 changed files with 570 additions and 457 deletions

View File

@@ -12,6 +12,8 @@ import dynamic from 'next/dynamic';
import useWebSocket from 'react-use-websocket';
import { toast } from 'sonner';
import { useOverviewOptions } from '../useOverviewOptions';
export interface LiveCounterProps {
data: number;
projectId: string;
@@ -25,6 +27,7 @@ const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), {
const FIFTEEN_SECONDS = 1000 * 15;
export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
const { setLiveHistogram } = useOverviewOptions();
const ws = String(process.env.NEXT_PUBLIC_API_URL)
.replace(/^https/, 'wss')
.replace(/^http/, 'ws');
@@ -52,8 +55,11 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
return (
<Tooltip>
<TooltipTrigger>
<div className="border border-border rounded h-8 px-3 leading-none flex items-center font-medium gap-2">
<TooltipTrigger asChild>
<button
onClick={() => setLiveHistogram((p) => !p)}
className="border border-border rounded h-8 px-3 leading-none flex items-center font-medium gap-2"
>
<div className="relative">
<div
className={cn(
@@ -80,10 +86,11 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
animateToNumber={counter}
locale="en"
/>
</div>
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{counter} unique visitors last 5 minutes
<p>{counter} unique visitors last 5 minutes</p>
<p>Click to see activity for the last 30 minutes</p>
</TooltipContent>
</Tooltip>
);

View File

@@ -1,5 +1,6 @@
'use client';
import { cn } from '@/utils/cn';
import { X } from 'lucide-react';
import { Button } from '../ui/button';
@@ -7,8 +8,10 @@ import { useOverviewOptions } from './useOverviewOptions';
export function OverviewFiltersButtons() {
const options = useOverviewOptions();
const activeFilter = options.filters.length > 0;
return (
<>
<div className={cn('flex flex-wrap gap-2', activeFilter && 'px-4 pb-4')}>
{options.referrer && (
<Button
size="sm"
@@ -196,6 +199,6 @@ export function OverviewFiltersButtons() {
<strong>{options.osVersion}</strong>
</Button>
)}
</>
</div>
);
}

View File

@@ -0,0 +1,68 @@
'use client';
import type { IChartInput } from '@/types';
import { cn } from '@/utils/cn';
import { ChevronsUpDownIcon } from 'lucide-react';
import AnimateHeight from 'react-animate-height';
import { Chart } from '../report/chart';
import { Widget, WidgetBody, WidgetHead } from '../Widget';
import { useOverviewOptions } from './useOverviewOptions';
interface OverviewLiveHistogramProps {
projectId: string;
}
export function OverviewLiveHistogram({
projectId,
}: OverviewLiveHistogramProps) {
const { liveHistogram, setLiveHistogram } = useOverviewOptions();
const report: IChartInput = {
projectId,
events: [
{
segment: 'user',
filters: [
{
id: '1',
name: 'name',
operator: 'is',
value: ['screen_view', 'session_start'],
},
],
id: 'A',
name: '*',
displayName: 'Active users',
},
],
chartType: 'histogram',
interval: 'minute',
range: '30min',
name: '',
metric: 'sum',
breakdowns: [],
lineType: 'monotone',
previous: true,
};
return (
<Widget>
<button onClick={() => setLiveHistogram((p) => !p)} className="w-full">
<WidgetHead
className={cn(
'flex justify-between items-center',
!liveHistogram && 'border-b-0'
)}
>
<div className="title">Active users last 30 minutes</div>
<ChevronsUpDownIcon size={16} />
</WidgetHead>
</button>
<AnimateHeight duration={500} height={liveHistogram ? 'auto' : 0}>
<WidgetBody>
<Chart {...report} />
</WidgetBody>
</AnimateHeight>
</Widget>
);
}

View File

@@ -1,8 +1,6 @@
'use client';
import { Suspense } from 'react';
import { Chart } from '@/components/report/chart';
import { ChartLoading } from '@/components/report/chart/ChartLoading';
import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget';
@@ -25,6 +23,7 @@ export default function OverviewTopDevices({
setBrowserVersion,
setOS,
setOSVersion,
setDevice,
} = useOverviewOptions();
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
devices: {
@@ -187,31 +186,32 @@ export default function OverviewTopDevices({
</WidgetButtons>
</WidgetHead>
<WidgetBody>
<Suspense fallback={<ChartLoading />}>
<Chart
hideID
{...widget.chart}
previous={false}
onClick={(item) => {
switch (widget.key) {
case 'browser':
setWidget('browser_version');
setBrowser(item.name);
break;
case 'browser_version':
setBrowserVersion(item.name);
break;
case 'os':
setWidget('os_version');
setOS(item.name);
break;
case 'os_version':
setOSVersion(item.name);
break;
}
}}
/>
</Suspense>
<Chart
hideID
{...widget.chart}
previous={false}
onClick={(item) => {
switch (widget.key) {
case 'devices':
setDevice(item.name);
break;
case 'browser':
setWidget('browser_version');
setBrowser(item.name);
break;
case 'browser_version':
setBrowserVersion(item.name);
break;
case 'os':
setWidget('os_version');
setOS(item.name);
break;
case 'os_version':
setOSVersion(item.name);
break;
}
}}
/>
</WidgetBody>
</Widget>
</>

View File

@@ -74,9 +74,7 @@ export default function OverviewTopEvents({
</WidgetButtons>
</WidgetHead>
<WidgetBody>
<Suspense fallback={<ChartLoading />}>
<Chart hideID {...widget.chart} previous={false} />
</Suspense>
<Chart hideID {...widget.chart} previous={false} />
</WidgetBody>
</Widget>
</>

View File

@@ -149,28 +149,26 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
</WidgetButtons>
</WidgetHead>
<WidgetBody>
<Suspense fallback={<ChartLoading />}>
<Chart
hideID
{...widget.chart}
previous={false}
onClick={(item) => {
switch (widget.key) {
case 'countries':
setWidget('regions');
setCountry(item.name);
break;
case 'regions':
setWidget('cities');
setRegion(item.name);
break;
case 'cities':
setCity(item.name);
break;
}
}}
/>
</Suspense>
<Chart
hideID
{...widget.chart}
previous={false}
onClick={(item) => {
switch (widget.key) {
case 'countries':
setWidget('regions');
setCountry(item.name);
break;
case 'regions':
setWidget('cities');
setRegion(item.name);
break;
case 'cities':
setCity(item.name);
break;
}
}}
/>
</WidgetBody>
</Widget>
</>

View File

@@ -120,16 +120,14 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
</WidgetButtons>
</WidgetHead>
<WidgetBody>
<Suspense fallback={<ChartLoading />}>
<Chart
hideID
{...widget.chart}
previous={false}
onClick={(item) => {
setPage(item.name);
}}
/>
</Suspense>
<Chart
hideID
{...widget.chart}
previous={false}
onClick={(item) => {
setPage(item.name);
}}
/>
</WidgetBody>
</Widget>
</>

View File

@@ -275,43 +275,41 @@ export default function OverviewTopSources({
</WidgetButtons>
</WidgetHead>
<WidgetBody>
<Suspense fallback={<ChartLoading />}>
<Chart
hideID
{...widget.chart}
previous={false}
onClick={(item) => {
switch (widget.key) {
case 'all':
setReferrerName(item.name);
setWidget('domain');
break;
case 'domain':
setReferrer(item.name);
break;
case 'type':
setReferrerType(item.name);
setWidget('domain');
break;
case 'utm_source':
setUtmSource(item.name);
break;
case 'utm_medium':
setUtmMedium(item.name);
break;
case 'utm_campaign':
setUtmCampaign(item.name);
break;
case 'utm_term':
setUtmTerm(item.name);
break;
case 'utm_content':
setUtmContent(item.name);
break;
}
}}
/>
</Suspense>
<Chart
hideID
{...widget.chart}
previous={false}
onClick={(item) => {
switch (widget.key) {
case 'all':
setReferrerName(item.name);
setWidget('domain');
break;
case 'domain':
setReferrer(item.name);
break;
case 'type':
setReferrerType(item.name);
setWidget('domain');
break;
case 'utm_source':
setUtmSource(item.name);
break;
case 'utm_medium':
setUtmMedium(item.name);
break;
case 'utm_campaign':
setUtmCampaign(item.name);
break;
case 'utm_term':
setUtmTerm(item.name);
break;
case 'utm_content':
setUtmContent(item.name);
break;
}
}}
/>
</WidgetBody>
</Widget>
</>

View File

@@ -1,9 +1,8 @@
'use client';
import { Children, useCallback, useEffect, useRef, useState } from 'react';
import { Children, useEffect, useRef, useState } from 'react';
import { useThrottle } from '@/hooks/useThrottle';
import { cn } from '@/utils/cn';
import throttle from 'lodash.throttle';
import { ChevronsUpDownIcon } from 'lucide-react';
import { last } from 'ramda';

View File

@@ -107,6 +107,12 @@ export function useOverviewOptions() {
parseAsString.withOptions(nuqsOptions)
);
// Toggles
const [liveHistogram, setLiveHistogram] = useQueryState(
'live',
parseAsBoolean.withDefault(false).withOptions(nuqsOptions)
);
const filters = useMemo(() => {
const filters: IChartInput['events'][number]['filters'] = [];
@@ -337,5 +343,9 @@ export function useOverviewOptions() {
setOS,
osVersion,
setOSVersion,
// Toggles
liveHistogram,
setLiveHistogram,
};
}