feat(v2): monitor feature
This commit is contained in:
parent
958b1c0932
commit
402b8a6955
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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 ? (
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
@ -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]),
|
||||
])
|
||||
|
@ -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',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
86
src/client/routes/monitor.tsx
Normal file
86
src/client/routes/monitor.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
45
src/client/routes/monitor/$monitorId.tsx
Normal file
45
src/client/routes/monitor/$monitorId.tsx
Normal 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>
|
||||
);
|
||||
}
|
55
src/client/routes/monitor/add.tsx
Normal file
55
src/client/routes/monitor/add.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user