This is an automated email from the ASF dual-hosted git repository.

young pushed a commit to branch young/feat/service-list-add
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git

commit 1b5d93d31b6c85fd91fb3ba8d531df5b3c1486ab
Author: Skye Young <isk...@outlook.com>
AuthorDate: Tue May 6 00:40:44 2025 +0800

    feat: service add page
---
 src/apis/services.ts                               | 43 +++++++++++++
 .../form-slice/FormPartService/index.tsx           | 70 ++++++++++++++++++++++
 .../form-slice/FormPartService/schema.ts           |  6 ++
 src/config/constant.ts                             |  1 +
 src/locales/en/common.json                         | 20 +++++++
 src/routeTree.gen.ts                               | 26 ++++++++
 src/routes/services/add.tsx                        | 65 ++++++++++++++++++++
 7 files changed, 231 insertions(+)

diff --git a/src/apis/services.ts b/src/apis/services.ts
new file mode 100644
index 000000000..e3527cae0
--- /dev/null
+++ b/src/apis/services.ts
@@ -0,0 +1,43 @@
+import { API_SERVICES } from '@/config/constant';
+import { req } from '@/config/req';
+import type { APISIXType } from '@/types/schema/apisix';
+import type { PageSearchType } from '@/types/schema/pageSearch';
+import { queryOptions } from '@tanstack/react-query';
+
+export type ServicePostType = APISIXType['ServicePost'];
+
+export const getServiceListQueryOptions = (props: PageSearchType) => {
+  const { page, pageSize } = props;
+  return queryOptions({
+    queryKey: ['services', page, pageSize],
+    queryFn: () =>
+      req
+        .get<unknown, APISIXType['RespServiceList']>(API_SERVICES, {
+          params: { page, page_size: pageSize },
+        })
+        .then((v) => v.data),
+  });
+};
+
+export const getServiceQueryOptions = (id: string) =>
+  queryOptions({
+    queryKey: ['service', id],
+    queryFn: () =>
+      req
+        .get<unknown, APISIXType['RespServiceDetail']>(`${API_SERVICES}/${id}`)
+        .then((v) => v.data),
+  });
+
+export const putServiceReq = (data: APISIXType['Service']) => {
+  const { id, ...rest } = data;
+  return req.put<APISIXType['Service'], APISIXType['RespServiceDetail']>(
+    `${API_SERVICES}/${id}`,
+    rest
+  );
+};
+
+export const postServiceReq = (data: ServicePostType) =>
+  req.post<ServicePostType, APISIXType['RespServiceDetail']>(
+    API_SERVICES,
+    data
+  );
diff --git a/src/components/form-slice/FormPartService/index.tsx 
b/src/components/form-slice/FormPartService/index.tsx
new file mode 100644
index 000000000..15f8f106a
--- /dev/null
+++ b/src/components/form-slice/FormPartService/index.tsx
@@ -0,0 +1,70 @@
+import { useTranslation } from 'react-i18next';
+import { useFormContext } from 'react-hook-form';
+import { FormSection } from '../FormSection';
+import { FormPartBasic } from '../FormPartBasic';
+import { FormItemPlugins } from '../FormItemPlugins';
+import { FormItemTextInput } from '@/components/form/TextInput';
+import { FormItemSwitch } from '@/components/form/Switch';
+import { FormItemTagsInput } from '@/components/form/TagInput';
+import { Divider, InputWrapper } from '@mantine/core';
+import { NamePrefixProvider } from '@/utils/useNamePrefix';
+import { FormPartUpstream } from '../FormPartUpstream';
+import type { ServicePostType } from './schema';
+
+const FormSectionUpstream = () => {
+  const { t } = useTranslation();
+  const { control } = useFormContext<ServicePostType>();
+  return (
+    <FormSection legend={t('form.upstream.title')}>
+      <FormSection legend={t('form.upstream.upstreamId')}>
+        <FormItemTextInput control={control} name="upstream_id" />
+      </FormSection>
+      <Divider my="xs" label={t('or')} />
+      <NamePrefixProvider value="upstream">
+        <FormPartUpstream />
+      </NamePrefixProvider>
+    </FormSection>
+  );
+};
+
+const FormSectionPlugins = () => {
+  const { t } = useTranslation();
+  return (
+    <FormSection legend={t('form.plugins.label')}>
+      <FormItemPlugins name="plugins" />
+    </FormSection>
+  );
+};
+
+const FormSectionSettings = () => {
+  const { t } = useTranslation();
+  const { control } = useFormContext<ServicePostType>();
+  return (
+    <FormSection legend={t('form.service.settings')}>
+      <InputWrapper label={t('form.service.enableWebsocket')}>
+        <FormItemSwitch control={control} name="enable_websocket" />
+      </InputWrapper>
+      <FormItemTextInput
+        control={control}
+        name="script"
+        label={t('form.service.script')}
+      />
+      <FormItemTagsInput
+        control={control}
+        name="hosts"
+        label={t('form.service.hosts')}
+      />
+    </FormSection>
+  );
+};
+
+export const FormPartService = () => {
+  return (
+    <>
+      <FormPartBasic />
+      <FormSectionSettings />
+      <FormSectionUpstream />
+      <FormSectionPlugins />
+    </>
+  );
+};
diff --git a/src/components/form-slice/FormPartService/schema.ts 
b/src/components/form-slice/FormPartService/schema.ts
new file mode 100644
index 000000000..ba070ad69
--- /dev/null
+++ b/src/components/form-slice/FormPartService/schema.ts
@@ -0,0 +1,6 @@
+import { APISIXServices } from '@/types/schema/apisix/services';
+import type { z } from 'zod';
+
+export const ServicePostSchema = APISIXServices.ServicePost;
+
+export type ServicePostType = z.infer<typeof ServicePostSchema>;
diff --git a/src/config/constant.ts b/src/config/constant.ts
index b48150c15..d8b53768c 100644
--- a/src/config/constant.ts
+++ b/src/config/constant.ts
@@ -7,6 +7,7 @@ export const API_ROUTES = '/routes';
 export const API_STREAM_ROUTES = '/stream_routes';
 export const API_UPSTREAMS = '/upstreams';
 export const API_PROTOS = '/protos';
