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/05/17 03:50:18 UTC

[incubator-apisix-dashboard] branch next updated: update SSL Certificate Page (#185)

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 ba82679  update SSL Certificate Page (#185)
ba82679 is described below

commit ba82679e5b3bec9dc9b36b12447b56f906b9fb69
Author: litesun <31...@users.noreply.github.com>
AuthorDate: Sun May 17 11:50:10 2020 +0800

    update SSL Certificate Page (#185)
    
    * update SSL Certificate Page
    
    * update style
    
    * drop edit button and add switch button
    
    * add list fields
    
    * add expireTime field
    
    * update
    
    * update ssl list
    
    * update
    
    * adjust code style
    
    * adjust code style
    
    * adjust code style
    
    * parse pem
    
    * update file upload
    
    * feat: refactor upload module
    
    * feat: added API for ssl module
    
    Co-authored-by: 琚致远 <ju...@apache.org>
---
 config/config.ts                                   |   4 +-
 package.json                                       |  17 +--
 src/models/ssl.ts                                  |   2 +-
 .../create/components/CertificateForm/index.tsx    |  75 ++++++++++++++
 .../components/CertificateUploader/index.tsx       | 115 +++++++++++++++++++++
 .../SSLModule/create/components/Step1/index.tsx    |  56 ++++++++++
 .../SSLModule/create/components/Step2/index.tsx    |  87 ++++++++++++++++
 .../SSLModule/create/components/Step3/index.tsx    |  32 ++++++
 .../SSLModule/create/components/Step4/index.tsx    |  36 +++++++
 src/pages/SSLModule/create/index.tsx               |  73 +++++++++++++
 src/pages/SSLModule/create/style.less              | 105 +++++++++++++++++++
 src/pages/SSLModule/list/index.tsx                 |  27 +++--
 yarn.lock                                          |  17 +++
 13 files changed, 628 insertions(+), 18 deletions(-)

diff --git a/config/config.ts b/config/config.ts
index 897ae99..c045c1e 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -82,9 +82,9 @@ export default defineConfig({
                   hideInMenu: true,
                 },
                 {
-                  path: '/ssl/create',
                   name: 'create',
-                  component: './SSLModule/detail',
+                  path: '/ssl/create',
+                  component: './SSLModule/create',
                   hideInMenu: true,
                 },
               ],
diff --git a/package.json b/package.json
index 2de27b6..cae1ab2 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,8 @@
     "classnames": "^2.2.6",
     "lodash": "^4.17.11",
     "moment": "^2.25.3",
+    "node-forge": "^0.9.1",
+    "nzh": "^1.0.3",
     "omit.js": "^1.0.2",
     "path-to-regexp": "2.4.0",
     "qs": "^6.9.0",
@@ -63,25 +65,28 @@
     "uuid": "^7.0.2"
   },
   "devDependencies": {
-    "@umijs/plugin-blocks": "^2.0.5",
-    "@umijs/preset-ant-design-pro": "^1.0.1",
-    "@umijs/preset-react": "^1.4.19",
-    "@umijs/preset-ui": "^2.0.9",
     "@ant-design/pro-cli": "^1.0.18",
     "@types/classnames": "^2.2.7",
     "@types/express": "^4.17.0",
     "@types/history": "^4.7.2",
     "@types/jest": "^25.1.0",
     "@types/lodash": "^4.14.144",
+    "@types/node-forge": "^0.9.3",
     "@types/qs": "^6.5.3",
     "@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.5",
+    "@umijs/plugin-blocks": "^2.0.5",
+    "@umijs/preset-ant-design-pro": "^1.0.1",
+    "@umijs/preset-react": "^1.4.19",
+    "@umijs/preset-ui": "^2.0.9",
+    "carlo": "^0.9.46",
     "chalk": "^4.0.0",
     "cross-env": "^7.0.0",
     "cross-port-killer": "^1.1.1",
+    "detect-installer": "^1.0.1",
     "enzyme": "^3.11.0",
     "express": "^4.17.1",
     "gh-pages": "^2.0.1",
@@ -93,9 +98,7 @@
     "node-fetch": "^2.6.0",
     "prettier": "^2.0.1",
     "pro-download": "1.0.1",
-    "stylelint": "^13.0.0",
-    "detect-installer": "^1.0.1",
-    "carlo": "^0.9.46"
+    "stylelint": "^13.0.0"
   },
   "optionalDependencies": {
     "puppeteer": "^2.0.0"
diff --git a/src/models/ssl.ts b/src/models/ssl.ts
index 47d956a..49edb08 100644
--- a/src/models/ssl.ts
+++ b/src/models/ssl.ts
@@ -1,5 +1,5 @@
 export interface SSL {
-  sni: string;
+  sni: string[];
   cert: string;
   key: string;
 }
diff --git a/src/pages/SSLModule/create/components/CertificateForm/index.tsx b/src/pages/SSLModule/create/components/CertificateForm/index.tsx
new file mode 100644
index 0000000..ef9ebbe
--- /dev/null
+++ b/src/pages/SSLModule/create/components/CertificateForm/index.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { Form, Input } from 'antd';
+import { useIntl } from 'umi';
+import { FormInstance } from 'antd/lib/form';
+import { FormData } from '../..';
+
+interface CertificateFormProps {
+  mode: 'EDIT' | 'VIEW';
+  data: FormData;
+  form: FormInstance;
+}
+
+const CertificateForm: React.FC<CertificateFormProps> = ({ mode, data, form }) => {
+  const { formatMessage } = useIntl();
+  const renderSNI = () => {
+    if (mode === 'VIEW') {
+      return (
+        <Form.Item
+          label="SNI"
+          name="sni"
+          rules={[
+            { required: true, message: formatMessage({ id: 'component.ssl.fieldSNIInvalid' }) },
+          ]}
+        >
+          <Input disabled={mode === 'VIEW'} />
+        </Form.Item>
+      );
+    }
+    return null;
+  };
+
+  const renderExpireTime = () => {
+    if (mode === 'VIEW') {
+      return (
+        <Form.Item
+          label="ExpireTime"
+          name="expireTime"
+          rules={[{ required: true, message: 'ExpireTime' }]}
+        >
+          <Input disabled={mode === 'VIEW'} />
+        </Form.Item>
+      );
+    }
+    return null;
+  };
+
+  return (
+    <Form form={form} layout="horizontal" initialValues={data}>
+      {renderSNI()}
+      <Form.Item
+        label="Cert"
+        name="cert"
+        rules={[
+          { required: true, message: formatMessage({ id: 'component.ssl.fieldCertInvalid' }) },
+          { min: 128, message: formatMessage({ id: 'component.ssl.fieldCertTooShort' }) },
+        ]}
+      >
+        <Input.TextArea rows={6} disabled={mode !== 'EDIT'} />
+      </Form.Item>
+      <Form.Item
+        label="Key"
+        name="key"
+        rules={[
+          { required: true, message: formatMessage({ id: 'component.ssl.fieldKeyInvalid' }) },
+          { min: 128, message: formatMessage({ id: 'component.ssl.fieldKeyTooShort' }) },
+        ]}
+      >
+        <Input.TextArea rows={6} disabled={mode !== 'EDIT'} />
+      </Form.Item>
+      {renderExpireTime()}
+    </Form>
+  );
+};
+
+export default CertificateForm;
diff --git a/src/pages/SSLModule/create/components/CertificateUploader/index.tsx b/src/pages/SSLModule/create/components/CertificateUploader/index.tsx
new file mode 100644
index 0000000..f83a8e3
--- /dev/null
+++ b/src/pages/SSLModule/create/components/CertificateUploader/index.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+import { Form, Button, Upload, message } from 'antd';
+import { UploadOutlined } from '@ant-design/icons';
+import { UploadChangeParam } from 'antd/lib/upload';
+import { UploadFile } from 'antd/lib/upload/interface';
+import { useForm } from 'antd/es/form/util';
+import forge from 'node-forge';
+import { FormData } from '../..';
+import styles from '../../style.less';
+
+export interface AltName {
+  value: string;
+}
+
+export type UploadType = 'PUBLIC_KEY' | 'PRIVATE_KEY';
+export interface UploadPublicSuccessData {
+  sni: string;
+  cert: string;
+  expireTime: Date;
+  publicKeyDefaultFileList: UploadFile[];
+}
+export interface UploadPrivateSuccessData {
+  key: string;
+  privateKeyDefaultFileList: UploadFile[];
+}
+
+interface UploaderProps {
+  onSuccess(data: UploadPublicSuccessData | UploadPrivateSuccessData): void;
+  onRemove(type: UploadType): void;
+  data: FormData;
+}
+
+const CertificateUploader: React.FC<UploaderProps> = ({ onSuccess, onRemove, data }) => {
+  const [form] = useForm();
+  const genUploadFile = (name = ''): UploadFile => {
+    return {
+      uid: Math.random().toString(36).slice(2),
+      name,
+      status: 'done',
+      size: 0,
+      type: '',
+    };
+  };
+  const onChange = (info: UploadChangeParam<UploadFile<any>>, type: UploadType) => {
+    if (!info.file.originFileObj) return;
+    const fileReader = new FileReader();
+    fileReader.readAsText(info.file.originFileObj);
+    // eslint-disable-next-line func-names
+    fileReader.onload = function (event) {
+      const { result } = event.currentTarget as any;
+      if (type === 'PUBLIC_KEY') {
+        try {
+          const cert = forge.pki.certificateFromPem(result);
+          const altNames = (cert.extensions.find((item) => item.name === 'subjectAltName')
+            .altNames as AltName[])
+            .map((item) => item.value)
+            .join(';');
+          const uploadPublicData: UploadPublicSuccessData = {
+            sni: altNames,
+            cert: result,
+            expireTime: cert.validity.notAfter,
+            publicKeyDefaultFileList: [genUploadFile(info.file.name)],
+          };
+          onSuccess(uploadPublicData);
+        } catch (error) {
+          message.error('证书解析失败');
+        }
+      } else {
+        const uploadprivateData: UploadPrivateSuccessData = {
+          key: result,
+          privateKeyDefaultFileList: [genUploadFile(info.file.name)],
+        };
+        onSuccess(uploadprivateData);
+      }
+    };
+  };
+
+  const publicUploadProps = {
+    defaultFileList: data.publicKeyDefaultFileList || [],
+  };
+
+  const privateUploadProps = {
+    defaultFileList: data.privateKeyDefaultFileList || [],
+  };
+
+  return (
+    <Form form={form} layout="horizontal" className={styles.stepForm}>
+      <Form.Item>
+        <Upload
+          className={styles.stepForm}
+          onChange={(info) => onChange(info, 'PUBLIC_KEY')}
+          onRemove={() => onRemove('PUBLIC_KEY')}
+          {...publicUploadProps}
+        >
+          <Button>
+            <UploadOutlined /> 点击上传公钥
+          </Button>
+        </Upload>
+      </Form.Item>
+      <Form.Item>
+        <Upload
+          className={styles.stepForm}
+          onChange={(info) => onChange(info, 'PRIVATE_KEY')}
+          onRemove={() => onRemove('PRIVATE_KEY')}
+          {...privateUploadProps}
+        >
+          <Button>
+            <UploadOutlined /> 点击上传私钥
+          </Button>
+        </Upload>
+      </Form.Item>
+    </Form>
+  );
+};
+export { CertificateUploader };
diff --git a/src/pages/SSLModule/create/components/Step1/index.tsx b/src/pages/SSLModule/create/components/Step1/index.tsx
new file mode 100644
index 0000000..bd702d4
--- /dev/null
+++ b/src/pages/SSLModule/create/components/Step1/index.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { Form, Button, Select } from 'antd';
+import styles from '../../style.less';
+import { StepProps } from '../..';
+
+const { Option } = Select;
+
+const formItemLayout = {
+  labelCol: {
+    span: 5,
+  },
+  wrapperCol: {
+    span: 19,
+  },
+};
+
+const Step1: React.FC<StepProps> = ({ data, onStepChange, onFormChange }) => {
+  const [form] = Form.useForm();
+  const { validateFields } = form;
+  const onValidateForm = () => {
+    validateFields().then((values) => {
+      onStepChange(1);
+      onFormChange({ createType: values.createType });
+    });
+  };
+
+  return (
+    <>
+      <Form
+        {...formItemLayout}
+        form={form}
+        layout="horizontal"
+        className={styles.stepForm}
+        initialValues={data}
+      >
+        <Form.Item
+          label="创建方式"
+          name="createType"
+          rules={[{ required: true, message: '请选择创建方式' }]}
+        >
+          <Select placeholder="请选择创建方式" onChange={() => onFormChange({}, true)}>
+            <Option value="INPUT">手动输入</Option>
+            <Option value="UPLOAD">上传证书</Option>
+          </Select>
+        </Form.Item>
+      </Form>
+      <div style={{ width: '100%', textAlign: 'center' }}>
+        <Button type="primary" onClick={onValidateForm}>
+          下一步
+        </Button>
+      </div>
+    </>
+  );
+};
+
+export default Step1;
diff --git a/src/pages/SSLModule/create/components/Step2/index.tsx b/src/pages/SSLModule/create/components/Step2/index.tsx
new file mode 100644
index 0000000..2346159
--- /dev/null
+++ b/src/pages/SSLModule/create/components/Step2/index.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { Form, Button, message } from 'antd';
+import forge from 'node-forge';
+import CertificateForm from '../CertificateForm';
+import {
+  CertificateUploader,
+  UploadPublicSuccessData,
+  UploadPrivateSuccessData,
+  UploadType,
+  AltName,
+} from '../CertificateUploader';
+import { StepProps } from '../..';
+
+const Step2: React.FC<StepProps> = ({ onStepChange, onFormChange, data }) => {
+  const [form] = Form.useForm();
+  const { validateFields } = form;
+
+  const onValidateForm = async () => {
+    if (data.createType === 'UPLOAD') {
+      if (!data.key || !data.cert) {
+        message.error('请检查证书');
+        return;
+      }
+      onStepChange(2);
+    }
+    if (data.createType === 'INPUT') {
+      validateFields().then((value) => {
+        try {
+          const cert = forge.pki.certificateFromPem(value.cert);
+          const altNames = (cert.extensions.find((item) => item.name === 'subjectAltName')
+            .altNames as AltName[])
+            .map((item) => item.value)
+            .join(';');
+          onFormChange({ ...value, sni: altNames, expireTime: cert.validity.notAfter });
+          onStepChange(2);
+        } catch (error) {
+          message.error('证书解析失败');
+        }
+      });
+    }
+  };
+  const onUploadSuccess = (
+    uploadSuccessData: UploadPublicSuccessData | UploadPrivateSuccessData,
+  ) => {
+    onFormChange(uploadSuccessData);
+  };
+  const onRemove = (type: UploadType) => {
+    if (type === 'PUBLIC_KEY') {
+      onFormChange({
+        publicKeyDefaultFileList: [],
+        cert: '',
+        sni: '',
+        expireTime: undefined,
+      });
+    } else {
+      onFormChange({
+        privateKeyDefaultFileList: [],
+        key: '',
+      });
+    }
+  };
+  return (
+    <>
+      {Boolean(data.createType === 'INPUT') && (
+        <CertificateForm mode="EDIT" form={form} data={data} />
+      )}
+      {Boolean(data.createType === 'UPLOAD') && (
+        <CertificateUploader onSuccess={onUploadSuccess} onRemove={onRemove} data={data} />
+      )}
+      <div style={{ width: '100%', textAlign: 'center' }}>
+        <Button
+          type="primary"
+          onClick={() => {
+            onStepChange(0);
+          }}
+          style={{ marginRight: '8px' }}
+        >
+          上一步
+        </Button>
+        <Button type="primary" onClick={onValidateForm}>
+          下一步
+        </Button>
+      </div>
+    </>
+  );
+};
+export default Step2;
diff --git a/src/pages/SSLModule/create/components/Step3/index.tsx b/src/pages/SSLModule/create/components/Step3/index.tsx
new file mode 100644
index 0000000..8b8a3f1
--- /dev/null
+++ b/src/pages/SSLModule/create/components/Step3/index.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Form, Button } from 'antd';
+import { create as createSSL } from '@/services/ssl';
+import CertificateForm from '../CertificateForm';
+import { StepProps } from '../..';
+
+const Step3: React.FC<StepProps> = ({ data, onStepChange }) => {
+  const [form] = Form.useForm();
+  const submit = () => {
+    createSSL({
+      sni: data.sni!.split(';'),
+      cert: data.cert!,
+      key: data.key!,
+    }).then(() => {
+      onStepChange(3);
+    });
+  };
+  return (
+    <div className="container">
+      <CertificateForm mode="VIEW" form={form} data={data} />
+      <div style={{ width: '100%', textAlign: 'center' }}>
+        <Button type="primary" onClick={() => onStepChange(1)} style={{ marginRight: '8px' }}>
+          上一步
+        </Button>
+        <Button type="primary" onClick={submit}>
+          提交
+        </Button>
+      </div>
+    </div>
+  );
+};
+export default Step3;
diff --git a/src/pages/SSLModule/create/components/Step4/index.tsx b/src/pages/SSLModule/create/components/Step4/index.tsx
new file mode 100644
index 0000000..1a503cc
--- /dev/null
+++ b/src/pages/SSLModule/create/components/Step4/index.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { Result, Button } from 'antd';
+import { history } from 'umi';
+import { StepProps } from '../..';
+
+const Step4: React.FC<StepProps> = ({ onStepChange, onFormChange }) => {
+  return (
+    <Result
+      status="success"
+      title="成功"
+      subTitle="SSL证书上传成功"
+      key="SSL证书上传成功"
+      extra={[
+        <Button
+          type="primary"
+          key="back"
+          onClick={() => {
+            history.replace('/ssl');
+          }}
+        >
+          回到列表页
+        </Button>,
+        <Button
+          key="reset"
+          onClick={() => {
+            onFormChange({}, true);
+            onStepChange(0);
+          }}
+        >
+          继续创建
+        </Button>,
+      ]}
+    />
+  );
+};
+export default Step4;
diff --git a/src/pages/SSLModule/create/index.tsx b/src/pages/SSLModule/create/index.tsx
new file mode 100644
index 0000000..24a3655
--- /dev/null
+++ b/src/pages/SSLModule/create/index.tsx
@@ -0,0 +1,73 @@
+import React, { useState } from 'react';
+import { Card, Steps } from 'antd';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import { UploadFile } from 'antd/es/upload/interface';
+import Step1 from './components/Step1';
+import Step2 from './components/Step2';
+import Step3 from './components/Step3';
+import Step4 from './components/Step4';
+import styles from './style.less';
+
+const { Step } = Steps;
+
+export interface FormData {
+  createType?: 'INPUT' | 'UPLOAD';
+  sni?: string;
+  cert?: string;
+  key?: string;
+  expireTime?: Date;
+  publicKeyDefaultFileList?: UploadFile[];
+  privateKeyDefaultFileList?: UploadFile[];
+}
+
+export interface StepProps {
+  data: FormData;
+  onStepChange(step: number): void;
+  onFormChange(data: FormData, reset?: boolean): void;
+}
+
+const Create: React.FC = () => {
+  const [currentStep, setCurrentStep] = useState(0);
+  const [formData, setFormData] = useState<Partial<FormData>>({
+    createType: 'INPUT',
+  });
+  const setpProps = {
+    data: formData,
+    onStepChange: setCurrentStep,
+    onFormChange: (params: FormData, reset?: boolean) => {
+      if (reset) {
+        setFormData({});
+      } else {
+        setFormData({ ...formData, ...params });
+      }
+    },
+  };
+
+  const renderStep = () => {
+    return (
+      <>
+        {Boolean(currentStep === 0) && <Step1 {...setpProps} />}
+        {Boolean(currentStep === 1) && <Step2 {...setpProps} />}
+        {Boolean(currentStep === 2) && <Step3 {...setpProps} />}
+        {Boolean(currentStep === 3) && <Step4 {...setpProps} />}
+      </>
+    );
+  };
+
+  return (
+    <PageHeaderWrapper content="">
+      <Card bordered={false}>
+        <>
+          <Steps current={currentStep} className={styles.steps}>
+            {['选择创建类型', '编辑上传信息', '预览', '完成'].map((item) => (
+              <Step title={item} key={item} />
+            ))}
+          </Steps>
+          {renderStep()}
+        </>
+      </Card>
+    </PageHeaderWrapper>
+  );
+};
+
+export default Create;
diff --git a/src/pages/SSLModule/create/style.less b/src/pages/SSLModule/create/style.less
new file mode 100644
index 0000000..944a740
--- /dev/null
+++ b/src/pages/SSLModule/create/style.less
@@ -0,0 +1,105 @@
+@import '~antd/es/style/themes/default.less';
+
+.card {
+  margin-bottom: 24px;
+}
+
+.heading {
+  margin: 0 0 16px 0;
+  font-size: 14px;
+  line-height: 22px;
+}
+
+.steps:global(.ant-steps) {
+  max-width: 750px;
+  margin: 16px auto;
+}
+
+.errorIcon {
+  margin-right: 24px;
+  color: @error-color;
+  cursor: pointer;
+
+  span.anticon {
+    margin-right: 4px;
+  }
+}
+
+.errorPopover {
+  :global {
+    .ant-popover-inner-content {
+      min-width: 256px;
+      max-height: 290px;
+      padding: 0;
+      overflow: auto;
+    }
+  }
+}
+
+.errorListItem {
+  padding: 8px 16px;
+  list-style: none;
+  border-bottom: 1px solid @border-color-split;
+  cursor: pointer;
+  transition: all 0.3s;
+
+  &:hover {
+    background: @item-active-bg;
+  }
+
+  &:last-child {
+    border: 0;
+  }
+
+  .errorIcon {
+    float: left;
+    margin-top: 4px;
+    margin-right: 12px;
+    padding-bottom: 22px;
+    color: @error-color;
+  }
+
+  .errorField {
+    margin-top: 2px;
+    color: @text-color-secondary;
+    font-size: 12px;
+  }
+}
+
+.editable {
+  td {
+    padding-top: 13px !important;
+    padding-bottom: 12.5px !important;
+  }
+}
+
+// custom footer for fixed footer toolbar
+.advancedForm + div {
+  padding-bottom: 64px;
+}
+
+.advancedForm {
+  :global {
+    .ant-form .ant-row:last-child .ant-form-item {
+      margin-bottom: 24px;
+    }
+
+    .ant-table td {
+      transition: none !important;
+    }
+  }
+}
+
+.optional {
+  color: @text-color-secondary;
+  font-style: normal;
+}
+
+.button-area {
+  display: flex;
+  justify-content: center;
+}
+.stepForm {
+  max-width: 500px;
+  margin: 40px auto 0;
+}
diff --git a/src/pages/SSLModule/list/index.tsx b/src/pages/SSLModule/list/index.tsx
index 67a78c2..b7423af 100644
--- a/src/pages/SSLModule/list/index.tsx
+++ b/src/pages/SSLModule/list/index.tsx
@@ -1,7 +1,7 @@
 import React, { useRef } from 'react';
 import { PageHeaderWrapper } from '@ant-design/pro-layout';
 import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
-import { Button, Modal, notification } from 'antd';
+import { Button, Modal, notification, Switch } from 'antd';
 import { history, useIntl } from 'umi';
 import { PlusOutlined } from '@ant-design/icons';
 
@@ -43,18 +43,29 @@ const List: React.FC = () => {
       title: 'SNI',
       dataIndex: ['value', 'sni'],
     },
+    // TODO: need to check api response
+    {
+      title: '关联路由',
+      dataIndex: [],
+    },
+    {
+      title: '过期时间',
+      dataIndex: [],
+    },
+    {
+      title: '是否启用',
+      valueType: 'option',
+      render: (_, record) => (
+        <>
+          <Switch defaultChecked onClick={() => console.log(record)} />
+        </>
+      ),
+    },
     {
       title: formatMessage({ id: 'component.global.action' }),
       valueType: 'option',
       render: (_, record) => (
         <>
-          <Button
-            type="primary"
-            style={{ marginRight: '10px' }}
-            onClick={() => history.push(`/ssl/${record.key}/edit`)}
-          >
-            {formatMessage({ id: 'component.global.edit' })}
-          </Button>
           <Button type="primary" danger onClick={() => onRemove(record.key)}>
             {formatMessage({ id: 'component.global.remove' })}
           </Button>
diff --git a/yarn.lock b/yarn.lock
index dcc9f91..0a48835 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2492,6 +2492,13 @@
   resolved "https://registry.npmjs.org/@types/mustache/-/mustache-4.0.1.tgz#e4d421ed2d06d463b120621774185a5cd1b92d77"
   integrity sha512-wH6Tu9mbiOt0n5EvdoWy0VGQaJMHfLIxY/6wS0xLC7CV1taM6gESEzcYy0ZlWvxxiiljYvfDIvz4hHbUUDRlhw==
 
+"@types/node-forge@^0.9.3":
+  version "0.9.3"
+  resolved "https://registry.npm.taobao.org/@types/node-forge/download/@types/node-forge-0.9.3.tgz#5f8299a3f2b069a7e165c807bef6b17464f8b8ad"
+  integrity sha1-X4KZo/KwaafhZcgHvvaxdGT4uK0=
+  dependencies:
+    "@types/node" "*"
+
 "@types/node@*":
   version "14.0.1"
   resolved "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz#5d93e0a099cd0acd5ef3d5bde3c086e1f49ff68c"
@@ -11987,6 +11994,11 @@ node-fetch@^1.0.1:
     encoding "^0.1.11"
     is-stream "^1.0.1"
 
+node-forge@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.npm.taobao.org/node-forge/download/node-forge-0.9.1.tgz?cache=0&sync_timestamp=1569524669712&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnode-forge%2Fdownload%2Fnode-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
+  integrity sha1-d1No5oRlWKtmdoWKTYxujRbGd7U=
+
 node-import-ts@^1.0.1, node-import-ts@^1.0.2:
   version "1.0.5"
   resolved "https://registry.npmjs.org/node-import-ts/-/node-import-ts-1.0.5.tgz#2b5aef3c8fa9babc9d1f948ddfeb62231c18d4f8"
@@ -12216,6 +12228,11 @@ nwsapi@^2.0.7, nwsapi@^2.1.3, nwsapi@^2.2.0:
   resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
   integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
 
+nzh@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.npm.taobao.org/nzh/download/nzh-1.0.4.tgz#bded5492cd7148fa5fe1c809fa61932a899769c5"
+  integrity sha1-ve1Uks1xSPpf4cgJ+mGTKomXacU=
+
 oauth-sign@~0.9.0:
   version "0.9.0"
   resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"