feat(v2): monitor feature

This commit is contained in:
moonrailgun 2024-03-24 04:18:45 +08:00
parent 958b1c0932
commit 402b8a6955
12 changed files with 267 additions and 38 deletions

View File

@ -1,10 +1,10 @@
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '../trpc'; import { defaultErrorHandler, defaultSuccessHandler, trpc } from '../trpc';
export function useMonitorUpsert() { export function useMonitorUpsert() {
const context = trpc.useContext(); const utils = trpc.useUtils();
const mutation = trpc.monitor.upsert.useMutation({ const mutation = trpc.monitor.upsert.useMutation({
onSuccess: (data) => { onSuccess: (data) => {
context.monitor.all.reset({ utils.monitor.all.refetch({
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
}); });

View File

@ -10,7 +10,7 @@ import { useFuseSearch } from '@/hooks/useFuseSearch';
export interface CommonListItem { export interface CommonListItem {
id: string; id: string;
title: string; title: string;
content: React.ReactNode; content?: React.ReactNode;
tags: string[]; tags: string[];
href: string; href: string;
} }
@ -46,12 +46,12 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
const finalList = searchResult ?? props.items; const finalList = searchResult ?? props.items;
return ( return (
<div className="flex flex-col h-full"> <div className="flex h-full flex-col">
{props.hasSearch && ( {props.hasSearch && (
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <div className="bg-background/95 supports-[backdrop-filter]:bg-background/60 p-4 backdrop-blur">
<form> <form>
<div className="relative"> <div className="relative">
<LuSearch className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <LuSearch className="text-muted-foreground absolute left-2 top-2.5 h-4 w-4" />
<Input <Input
placeholder="Search" placeholder="Search"
className="pl-8" className="pl-8"
@ -72,7 +72,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
<button <button
key={item.id} key={item.id}
className={cn( className={cn(
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent', 'hover:bg-accent flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all',
isSelected && 'bg-muted' isSelected && 'bg-muted'
)} )}
onClick={() => onClick={() =>
@ -88,7 +88,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
</div> </div>
</div> </div>
</div> </div>
<div className="line-clamp-2 text-xs text-muted-foreground"> <div className="text-muted-foreground line-clamp-2 text-xs">
{item.content} {item.content}
</div> </div>
{item.tags.length > 0 ? ( {item.tags.length > 0 ? (

View File

@ -66,6 +66,11 @@ a {
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* use for adapt new design, avoid preflight conflict */
.ant-btn-primary {
background-color: #1677ff;
}
/* https://ui.shadcn.com/themes */ /* https://ui.shadcn.com/themes */
@layer base { @layer base {
:root { :root {

View File

@ -5,6 +5,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { Link, useRouterState } from '@tanstack/react-router';
import React from 'react'; import React from 'react';
import { IconType } from 'react-icons'; import { IconType } from 'react-icons';
@ -14,50 +15,59 @@ interface NavProps {
title: string; title: string;
label?: string; label?: string;
icon: IconType; icon: IconType;
variant: 'default' | 'ghost'; to: string;
}[]; }[];
} }
export const Nav: React.FC<NavProps> = React.memo(({ links, isCollapsed }) => { export const Nav: React.FC<NavProps> = React.memo(({ links, isCollapsed }) => {
const pathname = useRouterState({
select: (state) => state.location.pathname,
});
return ( return (
<div <div
data-collapsed={isCollapsed} data-collapsed={isCollapsed}
className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2" className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2"
> >
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2"> <nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
{links.map((link, index) => {links.map((link, index) => {
isCollapsed ? ( const isSelect = pathname.startsWith(link.to);
const variant = isSelect ? 'default' : 'ghost';
return isCollapsed ? (
<Tooltip key={index} delayDuration={0}> <Tooltip key={index} delayDuration={0}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div <Link
className={cn( className={cn(
buttonVariants({ variant: link.variant, size: 'icon' }), buttonVariants({ variant: variant, size: 'icon' }),
'h-9 w-9 cursor-pointer', 'h-9 w-9 cursor-pointer',
link.variant === 'default' && variant === 'default' &&
'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white' 'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
)} )}
to={link.to}
> >
<link.icon className="h-4 w-4" /> <link.icon className="h-4 w-4" />
<span className="sr-only">{link.title}</span> <span className="sr-only">{link.title}</span>
</div> </Link>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-4"> <TooltipContent side="right" className="flex items-center gap-4">
{link.title} {link.title}
{link.label && ( {link.label && (
<span className="ml-auto text-muted-foreground"> <span className="text-muted-foreground ml-auto">
{link.label} {link.label}
</span> </span>
)} )}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
<div <Link
key={index} key={index}
to={link.to}
className={cn( className={cn(
buttonVariants({ variant: link.variant, size: 'sm' }), buttonVariants({ variant: variant, size: 'sm' }),
link.variant === 'default' && variant === 'default' &&
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white', 'dark:bg-muted dark:hover:bg-muted dark:text-white dark:hover:text-white',
'justify-start cursor-pointer' 'cursor-pointer justify-start'
)} )}
> >
<link.icon className="mr-2 h-4 w-4" /> <link.icon className="mr-2 h-4 w-4" />
@ -66,16 +76,15 @@ export const Nav: React.FC<NavProps> = React.memo(({ links, isCollapsed }) => {
<span <span
className={cn( className={cn(
'ml-auto', 'ml-auto',
link.variant === 'default' && variant === 'default' && 'text-background dark:text-white'
'text-background dark:text-white'
)} )}
> >
{link.label} {link.label}
</span> </span>
)} )}
</div> </Link>
) );
)} })}
</nav> </nav>
</div> </div>
); );

View File

@ -85,31 +85,31 @@ export const LayoutV2: React.FC<{
title: 'Website', title: 'Website',
label: String(serviceCount?.website ?? ''), label: String(serviceCount?.website ?? ''),
icon: LuAreaChart, icon: LuAreaChart,
variant: 'default', to: '/website',
}, },
{ {
title: 'Monitor', title: 'Monitor',
label: String(serviceCount?.monitor ?? ''), label: String(serviceCount?.monitor ?? ''),
icon: LuMonitorDot, icon: LuMonitorDot,
variant: 'ghost', to: '/monitor',
}, },
{ {
title: 'Servers', title: 'Servers',
label: '', label: '',
icon: LuServer, icon: LuServer,
variant: 'ghost', to: '/server',
}, },
{ {
title: 'Telemetry', title: 'Telemetry',
label: String(serviceCount?.telemetry ?? ''), label: String(serviceCount?.telemetry ?? ''),
icon: LuWifi, icon: LuWifi,
variant: 'ghost', to: '/telemetry',
}, },
{ {
title: 'Pages', title: 'Pages',
label: String(serviceCount?.page ?? ''), label: String(serviceCount?.page ?? ''),
icon: LuFilePieChart, icon: LuFilePieChart,
variant: 'ghost', to: '/page',
}, },
]} ]}
/> />

View File

@ -13,10 +13,13 @@
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as WebsiteImport } from './routes/website' import { Route as WebsiteImport } from './routes/website'
import { Route as RegisterImport } from './routes/register' import { Route as RegisterImport } from './routes/register'
import { Route as MonitorImport } from './routes/monitor'
import { Route as LoginImport } from './routes/login' import { Route as LoginImport } from './routes/login'
import { Route as IndexImport } from './routes/index' import { Route as IndexImport } from './routes/index'
import { Route as WebsiteAddImport } from './routes/website/add' import { Route as WebsiteAddImport } from './routes/website/add'
import { Route as WebsiteWebsiteIdImport } from './routes/website/$websiteId' import { Route as WebsiteWebsiteIdImport } from './routes/website/$websiteId'
import { Route as MonitorAddImport } from './routes/monitor/add'
import { Route as MonitorMonitorIdImport } from './routes/monitor/$monitorId'
// Create/Update Routes // Create/Update Routes
@ -30,6 +33,11 @@ const RegisterRoute = RegisterImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const MonitorRoute = MonitorImport.update({
path: '/monitor',
getParentRoute: () => rootRoute,
} as any)
const LoginRoute = LoginImport.update({ const LoginRoute = LoginImport.update({
path: '/login', path: '/login',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
@ -50,6 +58,16 @@ const WebsiteWebsiteIdRoute = WebsiteWebsiteIdImport.update({
getParentRoute: () => WebsiteRoute, getParentRoute: () => WebsiteRoute,
} as any) } as any)
const MonitorAddRoute = MonitorAddImport.update({
path: '/add',
getParentRoute: () => MonitorRoute,
} as any)
const MonitorMonitorIdRoute = MonitorMonitorIdImport.update({
path: '/$monitorId',
getParentRoute: () => MonitorRoute,
} as any)
// Populate the FileRoutesByPath interface // Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@ -62,6 +80,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginImport preLoaderRoute: typeof LoginImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/monitor': {
preLoaderRoute: typeof MonitorImport
parentRoute: typeof rootRoute
}
'/register': { '/register': {
preLoaderRoute: typeof RegisterImport preLoaderRoute: typeof RegisterImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
@ -70,6 +92,14 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof WebsiteImport preLoaderRoute: typeof WebsiteImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/monitor/$monitorId': {
preLoaderRoute: typeof MonitorMonitorIdImport
parentRoute: typeof MonitorImport
}
'/monitor/add': {
preLoaderRoute: typeof MonitorAddImport
parentRoute: typeof MonitorImport
}
'/website/$websiteId': { '/website/$websiteId': {
preLoaderRoute: typeof WebsiteWebsiteIdImport preLoaderRoute: typeof WebsiteWebsiteIdImport
parentRoute: typeof WebsiteImport parentRoute: typeof WebsiteImport
@ -86,6 +116,7 @@ declare module '@tanstack/react-router' {
export const routeTree = rootRoute.addChildren([ export const routeTree = rootRoute.addChildren([
IndexRoute, IndexRoute,
LoginRoute, LoginRoute,
MonitorRoute.addChildren([MonitorMonitorIdRoute, MonitorAddRoute]),
RegisterRoute, RegisterRoute,
WebsiteRoute.addChildren([WebsiteWebsiteIdRoute, WebsiteAddRoute]), WebsiteRoute.addChildren([WebsiteWebsiteIdRoute, WebsiteAddRoute]),
]) ])

View File

@ -1,9 +1,9 @@
import { createFileRoute, redirect } from '@tanstack/react-router'; import { createFileRoute, redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({
beforeLoad: () => { beforeLoad: ({ context }) => {
redirect({ redirect({
to: '/website', to: context.userInfo ? '/website' : '/login',
}); });
}, },
}); });

View File

@ -0,0 +1,86 @@
import { trpc } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader';
import { CommonList } from '@/components/CommonList';
import { CommonWrapper } from '@/components/CommonWrapper';
import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent';
import { LayoutV2 } from '@/pages/LayoutV2';
import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react';
import {
createFileRoute,
useNavigate,
useRouterState,
} from '@tanstack/react-router';
import { LuPlus } from 'react-icons/lu';
export const Route = createFileRoute('/monitor')({
beforeLoad: routeAuthBeforeLoad,
component: MonitorComponent,
});
function MonitorComponent() {
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { data = [] } = trpc.monitor.all.useQuery({
workspaceId,
});
const navigate = useNavigate();
const pathname = useRouterState({
select: (state) => state.location.pathname,
});
const items = data.map((item) => ({
id: item.id,
title: item.name,
tags: [item.type],
href: `/monitor/${item.id}`,
}));
useDataReady(
() => data.length > 0,
() => {
if (pathname === Route.fullPath) {
navigate({
to: '/monitor/$monitorId',
params: {
monitorId: data[0].id,
},
});
}
}
);
const handleClickAdd = useEvent(() => {
navigate({
to: '/monitor/add',
});
});
return (
<LayoutV2
list={
<CommonWrapper
header={
<CommonHeader
title={t('Monitor')}
actions={
<Button
variant="outline"
Icon={LuPlus}
onClick={handleClickAdd}
>
{t('Add')}
</Button>
}
/>
}
>
<CommonList hasSearch={true} items={items} />
</CommonWrapper>
}
/>
);
}

View File

@ -0,0 +1,45 @@
import { trpc } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader';
import { CommonWrapper } from '@/components/CommonWrapper';
import { ErrorTip } from '@/components/ErrorTip';
import { Loading } from '@/components/Loading';
import { NotFoundTip } from '@/components/NotFoundTip';
import { MonitorInfo } from '@/components/monitor/MonitorInfo';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/monitor/$monitorId')({
beforeLoad: routeAuthBeforeLoad,
component: MonitorDetailComponent,
});
function MonitorDetailComponent() {
const { monitorId } = Route.useParams<{ monitorId: string }>();
const workspaceId = useCurrentWorkspaceId();
const { data: monitor, isLoading } = trpc.monitor.get.useQuery({
workspaceId,
monitorId,
});
if (!monitorId) {
return <ErrorTip />;
}
if (isLoading) {
return <Loading />;
}
if (!monitor) {
return <NotFoundTip />;
}
return (
<CommonWrapper header={<CommonHeader title={monitor.name} />}>
<ScrollArea className="h-full overflow-hidden p-4">
<MonitorInfo monitorId={monitor.id} />
</ScrollArea>
</CommonWrapper>
);
}

View File

@ -0,0 +1,55 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { t, useTranslation } from '@i18next-toolkit/react';
import { Button } from '@/components/ui/button';
import { useEvent } from '@/hooks/useEvent';
import { useCurrentWorkspaceId } from '@/store/user';
import { trpc } from '@/api/trpc';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { CommonWrapper } from '@/components/CommonWrapper';
import {
MonitorInfoEditor,
MonitorInfoEditorValues,
} from '@/components/monitor/MonitorInfoEditor';
import { routeAuthBeforeLoad } from '@/utils/route';
import { useMonitorUpsert } from '@/api/model/monitor';
export const Route = createFileRoute('/monitor/add')({
beforeLoad: routeAuthBeforeLoad,
component: MonitorAddComponent,
});
function MonitorAddComponent() {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const addWebsiteMutation = trpc.website.add.useMutation();
const navigate = useNavigate();
const mutation = useMonitorUpsert();
const handleSubmit = useEvent(async (values: MonitorInfoEditorValues) => {
const res = await mutation.mutateAsync({
...values,
workspaceId,
});
navigate({
to: '/monitor/$monitorId',
params: {
monitorId: res.id,
},
});
});
return (
<CommonWrapper
header={<h1 className="text-xl font-bold">{t('Add Monitor')}</h1>}
>
<div className="p-4">
<Card>
<CardContent className="pt-4">
<MonitorInfoEditor onSave={handleSubmit} />
</CardContent>
</Card>
</div>
</CommonWrapper>
);
}

View File

@ -4,7 +4,6 @@ import { CommonWrapper } from '@/components/CommonWrapper';
import { ErrorTip } from '@/components/ErrorTip'; import { ErrorTip } from '@/components/ErrorTip';
import { Loading } from '@/components/Loading'; import { Loading } from '@/components/Loading';
import { NotFoundTip } from '@/components/NotFoundTip'; import { NotFoundTip } from '@/components/NotFoundTip';
import { Button } from '@/components/ui/button';
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn'; import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn';
import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable'; import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
@ -14,9 +13,8 @@ import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { Card } from 'antd'; import { Card } from 'antd';
import { LuArrowRight } from 'react-icons/lu';
export const Route = createFileRoute('/website/$websiteId')({ export const Route = createFileRoute('/website/$websiteId')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,

View File

@ -25,7 +25,7 @@ import { CommonWrapper } from '@/components/CommonWrapper';
export const Route = createFileRoute('/website/add')({ export const Route = createFileRoute('/website/add')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
component: WebsiteDetailComponent, component: WebsiteAddComponent,
}); });
const addFormSchema = z.object({ const addFormSchema = z.object({
@ -33,7 +33,7 @@ const addFormSchema = z.object({
domain: z.union([z.string().ip(), z.string().regex(hostnameRegex)]), domain: z.union([z.string().ip(), z.string().regex(hostnameRegex)]),
}); });
function WebsiteDetailComponent() { function WebsiteAddComponent() {
const { t } = useTranslation(); const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const addWebsiteMutation = trpc.website.add.useMutation(); const addWebsiteMutation = trpc.website.add.useMutation();