You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/03/21 01:49:23 UTC
[incubator-apisix-dashboard] branch next updated: feat: added
Plugin typings (#157)
This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch next
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git
The following commit(s) were added to refs/heads/next by this push:
new ff0459c feat: added Plugin typings (#157)
ff0459c is described below
commit ff0459c58d30db74196395141a107f9866af1dde
Author: 琚致远 <ju...@apache.org>
AuthorDate: Sat Mar 21 09:49:15 2020 +0800
feat: added Plugin typings (#157)
* feat: added Plugin typings
* style: ajust property position
* style: adjust interface
* feat: added plugin validator
* feat: update validator rule
* feat: destruction object
* feat: added array selector
* feat: update schema
* feat: uuid for key-auth
* feat: added transform for plugin
* codes clean
* feat: added modal title
* feat: refactor transformPropertyToRules
* feat: transform node-status
* feat: transform heartbeat
* style: form layout
* fix: object value
* fix: maxItems
* feat: refactor ArrayComponent
* refactor: PluginForm
* feat: added key
* feat: added base & publicPath
* feat: added i18n
---
config/config.ts | 2 +
package.json | 28 ++++--
src/components/PluginForm/index.tsx | 177 +++++++++++++++++++++++++++++++++++
src/components/PluginModal/index.tsx | 28 ++++++
src/locales/en-US/component.ts | 2 +
src/locales/zh-CN/component.ts | 2 +
src/services/plugin.ts | 7 ++
src/transforms/plugin.ts | 88 +++++++++++++++++
src/typings.d.ts | 45 ++++++++-
9 files changed, 372 insertions(+), 7 deletions(-)
diff --git a/config/config.ts b/config/config.ts
index 369ca8c..ca064fe 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -86,6 +86,8 @@ export default {
targets: {
ie: 11,
},
+ base: '/dashboard/',
+ publicPath: '/dashboard/',
// umi routes: https://umijs.org/zh/guide/router.html
routes: [
{
diff --git a/package.json b/package.json
index 8f08c2b..c8d9ec2 100644
--- a/package.json
+++ b/package.json
@@ -31,13 +31,23 @@
"tsc": "tsc",
"ui": "umi ui"
},
- "husky": { "hooks": { "pre-commit": "npm run lint-staged" } },
+ "husky": {
+ "hooks": {
+ "pre-commit": "npm run lint-staged"
+ }
+ },
"lint-staged": {
"**/*.less": "stylelint --syntax less",
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
- "**/*.{js,jsx,tsx,ts,less,md,json}": ["prettier --write"]
+ "**/*.{js,jsx,tsx,ts,less,md,json}": [
+ "prettier --write"
+ ]
},
- "browserslist": ["> 1%", "last 2 versions", "not ie <= 10"],
+ "browserslist": [
+ "> 1%",
+ "last 2 versions",
+ "not ie <= 10"
+ ],
"dependencies": {
"@ant-design/icons": "^4.0.0-alpha.19",
"@ant-design/pro-layout": "^5.0.0",
@@ -62,7 +72,8 @@
"umi-plugin-pro-block": "^1.3.2",
"umi-plugin-react": "^1.14.10",
"umi-request": "^1.0.8",
- "use-merge-value": "^1.0.1"
+ "use-merge-value": "^1.0.1",
+ "uuid": "^7.0.2"
},
"devDependencies": {
"@ant-design/pro-cli": "^1.0.18",
@@ -75,6 +86,7 @@
"@types/react": "^16.9.17",
"@types/react-dom": "^16.8.4",
"@types/react-helmet": "^5.0.13",
+ "@types/uuid": "^7.0.0",
"@umijs/fabric": "^2.0.2",
"chalk": "^3.0.0",
"cross-env": "^7.0.0",
@@ -96,8 +108,12 @@
"umi-plugin-pro": "^1.0.2",
"umi-types": "^0.5.9"
},
- "optionalDependencies": { "puppeteer": "^2.0.0" },
- "engines": { "node": ">=10.0.0" },
+ "optionalDependencies": {
+ "puppeteer": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
"checkFiles": [
"src/**/*.js*",
"src/**/*.ts*",
diff --git a/src/components/PluginForm/index.tsx b/src/components/PluginForm/index.tsx
new file mode 100644
index 0000000..5e7799a
--- /dev/null
+++ b/src/components/PluginForm/index.tsx
@@ -0,0 +1,177 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Input, Switch, Select, InputNumber, Button } from 'antd';
+import { useForm } from 'antd/es/form/util';
+import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+import { fetchPluginSchema } from '@/services/plugin';
+import { transformPropertyToRules } from '@/transforms/plugin';
+
+interface Props {
+ name: string;
+ initialData?: PluginSchema;
+ onFinish(values: any): void;
+}
+
+const formLayout = {
+ labelCol: { span: 10 },
+ wrapperCol: { span: 14 },
+};
+
+interface RenderComponentProps {
+ placeholder?: string;
+}
+
+const renderComponentByProperty = (
+ propertyValue: PluginProperty,
+ restProps?: RenderComponentProps,
+) => {
+ const { type, minimum, maximum } = propertyValue;
+
+ if (type === 'string') {
+ if (propertyValue.enum) {
+ return (
+ <Select>
+ {propertyValue.enum.map(enumValue => (
+ <Select.Option value={enumValue} key={enumValue}>
+ {enumValue}
+ </Select.Option>
+ ))}
+ </Select>
+ );
+ }
+ return <Input {...restProps} />;
+ }
+
+ if (type === 'boolean') {
+ return <Switch />;
+ }
+
+ if (type === 'number' || type === 'integer') {
+ return (
+ <InputNumber
+ min={minimum ?? Number.MIN_SAFE_INTEGER}
+ max={maximum ?? Number.MAX_SAFE_INTEGER}
+ />
+ );
+ }
+
+ return <Input {...restProps} />;
+};
+
+interface ArrayComponentProps {
+ schema: PluginSchema;
+ propertyName: string;
+ propertyValue: PluginProperty;
+}
+
+const ArrayComponent: React.FC<ArrayComponentProps> = ({
+ propertyName,
+ propertyValue,
+ schema,
+ children,
+}) => (
+ <Form.List key={propertyName} name={propertyName}>
+ {(fields, { add, remove }) => (
+ <>
+ {fields.map((field, index) => (
+ <Form.Item
+ key={field.key}
+ rules={transformPropertyToRules(schema!, propertyName, propertyValue)}
+ label={`${propertyName}-${index + 1}`}
+ >
+ {children}
+ {fields.length > 1 ? (
+ <MinusCircleOutlined onClick={() => remove(field.name)} />
+ ) : (
+ <React.Fragment />
+ )}
+ </Form.Item>
+ ))}
+ {/* BUG: There should also care about minItems */}
+ {fields.length < (propertyValue.maxItems ?? Number.MAX_SAFE_INTEGER) ? (
+ <Form.Item label={propertyName}>
+ <Button type="dashed" onClick={add}>
+ <PlusOutlined /> {formatMessage({ id: 'component.global.add' })}
+ </Button>
+ </Form.Item>
+ ) : null}
+ </>
+ )}
+ </Form.List>
+);
+
+const PluginForm: React.FC<Props> = ({ name, initialData = {}, onFinish }) => {
+ const [schema, setSchema] = useState<PluginSchema>();
+ const [form] = useForm();
+
+ useEffect(() => {
+ if (name) {
+ fetchPluginSchema(name).then(data => {
+ setSchema(data);
+ console.log(name, data);
+
+ const propertyDefaultData = {};
+ Object.entries(data.properties || {}).forEach(([propertyName, propertyValue]) => {
+ if (propertyValue.hasOwnProperty('default')) {
+ propertyDefaultData[propertyName] = propertyValue.default;
+ }
+ });
+ form.setFieldsValue(propertyDefaultData);
+
+ requestAnimationFrame(() => {
+ form.setFieldsValue(initialData);
+ });
+ });
+ }
+ }, [name]);
+
+ return (
+ <Form {...formLayout} form={form} onFinish={onFinish} labelAlign="left">
+ {Object.entries(schema?.properties || {}).map(([propertyName, propertyValue]) => {
+ // eslint-disable-next-line arrow-body-style
+ if (propertyValue.type === 'array') {
+ return (
+ <ArrayComponent
+ key={propertyName}
+ schema={schema!}
+ propertyName={propertyName}
+ propertyValue={propertyValue}
+ >
+ {renderComponentByProperty({ type: 'string' })}
+ </ArrayComponent>
+ );
+ }
+
+ if (propertyValue.type === 'object') {
+ return (
+ <ArrayComponent
+ key={propertyName}
+ schema={schema!}
+ propertyName={propertyName}
+ propertyValue={propertyValue}
+ >
+ {/* TODO: there should not be fixed value, and it should receive custom key */}
+ {renderComponentByProperty({ type: 'string' }, { placeholder: 'Header' })}
+ {renderComponentByProperty({ type: 'string' }, { placeholder: 'Value' })}
+ </ArrayComponent>
+ );
+ }
+
+ return (
+ <Form.Item
+ label={propertyName}
+ name={propertyName}
+ key={propertyName}
+ rules={transformPropertyToRules(schema!, propertyName, propertyValue)}
+ valuePropName={propertyValue.type === 'boolean' ? 'checked' : 'value'}
+ >
+ {renderComponentByProperty(propertyValue)}
+ </Form.Item>
+ );
+ })}
+ </Form>
+ );
+};
+
+export default PluginForm;
diff --git a/src/components/PluginModal/index.tsx b/src/components/PluginModal/index.tsx
new file mode 100644
index 0000000..dfdd4d0
--- /dev/null
+++ b/src/components/PluginModal/index.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { Modal } from 'antd';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+import PluginForm from '@/components/PluginForm';
+
+interface Props {
+ visible: boolean;
+ name: string;
+ initialData?: PluginSchema;
+ onFinish(values: any): void;
+}
+
+const PluginModal: React.FC<Props> = props => {
+ const { name, visible } = props;
+
+ return (
+ <Modal
+ destroyOnClose
+ visible={visible}
+ title={`${formatMessage({ id: 'component.global.edit.plugin' })} ${name}`}
+ >
+ <PluginForm {...props} />
+ </Modal>
+ );
+};
+
+export default PluginModal;
diff --git a/src/locales/en-US/component.ts b/src/locales/en-US/component.ts
index 112f2ef..0157420 100644
--- a/src/locales/en-US/component.ts
+++ b/src/locales/en-US/component.ts
@@ -6,11 +6,13 @@ export default {
'component.global.cancel': 'Cancel',
'component.global.submit': 'Submit',
'component.global.create': 'Create',
+ 'component.global.add': 'Add',
'component.global.save': 'Save',
'component.global.edit': 'Edit',
'component.global.action': 'Action',
'component.global.update': 'Update',
'component.global.get': 'Get',
+ 'component.global.edit.plugin': 'Edit plugin',
'component.status.success': 'Successfully',
'component.status.fail': 'Failed',
// SSL Module
diff --git a/src/locales/zh-CN/component.ts b/src/locales/zh-CN/component.ts
index 3e77ed0..5999e3c 100644
--- a/src/locales/zh-CN/component.ts
+++ b/src/locales/zh-CN/component.ts
@@ -6,11 +6,13 @@ export default {
'component.global.cancel': '取消',
'component.global.submit': '提交',
'component.global.create': '创建',
+ 'component.global.add': '增加',
'component.global.save': '保存',
'component.global.edit': '编辑',
'component.global.action': '操作',
'component.global.update': '更新',
'component.global.get': '获取',
+ 'component.global.edit.plugin': '编辑插件',
'component.status.success': '成功',
'component.status.fail': '失败',
// SSL 模块
diff --git a/src/services/plugin.ts b/src/services/plugin.ts
new file mode 100644
index 0000000..b75f23f
--- /dev/null
+++ b/src/services/plugin.ts
@@ -0,0 +1,7 @@
+import request from '@/utils/request';
+import { transformSchemaFromAPI } from '@/transforms/plugin';
+
+export const fetchList = () => request('/api/plugins/list');
+
+export const fetchPluginSchema = (name: string): Promise<PluginSchema> =>
+ request(`/api/schema/plugins/${name}`).then(data => transformSchemaFromAPI(data, name));
diff --git a/src/transforms/plugin.ts b/src/transforms/plugin.ts
new file mode 100644
index 0000000..39b66f7
--- /dev/null
+++ b/src/transforms/plugin.ts
@@ -0,0 +1,88 @@
+import { v4 as uuidv4 } from 'uuid';
+import { Rule } from 'antd/es/form';
+
+/**
+ * Transform schema data from API for target plugin.
+ */
+export const transformSchemaFromAPI = (schema: PluginSchema, pluginName: string): PluginSchema => {
+ if (pluginName === 'key-auth') {
+ return {
+ ...schema,
+ properties: {
+ key: {
+ ...schema.properties!.key,
+ default: uuidv4(),
+ },
+ },
+ };
+ }
+
+ if (pluginName === 'prometheus' || pluginName === 'node-status' || pluginName === 'heartbeat') {
+ return {
+ ...schema,
+ properties: {
+ enabled: {
+ // TODO: i18n
+ type: 'boolean',
+ default: false,
+ },
+ },
+ };
+ }
+
+ return schema;
+};
+
+/**
+ * Transform schema data to be compatible with API.
+ */
+// eslint-disable-next-line arrow-body-style
+export const transformSchemaToAPI = (schema: PluginSchema, pluginName: string) => {
+ return { schema, pluginName };
+};
+
+/**
+ * Transform schema's property to rules.
+ */
+export const transformPropertyToRules = (
+ schema: PluginSchema,
+ propertyName: string,
+ propertyValue: PluginProperty,
+): Rule[] => {
+ if (!schema) {
+ return [];
+ }
+
+ const { type, minLength, maxLength, minimum, maximum, pattern } = propertyValue;
+
+ const requiredRule = schema.required?.includes(propertyName) ? [{ required: true }] : [];
+ const typeRule = [{ type }];
+ const enumRule = propertyValue.enum ? [{ type: 'enum', enum: propertyValue.enum }] : [];
+ const rangeRule =
+ type !== 'string' &&
+ type !== 'array' &&
+ (propertyValue.hasOwnProperty('minimum') || propertyValue.hasOwnProperty('maximum'))
+ ? [
+ {
+ min: minimum ?? Number.MIN_SAFE_INTEGER,
+ max: maximum ?? Number.MAX_SAFE_INTEGER,
+ },
+ ]
+ : [];
+ const lengthRule =
+ type === 'string' || type === 'array'
+ ? [{ min: minLength ?? Number.MIN_SAFE_INTEGER, max: maxLength ?? Number.MAX_SAFE_INTEGER }]
+ : [];
+ const customPattern = pattern ? [{ pattern: new RegExp(pattern) }] : [];
+
+ const rules = [
+ ...requiredRule,
+ ...typeRule,
+ ...enumRule,
+ ...rangeRule,
+ ...lengthRule,
+ ...customPattern,
+ ];
+ const flattend = rules.reduce((prev, next) => ({ ...prev, ...next }));
+ return [flattend] as Rule[];
+};
diff --git a/src/typings.d.ts b/src/typings.d.ts
index c2f9e64..b52e470 100644
--- a/src/typings.d.ts
+++ b/src/typings.d.ts
@@ -37,4 +37,47 @@ declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefine
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;
-declare type PageMode = 'CREATE' | 'EDIT' | 'VIEW';
+type PageMode = 'CREATE' | 'EDIT' | 'VIEW';
+
+interface PluginProperty {
+ type: 'number' | 'string' | 'integer' | 'array' | 'boolean' | 'object';
+ // the same as type
+ default?: any;
+ description?: string;
+ // NOTE: maybe 0.00001
+ minimum?: number;
+ maximum?: number;
+ minLength?: number;
+ maxLength?: number;
+ minItems?: number;
+ maxItems?: number;
+ // e.g "^/.*"
+ pattern?: string;
+ enum?: string[];
+ requried?: string[];
+ minProperties?: number;
+ additionalProperties?: boolean;
+ items?: {
+ type: string;
+ anyOf?: Array<{
+ type?: string;
+ description?: string;
+ enum?: string[];
+ pattern?: string;
+ }>;
+ };
+}
+
+interface PluginSchema {
+ type: 'object';
+ id?: string;
+ required?: string[];
+ additionalProperties?: boolean;
+ minProperties?: number;
+ oneOf?: Array<{
+ required: string[];
+ }>;
+ properties?: {
+ [propertyName: string]: PluginProperty;
+ };
+}