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 && (
+
+ }
+ size="compact-xs"
+ variant="outline"
+ onClick={() => fileInputRef.current?.click()}
+ disabled={restField.disabled}
+ >
+ {uploadButtonText || t('form.btn.upload')}
+
+
+
+ )}
+ {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 (
+
+
+
+
+ );
+};
+
+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>;
};