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 6 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,5 @@
.factor :global {
Copy link
Collaborator

Choose a reason for hiding this comment

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

这个 :global 的意图是啥?

.ant-empty-normal {
margin: 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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: 'select' | 'input';
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) {
console.error('Save failed: ', errInfo);
}
};

let childNode = children;

const node = <Input ref={inputRef} onPressEnter={save} onBlur={save} />

if (editable) {
childNode = (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[
{
required: true,
},
]}
>
{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, key: string) {
const i18nObj = obj[`x-${index}-i18n`];
if(i18next.language === 'en-US') return i18nObj && i18nObj[i18next.language] || key || '';
else return i18nObj && i18nObj[i18next.language] || obj[index] || '';
}

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,
});
});
} else {
defaultColumns.push({
title: t(array.title),
dataIndex: 'Item',
editable: true,
});
}

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;
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@ export interface IPropsData {
name?: string;
category: string;
}

export interface IProps {
data: IPropsData;
onSuccess: () => void;
sharedData: string;
setSharedData: (newData: string) => void;
enabled: boolean;
setEnabled: (newEnabled: boolean) => void;
currentTabKey: string
}

const GlobalPluginDetail = forwardRef((props: IProps, ref) => {
const { data, onSuccess } = props;
const { data, onSuccess, sharedData, setSharedData, enabled, setEnabled, currentTabKey } = props;
const { name: pluginName = '', category = '' } = data || {};

const [searchParams] = useSearchParams();
Expand Down Expand Up @@ -83,7 +89,7 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => {
onSuccess: (res: IPluginData) => {
setPluginData(res);
setRawConfigurations(res.rawConfigurations);
setDefaultValue(res.rawConfigurations);
setDefaultValue(sharedData);
getConfig(pluginName);
},
});
Expand All @@ -101,7 +107,7 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => {
setDefaultValue(exampleRaw);
}
form.setFieldsValue({
enabled: pluginData?.enabled,
enabled: enabled,
});
},
});
Expand Down Expand Up @@ -151,6 +157,15 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => {
getData(pluginName);
}, [pluginName, queryName]);

useEffect(() => {
if (currentTabKey === "yaml" && sharedData) {
setDefaultValue(sharedData);
form.setFieldsValue({
enabled: enabled,
});
}
}, [currentTabKey]);

useImperativeHandle(ref, () => ({
submit: onSubmit,
}));
Expand All @@ -168,14 +183,20 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => {
{alertStatus.isShow && (
<Alert style={{ marginBottom: '10px' }} message={alertStatus.message} type="warning" showIcon />
)}
<Form name="basic" form={form} autoComplete="off">
<Form name="basic" form={form} autoComplete="off" layout="vertical" >
<Form.Item label={t('plugins.configForm.enableStatus')} name="enabled" valuePropName="checked">
<Switch />
<Switch onChange={(val) =>{
setEnabled(val);
}}/>
</Form.Item>

<Divider orientation="left">{t('plugins.configForm.dataEditor')}</Divider>
{!getConfigLoading && !getDataLoading && (
<CodeEditor defaultValue={defaultValue} onChange={(val) => setRawConfigurations(val)} />
<CodeEditor key={defaultValue} defaultValue={defaultValue}
onChange={(val) => {
setRawConfigurations(val);
setSharedData(val);
}} />
)}
{!getConfigLoading && !getDataLoading && !isRoutePlugin && !isDomainPlugin && (
<Space direction="horizontal" style={{ marginTop: "0.5rem" }}>
Expand All @@ -188,4 +209,4 @@ const GlobalPluginDetail = forwardRef((props: IProps, ref) => {
);
});

export default GlobalPluginDetail;
export default GlobalPluginDetail;
Loading