Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/apis/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { RoutePostType } from '@/components/form-slice/FormPartRoute/schema';
import { API_ROUTES } 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 const getRouteListQueryOptions = (props: PageSearchType) => {
const { page, pageSize } = props;
return queryOptions({
queryKey: ['routes', page, pageSize],
queryFn: () =>
req
.get<unknown, APISIXType['RespRouteList']>(API_ROUTES, {
params: { page, page_size: pageSize },
})
.then((v) => v.data),
});
};

export const postRouteReq = (data: RoutePostType) =>
req.post<unknown, APISIXType['RespRouteDetail']>(API_ROUTES, data);
2 changes: 1 addition & 1 deletion src/apis/upstreams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const getUpstreamReq = (id: string) =>

export const putUpstreamReq = (data: APISIXType['Upstream']) => {
const { id, ...rest } = data;
return req.put<APISIXType['Upstream'], APISIXType['RespGlobalRuleDetail']>(
return req.put<APISIXType['Upstream'], APISIXType['RespUpstreamDetail']>(
`${API_UPSTREAMS}/${id}`,
rest
);
Expand Down
17 changes: 12 additions & 5 deletions src/components/form-slice/FormPartBasic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,32 @@ import { FormItemTextInput } from '../form/TextInput';
import { FormItemTextarea } from '../form/Textarea';
import { useFormContext } from 'react-hook-form';
import type { APISIXType } from '@/types/schema/apisix';
import { useNamePrefix } from '@/utils/useNamePrefix';
import type { PropsWithChildren } from 'react';

export type FormPartBasicProps = Omit<FormSectionProps, 'form'>;
export type FormPartBasicProps = Omit<FormSectionProps, 'form'> &
PropsWithChildren;

export const FormPartBasic = (props: FormPartBasicProps) => {
const { children, ...restProps } = props;
const { control } = useFormContext<APISIXType['Basic']>();
const { t } = useTranslation();
const np = useNamePrefix();

return (
<FormSection legend={t('form.basic.title')} {...props}>
<FormSection legend={t('form.basic.title')} {...restProps}>
<FormItemTextInput
name="name"
name={np('name')}
label={t('form.basic.name')}
control={control}
/>
<FormItemTextarea
name="desc"
name={np('desc')}
label={t('form.basic.desc')}
control={control}
/>
<FormItemLabels name="labels" control={control} />
<FormItemLabels name={np('labels')} control={control} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I just tried it. I can only make it work by inputting , or by blurring it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enter should be able to work 🤔

{children}
</FormSection>
);
};
159 changes: 159 additions & 0 deletions src/components/form-slice/FormPartRoute/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FormItemNumberInput } from '@/components/form/NumberInput';
import { FormItemSwitch } from '@/components/form/Switch';
import { FormItemTagsInput } from '@/components/form/TagInput';
import { FormItemTextarea } from '@/components/form/Textarea';
import { FormItemTextInput } from '@/components/form/TextInput';
import { NamePrefixProvider } from '@/utils/useNamePrefix';
import { FormItemPlugins } from '../FormItemPlugins';
import { FormPartBasic } from '../FormPartBasic';
import { FormPartUpstream, FormSectionTimeout } from '../FormPartUpstream';
import { FormSection } from '../FormSection';
import { Divider, InputWrapper } from '@mantine/core';
import type { RoutePostType } from './schema';
import { FormItemSelect } from '@/components/form/Select';
import { APISIX } from '@/types/schema/apisix';
import { zGetDefault } from '@/utils/zod';

const FormPartBasicWithPriority = () => {
const { t } = useTranslation();
const { control } = useFormContext<RoutePostType>();
return (
<FormPartBasic>
<FormItemNumberInput
control={control}
name="priority"
label={t('form.route.priority')}
defaultValue={zGetDefault(APISIX.Route).priority!}
/>
<FormItemSelect
control={control}
name="status"
label={t('form.route.status')}
data={APISIX.RouteStatus.options.map((v) => v.value.toString())}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with this option list, but any plans to add an info icon and description for this field?

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After all pages are completed...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After all, it's strictly aligned with the API now.

defaultValue={APISIX.RouteStatus.options[1].value.toString()}
from={String}
to={Number}
/>
</FormPartBasic>
);
};

const FormSectionMatchRules = () => {
const { t } = useTranslation();
const { control } = useFormContext<RoutePostType>();
return (
<FormSection legend={t('form.route.matchRules')}>
<FormItemTagsInput
control={control}
name="methods"
label={t('form.route.methods')}
data={APISIX.HttpMethod.options.map((v) => v.value)}
searchValue=""
/>
<InputWrapper label={t('form.route.enableWebsocket')}>
<FormItemSwitch control={control} name="enable_websocket" />
</InputWrapper>
<FormItemTextInput
control={control}
name="uri"
label={t('form.route.uri')}
/>
<FormItemTagsInput
control={control}
name="uris"
label={t('form.route.uris')}
/>
<FormItemTextInput
control={control}
name="host"
label={t('form.route.host')}
/>
<FormItemTagsInput
control={control}
name="hosts"
label={t('form.route.hosts')}
/>
<FormItemTextInput
control={control}
name="remote_addr"
label={t('form.route.remoteAddr')}
/>
<FormItemTagsInput
control={control}
name="remote_addrs"
label={t('form.route.remoteAddrs')}
/>
<FormItemTagsInput
control={control}
name="vars"
label={t('form.route.vars')}
/>
<FormItemTextarea
control={control}
name="filter_func"
label={t('form.route.filterFunc')}
/>
</FormSection>
);
};

const FormSectionUpstream = () => {
const { t } = useTranslation();
const { control } = useFormContext<RoutePostType>();
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();
const { control } = useFormContext<RoutePostType>();
return (
<FormSection legend={t('form.plugins.label')}>
<FormItemTextInput
control={control}
name="plugin_config_id"
label={t('form.plugins.configId')}
/>
<Divider my="xs" label={t('or')} />
<FormItemPlugins name="plugins" />
</FormSection>
);
};

const FormSectionService = () => {
const { t } = useTranslation();
const { control } = useFormContext<RoutePostType>();
return (
<FormSection legend={t('form.route.service')}>
<FormItemTextInput
control={control}
name="service_id"
label={t('form.upstream.serviceId')}
/>
</FormSection>
);
};

export const FormPartRoute = () => {
return (
<>
<FormPartBasicWithPriority />
<FormSectionMatchRules />
<FormSectionService />
<FormSectionTimeout />
<FormSectionUpstream />
<FormSectionPlugins />
</>
);
};
10 changes: 10 additions & 0 deletions src/components/form-slice/FormPartRoute/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { APISIX } from '@/types/schema/apisix';
import type { z } from 'zod';

export const RoutePostSchema = APISIX.Route.omit({
id: true,
create_time: true,
update_time: true,
});

export type RoutePostType = z.infer<typeof RoutePostSchema>;
59 changes: 27 additions & 32 deletions src/components/form-slice/FormPartUpstream/FormItemNodes.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { APISIX, type APISIXType } from '@/types/schema/apisix';
import { zGetDefault } from '@/utils/zod';
import {
EditableProTable,
nanoid,
type ProColumns,
} from '@ant-design/pro-components';
import { EditableProTable, type ProColumns } from '@ant-design/pro-components';
import { Button, InputWrapper, type InputWrapperProps } from '@mantine/core';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -19,6 +15,8 @@ import { genControllerProps } from '../../form/util';
import { AntdConfigProvider } from '@/config/antdConfigProvider';
import { observer, useLocalObservable } from 'mobx-react-lite';
import { toJS } from 'mobx';
import { useClickOutside } from '@mantine/hooks';
import { nanoid } from 'nanoid';

type DataSource = APISIXType['UpstreamNode'] & APISIXType['ID'];

Expand All @@ -41,13 +39,12 @@ const zValidateField = <T extends ZodRawShape, R extends keyof T>(
return Promise.reject(new Error(error.message));
};

const genId = nanoid;

const genRecord = () => {
const genRecord = (data?: DataSource | APISIXType['UpstreamNode']) => {
const d = data || zGetDefault(APISIX.UpstreamNode);
return {
id: genId(),
...zGetDefault(APISIX.UpstreamNode),
};
id: nanoid(),
...d,
} as DataSource;
};

const objToUpstreamNodes = (data: APISIXType['UpstreamNodeObj']) => {
Expand All @@ -68,14 +65,7 @@ const parseToDataSource = (data: APISIXType['UpstreamNodeListOrObj']) => {
if (isNil(data)) val = [];
else if (Array.isArray(data)) val = data as APISIXType['UpstreamNodes'];
else val = objToUpstreamNodes(data as APISIXType['UpstreamNodeObj']);

return val.map((item) => {
const d: DataSource = {
id: `${item.host}-${item.port}-${item.weight}-${item.priority}`,
...item,
};
return d;
});
return val.map(genRecord);
};

const parseToUpstreamNodes = (data: DataSource[] | undefined) => {
Expand Down Expand Up @@ -175,6 +165,14 @@ const FormItemNodesCore = <T extends FieldValues>(
if (equals(toJS(this.values), data)) return;
this.values = data;
},
append(data: DataSource) {
this.values.push(data);
},
remove(id: string) {
const index = this.values.findIndex((item) => item.id === id);
if (index === -1) return;
this.values.splice(index, 1);
},
get editableKeys() {
return this.disabled ? [] : this.values.map((item) => item.id);
},
Expand All @@ -186,17 +184,19 @@ const FormItemNodesCore = <T extends FieldValues>(
ob.setDisabled(disabled);
}, [disabled, ob]);

const ref = useClickOutside(() => {
const vals = parseToUpstreamNodes(toJS(ob.values));
fOnChange?.(vals);
restProps.onChange?.(vals);
}, ['mouseup', 'touchend', 'mousedown', 'touchstart']);

return (
<InputWrapper
error={fieldState.error?.message}
label={label}
required={required}
withAsterisk={withAsterisk}
onBlur={() => {
const vals = parseToUpstreamNodes(ob.values);
fOnChange?.(vals);
restProps.onChange?.(vals);
}}
ref={ref}
>
<input name={fName} type="hidden" />
<AntdConfigProvider>
Expand All @@ -214,16 +214,14 @@ const FormItemNodesCore = <T extends FieldValues>(
onValuesChange(_, dataSource) {
ob.setValues(dataSource);
},
actionRender: (row, config) => {
actionRender: (row) => {
return [
<Button
key="delete"
variant="transparent"
size="compact-xs"
px={0}
onClick={() => {
config.onDelete?.(row.id, row);
}}
onClick={() => ob.remove(row.id)}
>
{t('form.btn.delete')}
</Button>,
Expand All @@ -239,10 +237,7 @@ const FormItemNodesCore = <T extends FieldValues>(
size="xs"
color="cyan"
style={{ borderColor: 'whitesmoke' }}
onClick={() => {
const d = genRecord();
ob.values.push(d);
}}
onClick={() => ob.append(genRecord())}
{...(disabled && { display: 'none' })}
>
{t('form.upstream.nodes.add')}
Expand Down
Loading