Skip to content

Commit 33ed7db

Browse files
authored
feat: protos page, FileUploadTextarea component (#3001)
* chore: add routeFileIgnorePrefix, rm useless css import * feat: FileUploadTextarea component * feat(req): adjust apisix return data.list * feat: proto schema, api path * feat: protos list page * feat: protos add page * feat: protos detail * fix(FormDisplayDate): date not exist show hyphen * fix: type * fix: solve comments
1 parent e86d9d1 commit 33ed7db

File tree

22 files changed

+642
-30
lines changed

22 files changed

+642
-30
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"antd": "^5.24.8",
2424
"axios": "^1.8.4",
2525
"clsx": "^2.1.1",
26+
"dayjs": "^1.11.13",
2627
"fast-clean": "^1.4.0",
2728
"i18next": "^25.0.1",
2829
"immer": "^10.1.1",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { InputWrapper, Text, type InputWrapperProps } from '@mantine/core';
2+
import dayjs from 'dayjs';
3+
4+
type FormDisplayDateProps = InputWrapperProps & {
5+
date: dayjs.ConfigType;
6+
};
7+
export const FormDisplayDate = (props: FormDisplayDateProps) => {
8+
const { date, ...rest } = props;
9+
return (
10+
<InputWrapper {...rest}>
11+
<Text size="sm" c="gray.6">
12+
{date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'}
13+
</Text>
14+
</InputWrapper>
15+
);
16+
};

src/components/form-slice/FormPartBasic.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,13 @@ import { FormItemTextarea } from '../form/Textarea';
66
import { useFormContext } from 'react-hook-form';
77
import type { A6Type } from '@/types/schema/apisix';
88

9-
export type FormPartBasicProps = Omit<FormSectionProps, 'form'> & {
10-
showId?: boolean;
11-
};
9+
export type FormPartBasicProps = Omit<FormSectionProps, 'form'>;
1210

1311
export const FormPartBasic = (props: FormPartBasicProps) => {
14-
const { showId, ...restProps } = props;
15-
const { control } = useFormContext<A6Type['Basic'] & { id?: string }>();
12+
const { control } = useFormContext<A6Type['Basic']>();
1613
const { t } = useTranslation();
1714
return (
18-
<FormSection legend={t('form.basic.title')} {...restProps}>
19-
{showId && (
20-
<FormItemTextInput
21-
name="id"
22-
label="ID"
23-
control={control}
24-
readOnly
25-
disabled
26-
/>
27-
)}
15+
<FormSection legend={t('form.basic.title')} {...props}>
2816
<FormItemTextInput
2917
name="name"
3018
label={t('form.basic.name')}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { A6Type } from '@/types/schema/apisix';
2+
import { useFormContext, type FieldValues } from 'react-hook-form';
3+
import { useTranslation } from 'react-i18next';
4+
import {
5+
FileUploadTextarea,
6+
type FileUploadTextareaProps,
7+
} from '../form/FileUploadTextarea';
8+
9+
export const FormPartProto = <T extends FieldValues>(
10+
props: Pick<FileUploadTextareaProps<T>, 'allowUpload'>
11+
) => {
12+
const { t } = useTranslation();
13+
const form = useFormContext<A6Type['ProtoPost']>();
14+
return (
15+
<FileUploadTextarea
16+
name="content"
17+
label={t('protos.form.content')}
18+
placeholder={t('protos.form.contentPlaceholder')}
19+
control={form.control}
20+
minRows={10}
21+
acceptFileTypes=".proto,.pb"
22+
{...props}
23+
/>
24+
);
25+
};

src/components/form-slice/FormPartUpstream/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,11 @@ export const FormSectionKeepAlive = () => {
196196
</FormSection>
197197
);
198198
};
199-
export const FormPartUpstream = ({ showId }: { showId?: boolean }) => {
199+
export const FormPartUpstream = () => {
200200
const { t } = useTranslation();
201201
return (
202202
<>
203-
<FormPartBasic showId={showId} />
203+
<FormPartBasic />
204204
<FormSection legend={t('form.upstream.findUpstreamFrom')}>
205205
<FormSection legend={t('form.upstream.nodes.title')}>
206206
<FormItemNodes
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useFormContext, useWatch } from 'react-hook-form';
2+
import type { A6Type } from '@/types/schema/apisix';
3+
import { FormItemTextInput } from '../form/TextInput';
4+
import { FormSection } from './FormSection';
5+
import { useTranslation } from 'react-i18next';
6+
import { FormDisplayDate } from './FormDisplayDate';
7+
import { Divider } from '@mantine/core';
8+
9+
export const FormSectionInfo = () => {
10+
const { control } = useFormContext<A6Type['Info']>();
11+
const { t } = useTranslation();
12+
const createTime = useWatch({ control, name: 'create_time' });
13+
const updateTime = useWatch({ control, name: 'update_time' });
14+
return (
15+
<FormSection legend={t('form.info.title')}>
16+
<FormItemTextInput
17+
control={control}
18+
name="id"
19+
label="ID"
20+
readOnly
21+
disabled
22+
/>
23+
<Divider my="lg" />
24+
<FormDisplayDate date={createTime} label={t('form.info.create_time')} />
25+
<FormDisplayDate date={updateTime} label={t('form.info.update_time')} />
26+
</FormSection>
27+
);
28+
};
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
Textarea as MTextarea,
3+
type TextareaProps as MTextareaProps,
4+
Box,
5+
Group,
6+
Button,
7+
Input,
8+
} from '@mantine/core';
9+
import {
10+
useController,
11+
type FieldValues,
12+
type UseControllerProps,
13+
} from 'react-hook-form';
14+
import { genControllerProps } from './util';
15+
import IconUpload from '~icons/material-symbols/upload';
16+
import { useRef, useState } from 'react';
17+
import { useTranslation } from 'react-i18next';
18+
19+
export type FileUploadTextareaProps<T extends FieldValues> = UseControllerProps<T> &
20+
MTextareaProps & {
21+
acceptFileTypes?: string;
22+
uploadButtonText?: string;
23+
maxFileSize?: number;
24+
onFileLoaded?: (content: string, fileName: string) => void;
25+
allowUpload?: boolean;
26+
};
27+
28+
export const FileUploadTextarea = <T extends FieldValues>(
29+
props: FileUploadTextareaProps<T>
30+
) => {
31+
const { allowUpload = true } = props;
32+
const { controllerProps, restProps } = genControllerProps(props, '');
33+
const {
34+
field: { value, onChange: fOnChange, ...restField },
35+
fieldState,
36+
} = useController<T>(controllerProps);
37+
const { t } = useTranslation();
38+
39+
const [fileError, setFileError] = useState<string | null>(null);
40+
const fileInputRef = useRef<HTMLInputElement>(null);
41+
42+
const {
43+
acceptFileTypes = '.txt,.json,.yaml,.yml',
44+
uploadButtonText = '',
45+
maxFileSize = 10 * 1024 * 1024,
46+
onFileLoaded,
47+
onChange,
48+
...textareaProps
49+
} = restProps;
50+
51+
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
52+
const file = event.target.files?.[0];
53+
if (!file) return;
54+
55+
if (file.size > maxFileSize) {
56+
const size = Math.round(maxFileSize / 1024 / 1024);
57+
setFileError(`${t('form.upload.fileOverSize')} ${size}MB`);
58+
return;
59+
}
60+
61+
setFileError(null);
62+
if (fileInputRef.current) fileInputRef.current.value = '';
63+
64+
const reader = new FileReader();
65+
reader.onload = (e) => {
66+
const content = e.target?.result as string;
67+
fOnChange(content);
68+
onFileLoaded?.(content, file.name);
69+
};
70+
71+
reader.onerror = (e) => {
72+
setFileError(`${t('form.upload.readError')} ${e.target?.error}`);
73+
};
74+
75+
reader.readAsText(file);
76+
};
77+
78+
return (
79+
<Box>
80+
<MTextarea
81+
value={value}
82+
onChange={(e) => {
83+
fOnChange(e);
84+
onChange?.(e);
85+
}}
86+
resize="vertical"
87+
autosize={restField.disabled}
88+
{...restField}
89+
{...textareaProps}
90+
/>
91+
{allowUpload && (
92+
<Group mb="xs" mt={4}>
93+
<Button
94+
leftSection={<IconUpload />}
95+
size="compact-xs"
96+
variant="outline"
97+
onClick={() => fileInputRef.current?.click()}
98+
disabled={restField.disabled}
99+
>
100+
{uploadButtonText || t('form.btn.upload')}
101+
</Button>
102+
<input
103+
type="file"
104+
accept={acceptFileTypes}
105+
onChange={handleFileChange}
106+
style={{ display: 'none' }}
107+
ref={fileInputRef}
108+
/>
109+
</Group>
110+
)}
111+
<Input.Error>{fieldState.error?.message || fileError}</Input.Error>
112+
</Box>
113+
);
114+
};

src/config/constant.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const LOCAL_STORAGE_ADMIN_KEY = 'apisix-admin-key';
55
export const API_PREFIX = '/apisix/admin';
66
export const API_ROUTES = '/routes';
77
export const API_UPSTREAMS = '/upstreams';
8+
export const API_PROTOS = '/protos';
89

910
export const APPSHELL_HEADER_HEIGHT = 60;
1011
export const APPSHELL_NAVBAR_WIDTH = 250;

src/config/navRoutes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ export const navRoutes: NavRoute[] = [
3434
to: '/secret',
3535
label: 'secret',
3636
},
37+
{
38+
to: '/protos',
39+
label: 'protos',
40+
},
3741
];

0 commit comments

Comments
 (0)