diff --git a/src/client/components/monitor/StatusPage/EditForm.tsx b/src/client/components/monitor/StatusPage/EditForm.tsx index 98ea0e5..04d5643 100644 --- a/src/client/components/monitor/StatusPage/EditForm.tsx +++ b/src/client/components/monitor/StatusPage/EditForm.tsx @@ -1,9 +1,8 @@ import { Button, Form, Input, Typography } from 'antd'; import React from 'react'; -import { slugRegex } from '../../../../shared'; -import { z } from 'zod'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MonitorPicker } from '../MonitorPicker'; +import { urlSlugValidator } from '../../../utils/validator'; const { Text } = Typography; @@ -62,14 +61,7 @@ export const MonitorStatusPageEditForm: React.FC required: true, }, { - validator(rule, value, callback) { - try { - z.string().regex(slugRegex).parse(value); - callback(); - } catch (err) { - callback('Not valid slug'); - } - }, + validator: urlSlugValidator, }, ]} > diff --git a/src/client/components/monitor/provider/ping.tsx b/src/client/components/monitor/provider/ping.tsx index ea9811d..a5194a5 100644 --- a/src/client/components/monitor/provider/ping.tsx +++ b/src/client/components/monitor/provider/ping.tsx @@ -1,8 +1,7 @@ import { Form, Input } from 'antd'; import React from 'react'; import { MonitorProvider } from './types'; -import { z } from 'zod'; -import { hostnameRegex } from '../../../../shared'; +import { hostnameValidator } from '../../../utils/validator'; export const MonitorPing: React.FC = React.memo(() => { return ( @@ -13,17 +12,7 @@ export const MonitorPing: React.FC = React.memo(() => { rules={[ { required: true }, { - validator(rule, value, callback) { - try { - z.union([ - z.string().ip(), - z.string().regex(hostnameRegex), - ]).parse(value); - callback(); - } catch (err) { - callback('Not valid host, it should be ip or hostname'); - } - }, + validator: hostnameValidator, }, ]} > diff --git a/src/client/components/website/WebsiteInfo.tsx b/src/client/components/website/WebsiteInfo.tsx index 456f9e9..5c13074 100644 --- a/src/client/components/website/WebsiteInfo.tsx +++ b/src/client/components/website/WebsiteInfo.tsx @@ -16,6 +16,7 @@ import { } from '../../api/trpc'; import { useQueryClient } from '@tanstack/react-query'; import { useEvent } from '../../hooks/useEvent'; +import { hostnameValidator } from '../../utils/validator'; export const WebsiteInfo: React.FC = React.memo(() => { const workspaceId = useCurrentWorkspaceId(); @@ -102,7 +103,12 @@ export const WebsiteInfo: React.FC = React.memo(() => { diff --git a/src/client/components/website/WebsiteList.tsx b/src/client/components/website/WebsiteList.tsx index f599dcd..9bb62c0 100644 --- a/src/client/components/website/WebsiteList.tsx +++ b/src/client/components/website/WebsiteList.tsx @@ -7,43 +7,40 @@ import { import { Button, Form, Input, Modal, Table, Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; import React, { useMemo, useState } from 'react'; -import { - addWorkspaceWebsite, - refreshWorkspaceWebsites, - useWorspaceWebsites, - WebsiteInfo, -} from '../../api/model/website'; +import { WebsiteInfo } from '../../api/model/website'; import { Loading } from '../Loading'; -import { NoWorkspaceTip } from '../NoWorkspaceTip'; -import { useRequest } from '../../hooks/useRequest'; -import { useUserStore } from '../../store/user'; +import { useCurrentWorkspaceId } from '../../store/user'; import { useEvent } from '../../hooks/useEvent'; import { useNavigate } from 'react-router'; import { PageHeader } from '../PageHeader'; import { ModalButton } from '../ModalButton'; +import { hostnameValidator } from '../../utils/validator'; +import { trpc } from '../../api/trpc'; export const WebsiteList: React.FC = React.memo(() => { const [isModalOpen, setIsModalOpen] = useState(false); - const currentWorkspace = useUserStore( - (state) => state.info?.currentWorkspace - ); + const workspaceId = useCurrentWorkspaceId(); const [form] = Form.useForm(); + const addWebsiteMutation = trpc.website.add.useMutation(); + const utils = trpc.useContext(); - const [{ loading }, handleAddWebsite] = useRequest(async () => { + const handleAddWebsite = useEvent(async () => { await form.validateFields(); const values = form.getFieldsValue(); - await addWorkspaceWebsite(currentWorkspace!.id, values.name, values.domain); - refreshWorkspaceWebsites(currentWorkspace!.id); + await addWebsiteMutation.mutateAsync({ + workspaceId, + name: values.name, + domain: values.domain, + }); + + utils.website.all.refetch(); + setIsModalOpen(false); form.resetFields(); }); - if (!currentWorkspace) { - return ; - } - return (
{ } /> - + handleAddWebsite()} onCancel={() => setIsModalOpen(false)} @@ -86,7 +83,12 @@ export const WebsiteList: React.FC = React.memo(() => { label="Domain" name="domain" tooltip="Your server domain, or ip." - rules={[{ required: true }]} + rules={[ + { required: true }, + { + validator: hostnameValidator, + }, + ]} > @@ -99,7 +101,9 @@ WebsiteList.displayName = 'WebsiteList'; const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo( (props) => { - const { websites, isLoading } = useWorspaceWebsites(props.workspaceId); + const { data: websites = [], isLoading } = trpc.website.all.useQuery({ + workspaceId: props.workspaceId, + }); const navigate = useNavigate(); const handleEdit = useEvent((websiteId) => { diff --git a/src/client/utils/validator.ts b/src/client/utils/validator.ts new file mode 100644 index 0000000..a80ce7c --- /dev/null +++ b/src/client/utils/validator.ts @@ -0,0 +1,27 @@ +import { RuleObject } from 'antd/es/form'; +import { z } from 'zod'; +import { hostnameRegex, slugRegex } from '../../shared'; + +type Validator = ( + rule: RuleObject, + value: any, + callback: (error?: string) => void +) => Promise | void; + +export const hostnameValidator: Validator = (rule, value, callback) => { + try { + z.union([z.string().ip(), z.string().regex(hostnameRegex)]).parse(value); + callback(); + } catch (err) { + callback('Not valid host, it should be ip or hostname'); + } +}; + +export const urlSlugValidator: Validator = (rule, value, callback) => { + try { + z.string().regex(slugRegex).parse(value); + callback(); + } catch (err) { + callback('Not valid slug'); + } +}; diff --git a/src/server/trpc/routers/website.ts b/src/server/trpc/routers/website.ts index 24994db..d17acd0 100644 --- a/src/server/trpc/routers/website.ts +++ b/src/server/trpc/routers/website.ts @@ -18,6 +18,13 @@ import { getSessionMetrics, getPageviewMetrics } from '../../model/website'; import { websiteInfoSchema } from '../../model/_schema'; import { OpenApiMeta } from 'trpc-openapi'; import { hostnameRegex } from '../../../shared'; +import { addWorkspaceWebsite } from '../../model/workspace'; + +const websiteNameSchema = z.string().max(100); +const websiteDomainSchema = z.union([ + z.string().max(500).regex(hostnameRegex), + z.string().max(500).ip(), +]); export const websiteRouter = router({ onlineCount: workspaceProcedure @@ -205,6 +212,29 @@ export const websiteRouter = router({ return []; }), + add: workspaceOwnerProcedure + .meta({ + openapi: { + method: 'POST', + tags: [OPENAPI_TAG.WEBSITE], + protect: true, + path: `/workspace/{workspaceId}/website/add`, + }, + }) + .input( + z.object({ + name: websiteNameSchema, + domain: websiteDomainSchema, + }) + ) + .output(websiteInfoSchema) + .mutation(async ({ input }) => { + const { workspaceId, name, domain } = input; + + const website = await addWorkspaceWebsite(workspaceId, name, domain); + + return website; + }), updateInfo: workspaceOwnerProcedure .meta( buildWebsiteOpenapi({ @@ -215,11 +245,8 @@ export const websiteRouter = router({ .input( z.object({ websiteId: z.string().cuid2(), - name: z.string().max(100), - domain: z.union([ - z.string().max(500).regex(hostnameRegex), - z.string().max(500).ip(), - ]), + name: websiteNameSchema, + domain: websiteDomainSchema, monitorId: z.string().cuid2().nullish(), }) )