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"