add 30 min active user histogram
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
68
apps/web/src/components/overview/overview-live-histogram.tsx
Normal file
68
apps/web/src/components/overview/overview-live-histogram.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user