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';
export function useMonitorUpsert() {
const context = trpc.useContext();
const utils = trpc.useUtils();
const mutation = trpc.monitor.upsert.useMutation({
onSuccess: (data) => {
context.monitor.all.reset({
utils.monitor.all.refetch({
workspaceId: data.workspaceId,
});

View File

@ -10,7 +10,7 @@ import { useFuseSearch } from '@/hooks/useFuseSearch';
export interface CommonListItem {
id: string;
title: string;
content: React.ReactNode;
content?: React.ReactNode;
tags: string[];
href: string;
}
@ -46,12 +46,12 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
const finalList = searchResult ?? props.items;
return (
<div className="flex flex-col h-full">
<div className="flex h-full flex-col">
{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>
<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
placeholder="Search"
className="pl-8"
@ -72,7 +72,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
<button
key={item.id}
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'
)}
onClick={() =>
@ -88,7 +88,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
</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}
</div>
{item.tags.length > 0 ? (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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