diff --git a/package.json b/package.json index 99fdd01dc..25a0a1909 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "antd": "^5.24.8", "axios": "^1.8.4", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "fast-clean": "^1.4.0", "i18next": "^25.0.1", "immer": "^10.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ddffe9da..a8f90bedb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 fast-clean: specifier: ^1.4.0 version: 1.4.0 diff --git a/src/components/form-slice/FormDisplayDate.tsx b/src/components/form-slice/FormDisplayDate.tsx new file mode 100644 index 000000000..3c26a3e64 --- /dev/null +++ b/src/components/form-slice/FormDisplayDate.tsx @@ -0,0 +1,16 @@ +import { InputWrapper, Text, type InputWrapperProps } from '@mantine/core'; +import dayjs from 'dayjs'; + +type FormDisplayDateProps = InputWrapperProps & { + date: dayjs.ConfigType; +}; +export const FormDisplayDate = (props: FormDisplayDateProps) => { + const { date, ...rest } = props; + return ( + + + {date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'} + + + ); +}; diff --git a/src/components/form-slice/FormPartBasic.tsx b/src/components/form-slice/FormPartBasic.tsx index 8865e311b..1eb027dca 100644 --- a/src/components/form-slice/FormPartBasic.tsx +++ b/src/components/form-slice/FormPartBasic.tsx @@ -6,25 +6,13 @@ import { FormItemTextarea } from '../form/Textarea'; import { useFormContext } from 'react-hook-form'; import type { A6Type } from '@/types/schema/apisix'; -export type FormPartBasicProps = Omit & { - showId?: boolean; -}; +export type FormPartBasicProps = Omit; export const FormPartBasic = (props: FormPartBasicProps) => { - const { showId, ...restProps } = props; - const { control } = useFormContext(); + const { control } = useFormContext(); const { t } = useTranslation(); return ( - - {showId && ( - - )} + ( + props: Pick, 'allowUpload'> +) => { + const { t } = useTranslation(); + const form = useFormContext(); + return ( + + ); +}; diff --git a/src/components/form-slice/FormPartUpstream/index.tsx b/src/components/form-slice/FormPartUpstream/index.tsx index 18d92d683..75c7081b7 100644 --- a/src/components/form-slice/FormPartUpstream/index.tsx +++ b/src/components/form-slice/FormPartUpstream/index.tsx @@ -196,11 +196,11 @@ export const FormSectionKeepAlive = () => { ); }; -export const FormPartUpstream = ({ showId }: { showId?: boolean }) => { +export const FormPartUpstream = () => { const { t } = useTranslation(); return ( <> - + { + const { control } = useFormContext(); + const { t } = useTranslation(); + const createTime = useWatch({ control, name: 'create_time' }); + const updateTime = useWatch({ control, name: 'update_time' }); + return ( + + + + + + + ); +}; diff --git a/src/components/form/FileUploadTextarea.tsx b/src/components/form/FileUploadTextarea.tsx new file mode 100644 index 000000000..39cdf7952 --- /dev/null +++ b/src/components/form/FileUploadTextarea.tsx @@ -0,0 +1,114 @@ +import { + Textarea as MTextarea, + type TextareaProps as MTextareaProps, + Box, + Group, + Button, + Input, +} from '@mantine/core'; +import { + useController, + type FieldValues, + type UseControllerProps, +} from 'react-hook-form'; +import { genControllerProps } from './util'; +import IconUpload from '~icons/material-symbols/upload'; +import { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +export type FileUploadTextareaProps = UseControllerProps & + MTextareaProps & { + acceptFileTypes?: string; + uploadButtonText?: string; + maxFileSize?: number; + onFileLoaded?: (content: string, fileName: string) => void; + allowUpload?: boolean; + }; + +export const FileUploadTextarea = ( + props: FileUploadTextareaProps +) => { + const { allowUpload = true } = props; + const { controllerProps, restProps } = genControllerProps(props, ''); + const { + field: { value, onChange: fOnChange, ...restField }, + fieldState, + } = useController(controllerProps); + const { t } = useTranslation(); + + const [fileError, setFileError] = useState(null); + const fileInputRef = useRef(null); + + const { + acceptFileTypes = '.txt,.json,.yaml,.yml', + uploadButtonText = '', + maxFileSize = 10 * 1024 * 1024, + onFileLoaded, + onChange, + ...textareaProps + } = restProps; + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + if (file.size > maxFileSize) { + const size = Math.round(maxFileSize / 1024 / 1024); + setFileError(`${t('form.upload.fileOverSize')} ${size}MB`); + return; + } + + setFileError(null); + if (fileInputRef.current) fileInputRef.current.value = ''; + + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result as string; + fOnChange(content); + onFileLoaded?.(content, file.name); + }; + + reader.onerror = (e) => { + setFileError(`${t('form.upload.readError')} ${e.target?.error}`); + }; + + reader.readAsText(file); + }; + + return ( + + { + fOnChange(e); + onChange?.(e); + }} + resize="vertical" + autosize={restField.disabled} + {...restField} + {...textareaProps} + /> + {allowUpload && ( + + + + + )} + {fieldState.error?.message || fileError} + + ); +}; diff --git a/src/config/constant.ts b/src/config/constant.ts index cb0c4ff24..c1e098e08 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -5,6 +5,7 @@ export const LOCAL_STORAGE_ADMIN_KEY = 'apisix-admin-key'; export const API_PREFIX = '/apisix/admin'; export const API_ROUTES = '/routes'; export const API_UPSTREAMS = '/upstreams'; +export const API_PROTOS = '/protos'; export const APPSHELL_HEADER_HEIGHT = 60; export const APPSHELL_NAVBAR_WIDTH = 250; diff --git a/src/config/navRoutes.ts b/src/config/navRoutes.ts index 5f6b9c5d1..e3a98d0b9 100644 --- a/src/config/navRoutes.ts +++ b/src/config/navRoutes.ts @@ -34,4 +34,8 @@ export const navRoutes: NavRoute[] = [ to: '/secret', label: 'secret', }, + { + to: '/protos', + label: 'protos', + }, ]; diff --git a/src/config/req.ts b/src/config/req.ts index 92de9c190..91dd631c6 100644 --- a/src/config/req.ts +++ b/src/config/req.ts @@ -28,7 +28,19 @@ type A6RespErr = { }; req.interceptors.response.use( - (res) => res, + (res) => { + // it's a apisix design + // when list is empty, it will be a object + // but we need a array + if ( + res.data?.list && + !Array.isArray(res.data.list) && + Object.keys(res.data.list).length === 0 + ) { + res.data.list = []; + } + return res; + }, (err) => { if (err.response) { const d = err.response.data as A6RespErr; diff --git a/src/locales/en/common.json b/src/locales/en/common.json index e0de62ad6..d2c738153 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -18,9 +18,19 @@ "add": "Add", "delete": "Delete", "edit": "Edit", - "save": "Save" + "save": "Save", + "upload": "Upload" }, "disabled": "Disabled, click switch to enable", + "info": { + "create_time": "Created At", + "title": "Information", + "update_time": "Updated At" + }, + "upload": { + "fileOverSize": "file size is too large", + "readError": "file read error:" + }, "upstream": { "checks": { "active": { @@ -139,6 +149,7 @@ "navbar": { "consumer": "Consumer", "pluginGlobalRules": "Plugin Global Rules", + "protos": "Protos", "route": "Route", "secret": "Secret", "service": "Service", @@ -147,6 +158,20 @@ }, "noData": "No Data", "or": "OR", + "protos": { + "add": { + "success": "Add Proto Successfully", + "title": "Add Proto" + }, + "detail": { + "title": "Proto Detail" + }, + "form": { + "content": "Content", + "contentPlaceholder": "Paste or upload .proto, .pb file" + }, + "title": "Protos" + }, "route": { "add": { "form": { diff --git a/src/main.tsx b/src/main.tsx index 4e4d43092..b9ae8b096 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,7 +9,6 @@ import { queryClient, router } from './config/global'; import '@mantine/core/styles.css'; import '@mantine/notifications/styles.css'; -import 'mantine-react-table/styles.css'; import './styles/global.css'; const theme = createTheme({}); diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 66f499405..534c6cf6b 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -17,11 +17,14 @@ import { Route as SslIndexImport } from './routes/ssl/index' import { Route as ServiceIndexImport } from './routes/service/index' import { Route as SecretIndexImport } from './routes/secret/index' import { Route as RouteIndexImport } from './routes/route/index' +import { Route as ProtosIndexImport } from './routes/protos/index' import { Route as PluginGlobalRulesIndexImport } from './routes/plugin-global-rules/index' import { Route as ConsumerIndexImport } from './routes/consumer/index' import { Route as UpstreamsAddImport } from './routes/upstreams/add' import { Route as RouteAddImport } from './routes/route/add' +import { Route as ProtosAddImport } from './routes/protos/add' import { Route as UpstreamsDetailUpstreamIdImport } from './routes/upstreams/detail.$upstreamId' +import { Route as ProtosDetailIdImport } from './routes/protos/detail.$id' // Create/Update Routes @@ -61,6 +64,12 @@ const RouteIndexRoute = RouteIndexImport.update({ getParentRoute: () => rootRoute, } as any) +const ProtosIndexRoute = ProtosIndexImport.update({ + id: '/protos/', + path: '/protos/', + getParentRoute: () => rootRoute, +} as any) + const PluginGlobalRulesIndexRoute = PluginGlobalRulesIndexImport.update({ id: '/plugin-global-rules/', path: '/plugin-global-rules/', @@ -85,12 +94,24 @@ const RouteAddRoute = RouteAddImport.update({ getParentRoute: () => rootRoute, } as any) +const ProtosAddRoute = ProtosAddImport.update({ + id: '/protos/add', + path: '/protos/add', + getParentRoute: () => rootRoute, +} as any) + const UpstreamsDetailUpstreamIdRoute = UpstreamsDetailUpstreamIdImport.update({ id: '/upstreams/detail/$upstreamId', path: '/upstreams/detail/$upstreamId', getParentRoute: () => rootRoute, } as any) +const ProtosDetailIdRoute = ProtosDetailIdImport.update({ + id: '/protos/detail/$id', + path: '/protos/detail/$id', + getParentRoute: () => rootRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -102,6 +123,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } + '/protos/add': { + id: '/protos/add' + path: '/protos/add' + fullPath: '/protos/add' + preLoaderRoute: typeof ProtosAddImport + parentRoute: typeof rootRoute + } '/route/add': { id: '/route/add' path: '/route/add' @@ -130,6 +158,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PluginGlobalRulesIndexImport parentRoute: typeof rootRoute } + '/protos/': { + id: '/protos/' + path: '/protos' + fullPath: '/protos' + preLoaderRoute: typeof ProtosIndexImport + parentRoute: typeof rootRoute + } '/route/': { id: '/route/' path: '/route' @@ -165,6 +200,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof UpstreamsIndexImport parentRoute: typeof rootRoute } + '/protos/detail/$id': { + id: '/protos/detail/$id' + path: '/protos/detail/$id' + fullPath: '/protos/detail/$id' + preLoaderRoute: typeof ProtosDetailIdImport + parentRoute: typeof rootRoute + } '/upstreams/detail/$upstreamId': { id: '/upstreams/detail/$upstreamId' path: '/upstreams/detail/$upstreamId' @@ -179,44 +221,53 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/protos/add': typeof ProtosAddRoute '/route/add': typeof RouteAddRoute '/upstreams/add': typeof UpstreamsAddRoute '/consumer': typeof ConsumerIndexRoute '/plugin-global-rules': typeof PluginGlobalRulesIndexRoute + '/protos': typeof ProtosIndexRoute '/route': typeof RouteIndexRoute '/secret': typeof SecretIndexRoute '/service': typeof ServiceIndexRoute '/ssl': typeof SslIndexRoute '/upstreams': typeof UpstreamsIndexRoute + '/protos/detail/$id': typeof ProtosDetailIdRoute '/upstreams/detail/$upstreamId': typeof UpstreamsDetailUpstreamIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/protos/add': typeof ProtosAddRoute '/route/add': typeof RouteAddRoute '/upstreams/add': typeof UpstreamsAddRoute '/consumer': typeof ConsumerIndexRoute '/plugin-global-rules': typeof PluginGlobalRulesIndexRoute + '/protos': typeof ProtosIndexRoute '/route': typeof RouteIndexRoute '/secret': typeof SecretIndexRoute '/service': typeof ServiceIndexRoute '/ssl': typeof SslIndexRoute '/upstreams': typeof UpstreamsIndexRoute + '/protos/detail/$id': typeof ProtosDetailIdRoute '/upstreams/detail/$upstreamId': typeof UpstreamsDetailUpstreamIdRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute + '/protos/add': typeof ProtosAddRoute '/route/add': typeof RouteAddRoute '/upstreams/add': typeof UpstreamsAddRoute '/consumer/': typeof ConsumerIndexRoute '/plugin-global-rules/': typeof PluginGlobalRulesIndexRoute + '/protos/': typeof ProtosIndexRoute '/route/': typeof RouteIndexRoute '/secret/': typeof SecretIndexRoute '/service/': typeof ServiceIndexRoute '/ssl/': typeof SslIndexRoute '/upstreams/': typeof UpstreamsIndexRoute + '/protos/detail/$id': typeof ProtosDetailIdRoute '/upstreams/detail/$upstreamId': typeof UpstreamsDetailUpstreamIdRoute } @@ -224,70 +275,85 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/protos/add' | '/route/add' | '/upstreams/add' | '/consumer' | '/plugin-global-rules' + | '/protos' | '/route' | '/secret' | '/service' | '/ssl' | '/upstreams' + | '/protos/detail/$id' | '/upstreams/detail/$upstreamId' fileRoutesByTo: FileRoutesByTo to: | '/' + | '/protos/add' | '/route/add' | '/upstreams/add' | '/consumer' | '/plugin-global-rules' + | '/protos' | '/route' | '/secret' | '/service' | '/ssl' | '/upstreams' + | '/protos/detail/$id' | '/upstreams/detail/$upstreamId' id: | '__root__' | '/' + | '/protos/add' | '/route/add' | '/upstreams/add' | '/consumer/' | '/plugin-global-rules/' + | '/protos/' | '/route/' | '/secret/' | '/service/' | '/ssl/' | '/upstreams/' + | '/protos/detail/$id' | '/upstreams/detail/$upstreamId' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + ProtosAddRoute: typeof ProtosAddRoute RouteAddRoute: typeof RouteAddRoute UpstreamsAddRoute: typeof UpstreamsAddRoute ConsumerIndexRoute: typeof ConsumerIndexRoute PluginGlobalRulesIndexRoute: typeof PluginGlobalRulesIndexRoute + ProtosIndexRoute: typeof ProtosIndexRoute RouteIndexRoute: typeof RouteIndexRoute SecretIndexRoute: typeof SecretIndexRoute ServiceIndexRoute: typeof ServiceIndexRoute SslIndexRoute: typeof SslIndexRoute UpstreamsIndexRoute: typeof UpstreamsIndexRoute + ProtosDetailIdRoute: typeof ProtosDetailIdRoute UpstreamsDetailUpstreamIdRoute: typeof UpstreamsDetailUpstreamIdRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + ProtosAddRoute: ProtosAddRoute, RouteAddRoute: RouteAddRoute, UpstreamsAddRoute: UpstreamsAddRoute, ConsumerIndexRoute: ConsumerIndexRoute, PluginGlobalRulesIndexRoute: PluginGlobalRulesIndexRoute, + ProtosIndexRoute: ProtosIndexRoute, RouteIndexRoute: RouteIndexRoute, SecretIndexRoute: SecretIndexRoute, ServiceIndexRoute: ServiceIndexRoute, SslIndexRoute: SslIndexRoute, UpstreamsIndexRoute: UpstreamsIndexRoute, + ProtosDetailIdRoute: ProtosDetailIdRoute, UpstreamsDetailUpstreamIdRoute: UpstreamsDetailUpstreamIdRoute, } @@ -302,21 +368,27 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", + "/protos/add", "/route/add", "/upstreams/add", "/consumer/", "/plugin-global-rules/", + "/protos/", "/route/", "/secret/", "/service/", "/ssl/", "/upstreams/", + "/protos/detail/$id", "/upstreams/detail/$upstreamId" ] }, "/": { "filePath": "index.tsx" }, + "/protos/add": { + "filePath": "protos/add.tsx" + }, "/route/add": { "filePath": "route/add.tsx" }, @@ -329,6 +401,9 @@ export const routeTree = rootRoute "/plugin-global-rules/": { "filePath": "plugin-global-rules/index.tsx" }, + "/protos/": { + "filePath": "protos/index.tsx" + }, "/route/": { "filePath": "route/index.tsx" }, @@ -344,6 +419,9 @@ export const routeTree = rootRoute "/upstreams/": { "filePath": "upstreams/index.tsx" }, + "/protos/detail/$id": { + "filePath": "protos/detail.$id.tsx" + }, "/upstreams/detail/$upstreamId": { "filePath": "upstreams/detail.$upstreamId.tsx" } diff --git a/src/routes/protos/add.tsx b/src/routes/protos/add.tsx new file mode 100644 index 000000000..187930b96 --- /dev/null +++ b/src/routes/protos/add.tsx @@ -0,0 +1,71 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { useTranslation } from 'react-i18next'; +import { req } from '@/config/req'; +import { useMutation } from '@tanstack/react-query'; +import { API_PROTOS } from '@/config/constant'; +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 { DevTool } from '@hookform/devtools'; +import { A6Proto } from '@/types/schema/apisix/proto'; +import type { A6Type } from '@/types/schema/apisix'; +import { useRouter as useReactRouter } from '@tanstack/react-router'; +import { FormPartProto } from '@/components/form-slice/FormPartProto'; +import { notifications } from '@mantine/notifications'; + +const defaultValues: A6Type['ProtoPost'] = { + content: '', +}; + +const ProtoAddForm = () => { + const { t } = useTranslation(); + const router = useReactRouter(); + + const postProto = useMutation({ + mutationFn: (data: object) => + req.post(API_PROTOS, data), + }); + + const form = useForm({ + resolver: zodResolver(A6Proto.ProtoPost), + shouldUnregister: true, + shouldFocusError: true, + defaultValues, + mode: 'onChange', + }); + + const submit = async (data: A6Type['ProtoPost']) => { + await postProto.mutateAsync(data); + notifications.show({ + id: 'add-proto', + message: t('protos.add.success'), + color: 'green', + }); + await router.navigate({ to: '/protos' }); + }; + + return ( + +
+ + {t('form.btn.add')} + + +
+ ); +}; + +function RouteComponent() { + const { t } = useTranslation(); + return ( + <> + + + + ); +} + +export const Route = createFileRoute('/protos/add')({ + component: RouteComponent, +}); diff --git a/src/routes/protos/detail.$id.tsx b/src/routes/protos/detail.$id.tsx new file mode 100644 index 000000000..69e98a09f --- /dev/null +++ b/src/routes/protos/detail.$id.tsx @@ -0,0 +1,69 @@ +import { useEffect } from 'react'; +import { A6, type A6Type } from '@/types/schema/apisix'; +import { createFileRoute, useParams } from '@tanstack/react-router'; +import { useTranslation } from 'react-i18next'; +import { req } from '@/config/req'; +import { useQuery } from '@tanstack/react-query'; +import { API_PROTOS } from '@/config/constant'; +import PageHeader from '@/components/page/PageHeader'; +import { FormTOCBox } from '@/components/form-slice/FormSection'; +import { FormProvider, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { DevTool } from '@hookform/devtools'; +import { Skeleton } from '@mantine/core'; +import { FormPartProto } from '@/components/form-slice/FormPartProto'; +import { FormSectionInfo } from '@/components/form-slice/FormSectionInfo'; + +const ProtoDetailForm = ({ id }: { id: string }) => { + const { data: protoData, isLoading } = useQuery({ + queryKey: ['proto', id], + queryFn: () => + req + .get(`${API_PROTOS}/${id}`) + .then((v) => v.data), + }); + + const form = useForm({ + resolver: zodResolver(A6.Proto), + shouldUnregister: true, + mode: 'onChange', + disabled: true, + }); + + // Update form values when data is loaded + useEffect(() => { + if (protoData?.value) { + form.reset(protoData.value); + } + }, [protoData, form]); + + if (isLoading) { + return ; + } + + return ( + + + + + + + + ); +}; + +function RouteComponent() { + const { t } = useTranslation(); + const { id } = useParams({ from: '/protos/detail/$id' }); + + return ( + <> + + + + ); +} + +export const Route = createFileRoute('/protos/detail/$id')({ + component: RouteComponent, +}); diff --git a/src/routes/protos/index.tsx b/src/routes/protos/index.tsx new file mode 100644 index 000000000..d500187dc --- /dev/null +++ b/src/routes/protos/index.tsx @@ -0,0 +1,150 @@ +import { req } from '@/config/req'; +import { queryClient } from '@/config/global'; +import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'; +import { createFileRoute, useRouter } from '@tanstack/react-router'; +import { useTranslation } from 'react-i18next'; +import type { A6Type } from '@/types/schema/apisix'; +import { API_PROTOS } from '@/config/constant'; +import { ProTable } from '@ant-design/pro-components'; +import type { ProColumns } from '@ant-design/pro-components'; +import { useEffect, useMemo } from 'react'; +import PageHeader from '@/components/page/PageHeader'; +import { RouteLinkBtn } from '@/components/Btn'; +import IconPlus from '~icons/material-symbols/add'; +import { AntdConfigProvider } from '@/config/antdConfigProvider'; +import { usePagination } from '@/utils/usePagination'; +import { + pageSearchSchema, + type PageSearchType, +} from '@/types/schema/pageSearch'; + +const genProtosQueryOptions = (props: PageSearchType) => { + const { page, pageSize } = props; + return queryOptions({ + queryKey: ['protos', page, pageSize], + queryFn: () => + req + .get(API_PROTOS, { + params: { + page, + page_size: pageSize, + }, + }) + .then((v) => v.data), + }); +}; + +const ToAddPageBtn = () => { + const { t } = useTranslation(); + const router = useRouter(); + return ( + } + size="compact-sm" + variant="gradient" + to={router.routesById['/protos/add'].to} + > + {t('protos.add.title')} + + ); +}; + +type DetailPageBtnProps = { + record: A6Type['RespProtoItem']; +}; +const DetailPageBtn = (props: DetailPageBtnProps) => { + const { record } = props; + const { t } = useTranslation(); + const router = useRouter(); + return ( + + {t('view')} + + ); +}; + +function RouteComponent() { + const { t } = useTranslation(); + + // Use the pagination hook + const { pagination, handlePageChange, updateTotal } = usePagination({ + queryKey: 'protos', + }); + + const protosQuery = useSuspenseQuery(genProtosQueryOptions(pagination)); + const { data, isLoading } = protosQuery; + + useEffect(() => { + if (data?.total) { + updateTotal(data.total); + } + }, [data?.total, updateTotal]); + + const columns = useMemo< + ProColumns[] + >(() => { + return [ + { + dataIndex: ['value', 'id'], + title: 'ID', + key: 'id', + valueType: 'text', + }, + { + title: t('actions'), + valueType: 'option', + key: 'option', + width: 120, + render: (_, record) => [], + }, + ]; + }, [t]); + + return ( + <> + + + , + }, + ], + }, + }} + /> + + + ); +} + +export const Route = createFileRoute('/protos/')({ + component: RouteComponent, + validateSearch: pageSearchSchema, + loaderDeps: ({ search }) => search, + loader: ({ deps }) => + queryClient.ensureQueryData(genProtosQueryOptions(deps)), +}); diff --git a/src/routes/upstreams/detail.$upstreamId.tsx b/src/routes/upstreams/detail.$upstreamId.tsx index 348a16921..f25ba888b 100644 --- a/src/routes/upstreams/detail.$upstreamId.tsx +++ b/src/routes/upstreams/detail.$upstreamId.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { type A6Type } from '@/types/schema/apisix'; import { createFileRoute, useParams } from '@tanstack/react-router'; import { useTranslation } from 'react-i18next'; @@ -17,6 +17,7 @@ import { import { DevTool } from '@hookform/devtools'; import { upstreamdefaultValues } from '@/components/form-slice/FormPartUpstream/config'; import { Skeleton } from '@mantine/core'; +import { FormSectionInfo } from '@/components/form-slice/FormSectionInfo'; export const Route = createFileRoute('/upstreams/detail/$upstreamId')({ component: RouteComponent, @@ -57,9 +58,8 @@ const UpstreamDetailForm = ({ upstreamId }: { upstreamId: string }) => { return ( -
- -
+ +
diff --git a/src/types/schema/apisix/common.ts b/src/types/schema/apisix/common.ts index 24f99ef18..7b236e75b 100644 --- a/src/types/schema/apisix/common.ts +++ b/src/types/schema/apisix/common.ts @@ -20,6 +20,13 @@ const ID = z.object({ id: z.string(), }); +const Timestamp = z.object({ + create_time: z.number(), + update_time: z.number(), +}); + +const Info = ID.merge(Timestamp); + export const A6Common = { Basic, Labels, @@ -27,4 +34,6 @@ export const A6Common = { Plugins, Expr, ID, + Timestamp, + Info, }; diff --git a/src/types/schema/apisix/index.ts b/src/types/schema/apisix/index.ts index 4f1bdee49..8c59340e0 100644 --- a/src/types/schema/apisix/index.ts +++ b/src/types/schema/apisix/index.ts @@ -1,4 +1,5 @@ import { A6Common } from './common'; +import { A6Proto } from './proto'; import { A6Route } from './route'; import { A6Upstream } from './upstream'; export type { A6Type } from './type'; @@ -6,4 +7,5 @@ export const A6 = { ...A6Common, ...A6Upstream, ...A6Route, + ...A6Proto, }; diff --git a/src/types/schema/apisix/proto.ts b/src/types/schema/apisix/proto.ts new file mode 100644 index 000000000..eb95da4b7 --- /dev/null +++ b/src/types/schema/apisix/proto.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; +import { A6Common } from './common'; + +const Proto = z + .object({ + content: z.string(), + }) + .merge(A6Common.Info); + +export const A6Proto = { + Proto, + ProtoPost: Proto.omit({ id: true, create_time: true, update_time: true }), +}; diff --git a/src/types/schema/apisix/type.ts b/src/types/schema/apisix/type.ts index 7e539edf7..529a38521 100644 --- a/src/types/schema/apisix/type.ts +++ b/src/types/schema/apisix/type.ts @@ -2,13 +2,14 @@ import { z } from 'zod'; import type { A6 } from '.'; import type { AxiosResponse } from 'axios'; +export type A6DetailResponse = { + key: string; + value: T; + createdIndex: number; + modifiedIndex: number; +}; export type A6ListResponse = { - list: Array<{ - key: string; - value: T; - createdIndex: number; - modifiedIndex: number; - }>; + list: Array>; total: number; }; @@ -20,4 +21,7 @@ export type A6Type = RawA6Type & { RespRouteList: AxiosResponse>; RespUpstreamList: AxiosResponse>; RespUpstreamItem: A6Type['RespUpstreamList']['data']['list'][number]; + RespProtoList: AxiosResponse>; + RespProtoItem: A6Type['RespProtoList']['data']['list'][number]; + RespProtoDetail: AxiosResponse>; };