Skip to content

Commit

Permalink
【GLCC】Higress Console 支持通过表单配置 Wasm 插件 (#322)
Browse files Browse the repository at this point in the history
  • Loading branch information
guluguluhhhh authored Oct 25, 2024
1 parent 76c6abd commit 0bfa6fa
Show file tree
Hide file tree
Showing 9 changed files with 991 additions and 24 deletions.
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 @@ -501,6 +501,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 @@ -545,6 +545,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;
}

289 changes: 289 additions & 0 deletions frontend/src/pages/plugin/components/PluginDrawer/ArrayForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
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;
required: boolean;
handleSave: (record: Item, valid: boolean) => void;
}

const EditableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
nodeType,
record,
required,
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 () => {
form.validateFields().then(values => {
handleSave({ ...record, ...values }, true);
}).catch(e => {
handleSave({ ...record, ...form.getFieldsValue() }, false);
})
};

let childNode = children;
let node;

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

switch (nodeType) {
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} onBlur={save}>
<Select.Option value>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,
message: `${title} ${t('misc.isRequired')}`,
},
]}
>
{node}
</Form.Item>
);
}

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

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

interface DataType {
uid: number;
new: boolean;
invalid: boolean;
}

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]) => {

Check warning on line 183 in frontend/src/pages/plugin/components/PluginDrawer/ArrayForm/index.tsx

View workflow job for this annotation

GitHub Actions / build (16.x)

[Critical] It is recommended to add polyfill for "Object.entries", This might be caused by a compatibility problem in "safari@9"
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 {
let translatedTitle = getLocalizedText(array, 'title', '');
defaultColumns.push({
title: translatedTitle,
dataIndex: 'Item',
editable: true,
required: 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(),
new: true,
invalid: true,
};
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, valid: boolean) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.uid === item.uid);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row,
new: false,
invalid: !valid,
});
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.nodeType,
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

0 comments on commit 0bfa6fa

Please sign in to comment.