blob: 0bb13c13fff42dadf047a3b067c34cb45e5f85b7 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EditableProTable, type ProColumns } from '@ant-design/pro-components';
import { Button, InputWrapper, type InputWrapperProps } from '@mantine/core';
import { useClickOutside } from '@mantine/hooks';
import { toJS } from 'mobx';
import { useLocalObservable } from 'mobx-react-lite';
import { nanoid } from 'nanoid';
import { equals, isNil } from 'rambdax';
import { useEffect, useMemo } from 'react';
import {
type FieldValues,
useController,
type UseControllerProps,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import type { ZodObject, ZodRawShape } from 'zod';
import { AntdConfigProvider } from '@/config/antdConfigProvider';
import { APISIX, type APISIXType } from '@/types/schema/apisix';
import { zGetDefault } from '@/utils/zod';
import { genControllerProps } from '../../form/util';
type DataSource = APISIXType['UpstreamNode'] & APISIXType['ID'];
const zValidateField = <T extends ZodRawShape, R extends keyof T>(
zObj: ZodObject<T>,
field: R,
value: unknown
) => {
const fieldSchema = zObj.shape[field];
const res = fieldSchema.safeParse(value);
if (res.success) {
return Promise.resolve();
}
const error = res.error.issues[0];
return Promise.reject(new Error(error.message));
};
const genRecord = (data?: DataSource | APISIXType['UpstreamNode']) => {
const d = data || zGetDefault(APISIX.UpstreamNode);
return {
id: nanoid(),
...d,
} as DataSource;
};
const objToUpstreamNodes = (data: APISIXType['UpstreamNodeObj']) => {
return Object.entries(data).map(([key, val]) => {
const [host, port] = key.split(':');
const d: APISIXType['UpstreamNode'] = {
host,
port: Number(port) || 1,
weight: val,
priority: 0,
};
return d;
});
};
const parseToDataSource = (data: APISIXType['UpstreamNodeListOrObj']) => {
let val: APISIXType['UpstreamNodes'];
if (isNil(data)) val = [];
else if (Array.isArray(data)) val = data as APISIXType['UpstreamNodes'];
else val = objToUpstreamNodes(data as APISIXType['UpstreamNodeObj']);
return val.map(genRecord);
};
const parseToUpstreamNodes = (data: DataSource[] | undefined) => {
if (!data?.length) return [];
return data.map((item) => {
const d: APISIXType['UpstreamNode'] = {
host: item.host,
port: item.port,
weight: item.weight,
priority: item.priority,
};
return d;
});
};
const genProps = (field: keyof APISIXType['UpstreamNode']) => {
return {
rules: [
{
validator: (_: unknown, value: unknown) =>
zValidateField(APISIX.UpstreamNode, field, value),
},
],
};
};
export type FormItemNodesProps<T extends FieldValues> =
UseControllerProps<T> & {
onChange?: (value: APISIXType['UpstreamNode'][]) => void;
defaultValue?: APISIXType['UpstreamNode'][];
} & Pick<InputWrapperProps, 'label' | 'required' | 'withAsterisk'>;
export const FormItemNodes = <T extends FieldValues>(
props: FormItemNodesProps<T>
) => {
const { controllerProps, restProps } = useMemo(
() => genControllerProps(props),
[props]
);
const { t } = useTranslation();
const {
field: { value, onChange: fOnChange, name: fName, disabled },
fieldState,
} = useController<T>(controllerProps);
const columns = useMemo<ProColumns<DataSource>[]>(
() => [
{
title: 'id',
dataIndex: 'id',
hidden: true,
},
{
title: t('form.upstreams.nodes.host.title'),
dataIndex: 'host',
valueType: 'text',
formItemProps: genProps('host'),
},
{
title: t('form.upstreams.nodes.port.title'),
dataIndex: 'port',
valueType: 'digit',
formItemProps: genProps('port'),
render: (_, entity) => {
return entity.port.toString();
},
},
{
title: t('form.upstreams.nodes.weight.title'),
dataIndex: 'weight',
valueType: 'digit',
formItemProps: genProps('weight'),
render: (_, entity) => {
return entity.weight.toString();
},
},
{
title: t('form.upstreams.nodes.priority.title'),
dataIndex: 'priority',
valueType: 'digit',
formItemProps: genProps('priority'),
render: (_, entity) => {
return entity.priority?.toString() || '-';
},
},
{
title: t('form.upstreams.nodes.action.title'),
valueType: 'option',
width: 100,
hidden: disabled,
render: () => null,
},
],
[disabled, t]
);
const { label, required, withAsterisk } = props;
const ob = useLocalObservable(() => ({
disabled: false,
setDisabled(disabled: boolean | undefined) {
this.disabled = disabled || false;
},
values: [] as DataSource[],
setValues(data: DataSource[]) {
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);
},
}));
useEffect(() => {
ob.setValues(parseToDataSource(value));
}, [ob, value]);
useEffect(() => {
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}
ref={ref}
>
<input name={fName} type="hidden" />
<AntdConfigProvider>
<EditableProTable<DataSource>
defaultSize="small"
rowKey="id"
bordered
controlled={false}
value={ob.values}
recordCreatorProps={false}
columns={columns}
editable={{
type: 'multiple',
editableKeys: ob.editableKeys,
onValuesChange(_, dataSource) {
ob.setValues(dataSource);
},
actionRender: (row) => {
return [
<Button
key="delete"
variant="transparent"
size="compact-xs"
px={0}
onClick={() => ob.remove(row.id)}
>
{t('form.btn.delete')}
</Button>,
];
},
}}
/>
</AntdConfigProvider>
<Button
fullWidth
variant="default"
mt={8}
size="xs"
color="cyan"
style={{ borderColor: 'whitesmoke' }}
onClick={() => ob.append(genRecord())}
{...(disabled && { display: 'none' })}
>
{t('form.upstreams.nodes.add')}
</Button>
</InputWrapper>
);
};