server side events and ui improvemnt
This commit is contained in:
@@ -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]'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>({});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user