update dashboard metrics and move away from round corners

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-05-20 21:22:20 +02:00
parent 4350670bbc
commit 3ecdf54d5c
13 changed files with 125 additions and 137 deletions

View File

@@ -12,7 +12,7 @@ export function StickyBelowHeader({
return ( return (
<div <div
className={cn( className={cn(
'top-0 z-20 rounded-lg border-b border-border bg-background md:sticky [[id=dashboard]_&]:top-16 [[id=dashboard]_&]:rounded-none', 'top-0 z-20 border-b border-border bg-background md:sticky [[id=dashboard]_&]:top-16 [[id=dashboard]_&]:rounded-none',
className className
)} )}
> >

View File

@@ -41,9 +41,6 @@ export default function Page({
<OverviewFiltersButtons /> <OverviewFiltersButtons />
</StickyBelowHeader> </StickyBelowHeader>
<div className="grid grid-cols-6 gap-4 p-4"> <div className="grid grid-cols-6 gap-4 p-4">
<div className="col-span-6">
<OverviewLiveHistogram projectId={projectId} />
</div>
<OverviewMetrics projectId={projectId} /> <OverviewMetrics projectId={projectId} />
<OverviewTopSources projectId={projectId} /> <OverviewTopSources projectId={projectId} />
<OverviewTopPages projectId={projectId} /> <OverviewTopPages projectId={projectId} />

View File

@@ -14,6 +14,7 @@ import {
eachDayOfInterval, eachDayOfInterval,
endOfMonth, endOfMonth,
format, format,
formatISO,
startOfMonth, startOfMonth,
subMonths, subMonths,
} from 'date-fns'; } from 'date-fns';
@@ -60,7 +61,9 @@ const ProfileActivity = ({ data }: Props) => {
end: endOfMonth(subMonths(startDate, 1)), end: endOfMonth(subMonths(startDate, 1)),
}).map((date) => { }).map((date) => {
const hit = data.find((item) => const hit = data.find((item) =>
item.date.includes(date.toISOString().split('T')[0]) item.date.includes(
formatISO(date, { representation: 'date' })
)
); );
return ( return (
<div <div
@@ -82,7 +85,9 @@ const ProfileActivity = ({ data }: Props) => {
end: endDate, end: endDate,
}).map((date) => { }).map((date) => {
const hit = data.find((item) => const hit = data.find((item) =>
item.date.includes(date.toISOString().split('T')[0]) item.date.includes(
formatISO(date, { representation: 'date' })
)
); );
return ( return (
<div <div

View File

@@ -18,9 +18,15 @@ interface PageProps {
params: { params: {
id: string; id: string;
}; };
searchParams: {
header: string;
};
} }
export default async function Page({ params: { id } }: PageProps) { export default async function Page({
params: { id },
searchParams,
}: PageProps) {
const share = await getShareOverviewById(id); const share = await getShareOverviewById(id);
if (!share) { if (!share) {
return notFound(); return notFound();
@@ -32,43 +38,42 @@ export default async function Page({ params: { id } }: PageProps) {
const organization = await getOrganizationBySlug(share.organizationSlug); const organization = await getOrganizationBySlug(share.organizationSlug);
return ( return (
<div className="bg-gradient-to-tl from-blue-950 to-blue-600 p-4 md:p-16"> <div>
<div className="mx-auto max-w-6xl"> {searchParams.header !== '0' && (
<div className="mb-4 flex items-end justify-between"> <div className="flex items-center justify-between border-b border-border bg-white p-4">
<div className="leading-none"> <div className="leading-none">
<span className="mb-4 text-white">{organization?.name}</span> <span className="mb-4">{organization?.name}</span>
<h1 className="text-xl font-medium text-white"> <h1 className="text-xl font-medium">{share.project?.name}</h1>
{share.project?.name}
</h1>
</div> </div>
<a href="https://openpanel.dev?utm_source=openpanel.dev&utm_medium=share"> <a
<Logo className="text-white max-sm:[&_span]:hidden" /> href="https://openpanel.dev?utm_source=openpanel.dev&utm_medium=share"
className="flex flex-col items-end text-lg font-medium"
>
<span className="text-xs">POWERED BY</span>
<span>openpanel.dev</span>
</a> </a>
</div> </div>
<div className="rounded-lg bg-slate-100 shadow ring-8 ring-blue-600/50 max-sm:-mx-3"> )}
<StickyBelowHeader> <div className="">
<div className="flex justify-between gap-2 p-4"> <StickyBelowHeader>
<div className="flex gap-2"> <div className="flex justify-between gap-2 p-4">
<OverviewReportRange /> <div className="flex gap-2">
{/* <OverviewFiltersDrawer projectId={projectId} mode="events" /> */} <OverviewReportRange />
</div> {/* <OverviewFiltersDrawer projectId={projectId} mode="events" /> */}
<div className="flex gap-2">
<ServerLiveCounter projectId={projectId} />
</div>
</div> </div>
<OverviewFiltersButtons /> <div className="flex gap-2">
</StickyBelowHeader> <ServerLiveCounter projectId={projectId} />
<div className="grid grid-cols-6 gap-4 p-4">
<div className="col-span-6">
<OverviewLiveHistogram projectId={projectId} />
</div> </div>
<OverviewMetrics projectId={projectId} />
<OverviewTopSources projectId={projectId} />
<OverviewTopPages projectId={projectId} />
<OverviewTopDevices projectId={projectId} />
<OverviewTopEvents projectId={projectId} />
<OverviewTopGeo projectId={projectId} />
</div> </div>
<OverviewFiltersButtons />
</StickyBelowHeader>
<div className="mx-auto grid max-w-7xl grid-cols-6 gap-4 p-4">
<OverviewMetrics projectId={projectId} />
<OverviewTopSources projectId={projectId} />
<OverviewTopPages projectId={projectId} />
<OverviewTopDevices projectId={projectId} />
<OverviewTopEvents projectId={projectId} />
<OverviewTopGeo projectId={projectId} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -12,8 +12,6 @@ import { useQueryClient } from '@tanstack/react-query';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useOverviewOptions } from '../useOverviewOptions';
export interface LiveCounterProps { export interface LiveCounterProps {
data: number; data: number;
projectId: string; projectId: string;
@@ -27,7 +25,6 @@ const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), {
const FIFTEEN_SECONDS = 1000 * 15; const FIFTEEN_SECONDS = 1000 * 15;
export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) { export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
const { setLiveHistogram } = useOverviewOptions();
const client = useQueryClient(); const client = useQueryClient();
const [counter, setCounter] = useState(data); const [counter, setCounter] = useState(data);
const lastRefresh = useRef(Date.now()); const lastRefresh = useRef(Date.now());
@@ -47,38 +44,33 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger className="flex h-8 items-center gap-2 rounded border border-border px-3 font-medium leading-none">
<button <div className="relative">
onClick={() => setLiveHistogram((p) => !p)} <div
className="flex h-8 items-center gap-2 rounded border border-border px-3 font-medium leading-none" className={cn(
> 'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
<div className="relative"> counter === 0 && 'bg-destructive opacity-0'
<div )}
className={cn(
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
counter === 0 && 'bg-destructive opacity-0'
)}
></div>
<div
className={cn(
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
counter === 0 && 'bg-destructive'
)}
></div>
</div>
<AnimatedNumbers
includeComma
transitions={(index) => ({
type: 'spring',
duration: index + 0.3,
damping: 10,
stiffness: 200,
})}
animateToNumber={counter}
locale="en"
/> />
</button> <div
className={cn(
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
counter === 0 && 'bg-destructive'
)}
/>
</div>
<AnimatedNumbers
includeComma
transitions={(index) => ({
type: 'spring',
duration: index + 0.3,
damping: 10,
stiffness: 200,
})}
animateToNumber={counter}
locale="en"
/>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
<p>{counter} unique visitors last 5 minutes</p> <p>{counter} unique visitors last 5 minutes</p>

View File

@@ -5,9 +5,7 @@ import { cn } from '@/utils/cn';
import type { IChartInput } from '@openpanel/validation'; import type { IChartInput } from '@openpanel/validation';
import AnimateHeight from '../animate-height';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import { useOverviewOptions } from './useOverviewOptions';
interface OverviewLiveHistogramProps { interface OverviewLiveHistogramProps {
projectId: string; projectId: string;
@@ -16,7 +14,6 @@ interface OverviewLiveHistogramProps {
export function OverviewLiveHistogram({ export function OverviewLiveHistogram({
projectId, projectId,
}: OverviewLiveHistogramProps) { }: OverviewLiveHistogramProps) {
const { liveHistogram } = useOverviewOptions();
const report: IChartInput = { const report: IChartInput = {
projectId, projectId,
events: [ events: [
@@ -80,11 +77,11 @@ export function OverviewLiveHistogram({
]; ];
return ( return (
<Wrapper count={0} open={liveHistogram}> <Wrapper count={0}>
{staticArray.map((percent, i) => ( {staticArray.map((percent, i) => (
<div <div
key={i} key={i}
className="flex-1 animate-pulse rounded-md bg-slate-200 dark:bg-slate-800" className="flex-1 animate-pulse rounded bg-slate-200 dark:bg-slate-800"
style={{ height: `${percent}%` }} style={{ height: `${percent}%` }}
/> />
))} ))}
@@ -97,14 +94,14 @@ export function OverviewLiveHistogram({
} }
return ( return (
<Wrapper open={liveHistogram} count={liveCount}> <Wrapper count={liveCount}>
{minutes.map((minute) => { {minutes.map((minute) => {
return ( return (
<Tooltip key={minute.date}> <Tooltip key={minute.date}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div <div
className={cn( className={cn(
'flex-1 rounded-md transition-all ease-in-out hover:scale-110', 'flex-1 rounded transition-all ease-in-out hover:scale-110',
minute.count === 0 ? 'bg-slate-200' : 'bg-blue-600' minute.count === 0 ? 'bg-slate-200' : 'bg-blue-600'
)} )}
style={{ style={{
@@ -127,22 +124,19 @@ export function OverviewLiveHistogram({
} }
interface WrapperProps { interface WrapperProps {
open: boolean;
children: React.ReactNode; children: React.ReactNode;
count: number; count: number;
} }
function Wrapper({ open, children, count }: WrapperProps) { function Wrapper({ children, count }: WrapperProps) {
return ( return (
<AnimateHeight open={open}> <div className="flex h-full flex-col">
<div className="flex flex-col"> <div className="relative mb-1 text-xs font-medium text-muted-foreground">
<div className="relative -top-2 text-center text-xs font-medium text-muted-foreground"> {count} unique vistors last 30 minutes
{count} unique vistors last 30 minutes
</div>
<div className="relative flex aspect-[6/1] max-h-[150px] w-full flex-1 items-end gap-0.5 md:aspect-[10/1] md:gap-2">
{children}
</div>
</div> </div>
</AnimateHeight> <div className="relative flex h-full w-full flex-1 items-end gap-1">
{children}
</div>
</div>
); );
} }

View File

@@ -1,14 +1,14 @@
'use client'; 'use client';
import { WidgetHead } from '@/components/overview/overview-widget';
import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { ChartSwitch } from '@/components/report/chart'; import { ChartSwitch } from '@/components/report/chart';
import { Widget, WidgetBody } from '@/components/widget';
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import type { IChartInput } from '@openpanel/validation'; import type { IChartInput } from '@openpanel/validation';
import { OverviewLiveHistogram } from './overview-live-histogram';
interface OverviewMetricsProps { interface OverviewMetricsProps {
projectId: string; projectId: string;
} }
@@ -191,30 +191,40 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
const selectedMetric = reports[metric]!; const selectedMetric = reports[metric]!;
return ( return (
<div className="card col-span-6 p-4"> <>
<div className="-mx-2 -mt-2 mb-2 grid grid-cols-6 gap-2"> <div className="relative -top-0.5 col-span-6 -m-4 mb-0 md:m-0">
{reports.map((report, index) => ( <div className="card mb-2 grid grid-cols-4">
<button {reports.map((report, index) => (
key={index} <button
key={index}
className={cn(
'col-span-2 flex-1 p-4 shadow-[0_0_0_0.5px] shadow-border md:col-span-1',
index === metric && 'bg-slate-50'
)}
onClick={() => {
setMetric(index);
}}
>
<ChartSwitch hideID {...report} />
</button>
))}
<div
className={cn( className={cn(
'col-span-3 rounded-lg border border-background p-2 transition-all max-md:flex-1 md:col-span-2 lg:col-span-1 [&_[role="heading"]]:text-xs', 'col-span-4 min-h-28 flex-1 p-4 shadow-[0_0_0_0.5px] shadow-border max-md:row-start-1 md:col-span-2'
index === metric && 'border-border'
)} )}
onClick={() => {
setMetric(index);
}}
> >
<ChartSwitch hideID {...report} /> <OverviewLiveHistogram projectId={projectId} />
{/* add active border */} </div>
</button> </div>
))} <div className="card col-span-6 p-4">
<ChartSwitch
key={selectedMetric.id}
hideID
{...selectedMetric}
chartType="linear"
/>
</div>
</div> </div>
<ChartSwitch </>
key={selectedMetric.id}
hideID
{...selectedMetric}
chartType="linear"
/>
</div>
); );
} }

View File

@@ -46,12 +46,6 @@ export function useOverviewOptions() {
parseAsInteger.withDefault(0).withOptions(nuqsOptions) parseAsInteger.withDefault(0).withOptions(nuqsOptions)
); );
// Toggles
const [liveHistogram, setLiveHistogram] = useQueryState(
'live',
parseAsBoolean.withDefault(true).withOptions(nuqsOptions)
);
return { return {
previous, previous,
setPrevious, setPrevious,
@@ -72,9 +66,5 @@ export function useOverviewOptions() {
// Computed // Computed
interval, interval,
// Toggles
liveHistogram,
setLiveHistogram,
}; };
} }

View File

@@ -71,9 +71,9 @@ export function MetricCard({
{({ width, height }) => ( {({ width, height }) => (
<AreaChart <AreaChart
width={width} width={width}
height={height / 2.5} height={height / 3}
data={serie.data} data={serie.data}
style={{ marginTop: (height / 2.5) * 1.5 }} style={{ marginTop: (height / 3) * 2 }}
> >
<Area <Area
dataKey="count" dataKey="count"
@@ -89,12 +89,9 @@ export function MetricCard({
</div> </div>
<div className="relative"> <div className="relative">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2 text-left font-medium"> <div className="flex min-w-0 items-center gap-2 text-left font-semibold">
<ColorSquare>{serie.event.id}</ColorSquare> <ColorSquare>{serie.event.id}</ColorSquare>
<span <span className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
role="heading"
className="overflow-hidden text-ellipsis whitespace-nowrap"
>
{serie.name || serie.event.displayName || serie.event.name} {serie.name || serie.event.displayName || serie.event.name}
</span> </span>
</div> </div>

View File

@@ -25,7 +25,7 @@ function BarHover({ x, y, width, height, top, left, right, bottom }: any) {
return ( return (
<rect <rect
{...{ x, y, width, height, top, left, right, bottom }} {...{ x, y, width, height, top, left, right, bottom }}
rx="8" rx="3"
fill={bg} fill={bg}
fillOpacity={0.5} fillOpacity={0.5}
/> />
@@ -79,7 +79,7 @@ export function ReportHistogramChart({
dataKey={`${serie.id}:prev:count`} dataKey={`${serie.id}:prev:count`}
fill={getChartColor(serie.index)} fill={getChartColor(serie.index)}
fillOpacity={0.2} fillOpacity={0.2}
radius={8} radius={3}
/> />
)} )}
<Bar <Bar
@@ -87,7 +87,7 @@ export function ReportHistogramChart({
name={serie.name} name={serie.name}
dataKey={`${serie.id}:count`} dataKey={`${serie.id}:count`}
fill={getChartColor(serie.index)} fill={getChartColor(serie.index)}
radius={8} radius={3}
/> />
</React.Fragment> </React.Fragment>
); );

View File

@@ -144,9 +144,7 @@
} }
.card { .card {
box-shadow: 0 1px 2px 0.5px rgba(0, 0, 0, 0.08); @apply border border-border bg-background;
border: 0 !important;
@apply rounded-xl bg-background;
} }
} }

View File

@@ -60,7 +60,7 @@ export async function Hero() {
<div className="mt-16 w-full md:pt-16"> <div className="mt-16 w-full md:pt-16">
<div className="flex h-[max(90vh,650px)] rounded-2xl bg-black/5 md:p-2"> <div className="flex h-[max(90vh,650px)] rounded-2xl bg-black/5 md:p-2">
<iframe <iframe
src="https://dashboard.openpanel.dev/share/overview/ZQsEhG" src="https://dashboard.openpanel.dev/share/overview/ZQsEhG?header=0"
className="h-[max(90vh,650px)] w-full rounded-xl" className="h-[max(90vh,650px)] w-full rounded-xl"
title="Openpanel Dashboard" title="Openpanel Dashboard"
scrolling="no" scrolling="no"

View File

@@ -26,7 +26,7 @@ export default function Page() {
Analytics 😉 Curious how it looks? Analytics 😉 Curious how it looks?
</Lead2> </Lead2>
<ALink <ALink
href="https://dashboard.openpanel.dev/share/overview/ZQsEhG" href="https://dashboard.openpanel.dev/share/overview/ZQsEhG?header=0"
target="_blank" target="_blank"
className="mt-8" className="mt-8"
variant={'outline'} variant={'outline'}