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, +});