+export const API_SERVICES = '/services';
 export const API_GLOBAL_RULES = '/global_rules';
 export const API_PLUGINS = '/plugins';
 export const API_PLUGINS_LIST = '/plugins/list';
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 2e5a9b79b..aa3692dab 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -61,6 +61,12 @@
       "vars": "Vars"
     },
     "search": "Search",
+    "service": {
+      "enableWebsocket": "Enable WebSocket",
+      "script": "Script",
+      "hosts": "Hosts",
+      "settings": "Service Settings"
+    },
     "streamRoute": {
       "protocol": {
         "conf": "Conf",
@@ -269,6 +275,20 @@
     "title": "Routes"
   },
   "seconds": "Seconds",
+  "service": {
+    "add": {
+      "success": "Add Service Successfully",
+      "title": "Add Service"
+    },
+    "detail": {
+      "title": "Service Detail"
+    },
+    "edit": {
+      "success": "Edit Service Successfully",
+      "title": "Edit Service"
+    },
+    "title": "Services"
+  },
   "setting": {
     "adminKey": "Admin Key",
     "title": "Setting"
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 257905a58..067e81a92 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -24,6 +24,7 @@ import { Route as GlobalrulesIndexImport } from 
'./routes/global_rules/index'
 import { Route as ConsumersIndexImport } from './routes/consumers/index'
 import { Route as UpstreamsAddImport } from './routes/upstreams/add'
 import { Route as StreamroutesAddImport } from './routes/stream_routes/add'
+import { Route as ServicesAddImport } from './routes/services/add'
 import { Route as RoutesAddImport } from './routes/routes/add'
 import { Route as ProtosAddImport } from './routes/protos/add'
 import { Route as GlobalrulesAddImport } from './routes/global_rules/add'
@@ -113,6 +114,12 @@ const StreamroutesAddRoute = StreamroutesAddImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
+const ServicesAddRoute = ServicesAddImport.update({
+  id: '/services/add',
+  path: '/services/add',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const RoutesAddRoute = RoutesAddImport.update({
   id: '/routes/add',
   path: '/routes/add',
@@ -193,6 +200,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof RoutesAddImport
       parentRoute: typeof rootRoute
     }
+    '/services/add': {
+      id: '/services/add'
+      path: '/services/add'
+      fullPath: '/services/add'
+      preLoaderRoute: typeof ServicesAddImport
+      parentRoute: typeof rootRoute
+    }
     '/stream_routes/add': {
       id: '/stream_routes/add'
       path: '/stream_routes/add'
@@ -322,6 +336,7 @@ export interface FileRoutesByFullPath {
   '/global_rules/add': typeof GlobalrulesAddRoute
   '/protos/add': typeof ProtosAddRoute
   '/routes/add': typeof RoutesAddRoute
+  '/services/add': typeof ServicesAddRoute
   '/stream_routes/add': typeof StreamroutesAddRoute
   '/upstreams/add': typeof UpstreamsAddRoute
   '/consumers': typeof ConsumersIndexRoute
@@ -346,6 +361,7 @@ export interface FileRoutesByTo {
   '/global_rules/add': typeof GlobalrulesAddRoute
   '/protos/add': typeof ProtosAddRoute
   '/routes/add': typeof RoutesAddRoute
+  '/services/add': typeof ServicesAddRoute
   '/stream_routes/add': typeof StreamroutesAddRoute
   '/upstreams/add': typeof UpstreamsAddRoute
   '/consumers': typeof ConsumersIndexRoute
@@ -371,6 +387,7 @@ export interface FileRoutesById {
   '/global_rules/add': typeof GlobalrulesAddRoute
   '/protos/add': typeof ProtosAddRoute
   '/routes/add': typeof RoutesAddRoute
+  '/services/add': typeof ServicesAddRoute
   '/stream_routes/add': typeof StreamroutesAddRoute
   '/upstreams/add': typeof UpstreamsAddRoute
   '/consumers/': typeof ConsumersIndexRoute
@@ -397,6 +414,7 @@ export interface FileRouteTypes {
     | '/global_rules/add'
     | '/protos/add'
     | '/routes/add'
+    | '/services/add'
     | '/stream_routes/add'
     | '/upstreams/add'
     | '/consumers'
@@ -420,6 +438,7 @@ export interface FileRouteTypes {
     | '/global_rules/add'
     | '/protos/add'
     | '/routes/add'
+    | '/services/add'
     | '/stream_routes/add'
     | '/upstreams/add'
     | '/consumers'
@@ -443,6 +462,7 @@ export interface FileRouteTypes {
     | '/global_rules/add'
     | '/protos/add'
     | '/routes/add'
+    | '/services/add'
     | '/stream_routes/add'
     | '/upstreams/add'
     | '/consumers/'
@@ -468,6 +488,7 @@ export interface RootRouteChildren {
   GlobalrulesAddRoute: typeof GlobalrulesAddRoute
   ProtosAddRoute: typeof ProtosAddRoute
   RoutesAddRoute: typeof RoutesAddRoute
+  ServicesAddRoute: typeof ServicesAddRoute
   StreamroutesAddRoute: typeof StreamroutesAddRoute
   UpstreamsAddRoute: typeof UpstreamsAddRoute
   ConsumersIndexRoute: typeof ConsumersIndexRoute
@@ -492,6 +513,7 @@ const rootRouteChildren: RootRouteChildren = {
   GlobalrulesAddRoute: GlobalrulesAddRoute,
   ProtosAddRoute: ProtosAddRoute,
   RoutesAddRoute: RoutesAddRoute,
+  ServicesAddRoute: ServicesAddRoute,
   StreamroutesAddRoute: StreamroutesAddRoute,
   UpstreamsAddRoute: UpstreamsAddRoute,
   ConsumersIndexRoute: ConsumersIndexRoute,
@@ -525,6 +547,7 @@ export const routeTree = rootRoute
         "/global_rules/add",
         "/protos/add",
         "/routes/add",
+        "/services/add",
         "/stream_routes/add",
         "/upstreams/add",
         "/consumers/",
@@ -556,6 +579,9 @@ export const routeTree = rootRoute
     "/routes/add": {
       "filePath": "routes/add.tsx"
     },
+    "/services/add": {
+      "filePath": "services/add.tsx"
+    },
     "/stream_routes/add": {
       "filePath": "stream_routes/add.tsx"
     },
diff --git a/src/routes/services/add.tsx b/src/routes/services/add.tsx
new file mode 100644
index 000000000..c59b265e4
--- /dev/null
+++ b/src/routes/services/add.tsx
@@ -0,0 +1,65 @@
+import { createFileRoute, useRouter } from '@tanstack/react-router';
+import { useTranslation } from 'react-i18next';
+import { useMutation } from '@tanstack/react-query';
+import PageHeader from '@/components/page/PageHeader';
+import { FormProvider, useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { FormSubmitBtn } from '@/components/form/Btn';
+import { FormTOCBox } from '@/components/form-slice/FormSection';
+import { notifications } from '@mantine/notifications';
+import { pipeProduce } from '@/utils/producer';
+import { FormPartService } from '@/components/form-slice/FormPartService';
+import { ServicePostSchema } from 
'@/components/form-slice/FormPartService/schema';
+import { postServiceReq } from '@/apis/services';
+
+const ServiceAddForm = () => {
+  const { t } = useTranslation();
+  const router = useRouter();
+
+  const postService = useMutation({
+    mutationFn: postServiceReq,
+    async onSuccess() {
+      notifications.show({
+        message: t('service.add.success'),
+        color: 'green',
+      });
+      await router.navigate({ to: '/services' });
+    },
+  });
+
+  const form = useForm({
+    resolver: zodResolver(ServicePostSchema),
+    shouldUnregister: true,
+    shouldFocusError: true,
+    mode: 'all',
+  });
+
+  return (
+    <FormProvider {...form}>
+      <form
+        onSubmit={form.handleSubmit((d) =>
+          postService.mutateAsync(pipeProduce()(d))
+        )}
+      >
+        <FormPartService />
+        <FormSubmitBtn>{t('form.btn.add')}</FormSubmitBtn>
+      </form>
+    </FormProvider>
+  );
+};
+
+function RouteComponent() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <PageHeader title={t('service.add.title')} />
+      <FormTOCBox>
+        <ServiceAddForm />
+      </FormTOCBox>
+    </>
+  );
+}
+
+export const Route = createFileRoute('/services/add')({
+  component: RouteComponent,
+});

Reply via email to