This is an automated email from the ASF dual-hosted git repository.
sunyi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 938b2b9 feat: add plugin template config feature (#1540)
938b2b9 is described below
commit 938b2b9f34c5fa5e7ef6fddef29acb29b25c09a1
Author: litesun <[email protected]>
AuthorDate: Fri Mar 5 12:33:09 2021 +0800
feat: add plugin template config feature (#1540)
Co-authored-by: guoqqqi <[email protected]>
---
.gitignore | 3 +
web/config/routes.ts | 12 ++
web/cypress/fixtures/data.json | 8 +-
web/cypress/fixtures/selector.json | 7 +-
.../consumer/create_and_delete_consumer.spec.js | 9 +-
.../create-edit-delete-plugin-template.spec.js | 84 +++++++++++
.../create-plugin-template-with-route.spec.js | 102 +++++++++++++
.../route/create-edit-delete-route.spec.js | 5 +-
.../LabelsfDrawer}/LabelsDrawer.tsx | 17 ++-
.../locales/en-US.ts => LabelsfDrawer/index.ts} | 5 +-
web/src/components/Plugin/PluginPage.tsx | 78 ++++++++--
web/src/components/Plugin/locales/en-US.ts | 4 +
web/src/components/Plugin/locales/zh-CN.ts | 4 +
web/src/components/Plugin/service.ts | 6 +
web/src/helpers.tsx | 30 ++++
web/src/locales/en-US/menu.ts | 1 +
web/src/locales/zh-CN/menu.ts | 1 +
web/src/pages/PluginTemplate/Create.tsx | 121 ++++++++++++++++
web/src/pages/PluginTemplate/List.tsx | 160 +++++++++++++++++++++
.../PluginTemplate/components/Preview.tsx} | 23 ++-
.../components/Step1.tsx} | 109 +++++---------
web/src/pages/PluginTemplate/service.ts | 59 ++++++++
.../en-US.ts => pages/PluginTemplate/typing.d.ts} | 17 ++-
web/src/pages/Route/Create.tsx | 13 +-
web/src/pages/Route/List.tsx | 6 +-
.../Route/components/CreateStep4/CreateStep4.tsx | 8 +-
web/src/pages/Route/components/Step1/MetaView.tsx | 6 +-
web/src/pages/Route/components/Step3/index.tsx | 11 +-
web/src/pages/Route/constants.ts | 1 +
web/src/pages/Route/locales/en-US.ts | 1 +
web/src/pages/Route/locales/zh-CN.ts | 1 +
web/src/pages/Route/service.ts | 4 +-
web/src/pages/Route/transform.ts | 36 +----
web/src/pages/Route/typing.d.ts | 12 +-
web/src/typings.d.ts | 4 +
35 files changed, 798 insertions(+), 170 deletions(-)
diff --git a/.gitignore b/.gitignore
index 04f148e..1997f52 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,3 +51,6 @@ api/build-tools/apisix
api/coverage.txt
api/dag-to-lua/
+# frontend e2e test output
+web/.nyc_output
+web/coverage
diff --git a/web/config/routes.ts b/web/config/routes.ts
index 097102d..b47c7cd 100644
--- a/web/config/routes.ts
+++ b/web/config/routes.ts
@@ -100,6 +100,18 @@ const routes = [
component: './Setting',
},
{
+ path: '/plugin-template/list',
+ component: './PluginTemplate/List',
+ },
+ {
+ path: 'plugin-template/create',
+ component: './PluginTemplate/Create',
+ },
+ {
+ path: '/plugin-template/:id/edit',
+ component: './PluginTemplate/Create',
+ },
+ {
path: '/user/login',
component: './User/Login',
layout: false,
diff --git a/web/cypress/fixtures/data.json b/web/cypress/fixtures/data.json
index 536a71b..94be061 100644
--- a/web/cypress/fixtures/data.json
+++ b/web/cypress/fixtures/data.json
@@ -27,5 +27,11 @@
"updateSuccessfully": "Update Configuration Successfully",
"deleteSSLSuccess": "Remove target SSL successfully",
"sslErrorAlert": "key and cert don't match",
- "pluginErrorAlert": "Invalid plugin data"
+ "pluginErrorAlert": "Invalid plugin data",
+ "pluginTemplateName": "test_plugin_template1",
+ "pluginTemplateName2": "test_plugin_template2",
+ "createPluginTemplateSuccess": "Create Plugin Template Successfully",
+ "editPluginTemplateSuccess": "Edit Plugin Template Successfully",
+ "deletePluginTemplateSuccess": "Delete Plugin Template Successfully",
+ "pluginTemplateErrorAlert": "Request Error Code: 10000"
}
diff --git a/web/cypress/fixtures/selector.json
b/web/cypress/fixtures/selector.json
index ffb125a..e7b2abb 100644
--- a/web/cypress/fixtures/selector.json
+++ b/web/cypress/fixtures/selector.json
@@ -64,5 +64,10 @@
"passwordInput": "#control-ref_password",
"drawer": ".ant-drawer-content",
"codemirrorScroll": ".CodeMirror-scroll",
- "drawerClose": ".ant-drawer-close"
+ "drawerClose": ".ant-drawer-close",
+ "descriptionSelector": "[title=Description]",
+ "customSelector": "[title=Custom]",
+ "errorAlertClose": ".anticon-close",
+ "redirectURIInput": "#redirectURI",
+ "redirectCodeSelector": "#ret_code"
}
diff --git
a/web/cypress/integration/consumer/create_and_delete_consumer.spec.js
b/web/cypress/integration/consumer/create_and_delete_consumer.spec.js
index 69cd8c5..4cd5d51 100644
--- a/web/cypress/integration/consumer/create_and_delete_consumer.spec.js
+++ b/web/cypress/integration/consumer/create_and_delete_consumer.spec.js
@@ -36,7 +36,9 @@ context('Create and Delete Consumer', () => {
// plugin config
cy.contains(this.domSelector.pluginCard, 'key-auth').within(() => {
- cy.get('button').first().click();
+ cy.get('button').click({
+ force: true
+ });
});
cy.get(this.domSelector.disabledSwitcher).click();
@@ -91,7 +93,9 @@ context('Create and Delete Consumer', () => {
// plugin config
cy.contains(this.domSelector.pluginCard, 'key-auth').within(() => {
- cy.get('button').first().click();
+ cy.get('button').click({
+ force: true
+ });
});
// edit codeMirror
cy.get(this.domSelector.codeMirror)
@@ -107,4 +111,3 @@ context('Create and Delete Consumer', () => {
cy.get(this.domSelector.notification).should('contain',
this.data.pluginErrorAlert);
});
});
-
diff --git
a/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
new file mode 100644
index 0000000..1294c6c
--- /dev/null
+++
b/web/cypress/integration/pluginTemplate/create-edit-delete-plugin-template.spec.js
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+/* eslint-disable no-undef */
+
+context('Create Edit and Delete PluginTemplate', () => {
+ const timeout = 5000;
+ beforeEach(() => {
+ cy.login();
+
+ cy.fixture('selector.json').as('domSelector');
+ cy.fixture('data.json').as('data');
+ });
+
+ it('should create pluginTemplate', function () {
+ cy.visit('/');
+ cy.contains('Route').click();
+ cy.contains('Plugin Template Config').click();
+ cy.contains('Create').click();
+
+ cy.get(this.domSelector.description).type(this.data.pluginTemplateName);
+ cy.contains('Next').click();
+ cy.contains('Enable').click({
+ force: true
+ });
+ cy.focused(this.domSelector.drawer).should('exist');
+ cy.get(this.domSelector.drawer, {
+ timeout
+ }).within(() => {
+ cy.get(this.domSelector.disabledSwitcher).click({
+ force: true,
+ });
+ });
+ cy.contains('Submit').click();
+ cy.contains('Next').click();
+ cy.contains('Submit').click();
+ cy.get(this.domSelector.notification).should('contain',
this.data.createPluginTemplateSuccess);
+ });
+
+ it('should edit the pluginTemplate', function () {
+ cy.visit('plugin-template/list');
+
+ cy.get(this.domSelector.refresh).click();
+
cy.get(this.domSelector.descriptionSelector).type(this.data.pluginTemplateName);
+ cy.contains('button', 'Search').click();
+
cy.contains(this.data.pluginTemplateName).siblings().contains('Edit').click();
+
+
cy.get(this.domSelector.description).clear().type(this.data.pluginTemplateName2);
+ cy.contains('Next').click();
+ cy.contains('Next').click();
+ cy.contains('Submit').click();
+
+ cy.get(this.domSelector.notification).should('contain',
this.data.editPluginTemplateSuccess);
+ });
+
+ it('should delete pluginTemplate', function () {
+ cy.visit('plugin-template/list');
+
+ cy.get(this.domSelector.refresh).click();
+
cy.get(this.domSelector.descriptionSelector).type(this.data.pluginTemplateName);
+ cy.contains('button', 'Search').click();
+ cy.get(this.domSelector.empty).should('exist');
+
+ cy.contains('button', 'Reset').click();
+
cy.get(this.domSelector.descriptionSelector).type(this.data.pluginTemplateName2);
+ cy.contains('button', 'Search').click();
+
cy.contains(this.data.pluginTemplateName2).siblings().contains('Delete').click();
+ cy.contains('button', 'Confirm').click();
+ cy.get(this.domSelector.notification).should('contain',
this.data.deletePluginTemplateSuccess);
+ });
+});
diff --git
a/web/cypress/integration/pluginTemplate/create-plugin-template-with-route.spec.js
b/web/cypress/integration/pluginTemplate/create-plugin-template-with-route.spec.js
new file mode 100644
index 0000000..67bd664
--- /dev/null
+++
b/web/cypress/integration/pluginTemplate/create-plugin-template-with-route.spec.js
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+/* eslint-disable no-undef */
+
+context('Create PluginTemplate Binding To Route', () => {
+ beforeEach(() => {
+ cy.login();
+
+ cy.fixture('selector.json').as('domSelector');
+ cy.fixture('data.json').as('data');
+ });
+
+ it('should create test pluginTemplate', function () {
+ cy.visit('/');
+ cy.contains('Route').click();
+ cy.contains('Plugin Template Config').click();
+ cy.contains('Create').click();
+ cy.get(this.domSelector.description).type(this.data.pluginTemplateName);
+ cy.contains('Next').click();
+ cy.contains('Next').click();
+ cy.contains('Submit').click();
+ cy.get(this.domSelector.notification).should('contain',
this.data.createPluginTemplateSuccess);
+
+ cy.visit('routes/list');
+ cy.contains('Create').click();
+ cy.get(this.domSelector.name).type(this.data.routeName);
+ cy.contains('Next').click();
+ cy.get(this.domSelector.nodes_0_host).type(this.data.ip1);
+ cy.contains('Next').click();
+ cy.get(this.domSelector.customSelector).click();
+ cy.contains(this.data.pluginTemplateName).click();
+
+ cy.contains('Next').click();
+ cy.contains('Submit').click();
+ cy.contains(this.data.submitSuccess);
+ cy.contains('Goto List').click();
+ cy.url().should('contains', 'routes/list');
+ });
+
+ it('should delete the pluginTemplate failure', function () {
+ cy.visit('plugin-template/list');
+ cy.get(this.domSelector.refresh).click();
+
+
cy.get(this.domSelector.descriptionSelector).type(this.data.pluginTemplateName);
+ cy.contains('button', 'Search').click();
+
cy.contains(this.data.pluginTemplateName).siblings().contains('Delete').click();
+ cy.contains('button', 'Confirm').click();
+ cy.get(this.domSelector.notification).should('contain',
this.data.pluginTemplateErrorAlert);
+ cy.get(this.domSelector.errorAlertClose).should('be.visible').click();
+ });
+
+ it('should edit the route with pluginTemplate', function () {
+ cy.visit('routes/list');
+
+ cy.get(this.domSelector.nameSelector).type(this.data.routeName);
+ cy.contains('Search').click();
+ cy.contains(this.data.routeName).siblings().contains('Edit').click();
+
+ cy.contains('Forbidden').click();
+ cy.contains('Custom').click();
+ cy.get(this.domSelector.redirectURIInput).clear().type('123');
+ cy.get(this.domSelector.redirectCodeSelector).click();
+ cy.contains('301(Permanent Redirect)').click();
+ cy.contains('Next').click();
+ cy.contains('Submit').click();
+ cy.contains(this.data.submitSuccess);
+ cy.contains('Goto List').click();
+ cy.url().should('contains', 'routes/list');
+ });
+
+ it('should delete the pluginTemplate successfully', function () {
+ cy.visit('plugin-template/list');
+
+ cy.get(this.domSelector.refresh).click();
+
cy.get(this.domSelector.descriptionSelector).type(this.data.pluginTemplateName);
+ cy.contains('button', 'Search').click();
+
cy.contains(this.data.pluginTemplateName).siblings().contains('Delete').click();
+ cy.contains('button', 'Confirm').click();
+ cy.get(this.domSelector.notification).should('contain',
this.data.deletePluginTemplateSuccess);
+
+ cy.visit('/routes/list');
+ cy.get(this.domSelector.nameSelector).type(this.data.routeName);
+ cy.contains('Search').click();
+ cy.contains(this.data.routeName).siblings().contains('Delete').click();
+ cy.contains('button', 'Confirm').click();
+ cy.get(this.domSelector.notification).should('contain',
this.data.deleteRouteSuccess);
+ });
+});
diff --git a/web/cypress/integration/route/create-edit-delete-route.spec.js
b/web/cypress/integration/route/create-edit-delete-route.spec.js
index f799a69..86d9fb2 100644
--- a/web/cypress/integration/route/create-edit-delete-route.spec.js
+++ b/web/cypress/integration/route/create-edit-delete-route.spec.js
@@ -73,7 +73,9 @@ context('Create and Delete Route', () => {
// config prometheus plugin
cy.contains(this.domSelector.pluginCard, 'prometheus').within(() => {
- cy.get('button').first().click();
+ cy.get('button').first().click({
+ force: true
+ });
});
cy.contains('button', 'Cancel').click();
cy.contains('Next').click();
@@ -138,4 +140,3 @@ context('Create and Delete Route', () => {
cy.get(this.domSelector.notification).should('contain',
this.data.deleteRouteSuccess);
});
});
-
diff --git a/web/src/pages/Route/components/Step1/LabelsDrawer.tsx
b/web/src/components/LabelsfDrawer/LabelsDrawer.tsx
similarity index 93%
rename from web/src/pages/Route/components/Step1/LabelsDrawer.tsx
rename to web/src/components/LabelsfDrawer/LabelsDrawer.tsx
index 5a17682..33f5880 100644
--- a/web/src/pages/Route/components/Step1/LabelsDrawer.tsx
+++ b/web/src/components/LabelsfDrawer/LabelsDrawer.tsx
@@ -19,22 +19,23 @@ import { AutoComplete, Button, Col, Drawer, Form,
notification, Row } from 'antd
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { useIntl } from 'umi';
-import { transformLableValueToKeyValue } from '../../transform';
-import { fetchLabelList } from '../../service';
+import { transformLableValueToKeyValue } from '../../helpers';
type Props = {
title?: string;
actionName: string;
dataSource: string[];
+ filterList?: string[],
+ fetchLabelList: any,
disabled: boolean;
onClose: () => void;
} & Pick<RouteModule.Step1PassProps, 'onChange'>;
-const LabelList = (disabled: boolean, labelList: RouteModule.LabelList) => {
+const LabelList = (disabled: boolean, labelList: LabelList, filterList:
string[] = []) => {
const { formatMessage } = useIntl();
const keyOptions = Object.keys(labelList || {})
- .filter((item) => item !== 'API_VERSION')
+ .filter((item) => !filterList.includes(item))
.map((item) => ({ value: item }));
return (
<Form.List name="labels">
@@ -116,14 +117,16 @@ const LabelsDrawer: React.FC<Props> = ({
actionName = '',
disabled = false,
dataSource = [],
+ filterList = [],
+ fetchLabelList,
onClose,
- onChange = () => {},
+ onChange = () => { },
}) => {
const transformLabel = transformLableValueToKeyValue(dataSource);
const { formatMessage } = useIntl();
const [form] = Form.useForm();
- const [labelList, setLabelList] = useState<RouteModule.LabelList>({});
+ const [labelList, setLabelList] = useState<LabelList>({});
form.setFieldsValue({ labels: transformLabel });
useEffect(() => {
@@ -172,7 +175,7 @@ const LabelsDrawer: React.FC<Props> = ({
}
>
<Form form={form} layout="horizontal">
- {LabelList(disabled, labelList || {})}
+ {LabelList(disabled, labelList || {}, filterList)}
</Form>
</Drawer>
);
diff --git a/web/src/components/Plugin/locales/en-US.ts
b/web/src/components/LabelsfDrawer/index.ts
similarity index 83%
copy from web/src/components/Plugin/locales/en-US.ts
copy to web/src/components/LabelsfDrawer/index.ts
index 00e9689..a42e5e5 100644
--- a/web/src/components/Plugin/locales/en-US.ts
+++ b/web/src/components/LabelsfDrawer/index.ts
@@ -14,7 +14,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export default {
- 'component.plugin.tip1': 'NOTE: After customizing the plugin, you need to
update schema.json.',
- 'component.plugin.tip2': 'How to update?',
-};
+export { default } from './LabelsDrawer';
diff --git a/web/src/components/Plugin/PluginPage.tsx
b/web/src/components/Plugin/PluginPage.tsx
index 26d9b22..03d2fec 100644
--- a/web/src/components/Plugin/PluginPage.tsx
+++ b/web/src/components/Plugin/PluginPage.tsx
@@ -15,23 +15,25 @@
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';
-import { Anchor, Layout, Card, Button } from 'antd';
+import { Anchor, Layout, Card, Button, Form, Select, Alert } from 'antd';
import { PanelSection } from '@api7-dashboard/ui';
import { omit, orderBy } from 'lodash';
import { useIntl } from 'umi';
import PluginDetail from './PluginDetail';
-import { fetchList } from './service';
+import { fetchList, fetchPluginTemplateList } from './service';
import { PLUGIN_ICON_LIST, PLUGIN_FILTER_LIST } from './data';
import defaultPluginImg from '../../../public/static/default-plugin.png';
type Props = {
readonly?: boolean;
type?: 'global' | 'scoped';
- initialData?: PluginComponent.Data;
+ initialData?: PluginComponent.Data,
+ plugin_config_id?: string,
schemaType?: PluginComponent.Schema;
referPage?: PluginComponent.ReferPage;
- onChange?: (data: PluginComponent.Data) => void;
+ showSelector?: boolean,
+ onChange?: (plugins: PluginComponent.Data, plugin_config_id?: string) =>
void;
};
const PanelSectionStyle = {
@@ -50,19 +52,24 @@ const NEVER_EXIST_PLUGIN_FLAG = 'NEVER_EXIST_PLUGIN_FLAG';
const PluginPage: React.FC<Props> = ({
readonly = false,
initialData = {},
+ plugin_config_id = "",
schemaType = 'route',
referPage = '',
type = 'scoped',
- onChange = () => {},
+ showSelector = false,
+ onChange = () => { },
}) => {
const { formatMessage } = useIntl();
-
+ const [form] = Form.useForm();
const [pluginList, setPluginList] = useState<PluginComponent.Meta[]>([]);
+ const [pluginTemplateList, setPluginTemplateList] =
useState<PluginTemplateModule.ResEntity[]>([]);
const [name, setName] = useState<string>(NEVER_EXIST_PLUGIN_FLAG);
const [typeList, setTypeList] = useState<string[]>([]);
+ const [plugins, setPlugins] = useState({});
const firstUpperCase = ([first, ...rest]: string) => first.toUpperCase() +
rest.join('');
useEffect(() => {
+ setPlugins(initialData);
fetchList().then((data) => {
const filteredData = data.filter(
(item) =>
@@ -79,6 +86,10 @@ const PluginPage: React.FC<Props> = ({
});
setTypeList(categoryList.sort());
});
+ fetchPluginTemplateList().then((data) => {
+ setPluginTemplateList(data);
+ form.setFieldsValue({ plugin_config_id })
+ })
}, []);
const PluginList = () => (
@@ -104,6 +115,54 @@ const PluginPage: React.FC<Props> = ({
</Anchor>
</Sider>
<Content style={{ padding: '0 10px', backgroundColor: '#fff', minHeight:
1400 }}>
+ {showSelector && (
+ <>
+ <Form form={form}>
+ <Form.Item
+ label={formatMessage({ id: 'component.select.pluginTemplate'
})}
+ name="plugin_config_id"
+ shouldUpdate={(prev, next) => {
+ if (prev.plugin_config_id !== next.plugin_config_id) {
+ const id = next.plugin_config_id;
+ if (id) {
+ form.setFieldsValue({
+ plugin_config_id: id,
+ });
+ }
+ }
+ return prev.plugin_config_id !== next.plugin_config_id;
+ }}
+ >
+ <Select
+ data-cy="pluginTemplateSelector"
+ disabled={readonly}
+ onChange={(id) => {
+ form.setFieldsValue({
+ plugin_config_id: id,
+ });
+ onChange(plugins, id as string);
+ }}
+ >
+ {[
+ {
+ id: '',
+ desc: formatMessage({ id:
'component.step.select.pluginTemplate.select.option' }),
+ },
+ ...pluginTemplateList,
+ ].map((item) => (
+ <Select.Option value={item.id!} key={item.id}>
+ {item.desc}
+ </Select.Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </Form>
+ <Alert message={<>
+ <p>{formatMessage({ id: 'component.plugin.pluginTemplate.tip1'
})}</p>
+ <p>{formatMessage({ id: 'component.plugin.pluginTemplate.tip2'
})}</p>
+ </>} type="info" />
+ </>
+ )}
{typeList.map((typeItem) => {
return (
<PanelSection
@@ -187,14 +246,15 @@ const PluginPage: React.FC<Props> = ({
setName(NEVER_EXIST_PLUGIN_FLAG);
}}
onChange={({ codemirrorData, formData, shouldDelete }) => {
- let plugins = {
+ let newPlugins = {
...initialData,
[name]: { ...codemirrorData, disable: !formData.disable },
};
if (shouldDelete === true) {
- plugins = omit(plugins, name);
+ newPlugins = omit(newPlugins, name);
}
- onChange(plugins);
+ onChange(newPlugins, form.getFieldValue('plugin_config_id'));
+ setPlugins(newPlugins);
setName(NEVER_EXIST_PLUGIN_FLAG);
}}
/>
diff --git a/web/src/components/Plugin/locales/en-US.ts
b/web/src/components/Plugin/locales/en-US.ts
index 00e9689..f70e668 100644
--- a/web/src/components/Plugin/locales/en-US.ts
+++ b/web/src/components/Plugin/locales/en-US.ts
@@ -17,4 +17,8 @@
export default {
'component.plugin.tip1': 'NOTE: After customizing the plugin, you need to
update schema.json.',
'component.plugin.tip2': 'How to update?',
+ 'component.select.pluginTemplate': 'Select a plugin template',
+ 'component.step.select.pluginTemplate.select.option': 'Custom',
+ 'component.plugin.pluginTemplate.tip1': '1. When a route already have
plugins field configured, the plugins in the plugin template will be merged
into it.',
+ 'component.plugin.pluginTemplate.tip2': '2. The same plugin in the plugin
template will override one in the plugins'
};
diff --git a/web/src/components/Plugin/locales/zh-CN.ts
b/web/src/components/Plugin/locales/zh-CN.ts
index d4d9b64..afc85c1 100644
--- a/web/src/components/Plugin/locales/zh-CN.ts
+++ b/web/src/components/Plugin/locales/zh-CN.ts
@@ -17,4 +17,8 @@
export default {
'component.plugin.tip1': '注意:自定义插件后(修改、新增、删除等),需更新 schema.json。',
'component.plugin.tip2': '如何更新?',
+ "component.select.pluginTemplate": '选择插件模板',
+ 'component.step.select.pluginTemplate.select.option': '手动配置',
+ 'component.plugin.pluginTemplate.tip1': '1. 若路由已配置插件,则插件模板数据将与已配置的插件数据合并。',
+ 'component.plugin.pluginTemplate.tip2': '2. 插件模板相同的插件会覆盖掉原有的插件。'
};
diff --git a/web/src/components/Plugin/service.ts
b/web/src/components/Plugin/service.ts
index b3177b0..5a1b427 100644
--- a/web/src/components/Plugin/service.ts
+++ b/web/src/components/Plugin/service.ts
@@ -51,3 +51,9 @@ export const fetchSchema = async (
}
return cachedPluginSchema[schemaType][name];
};
+
+export const fetchPluginTemplateList = () => {
+ return
request<Res<ResListData<PluginTemplateModule.ResEntity>>>('/plugin_configs').then((data)
=> {
+ return data.data.rows;
+ });
+};
diff --git a/web/src/helpers.tsx b/web/src/helpers.tsx
index de5bc4a..5ee05b0 100644
--- a/web/src/helpers.tsx
+++ b/web/src/helpers.tsx
@@ -130,3 +130,33 @@ export const timestampToLocaleString = (timestamp: number)
=> {
return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss');
};
+
+export const transformLableValueToKeyValue = (data: string[]) => {
+ return (data || []).map((item) => {
+ const index = item.indexOf(':');
+ const labelKey = item.substring(0, index);
+ const labelValue = item.substring(index + 1);
+ return { labelKey, labelValue, key: Math.random().toString(36).slice(2) };
+ });
+};
+
+export const transformLabelList = (data: ResponseLabelList) => {
+ if (!data) {
+ return {};
+ }
+ const transformData = {};
+ data.forEach((item) => {
+ const key = Object.keys(item)[0];
+ const value = item[key];
+ if (!transformData[key]) {
+ transformData[key] = [];
+ transformData[key].push(value);
+ return;
+ }
+
+ if (transformData[key] && !transformData[key][value]) {
+ transformData[key].push(value);
+ }
+ });
+ return transformData;
+};
diff --git a/web/src/locales/en-US/menu.ts b/web/src/locales/en-US/menu.ts
index c2dac51..e84a578 100644
--- a/web/src/locales/en-US/menu.ts
+++ b/web/src/locales/en-US/menu.ts
@@ -65,6 +65,7 @@ export default {
'menu.editor.koni': 'Koni Editor',
'menu.metrics': 'Metrics',
'menu.routes': 'Route',
+ 'menu.pluginTemplate': 'Plugin Template',
'menu.ssl': 'SSL',
'menu.upstream': 'Upstream',
'menu.consumer': 'Consumer',
diff --git a/web/src/locales/zh-CN/menu.ts b/web/src/locales/zh-CN/menu.ts
index 59129c1..19b4c6e 100644
--- a/web/src/locales/zh-CN/menu.ts
+++ b/web/src/locales/zh-CN/menu.ts
@@ -62,6 +62,7 @@ export default {
'menu.editor.koni': '拓扑编辑器',
'menu.metrics': '监控',
'menu.routes': '路由',
+ 'menu.pluginTemplate': '插件模板',
'menu.ssl': '证书',
'menu.upstream': '上游',
'menu.consumer': '消费者',
diff --git a/web/src/pages/PluginTemplate/Create.tsx
b/web/src/pages/PluginTemplate/Create.tsx
new file mode 100644
index 0000000..3da8cb1
--- /dev/null
+++ b/web/src/pages/PluginTemplate/Create.tsx
@@ -0,0 +1,121 @@
+/*
+ * 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 React, { useState, useEffect } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Steps, notification, Form } from 'antd';
+import { history, useIntl } from 'umi';
+
+import ActionBar from '@/components/ActionBar';
+import PluginPage from '@/components/Plugin';
+import { transformLableValueToKeyValue } from '@/helpers';
+
+import Step1 from './components/Step1';
+import Preview from './components/Preview';
+import { fetchItem, create, update, } from './service';
+
+const Page: React.FC = (props) => {
+ const [step, setStep] = useState(1);
+ const [plugins, setPlugins] = useState<PluginComponent.Data>({});
+ const [form1] = Form.useForm();
+ const { formatMessage } = useIntl();
+
+ useEffect(() => {
+ const { id } = (props as any).match.params;
+ if (id) {
+ fetchItem(id).then(({ data }) => {
+ const { desc, labels = {}, ...rest } = data;
+ form1.setFieldsValue({
+ id, desc, custom_normal_labels: Object.keys(labels)
+ .map((key) => `${key}:${labels[key]}`)
+ });
+ setPlugins(rest.plugins);
+ });
+ }
+ }, []);
+
+ const onSubmit = () => {
+ const { desc, custom_normal_labels } = form1.getFieldsValue();
+ const labels: Record<string, string> = {};
+ transformLableValueToKeyValue(custom_normal_labels || []).forEach(({
labelKey, labelValue }) => {
+ labels[labelKey] = labelValue;
+ });
+ const data = { desc, labels, plugins } as PluginTemplateModule.Entity;
+
+ const { id } = (props as any).match.params;
+ (id ? update(id, data) : create(data))
+ .then(() => {
+ notification.success({
+ message: `${id
+ ? formatMessage({ id: 'component.global.edit' })
+ : formatMessage({ id: 'component.global.create' })
+ } ${formatMessage({ id: 'menu.pluginTemplate' })} ${formatMessage({
+ id: 'component.status.success',
+ })}`,
+ });
+ history.push('/plugin-template/list');
+ })
+ .catch(() => {
+ setStep(3);
+ });
+ };
+
+ const onStepChange = (nextStep: number) => {
+ if (step === 1) {
+ form1.validateFields().then(() => {
+ setStep(nextStep);
+ });
+ } else if (nextStep === 3) {
+ setStep(3);
+ } else if (nextStep === 4) {
+ onSubmit();
+ } else {
+ setStep(nextStep);
+ }
+ };
+
+ return (
+ <>
+ <PageContainer
+ title={`${(props as any).match.params.id
+ ? formatMessage({ id: 'component.global.edit' })
+ : formatMessage({ id: 'component.global.create' })
+ } ${formatMessage({ id: 'menu.pluginTemplate' })}`}
+ >
+ <Card bordered={false}>
+ <Steps current={step - 1} style={{ marginBottom: 30 }}>
+ <Steps.Step
+ title={formatMessage({ id:
'component.global.steps.stepTitle.basicInformation' })}
+ />
+ <Steps.Step
+ title={formatMessage({ id:
'component.global.steps.stepTitle.pluginConfig' })}
+ />
+ <Steps.Step title={formatMessage({ id:
'component.global.steps.stepTitle.preview' })} />
+ </Steps>
+
+ {step === 1 && <Step1 form={form1} />}
+ {step === 2 && (
+ <PluginPage initialData={plugins} onChange={setPlugins}
schemaType="route" />
+ )}
+ {step === 3 && <Preview form1={form1} plugins={plugins} />}
+ </Card>
+ </PageContainer>
+ <ActionBar step={step} lastStep={3} onChange={onStepChange} />
+ </>
+ );
+};
+
+export default Page;
diff --git a/web/src/pages/PluginTemplate/List.tsx
b/web/src/pages/PluginTemplate/List.tsx
new file mode 100644
index 0000000..6e0fa45
--- /dev/null
+++ b/web/src/pages/PluginTemplate/List.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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 React, { useEffect, useRef, useState } from 'react';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import { history, useIntl } from 'umi';
+import ProTable from '@ant-design/pro-table';
+import type { ActionType, ProColumns } from '@ant-design/pro-table';
+import { Button, notification, Popconfirm, Select, Space, Tag } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+
+import { fetchList, remove, fetchLabelList } from './service';
+
+const Page: React.FC = () => {
+ const ref = useRef<ActionType>();
+ const [labelList, setLabelList] = useState<LabelList>({});
+ const { formatMessage } = useIntl();
+
+ useEffect(() => {
+ fetchLabelList().then(setLabelList);
+ }, []);
+
+ const handleTableActionSuccessResponse = (msgTip: string) => {
+ notification.success({
+ message: msgTip,
+ });
+
+ ref.current?.reload();
+ };
+
+ const columns: ProColumns<PluginTemplateModule.ResEntity>[] = [
+ {
+ title: 'ID',
+ dataIndex: 'id',
+ hideInSearch: true,
+ },
+ {
+ title: formatMessage({ id: 'component.global.description' }),
+ dataIndex: 'desc',
+ },
+ {
+ title: formatMessage({ id: 'component.global.labels' }),
+ dataIndex: 'labels',
+ render: (_, record) => {
+ return Object.keys(record.labels || {})
+ .map((item) => (
+ <Tag key={Math.random().toString(36).slice(2)}>
+ {item}:{record.labels[item]}
+ </Tag>
+ ));
+ }, renderFormItem: (_, { type }) => {
+ if (type === 'form') {
+ return null;
+ }
+
+ return (
+ <Select
+ mode="tags"
+ style={{ width: '100%' }}
+ tagRender={(props) => {
+ const { value, closable, onClose } = props;
+ return (
+ <Tag closable={closable} onClose={onClose} style={{
marginRight: 3 }}>
+ {value}
+ </Tag>
+ );
+ }}
+ >
+ {Object.keys(labelList)
+ .map((key) => {
+ return (
+ <Select.OptGroup label={key}
key={Math.random().toString(36).slice(2)}>
+ {(labelList[key] || []).map((value: string) => (
+ <Select.Option key={Math.random().toString(36).slice(2)}
value={`${key}:${value}`}>
+ {value}
+ </Select.Option>
+ ))}
+ </Select.OptGroup>
+ );
+ })}
+ </Select>
+ );
+ },
+ },
+ {
+ title: formatMessage({ id: 'component.global.operation' }),
+ valueType: 'option',
+ render: (_, record) => (
+ <>
+ <Space align="baseline">
+ <Button
+ type="primary"
+ onClick={() => {
+ history.push(`/plugin-template/${record.id}/edit`)
+ }}
+ style={{ marginRight: 10 }}
+ >
+ {formatMessage({ id: 'component.global.edit' })}
+ </Button>
+
+ <Popconfirm
+ title={formatMessage({ id:
'component.global.popconfirm.title.delete' })}
+ onConfirm={() => {
+ remove(record.id!).then(() => {
+ handleTableActionSuccessResponse(
+ `${formatMessage({ id: 'component.global.delete' })}
${formatMessage({
+ id: 'menu.pluginTemplate',
+ })} ${formatMessage({ id: 'component.status.success' })}`,
+ );
+ });
+ }}
+ okText={formatMessage({ id: 'component.global.confirm' })}
+ cancelText={formatMessage({ id: 'component.global.cancel' })}
+ >
+ <Button type="primary" danger>
+ {formatMessage({ id: 'component.global.delete' })}
+ </Button>
+ </Popconfirm>
+ </Space>
+ </>
+ ),
+ },
+ ];
+
+ return (
+ <PageHeaderWrapper title={formatMessage({ id: 'page.plugin.list' })}>
+ <ProTable<PluginTemplateModule.ResEntity>
+ actionRef={ref}
+ rowKey="id"
+ columns={columns}
+ request={fetchList}
+ search={{
+ searchText: formatMessage({ id: 'component.global.search' }),
+ resetText: formatMessage({ id: 'component.global.reset' }),
+ }}
+ toolBarRender={() => [
+ <Button type="primary" onClick={() =>
history.push('/plugin-template/create')}>
+ <PlusOutlined />
+ {formatMessage({ id: 'component.global.create' })}
+ </Button>,
+ ]}
+ />
+ </PageHeaderWrapper>
+ );
+};
+
+export default Page;
diff --git a/web/src/components/Plugin/locales/en-US.ts
b/web/src/pages/PluginTemplate/components/Preview.tsx
similarity index 65%
copy from web/src/components/Plugin/locales/en-US.ts
copy to web/src/pages/PluginTemplate/components/Preview.tsx
index 00e9689..4e140ad 100644
--- a/web/src/components/Plugin/locales/en-US.ts
+++ b/web/src/pages/PluginTemplate/components/Preview.tsx
@@ -14,7 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export default {
- 'component.plugin.tip1': 'NOTE: After customizing the plugin, you need to
update schema.json.',
- 'component.plugin.tip2': 'How to update?',
+import React from 'react';
+import type { FormInstance } from 'antd/lib/form';
+
+import PluginPage from '@/components/Plugin';
+import Step1 from './Step1';
+
+type Props = {
+ form1: FormInstance;
+ plugins: PluginComponent.Data;
};
+
+const Page: React.FC<Props> = ({ form1, plugins }) => {
+ return (
+ <>
+ <Step1 form={form1} disabled />
+ <PluginPage initialData={plugins} readonly />
+ </>
+ );
+};
+
+export default Page;
diff --git a/web/src/pages/Route/components/Step1/MetaView.tsx
b/web/src/pages/PluginTemplate/components/Step1.tsx
similarity index 52%
copy from web/src/pages/Route/components/Step1/MetaView.tsx
copy to web/src/pages/PluginTemplate/components/Step1.tsx
index e0728dc..dbeea66 100644
--- a/web/src/pages/Route/components/Step1/MetaView.tsx
+++ b/web/src/pages/PluginTemplate/components/Step1.tsx
@@ -14,30 +14,35 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React, { useEffect, useState } from 'react';
-import Form from 'antd/es/form';
-import { Input, Switch, Select, Button, Tag, AutoComplete } from 'antd';
+import React, { useState } from 'react';
+import { Button, Form, Input, Select, Tag } from 'antd';
+import type { FormInstance } from 'antd/lib/form';
import { useIntl } from 'umi';
-import { PanelSection } from '@api7-dashboard/ui';
-import { FORM_ITEM_WITHOUT_LABEL } from '@/pages/Route/constants';
-import LabelsDrawer from './LabelsDrawer';
-import { fetchLabelList } from '../../service';
+import LabelsDrawer from '@/components/LabelsfDrawer';
+import { fetchLabelList } from '../service';
-const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, form,
isEdit, onChange }) => {
- const { formatMessage } = useIntl();
- const [visible, setVisible] = useState(false);
- const [labelList, setLabelList] = useState<RouteModule.LabelList>({});
+const FORM_LAYOUT = {
+ labelCol: {
+ span: 2,
+ },
+ wrapperCol: {
+ span: 8,
+ },
+};
- useEffect(() => {
- // TODO: use a better state name
- fetchLabelList().then(setLabelList);
- }, []);
+type Props = {
+ form: FormInstance;
+ disabled?: boolean;
+};
+
+const Step1: React.FC<Props> = ({ form, disabled }) => {
+ const [visible, setVisible] = useState(false);
+ const { formatMessage } = useIntl();
const NormalLabelComponent = () => {
const field = 'custom_normal_labels';
const title = 'Label Manager';
-
return (
<React.Fragment>
<Form.Item label={formatMessage({ id: 'component.global.labels' })}
name={field}>
@@ -58,7 +63,7 @@ const MetaView: React.FC<RouteModule.Step1PassProps> = ({
disabled, form, isEdit
}}
/>
</Form.Item>
- <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
+ <Form.Item wrapperCol={{ offset: 2 }}>
<Button type="dashed" disabled={disabled} onClick={() =>
setVisible(true)}>
{formatMessage({ id: 'component.global.manage' })}
</Button>
@@ -73,8 +78,13 @@ const MetaView: React.FC<RouteModule.Step1PassProps> = ({
disabled, form, isEdit
actionName={field}
dataSource={labels}
disabled={disabled || false}
- onChange={onChange}
+ onChange={({ data }) => {
+ const handledLabels = [...new
Set([...(form.getFieldValue('custom_normal_labels') || []), ...data])];
+ form.setFieldsValue({ ...form.getFieldsValue(),
custom_normal_labels: handledLabels });
+ }}
onClose={() => setVisible(false)}
+ filterList={[]}
+ fetchLabelList={fetchLabelList}
/>
);
}}
@@ -84,70 +94,19 @@ const MetaView: React.FC<RouteModule.Step1PassProps> = ({
disabled, form, isEdit
);
};
- const VersionLabelComponent = () => {
- return (
- <React.Fragment>
- <Form.Item
- label={formatMessage({ id: 'component.global.version' })}
- name="custom_version_label"
- >
- <AutoComplete
- options={(labelList.API_VERSION || []).map((item) => ({ value:
item }))}
- disabled={disabled}
- />
- </Form.Item>
- </React.Fragment>
- );
- };
-
return (
- <PanelSection title={formatMessage({ id:
'page.route.panelSection.title.nameDescription' })}>
- <Form.Item
- label={formatMessage({ id: 'component.global.name' })}
- name="name"
- rules={[
- {
- required: true,
- message: `${formatMessage({ id: 'component.global.pleaseEnter' })}
${formatMessage({
- id: 'page.route.form.itemLabel.apiName',
- })}`,
- },
- {
- pattern: new RegExp(/^[a-zA-Z][a-zA-Z0-9_-]{0,100}$/, 'g'),
- message: formatMessage({ id:
'page.route.form.itemRulesPatternMessage.apiNameRule' }),
- },
- ]}
- extra={formatMessage({ id:
'page.route.form.itemRulesPatternMessage.apiNameRule' })}
- >
- <Input
+ <Form {...FORM_LAYOUT} form={form}>
+ <Form.Item label={formatMessage({ id: 'component.global.description' })}
name="desc">
+ <Input.TextArea
placeholder={`${formatMessage({ id: 'component.global.pleaseEnter'
})} ${formatMessage({
- id: 'page.route.form.itemLabel.apiName',
+ id: 'component.global.description',
})}`}
disabled={disabled}
/>
</Form.Item>
-
<NormalLabelComponent />
- <VersionLabelComponent />
-
- <Form.Item label={formatMessage({ id: 'component.global.description' })}
name="desc">
- <Input.TextArea
- placeholder={formatMessage({ id:
'component.global.input.placeholder.description' })}
- disabled={disabled}
- showCount
- maxLength={256}
- />
- </Form.Item>
-
- <Form.Item
- label={formatMessage({ id: 'page.route.publish' })}
- name="status"
- valuePropName="checked"
- >
- <Switch disabled={isEdit} />
- </Form.Item>
- </PanelSection>
+ </Form>
);
};
-export default MetaView;
+export default Step1;
diff --git a/web/src/pages/PluginTemplate/service.ts
b/web/src/pages/PluginTemplate/service.ts
new file mode 100644
index 0000000..edd449d
--- /dev/null
+++ b/web/src/pages/PluginTemplate/service.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { request } from 'umi';
+
+import { transformLabelList } from '@/helpers';
+
+export const fetchList = ({ current = 1, pageSize = 10, ...res }) => {
+ const { labels = [] } = res;
+
+ return request('/plugin_configs', {
+ params: {
+ search: res.desc,
+ label: labels.join(','),
+ page: current,
+ page_size: pageSize,
+ },
+ }).then(({ data }) => {
+ return {
+ data: data.rows,
+ total: data.total_size,
+ }
+ });
+}
+
+export const remove = (rid: string) => request(`/plugin_configs/${rid}`, {
method: 'DELETE' });
+
+export const fetchItem = (id: string) =>
+ request<{ data: PluginTemplateModule.ResEntity }>(`/plugin_configs/${id}`);
+
+export const create = (data: PluginTemplateModule.Entity) =>
+ request('/plugin_configs', {
+ method: 'POST',
+ data,
+ });
+
+export const update = (id: string, data: PluginTemplateModule.Entity) =>
+ request(`/plugin_configs/${id}`, {
+ method: 'PATCH',
+ data,
+ });
+
+export const fetchLabelList = () =>
+ request('/labels/plugin_config').then(
+ ({ data }) => transformLabelList(data.rows) as LabelList,
+ );
diff --git a/web/src/components/Plugin/locales/en-US.ts
b/web/src/pages/PluginTemplate/typing.d.ts
similarity index 77%
copy from web/src/components/Plugin/locales/en-US.ts
copy to web/src/pages/PluginTemplate/typing.d.ts
index 00e9689..4e2ee88 100644
--- a/web/src/components/Plugin/locales/en-US.ts
+++ b/web/src/pages/PluginTemplate/typing.d.ts
@@ -14,7 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export default {
- 'component.plugin.tip1': 'NOTE: After customizing the plugin, you need to
update schema.json.',
- 'component.plugin.tip2': 'How to update?',
-};
+
+declare namespace PluginTemplateModule {
+ type Entity = {
+ desc: string;
+ labels: Record<string, string>;
+ plugins: Record<string, any>;
+ };
+
+ type ResEntity = Entity & {
+ id: string;
+ update_time: string;
+ };
+}
diff --git a/web/src/pages/Route/Create.tsx b/web/src/pages/Route/Create.tsx
index 0de1d73..f6ac4dd 100644
--- a/web/src/pages/Route/Create.tsx
+++ b/web/src/pages/Route/Create.tsx
@@ -149,8 +149,8 @@ const Page: React.FC<Props> = (props) => {
<Step3
data={step3Data}
isForceHttps={form1.getFieldValue('redirectOption') === 'forceHttps'}
- onChange={({ plugins, script = INIT_CHART }) => {
- setStep3Data({ plugins, script });
+ onChange={({ plugins, script = INIT_CHART, plugin_config_id }) => {
+ setStep3Data({ plugins, script, plugin_config_id });
setChart(script);
}}
/>
@@ -262,11 +262,10 @@ const Page: React.FC<Props> = (props) => {
return (
<>
<PageHeaderWrapper
- title={`${
- (props as any).match.params.rid
- ? formatMessage({ id: 'component.global.edit' })
- : formatMessage({ id: 'component.global.create' })
- } ${formatMessage({ id: 'menu.routes' })}`}
+ title={`${(props as any).match.params.rid
+ ? formatMessage({ id: 'component.global.edit' })
+ : formatMessage({ id: 'component.global.create' })
+ } ${formatMessage({ id: 'menu.routes' })}`}
>
<Card bordered={false}>
<Steps current={step - 1} className={styles.steps}>
diff --git a/web/src/pages/Route/List.tsx b/web/src/pages/Route/List.tsx
index 1abf05f..e82dfbe 100644
--- a/web/src/pages/Route/List.tsx
+++ b/web/src/pages/Route/List.tsx
@@ -69,7 +69,7 @@ const Page: React.FC = () => {
YAML,
}
- const [labelList, setLabelList] = useState<RouteModule.LabelList>({});
+ const [labelList, setLabelList] = useState<LabelList>({});
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const [uploadFileList, setUploadFileList] = useState<RcFile[]>([]);
const [showImportModal, setShowImportModal] = useState(false);
@@ -426,6 +426,10 @@ const Page: React.FC = () => {
resetText: formatMessage({ id: 'component.global.reset' }),
}}
toolBarRender={() => [
+ <Button type="primary" onClick={() => {
history.push('/plugin-template/list') }}>
+ <PlusOutlined />
+ {formatMessage({ id: 'page.route.pluginTemplateConfig' })}
+ </Button>,
<Button type="primary" onClick={() =>
history.push(`/routes/create`)}>
<PlusOutlined />
{formatMessage({ id: 'component.global.create' })}
diff --git a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
index 11a1034..f999f09 100644
--- a/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
+++ b/web/src/pages/Route/components/CreateStep4/CreateStep4.tsx
@@ -39,7 +39,7 @@ const style = {
const CreateStep4: React.FC<Props> = ({ form1, form2, redirect, upstreamRef,
...rest }) => {
const { formatMessage } = useIntl();
- const { plugins = {}, script = {} } = rest.step3Data;
+ const { plugins = {}, script = {}, plugin_config_id = '' } = rest.step3Data;
return (
<>
@@ -59,11 +59,9 @@ const CreateStep4: React.FC<Props> = ({ form1, form2,
redirect, upstreamRef, ...
<h2 style={style}>
{formatMessage({ id:
'component.global.steps.stepTitle.pluginConfig' })}
</h2>
- {Boolean(Object.keys(plugins).length !== 0) && (
- <PluginPage initialData={rest.step3Data.plugins} readonly />
- )}
+ {Boolean(Object.keys(plugins).length !== 0 || plugin_config_id !==
'') && <PluginPage initialData={plugins} plugin_config_id={plugin_config_id}
showSelector readonly />}
{Boolean(Object.keys(script).length !== 0) && (
- <PluginOrchestration data={rest.step3Data.script.chart} readonly
onChange={() => {}} />
+ <PluginOrchestration data={rest.step3Data.script.chart} readonly
onChange={() => { }} />
)}
</>
)}
diff --git a/web/src/pages/Route/components/Step1/MetaView.tsx
b/web/src/pages/Route/components/Step1/MetaView.tsx
index e0728dc..8bfb2e1 100644
--- a/web/src/pages/Route/components/Step1/MetaView.tsx
+++ b/web/src/pages/Route/components/Step1/MetaView.tsx
@@ -21,13 +21,13 @@ import { useIntl } from 'umi';
import { PanelSection } from '@api7-dashboard/ui';
import { FORM_ITEM_WITHOUT_LABEL } from '@/pages/Route/constants';
-import LabelsDrawer from './LabelsDrawer';
+import LabelsDrawer from '@/components/LabelsfDrawer';
import { fetchLabelList } from '../../service';
const MetaView: React.FC<RouteModule.Step1PassProps> = ({ disabled, form,
isEdit, onChange }) => {
const { formatMessage } = useIntl();
const [visible, setVisible] = useState(false);
- const [labelList, setLabelList] = useState<RouteModule.LabelList>({});
+ const [labelList, setLabelList] = useState<LabelList>({});
useEffect(() => {
// TODO: use a better state name
@@ -75,6 +75,8 @@ const MetaView: React.FC<RouteModule.Step1PassProps> = ({
disabled, form, isEdit
disabled={disabled || false}
onChange={onChange}
onClose={() => setVisible(false)}
+ filterList={["API_VERSION"]}
+ fetchLabelList={fetchLabelList}
/>
);
}}
diff --git a/web/src/pages/Route/components/Step3/index.tsx
b/web/src/pages/Route/components/Step3/index.tsx
index fc1aa26..d610817 100644
--- a/web/src/pages/Route/components/Step3/index.tsx
+++ b/web/src/pages/Route/components/Step3/index.tsx
@@ -26,8 +26,9 @@ type Props = {
data: {
plugins: PluginComponent.Data;
script: Record<string, any>;
+ plugin_config_id?: string;
};
- onChange: (data: { plugins: PluginComponent.Data; script: any }) => void;
+ onChange: (data: { plugins: PluginComponent.Data; script: any,
plugin_config_id?: string; }) => void;
readonly?: boolean;
isForceHttps: boolean;
};
@@ -35,7 +36,7 @@ type Props = {
type Mode = 'NORMAL' | 'DRAW';
const Page: React.FC<Props> = ({ data, onChange, readonly = false,
isForceHttps }) => {
- const { plugins = {}, script = {} } = data;
+ const { plugins = {}, script = {}, plugin_config_id = '' } = data;
// NOTE: Currently only compatible with chrome
const disableDraw = !isChrome || isForceHttps;
@@ -84,9 +85,13 @@ const Page: React.FC<Props> = ({ data, onChange, readonly =
false, isForceHttps
{Boolean(mode === 'NORMAL') && (
<PluginPage
initialData={plugins}
+ plugin_config_id={plugin_config_id}
schemaType="route"
referPage="route"
- onChange={(pluginsData) => onChange({ plugins: pluginsData, script:
{} })}
+ showSelector
+ onChange={(pluginsData, id) => {
+ onChange({ plugins: pluginsData, script: {}, plugin_config_id: id
})
+ }}
/>
)}
{Boolean(mode === 'DRAW') && (
diff --git a/web/src/pages/Route/constants.ts b/web/src/pages/Route/constants.ts
index f2a9b39..7707486 100644
--- a/web/src/pages/Route/constants.ts
+++ b/web/src/pages/Route/constants.ts
@@ -60,6 +60,7 @@ export const DEFAULT_STEP_1_DATA: RouteModule.Form1Data = {
export const DEFAULT_STEP_3_DATA: RouteModule.Step3Data = {
plugins: {},
script: {},
+ plugin_config_id: ""
};
export const INIT_CHART = {
diff --git a/web/src/pages/Route/locales/en-US.ts
b/web/src/pages/Route/locales/en-US.ts
index 3147138..39d9899 100644
--- a/web/src/pages/Route/locales/en-US.ts
+++ b/web/src/pages/Route/locales/en-US.ts
@@ -18,6 +18,7 @@ export default {
'page.route.button.returnList': 'Goto List',
'page.route.button.send': 'Send',
'page.route.onlineDebug': 'Online Debug',
+ 'page.route.pluginTemplateConfig': 'Plugin Template Config',
'page.route.parameterPosition': 'Parameter Position',
'page.route.httpRequestHeader': 'HTTP Request Header',
diff --git a/web/src/pages/Route/locales/zh-CN.ts
b/web/src/pages/Route/locales/zh-CN.ts
index e9845f5..74c0bf7 100644
--- a/web/src/pages/Route/locales/zh-CN.ts
+++ b/web/src/pages/Route/locales/zh-CN.ts
@@ -41,6 +41,7 @@ export default {
'page.route.published': '已发布',
'page.route.unpublished': '未发布',
'page.route.onlineDebug': '在线调试',
+ 'page.route.pluginTemplateConfig': '插件模版配置',
'page.route.service': '服务',
'page.route.instructions': '说明',
'page.route.import': '导入',
diff --git a/web/src/pages/Route/service.ts b/web/src/pages/Route/service.ts
index 538c186..1d3792e 100644
--- a/web/src/pages/Route/service.ts
+++ b/web/src/pages/Route/service.ts
@@ -21,8 +21,8 @@ import {
transformStepData,
transformRouteData,
transformUpstreamNodes,
- transformLabelList,
} from './transform';
+import { transformLabelList } from '@/helpers';
export const create = (data: RouteModule.RequestData) =>
request(`/routes`, {
@@ -97,7 +97,7 @@ export const checkHostWithSSL = (hosts: string[]) =>
export const fetchLabelList = () =>
request('/labels/route').then(
- ({ data }) => transformLabelList(data.rows) as RouteModule.LabelList,
+ ({ data }) => transformLabelList(data.rows) as LabelList,
);
export const updateRouteStatus = (rid: string, status:
RouteModule.RouteStatus) =>
diff --git a/web/src/pages/Route/transform.ts b/web/src/pages/Route/transform.ts
index 3b9fa4f..a16cf27 100644
--- a/web/src/pages/Route/transform.ts
+++ b/web/src/pages/Route/transform.ts
@@ -16,14 +16,7 @@
*/
import { omit, pick, cloneDeep } from 'lodash';
-export const transformLableValueToKeyValue = (data: string[]) => {
- return (data || []).map((item) => {
- const index = item.indexOf(':');
- const labelKey = item.substring(0, index);
- const labelValue = item.substring(index + 1);
- return { labelKey, labelValue, key: Math.random().toString(36).slice(2) };
- });
-};
+import { transformLableValueToKeyValue } from '@/helpers';
// Transform Route data then sent to API
export const transformStepData = ({
@@ -113,6 +106,7 @@ export const transformStepData = ({
form1Data.hosts.filter(Boolean).length === 0 ? 'hosts' : '',
form1Data.redirectOption === 'disabled' ? 'redirect' : '',
data.remote_addrs?.filter(Boolean).length === 0 ? 'remote_addrs' : '',
+ step3DataCloned.plugin_config_id === '' ? 'plugin_config_id' : ''
]);
}
@@ -128,10 +122,10 @@ export const transformStepData = ({
'redirect',
'vars',
'plugins',
+ 'labels',
service_id.length !== 0 ? 'service_id' : '',
form1Data.hosts.filter(Boolean).length !== 0 ? 'hosts' : '',
data.remote_addrs?.filter(Boolean).length !== 0 ? 'remote_addrs' : '',
- form1Data.custom_version_label.length !== 0 ? 'labels' : '',
]);
};
@@ -222,11 +216,12 @@ export const transformRouteData = (data:
RouteModule.Body) => {
const form2Data: RouteModule.Form2Data = upstream || { upstream_id };
- const { plugins, script } = data;
+ const { plugins, script, plugin_config_id = '' } = data;
const step3Data: RouteModule.Step3Data = {
plugins,
script,
+ plugin_config_id,
};
return {
@@ -236,24 +231,3 @@ export const transformRouteData = (data: RouteModule.Body)
=> {
advancedMatchingRules,
};
};
-
-export const transformLabelList = (data: RouteModule.ResponseLabelList) => {
- if (!data) {
- return {};
- }
- const transformData = {};
- data.forEach((item) => {
- const key = Object.keys(item)[0];
- const value = item[key];
- if (!transformData[key]) {
- transformData[key] = [];
- transformData[key].push(value);
- return;
- }
-
- if (transformData[key] && !transformData[key][value]) {
- transformData[key].push(value);
- }
- });
- return transformData;
-};
diff --git a/web/src/pages/Route/typing.d.ts b/web/src/pages/Route/typing.d.ts
index 6e34a11..069ea3e 100644
--- a/web/src/pages/Route/typing.d.ts
+++ b/web/src/pages/Route/typing.d.ts
@@ -34,6 +34,7 @@ declare namespace RouteModule {
plugins: PluginPage.PluginData;
// TEMP
script: any;
+ plugin_config_id?: string
};
type UpstreamHost = {
@@ -90,6 +91,7 @@ declare namespace RouteModule {
};
upstream_id?: string;
plugins: Record<string, any>;
+ plugin_config_id?: string;
script: Record<string, any>;
url?: string;
enable_websocket?: boolean;
@@ -104,16 +106,6 @@ declare namespace RouteModule {
key: string;
};
- type ResponseLabelList = Record<string, string>[];
-
- type LabelList = Record<string, string[]>;
-
- type LabelTableProps = {
- labelKey: string;
- labelValue: string;
- key: string;
- };
-
type Step1PassProps = {
form: FormInstance;
advancedMatchingRules: MatchingRule[];
diff --git a/web/src/typings.d.ts b/web/src/typings.d.ts
index fa45e4d..dc17a7c 100644
--- a/web/src/typings.d.ts
+++ b/web/src/typings.d.ts
@@ -69,3 +69,7 @@ type ResListData<T> = {
};
type HttpMethod = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'OPTIONS' | 'HEAD' |
'PATCH';
+
+type ResponseLabelList = Record<string, string>[];
+
+type LabelList = Record<string, string[]>;