Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【GLCC】Higress Console 支持通过表单配置 Wasm 插件 #322

Merged
merged 25 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
13 changes: 10 additions & 3 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@ice/plugin-request": "^1.0.0",
"@ice/plugin-store": "^1.0.0",
"@iceworks/spec": "^1.0.0",
"@types/js-yaml": "^4.0.9",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,10 @@
"seconds": "Sec(s)",
"tbd": "Still in development. To be released soon...",
"yes": "Yes",
"no": "No"
"no": "No",
"switchToYAML": "YAML View",
"switchToForm": "Form View",
"isRequired": "is required",
"invalidSchema": "Since schema information cannot be properly parsed, this plugin only supports YAML editing."
}
}
6 changes: 5 additions & 1 deletion frontend/src/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,10 @@
"seconds": "秒",
"tbd": "页面开发中,即将推出...",
"yes": "是",
"no": "否"
"no": "否",
"switchToYAML": "YAML视图",
"switchToForm": "表单视图",
"isRequired": "是必填的",
"invalidSchema": "由于 schema 信息无法正常解析,本插件只支持 YAML 编辑方式。"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.ant-empty-normal {
margin: 0;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Select, Table } from 'antd';
import type { FormInstance } from 'antd/es/form';
import { uniqueId } from 'lodash';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './index.module.css';
import i18next from 'i18next';

const EditableContext = React.createContext<FormInstance<any> | null>(null);

interface Item {
key: string;
}

interface EditableRowProps {
index: number;
}

const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};

interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
children: React.ReactNode;
dataIndex: keyof Item;
record: Item;
nodeType: string;
handleSave: (record: Item) => void;
}

const EditableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
nodeType,
record,
handleSave,
...restProps
}) => {
const { t } = useTranslation();
const [editing, setEditing] = useState(true);
const inputRef = useRef(null);
const form = useContext(EditableContext)!;

const matchOptions = ['PRE', 'EQUAL', 'REGULAR'].map((v) => {
return { label: t(`route.matchTypes.${v}`), value: v };
});

useEffect(() => {
form.setFieldsValue({ ...record });
}, [editing]);

const save = async () => {
try {
const values = await form.validateFields();
handleSave({ ...record, ...values });
} catch (errInfo) {
handleSave({ ...record, ...form.getFieldsValue() });
CH3CHO marked this conversation as resolved.
Show resolved Hide resolved
}
};

let childNode = children;
let node;

const handleInputChange = (name, value) => {
form.setFieldValue(name, value);
};

switch (nodeType) {
CH3CHO marked this conversation as resolved.
Show resolved Hide resolved
case 'string':
node = (
<Input
ref={inputRef}
onPressEnter={save}
onBlur={save}
onChange={(e) => handleInputChange(dataIndex, e.target.value)}
/>
);
break;
case 'integer':
node = (
<Input
type="number"
ref={inputRef}
onPressEnter={save}
onBlur={save}
onChange={(e) => handleInputChange(dataIndex, parseInt(e.target.value, 10))}
/>
);
break;
case 'number':
node = (
<Input
type="number"
step="any"
ref={inputRef}
onPressEnter={save}
onBlur={save}
onChange={(e) => handleInputChange(dataIndex, parseFloat(e.target.value))}
/>
)
break;
case 'boolean':
node = (
<Select ref={inputRef} onPressEnter={save} onBlur={save}>
CH3CHO marked this conversation as resolved.
Show resolved Hide resolved
<Select.Option value={true}>true</Select.Option>
<Select.Option value={false}>false</Select.Option>
</Select>
);
break;
default:
node = (
<Input
ref={inputRef}
onPressEnter={save}
onBlur={save}
onChange={(e) => handleInputChange(dataIndex, e.target.value)}
/>
);
}

if (editable) {
childNode = (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[
{
required: true,
message: `${title} ` + `${t('misc.isRequired')}`
},
]}
>
{node}
</Form.Item>
);
}

return <td {...restProps}>{childNode}</td>;
};

type EditableTableProps = Parameters<typeof Table>[0];

interface DataType {
uid: number;
}

type ColumnTypes = Exclude<EditableTableProps['columns'], undefined>;

const ArrayForm: React.FC = ({ array, value, onChange }) => {
const { t } = useTranslation();

const initDataSource = value || [];
for (const item of initDataSource) {
if (!item.uid) {
item.uid = uniqueId();
}
}

const [dataSource, setDataSource] = useState<DataType[]>(value || []);

function getLocalizedText(obj: any, index: string, defaultText: string) {
const i18nObj = obj[`x-${index}-i18n`];
return i18nObj && i18nObj[i18next.language] || obj[index] || defaultText || '';
}

const defaultColumns: any[] = [];
if (array.type === 'object') {
Object.entries(array.properties).forEach(([key, prop]) => {
let translatedTitle = getLocalizedText(prop, 'title', key);
const isRequired = (array.required || []).includes(key);
defaultColumns.push({
title: translatedTitle,
dataIndex: key,
editable: true,
required: isRequired,
nodeType: prop.type,
});
});
} else {
defaultColumns.push({
title: t(array.title),
dataIndex: 'Item',
editable: true,
nodeType: array.type,
});
}

defaultColumns.push({
dataIndex: 'operation',
width: 60,
render: (_, record: { uid: number }) =>
(dataSource.length >= 1 ? (
<div onClick={() => handleDelete(record.uid)}>
<DeleteOutlined />
</div>
) : null),
});

const handleAdd = () => {
const newData: DataType = {
uid: uniqueId(),
};
setDataSource([...dataSource, newData]);
onChange([...dataSource, newData]);
};

const handleDelete = (uid: number) => {
const newData = dataSource.filter((item) => item.uid !== uid);
setDataSource(newData);
onChange(newData);
};

const handleSave = (row: DataType) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.uid === item.uid);
const item = newData[index];
newData.splice(index, 1, {
CH3CHO marked this conversation as resolved.
Show resolved Hide resolved
...item,
...row,
});
setDataSource(newData);
onChange(newData);
};

const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};

const columns = defaultColumns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record: DataType) => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
required: col.required,
nodeType: col.dataIndex === 'matchType' ? 'select' : 'input',
handleSave,
}),
};
});

return (
<div>
<Table
components={components}
size="small"
className={styles.factor}
dataSource={dataSource}
columns={columns as ColumnTypes}
pagination={false}
rowKey={(record) => record.uid}
/>
<Button onClick={handleAdd} type="link">
<PlusOutlined />
</Button>
</div>
);
};

export default ArrayForm;
Loading