You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by su...@apache.org on 2021/04/15 01:01:04 UTC

[apisix-dashboard] branch master updated: feat: added cors plugin form (#1733)

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 fb75809  feat: added cors plugin form (#1733)
fb75809 is described below

commit fb758091ddcbb445f6b529b3f26defbeddddab91
Author: guoqqqi <72...@users.noreply.github.com>
AuthorDate: Thu Apr 15 09:00:56 2021 +0800

    feat: added cors plugin form (#1733)
---
 .../create-upstream-with-cors-form.spec.js         | 101 ++++++++++++
 .../route/create-route-with-cors-form.spec.js      |  93 +++++++++++
 web/src/components/Plugin/PluginDetail.tsx         |  30 +++-
 web/src/components/Plugin/UI/cors.tsx              | 179 +++++++++++++++++++++
 web/src/components/Plugin/UI/plugin.tsx            |   5 +-
 web/src/components/Plugin/locales/en-US.ts         |   9 ++
 web/src/components/Plugin/locales/zh-CN.ts         |   9 ++
 7 files changed, 419 insertions(+), 7 deletions(-)

diff --git a/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js b/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js
new file mode 100644
index 0000000..3bf2111
--- /dev/null
+++ b/web/cypress/integration/consumer/create-upstream-with-cors-form.spec.js
@@ -0,0 +1,101 @@
+/*
+ * 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 and Delete Consumer', () => {
+  beforeEach(() => {
+    cy.login();
+
+    cy.fixture('selector.json').as('domSelector');
+    cy.fixture('data.json').as('data');
+  });
+
+  const selector = {
+    max_age: "#max_age"
+  }
+
+  const data = {
+    time: 2,
+  }
+
+  it('creates consumer with cors form', function () {
+    cy.visit('/');
+    cy.contains('Consumer').click();
+    cy.get(this.domSelector.empty).should('be.visible');
+    cy.contains('Create').click();
+    // basic information
+    cy.get(this.domSelector.username).type(this.data.consumerName);
+    cy.get(this.domSelector.description).type(this.data.description);
+    cy.contains('Next').click();
+
+    // config auth plugin
+    cy.contains(this.domSelector.pluginCard, 'key-auth').within(() => {
+      cy.contains('Enable').click({ force: true });
+    });
+    cy.focused(this.domSelector.drawer).should('exist');
+    cy.get(this.domSelector.disabledSwitcher).click().should('have.class', 'ant-switch-checked');
+    // edit codemirror
+    cy.get(this.domSelector.codeMirror)
+      .first()
+      .then((editor) => {
+        editor[0].CodeMirror.setValue(
+          JSON.stringify({
+            key: 'test',
+          }),
+        );
+        cy.contains('button', 'Submit').click();
+      });
+
+    cy.contains(this.domSelector.pluginCard, 'cors').within(() => {
+      cy.contains('Enable').click({
+        force: true,
+      });
+    });
+
+    cy.get(this.domSelector.drawer).should('be.visible');
+
+    cy.get(selector.max_age).clear();
+    // config proxy-mirror form
+    cy.get(this.domSelector.drawer).within(() => {
+      cy.contains('Submit').click({
+        force: true,
+      });
+    });
+    cy.get(this.domSelector.notification).should('contain', 'Invalid plugin data');
+    cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist');
+
+    cy.get(selector.max_age).type(data.time);
+    cy.get(this.domSelector.drawer).within(() => {
+      cy.contains('Submit').click({
+        force: true,
+      });
+    });
+    cy.get(this.domSelector.drawer).should('not.exist');
+
+    cy.contains('button', 'Next').click();
+    cy.contains('button', 'Submit').click();
+    cy.get(this.domSelector.notification).should('contain', this.data.createConsumerSuccess);
+    cy.get(this.domSelector.notificationCloseIcon).click().should('not.exist');
+  });
+
+  it('delete the consumer', function () {
+    cy.visit('/consumer/list');
+    cy.contains(this.data.consumerName).should('be.visible').siblings().contains('Delete').click();
+    cy.contains('button', 'Confirm').click();
+    cy.get(this.domSelector.notification).should('contain', this.data.deleteConsumerSuccess);
+  });
+});
diff --git a/web/cypress/integration/route/create-route-with-cors-form.spec.js b/web/cypress/integration/route/create-route-with-cors-form.spec.js
new file mode 100644
index 0000000..915effb
--- /dev/null
+++ b/web/cypress/integration/route/create-route-with-cors-form.spec.js
@@ -0,0 +1,93 @@
+/*
+ * 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 and delete route with cors form', () => {
+  const selector = {
+    allow_credential: "#allow_credential",
+    allow_origins_by_regex: "#allow_origins_by_regex_0"
+  }
+
+  beforeEach(() => {
+    cy.login();
+
+    cy.fixture('selector.json').as('domSelector');
+    cy.fixture('data.json').as('data');
+  });
+
+  it('should create route with cors form', function () {
+    cy.visit('/');
+    cy.contains('Route').click();
+    cy.get(this.domSelector.empty).should('be.visible');
+    cy.contains('Create').click();
+    cy.contains('Next').click().click();
+    cy.get(this.domSelector.name).type('routeName');
+    cy.get(this.domSelector.description).type('desc');
+    cy.contains('Next').click();
+
+    cy.get(this.domSelector.nodes_0_host).type('127.0.0.1');
+    cy.contains('Next').click();
+
+    // config cors plugin
+    cy.contains('cors').parents(this.domSelector.pluginCardBordered).within(() => {
+      cy.get('button').click({
+        force: true
+      });
+    });
+
+    cy.get(this.domSelector.drawer).should('be.visible').within(() => {
+      cy.get(this.domSelector.disabledSwitcher).click();
+      cy.get(this.domSelector.checkedSwitcher).should('exist');
+    });
+
+    // config cors form
+    cy.get(selector.allow_credential).click();
+    cy.get(selector.allow_origins_by_regex).type('.*.test.com');
+    cy.get(this.domSelector.drawer).within(() => {
+      cy.contains('Submit').click({
+        force: true,
+      });
+    });
+    cy.get(this.domSelector.drawer).should('not.exist');
+
+    cy.contains('button', 'Next').click();
+    cy.contains('button', 'Submit').click();
+    cy.contains(this.data.submitSuccess);
+
+    // back to route list page
+    cy.contains('Goto List').click();
+    cy.url().should('contains', 'routes/list');
+  });
+
+  it('should delete the route', function () {
+    cy.visit('/routes/list');
+    const {
+      domSelector,
+      data
+    } = this;
+
+    cy.get(domSelector.name).clear().type('routeName');
+    cy.contains('Search').click();
+    cy.contains('routeName').siblings().contains('More').click();
+    cy.contains('Delete').click();
+    cy.get(domSelector.deleteAlert).should('be.visible').within(() => {
+      cy.contains('OK').click();
+    });
+    cy.get(domSelector.notification).should('contain', data.deleteRouteSuccess);
+    cy.get(domSelector.notificationCloseIcon).click();
+  });
+});
diff --git a/web/src/components/Plugin/PluginDetail.tsx b/web/src/components/Plugin/PluginDetail.tsx
index a807b2d..385f967 100644
--- a/web/src/components/Plugin/PluginDetail.tsx
+++ b/web/src/components/Plugin/PluginDetail.tsx
@@ -116,6 +116,24 @@ const PluginDetail: React.FC<Props> = ({
     modeOptions.push({ label: formatMessage({ id: 'component.plugin.form' }), value: codeMirrorModeList.UIForm });
   }
 
+  const getUIFormData = () => {
+    if (name === 'cors') {
+      const formData = UIForm.getFieldsValue();
+      const newMethods = formData.allow_methods.join(",");
+      return { ...formData, allow_methods: newMethods };
+    }
+    return UIForm.getFieldsValue();
+  };
+
+  const setUIFormData = (formData: any) => {
+    if (name === 'cors' && formData) {
+      const methods = (formData.allow_methods || '').length ? formData.allow_methods.split(",") : ["*"];
+      UIForm.setFieldsValue({ ...formData, allow_methods: methods });
+      return;
+    }
+    UIForm.setFieldsValue(formData);
+  };
+
   useEffect(() => {
     form.setFieldsValue({
       disable: initialData[name] && !initialData[name].disable,
@@ -123,7 +141,7 @@ const PluginDetail: React.FC<Props> = ({
     });
     if (PLUGIN_UI_LIST.includes(name)) {
       setCodeMirrorMode(codeMirrorModeList.UIForm);
-      UIForm.setFieldsValue(initialData[name]);
+      setUIFormData(initialData[name]);
     };
   }, []);
 
@@ -189,7 +207,7 @@ const PluginDetail: React.FC<Props> = ({
           );
         } else {
           ref.current.editor.setValue(
-            js_beautify(JSON.stringify(UIForm.getFieldsValue()), {
+            js_beautify(JSON.stringify(getUIFormData()), {
               indent_size: 2,
             }),
           );
@@ -197,7 +215,7 @@ const PluginDetail: React.FC<Props> = ({
         break;
       }
       case codeMirrorModeList.YAML: {
-        const { data: jsonData, error } = json2yaml(codeMirrorMode === codeMirrorModeList.JSON ? ref.current.editor.getValue() : JSON.stringify(UIForm.getFieldsValue()));
+        const { data: jsonData, error } = json2yaml(codeMirrorMode === codeMirrorModeList.JSON ? ref.current.editor.getValue() : JSON.stringify(getUIFormData()));
 
         if (error) {
           notification.error({
@@ -211,7 +229,7 @@ const PluginDetail: React.FC<Props> = ({
 
       case codeMirrorModeList.UIForm: {
         if (codeMirrorMode === codeMirrorModeList.JSON) {
-          UIForm.setFieldsValue(JSON.parse(ref.current.editor.getValue()));
+          setUIFormData(JSON.parse(ref.current.editor.getValue()));
         } else {
           const { data: yamlData, error } = yaml2json(ref.current.editor.getValue(), true);
           if (error) {
@@ -220,7 +238,7 @@ const PluginDetail: React.FC<Props> = ({
             });
             return;
           }
-          UIForm.setFieldsValue(JSON.parse(yamlData));
+          setUIFormData(JSON.parse(yamlData));
         }
         break;
       }
@@ -292,7 +310,7 @@ const PluginDetail: React.FC<Props> = ({
                     } else if (codeMirrorMode === codeMirrorModeList.YAML) {
                       editorData = yaml2json(ref.current?.editor.getValue(), false).data;
                     } else {
-                      editorData = UIForm.getFieldsValue();
+                      editorData = getUIFormData();
                     }
 
                     validateData(name, editorData).then((value) => {
diff --git a/web/src/components/Plugin/UI/cors.tsx b/web/src/components/Plugin/UI/cors.tsx
new file mode 100644
index 0000000..d075fc4
--- /dev/null
+++ b/web/src/components/Plugin/UI/cors.tsx
@@ -0,0 +1,179 @@
+/*
+ * 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 from 'react';
+import type { FormInstance } from 'antd/es/form';
+import { Button, Col, Form, Input, InputNumber, Row, Select, Switch } from 'antd';
+import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
+import { useIntl } from '@/.umi/plugin-locale/localeExports';
+
+type Props = {
+  form: FormInstance;
+  ref?: any;
+};
+
+const FORM_ITEM_LAYOUT = {
+  labelCol: {
+    span: 7,
+  },
+  wrapperCol: {
+    span: 8
+  },
+};
+
+export const FORM_ITEM_WITHOUT_LABEL = {
+  wrapperCol: {
+    sm: { span: 8, offset: 7 },
+  },
+};
+
+const Cors: React.FC<Props> = ({ form }) => {
+  const { formatMessage } = useIntl();
+
+  const HTTPMethods: React.FC = () => (
+    <Form.Item
+      label="allow_methods"
+      tooltip={formatMessage({ id: 'component.pluginForm.cors.allow_methods.tooltip' })}
+    >
+      <Row>
+        <Col span={24}>
+          <Form.Item
+            name="allow_methods"
+            initialValue={["*"]}
+          >
+            <Select
+              mode="multiple"
+              optionLabelProp="label"
+              onChange={(value) => {
+                ((value as string[]).join(","));
+                if ((value as string[]).includes('*')) {
+                  form.setFieldsValue({
+                    allow_methods: ['*'],
+                  });
+                }
+              }}
+            >
+              {['*', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH', 'CONNECT', 'TRACE'].map((item) => {
+                return <Select.Option value={item} key={item}>{item}</Select.Option>
+              })}
+            </Select>
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form.Item >
+  );
+
+  return (
+    <Form
+      form={form}
+      {...FORM_ITEM_LAYOUT}
+      initialValues={{ allow_origins_by_regex: [''] }}
+    >
+      <Form.Item
+        extra={formatMessage({ id: 'component.pluginForm.cors.allow_origins.extra' })}
+        name="allow_origins"
+        label="allow_origins"
+        initialValue="*"
+        tooltip={formatMessage({ id: 'component.pluginForm.cors.allow_origins.tooltip' })}
+      >
+        <Input />
+      </Form.Item>
+      <HTTPMethods />
+
+      <Form.Item
+        name="allow_headers"
+        label="allow_headers"
+        initialValue="*"
+        tooltip={formatMessage({ id: 'component.pluginForm.cors.allow_headers.tooltip' })}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name="expose_headers"
+        label="expose_headers"
+        initialValue="*"
+        tooltip={formatMessage({ id: 'component.pluginForm.cors.expose_headers.tooltip' })}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name="max_age"
+        label="max_age"
+        initialValue={5}
+        tooltip={formatMessage({ id: 'component.pluginForm.cors.max_age.tooltip' })}
+      >
+        <InputNumber />
+      </Form.Item>
+      <Form.Item
+        name="allow_credential"
+        label="allow_credential"
+        valuePropName="checked"
+        initialValue={false}
+        tooltip={formatMessage({ id: 'component.pluginForm.cors.allow_credential.tooltip' })}
+      >
+        <Switch />
+      </Form.Item>
+
+      <Form.List name={['allow_origins_by_regex']}>
+        {(fields, { add, remove }) => {
+          return (
+            <div>
+              {fields.map((field, index) => (
+                <Form.Item
+                  {...(index === 0 ? FORM_ITEM_LAYOUT : FORM_ITEM_WITHOUT_LABEL)}
+                  label={index === 0 && 'allow_origins_by_regex'}
+                  key={field.key}
+                  tooltip={formatMessage({ id: 'component.pluginForm.cors.allow_origins_by_regex.tooltip' })}
+                >
+                  <Form.Item
+                    {...field}
+                    validateTrigger={['onChange', 'onBlur']}
+                    noStyle
+                  >
+                    <Input style={{ width: '80%' }} />
+                  </Form.Item>
+                  {fields.length > 1 ? (
+                    <MinusCircleOutlined
+                      className="dynamic-delete-button"
+                      style={{ margin: '0 8px' }}
+                      onClick={() => {
+                        remove(field.name);
+                      }}
+                    />
+                  ) : null}
+                </Form.Item>
+              ))}
+              {
+                <Form.Item {...FORM_ITEM_WITHOUT_LABEL}>
+                  <Button
+                    type="dashed"
+                    onClick={() => {
+                      add();
+                    }}
+                  >
+                    <PlusOutlined /> {formatMessage({ id: 'component.global.create' })}
+                  </Button>
+                </Form.Item>
+              }
+            </div>
+          );
+        }}
+      </Form.List>
+    </Form >
+  );
+}
+
+export default Cors;
diff --git a/web/src/components/Plugin/UI/plugin.tsx b/web/src/components/Plugin/UI/plugin.tsx
index e4e4cda..5c82aa9 100644
--- a/web/src/components/Plugin/UI/plugin.tsx
+++ b/web/src/components/Plugin/UI/plugin.tsx
@@ -24,6 +24,7 @@ import LimitReq from './limit-req';
 import ApiBreaker from './api-breaker';
 import ProxyMirror from './proxy-mirror';
 import LimitConn from './limit-conn';
+import Cors from './cors';
 
 type Props = {
   name: string,
@@ -31,7 +32,7 @@ type Props = {
   renderForm: boolean
 }
 
-export const PLUGIN_UI_LIST = ['api-breaker', 'basic-auth', 'limit-req', 'limit-conn', 'proxy-mirror'];
+export const PLUGIN_UI_LIST = ['api-breaker', 'basic-auth', 'limit-conn', 'cors', 'proxy-mirror', 'limit-req'];
 
 export const PluginForm: React.FC<Props> = ({ name, renderForm, form }) => {
 
@@ -44,6 +45,8 @@ export const PluginForm: React.FC<Props> = ({ name, renderForm, form }) => {
       return <ApiBreaker form={form} />
     case 'basic-auth':
       return <BasicAuth form={form} />
+    case 'cors':
+      return <Cors form={form} />
     case 'limit-req':
       return <LimitReq form={form} />
     case 'proxy-mirror':
diff --git a/web/src/components/Plugin/locales/en-US.ts b/web/src/components/Plugin/locales/en-US.ts
index f65bfa0..3e896cc 100644
--- a/web/src/components/Plugin/locales/en-US.ts
+++ b/web/src/components/Plugin/locales/en-US.ts
@@ -21,6 +21,15 @@ export default {
   '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',
+  // cors
+  'component.pluginForm.cors.allow_origins.tooltip': 'Which Origins is allowed to enable CORS, format as:scheme://host:port, for example: https://somehost.com:8081. Multiple origin use , to split. When allow_credential is false, you can use * to indicate allow any origin. you also can allow all any origins forcefully using ** even already enable allow_credential, but it will bring some security risks.',
+  'component.pluginForm.cors.allow_origins.extra': 'For example: https://somehost.com:8081',
+  'component.pluginForm.cors.allow_methods.tooltip': 'Which Method is allowed to enable CORS, such as: GET, POST etc. Multiple method use , to split. When allow_credential is false, you can use * to indicate allow all any method. You also can allow any method forcefully using ** even already enable allow_credential, but it will bring some security risks.',
+  'component.pluginForm.cors.allow_headers.tooltip': 'Which headers are allowed to set in request when access cross-origin resource. Multiple value use , to split. When allow_credential is false, you can use * to indicate allow all request headers. You also can allow any header forcefully using ** even already enable allow_credential, but it will bring some security risks.',
+  'component.pluginForm.cors.expose_headers.tooltip': '	Which headers are allowed to set in response when access cross-origin resource. Multiple value use , to split.',
+  'component.pluginForm.cors.max_age.tooltip': 'Maximum number of seconds the results can be cached.. Within this time range, the browser will reuse the last check result. -1 means no cache. Please note that the maximum value is depended on browser, please refer to MDN for details.',
+  'component.pluginForm.cors.allow_credential.tooltip': 'If you set this option to true, you can not use \'*\' for other options.',
+  'component.pluginForm.cors.allow_origins_by_regex.tooltip': 'Use regex expressions to match which origin is allowed to enable CORS, for example, [".*.test.com"] can use to match all subdomain of test.com.',
 
   // api-breaker
   'component.pluginForm.api-breaker.break_response_code.tooltip': 'Return error code when unhealthy.',
diff --git a/web/src/components/Plugin/locales/zh-CN.ts b/web/src/components/Plugin/locales/zh-CN.ts
index a0decff..1dca43a 100644
--- a/web/src/components/Plugin/locales/zh-CN.ts
+++ b/web/src/components/Plugin/locales/zh-CN.ts
@@ -21,6 +21,15 @@ export default {
   'component.step.select.pluginTemplate.select.option': '手动配置',
   'component.plugin.pluginTemplate.tip1': '1. 若路由已配置插件,则插件模板数据将与已配置的插件数据合并。',
   'component.plugin.pluginTemplate.tip2': '2. 插件模板相同的插件会覆盖掉原有的插件。',
+  // cors
+  'component.pluginForm.cors.allow_origins.tooltip': '允许跨域访问的 Origin,格式如:scheme://host:port,比如: https://somehost.com:8081 。多个值使用 , 分割,allow_credential 为 false 时可以使用 * 来表示所有 Origin 均允许通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Origin 都通过,但请注意这样存在安全隐患。',
+  'component.pluginForm.cors.allow_origins.extra': '例如: https://somehost.com:8081',
+  'component.pluginForm.cors.allow_methods.tooltip': '允许跨域访问的 Method,比如: GET,POST等。多个值使用 , 分割,allow_credential 为 false 时可以使用 * 来表示所有 Origin 均允许通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Method 都通过,但请注意这样存在安全隐患。',
+  'component.pluginForm.cors.allow_headers.tooltip': '允许跨域访问时请求方携带哪些非 CORS规范 以外的 Header, 多个值使用 , 分割,allow_credential 为 false 时可以使用 * 来表示所 有 Header 均允许通过。你也可以在启用了 allow_credential 后使用 ** 强制允许所有 Method 都通过,但请注意这样存在安全隐患。',
+  'component.pluginForm.cors.expose_headers.tooltip': '允许跨域访问时响应方携带哪些非 CORS规范 以外的 Header, 多个值使用 , 分割。',
+  'component.pluginForm.cors.max_age.tooltip': '浏览器缓存 CORS 结果的最大时间,单位为秒,在这个时间范围内浏览器会复用上一次的检查结果,-1 表示不缓存。',
+  'component.pluginForm.cors.allow_credential.tooltip': '是否允许跨域访问的请求方携带凭据(如 Cookie 等)。根据 CORS 规范,如果设置该选项为 true,那么将不能在其他选项中使用 * 。',
+  'component.pluginForm.cors.allow_origins_by_regex.tooltip': '使用正则表达式数组来匹配允许跨域访问的 Origin, 如[".*.test.com"] 可以匹配任何test.com的子域名 * 。',
 
   // api-breaker
   'component.pluginForm.api-breaker.break_response_code.tooltip': '不健康返回错误码。',