server side events and ui improvemnt

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-09 15:05:59 +01:00
parent 04453c673f
commit 484a6b1d41
73 changed files with 1095 additions and 650 deletions

View File

@@ -109,7 +109,7 @@ export function ListReports({ reports }: ListReportsProps) {
</Link>
<div
className={cn(
'p-4 pl-2',
'p-4',
report.chartType === 'bar' && 'overflow-auto max-h-[300px]'
)}
>

View File

@@ -5,13 +5,13 @@ import { Card, CardActions, CardActionsItem } from '@/components/Card';
import { FullPageEmptyState } from '@/components/FullPageEmptyState';
import { Button } from '@/components/ui/button';
import { ToastAction } from '@/components/ui/toast';
import { toast } from '@/components/ui/use-toast';
import { useAppParams } from '@/hooks/useAppParams';
import { pushModal } from '@/modals';
import type { IServiceDashboards } from '@/server/services/dashboard.service';
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
interface ListDashboardsProps {
dashboards: IServiceDashboards;

View File

@@ -39,7 +39,7 @@ function LinkWithIcon({
return (
<Link
className={cn(
'text-slate-600 text-sm font-medium flex gap-2 items-center px-3 py-2 transition-colors hover:bg-blue-100 leading-none rounded-md transition-all',
'text-slate-800 text-sm font-medium flex gap-2 items-center px-3 py-2 transition-colors hover:bg-blue-100 leading-none rounded-md transition-all',
active && 'bg-blue-50',
className
)}
@@ -134,13 +134,14 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) {
<div className="flex flex-col gap-2">
{dashboards.map((item) => (
<LinkWithIcon
className="py-1"
key={item.id}
icon={LayoutPanelTopIcon}
label={
<div className="flex flex-col gap-0.5">
<div className="flex justify-between gap-0.5 items-center">
<span>{item.name}</span>
<span className="text-xs">{item.project.name}</span>
<span className="text-xs text-muted-foreground">
{item.project.name}
</span>
</div>
}
href={`/${item.organization_slug}/${item.project_id}/dashboards/${item.id}`}

View File

@@ -1,8 +1,10 @@
'use client';
import { Combobox } from '@/components/ui/combobox';
import { useAppParams } from '@/hooks/useAppParams';
import type { IServiceOrganization } from '@/server/services/organization.service';
import { Building } from 'lucide-react';
import { useRouter } from 'next/navigation';
interface LayoutOrganizationSelectorProps {
organizations: IServiceOrganization[];
@@ -12,6 +14,7 @@ export default function LayoutOrganizationSelector({
organizations,
}: LayoutOrganizationSelectorProps) {
const params = useAppParams();
const router = useRouter();
const organization = organizations.find(
(item) => item.slug === params.organizationId
@@ -22,9 +25,22 @@ export default function LayoutOrganizationSelector({
}
return (
<div className="border border-border p-3 flex gap-2 rounded items-center">
<Building size={20} />
<span className="font-medium text-sm">{organization.name}</span>
</div>
<Combobox
className="w-full"
placeholder="Select organization"
icon={Building}
value={organization.slug}
items={
organizations
.filter((item) => item.slug)
.map((item) => ({
label: item.name,
value: item.slug!,
})) ?? []
}
onChange={(value) => {
router.push(`/${value}`);
}}
/>
);
}

View File

@@ -18,6 +18,7 @@ export default function LayoutProjectSelector({
return (
<div>
<Combobox
align="end"
className="w-auto min-w-0 max-sm:max-w-[100px]"
placeholder={'Select project'}
onChange={(value) => {

View File

@@ -0,0 +1,93 @@
'use client';
import { useEffect, useState } from 'react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useAppParams } from '@/hooks/useAppParams';
import { cn } from '@/utils/cn';
import { useQueryClient } from '@tanstack/react-query';
import AnimatedNumbers from 'react-animated-numbers';
import { toast } from 'sonner';
import { getSafeJson } from '@mixan/common';
import type { IServiceCreateEventPayload } from '@mixan/db';
interface LiveCounterProps {
initialCount: number;
}
export function LiveCounter({ initialCount = 0 }: LiveCounterProps) {
const client = useQueryClient();
const [counter, setCounter] = useState(initialCount);
const { projectId } = useAppParams();
const [es] = useState(
typeof window != 'undefined' &&
new EventSource(`http://localhost:3333/live/events/${projectId}`)
);
useEffect(() => {
if (!es) {
return () => {};
}
function handler(event: MessageEvent<string>) {
const parsed = getSafeJson<{
visitors: number;
event: IServiceCreateEventPayload | null;
}>(event.data);
if (parsed) {
setCounter(parsed.visitors);
if (parsed.event) {
client.refetchQueries({
type: 'active',
});
toast('New event', {
description: `${parsed.event.name}`,
duration: 2000,
});
}
}
}
es.addEventListener('message', handler);
return () => es.removeEventListener('message', handler);
}, []);
return (
<Tooltip>
<TooltipTrigger>
<div className="border border-border rounded h-8 px-3 leading-none flex items-center font-medium gap-2">
<div className="relative">
<div
className={cn(
'bg-emerald-500 h-3 w-3 rounded-full animate-ping opacity-100 transition-all',
counter === 0 && 'bg-destructive opacity-0'
)}
></div>
<div
className={cn(
'bg-emerald-500 h-3 w-3 rounded-full absolute top-0 left-0 transition-all',
counter === 0 && 'bg-destructive'
)}
></div>
</div>
<AnimatedNumbers
includeComma
transitions={(index) => ({
type: 'spring',
duration: index + 0.3,
})}
animateToNumber={counter}
locale="en"
/>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{counter} unique visitors last 5 minutes
</TooltipContent>
</Tooltip>
);
}

View File

@@ -1,5 +1,6 @@
'use client';
import { Suspense } from 'react';
import { OverviewFilters } from '@/components/overview/overview-filters';
import { OverviewFiltersButtons } from '@/components/overview/overview-filters-buttons';
import OverviewTopDevices from '@/components/overview/overview-top-devices';
@@ -10,6 +11,8 @@ import OverviewTopSources from '@/components/overview/overview-top-sources';
import { WidgetHead } from '@/components/overview/overview-widget';
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { Chart } from '@/components/report/chart';
import { ChartLoading } from '@/components/report/chart/ChartLoading';
import { MetricCardLoading } from '@/components/report/chart/MetricCard';
import { ReportRange } from '@/components/report/ReportRange';
import { Button } from '@/components/ui/button';
import {
@@ -27,6 +30,7 @@ import { Eye, FilterIcon, Globe2Icon, LockIcon, X } from 'lucide-react';
import Link from 'next/link';
import { StickyBelowHeader } from './layout-sticky-below-header';
import { LiveCounter } from './live-counter';
export default function OverviewMetrics() {
const { previous, range, setRange, interval, metric, setMetric, filters } =
@@ -200,6 +204,7 @@ export default function OverviewMetrics() {
onChange={(value) => setRange(value)}
/>
<div className="flex-wrap flex gap-2">
<LiveCounter initialCount={0} />
<OverviewFiltersButtons />
<SheetTrigger asChild>
<Button size="sm" variant="cta" icon={FilterIcon}>
@@ -240,8 +245,9 @@ export default function OverviewMetrics() {
setMetric(index);
}}
>
<Chart hideID {...report} />
<Suspense fallback={<MetricCardLoading />}>
<Chart hideID {...report} />
</Suspense>
{/* add active border */}
<div
className={cn(
@@ -256,14 +262,18 @@ export default function OverviewMetrics() {
<div className="title">{selectedMetric.events[0]?.displayName}</div>
</WidgetHead>
<WidgetBody>
<Chart hideID {...selectedMetric} chartType="linear" />
<Suspense fallback={<ChartLoading />}>
<Chart hideID {...selectedMetric} chartType="linear" />
</Suspense>
</WidgetBody>
</Widget>
<OverviewTopSources />
<OverviewTopPages />
<OverviewTopDevices />
<OverviewTopGeo />
<OverviewTopEvents />
<div className="col-span-6">
<OverviewTopGeo />
</div>
</div>
<SheetContent className="!max-w-lg w-full" side="left">

View File

@@ -1,8 +1,9 @@
'use client';
import { useEffect } from 'react';
import { Suspense, useEffect } from 'react';
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
import { Chart } from '@/components/report/chart';
import { ChartLoading } from '@/components/report/chart/ChartLoading';
import { ReportChartType } from '@/components/report/ReportChartType';
import { ReportInterval } from '@/components/report/ReportInterval';
import { ReportLineType } from '@/components/report/ReportLineType';
@@ -16,11 +17,9 @@ import {
} from '@/components/report/reportSlice';
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { useDispatch, useSelector } from '@/redux';
import type { IServiceReport } from '@/server/services/reports.service';
import { timeRanges } from '@/utils/constants';
import { GanttChartSquareIcon } from 'lucide-react';
interface ReportEditorProps {
@@ -73,7 +72,11 @@ export default function ReportEditor({
</div>
</StickyBelowHeader>
<div className="flex flex-col gap-4 p-4">
{report.ready && <Chart {...report} editMode />}
{report.ready && (
<Suspense fallback={<ChartLoading />}>
<Chart {...report} editMode />
</Suspense>
)}
</div>
<SheetContent className="!max-w-lg w-full" side="left">
<ReportSidebar />

View File

@@ -3,11 +3,11 @@
import { api, handleError } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import type { getOrganizationBySlug } from '@/server/services/organization.service';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const validator = z.object({

View File

@@ -1,13 +1,13 @@
import { api } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
import { useAppParams } from '@/hooks/useAppParams';
import { zInviteUser } from '@/utils/validation';
import { zodResolver } from '@hookform/resolvers/zod';
import { SendIcon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import type { z } from 'zod';
type IForm = z.infer<typeof zInviteUser>;

View File

@@ -3,12 +3,12 @@
import { api, handleError } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import type { getUserById } from '@/server/services/user.service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const validator = z.object({

View File

@@ -1,9 +1,9 @@
import type { Toast } from '@/components/ui/use-toast';
import { toast } from '@/components/ui/use-toast';
import type { AppRouter } from '@/server/api/root';
import type { TRPCClientErrorBase } from '@trpc/react-query';
import { createTRPCReact } from '@trpc/react-query';
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import { toast } from 'sonner';
export const api = createTRPCReact<AppRouter>({});

View File

@@ -4,7 +4,9 @@ import Providers from './providers';
import '@/styles/globals.css';
export const metadata = {};
export const metadata = {
title: 'Overview - Openpanel.dev',
};
export const viewport = {
width: 'device-width',