Jelajahi Sumber

fix: 证书管理

sun-chaochao 3 tahun lalu
induk
melakukan
d9f2e05c0f

TEMPAT SAMPAH
public/images/certificate.png


+ 1 - 1
src/pages/Northbound/AliCloud/Detail/index.tsx

@@ -318,7 +318,7 @@ const Detail = observer(() => {
         <Row gutter={24}>
           <Col span={14}>
             <TitleComponent data={'基本信息'} />
-            <Form form={form} layout="vertical" onAutoSubmit={console.log}>
+            <Form form={form} layout="vertical">
               <SchemaField
                 schema={schema}
                 scope={{

+ 23 - 4
src/pages/Northbound/AliCloud/index.tsx

@@ -44,7 +44,10 @@ const AliCloud = () => {
               }
             : undefined
         }
-        onClick={() => {}}
+        onClick={() => {
+          const url = `${getMenuPathByParams(MENUS_CODE['Northbound/AliCloud/Detail'], record.id)}`;
+          history.push(url);
+        }}
       >
         <EditOutlined />
         {type !== 'table' &&
@@ -96,7 +99,20 @@ const AliCloud = () => {
         popConfirm={{
           title: '确认删除?',
           disabled: record.state.value === 'started',
-          onConfirm: () => {},
+          onConfirm: async () => {
+            if (record?.state?.value === 'disabled') {
+              await service.remove(record.id);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            } else {
+              message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+            }
+          },
         }}
         tooltip={{
           title:
@@ -124,9 +140,12 @@ const AliCloud = () => {
     {
       title: '状态',
       dataIndex: 'state',
-      render: (text: any) => (
+      render: (text: any, record: any) => (
         <span>
-          <Badge status={text.value === 'disabled' ? 'error' : 'success'} text={text.text} />
+          <Badge
+            status={record?.state?.value === 'disabled' ? 'error' : 'success'}
+            text={record?.state?.text}
+          />
         </span>
       ),
       valueType: 'select',

+ 52 - 0
src/pages/account/NotificationRecord/detail/index.tsx

@@ -0,0 +1,52 @@
+import { Descriptions, Modal } from 'antd';
+import type { AccessLogItem } from '@/pages/Log/Access/typings';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
+
+interface Props {
+  data: Partial<AccessLogItem>;
+  close: () => void;
+}
+
+const Detail = (props: Props) => {
+  const [data, setDada] = useState<Partial<AccessLogItem>>(props.data || {});
+
+  useEffect(() => {
+    setDada(props.data);
+  }, [props.data]);
+
+  return (
+    <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={1000}>
+      <Descriptions bordered>
+        <Descriptions.Item label="URL">{data?.url}</Descriptions.Item>
+        <Descriptions.Item label="请求方法" span={2}>
+          {data?.httpMethod}
+        </Descriptions.Item>
+        <Descriptions.Item label="动作">{data?.action}</Descriptions.Item>
+        <Descriptions.Item label="类名" span={2}>
+          {data?.target}
+        </Descriptions.Item>
+        <Descriptions.Item label="方法名">{data?.method}</Descriptions.Item>
+        <Descriptions.Item label="IP" span={2}>
+          {data?.ip}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求时间">
+          {moment(data?.requestTime).format('YYYY-MM-DD HH:mm:ss')}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求耗时" span={2}>
+          {(data?.responseTime || 0) - (data?.requestTime || 0)}ms
+        </Descriptions.Item>
+        <Descriptions.Item label="请求头" span={3}>
+          {JSON.stringify(data?.httpHeaders)}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求参数" span={3}>
+          {JSON.stringify(data?.parameters)}
+        </Descriptions.Item>
+        <Descriptions.Item label="异常信息" span={3}>
+          {data?.exception}
+        </Descriptions.Item>
+      </Descriptions>
+    </Modal>
+  );
+};
+export default Detail;

+ 110 - 0
src/pages/account/NotificationRecord/index.tsx

@@ -0,0 +1,110 @@
+import { useIntl } from '@/.umi/plugin-locale/localeExports';
+import PermissionButton from '@/components/PermissionButton';
+import SearchComponent from '@/components/SearchComponent';
+import BaseService from '@/utils/BaseService';
+import { ReadOutlined, SearchOutlined } from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-layout';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { useRef, useState } from 'react';
+import type { NotifitionRecord } from './typings';
+import Detail from './detail';
+
+export const service = new BaseService<NotifitionRecord>('network/certificate');
+
+const NotificationRecord = () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<NotifitionRecord | undefined>(undefined);
+
+  const columns: ProColumns<NotifitionRecord>[] = [
+    {
+      dataIndex: 'instance',
+      title: '类型',
+    },
+    {
+      dataIndex: 'name',
+      title: '消息',
+    },
+    {
+      dataIndex: 'description',
+      title: '通知时间',
+    },
+    {
+      dataIndex: 'state',
+      title: '状态',
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <PermissionButton
+          key={'update'}
+          type={'link'}
+          isPermission={true}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: '标为已读',
+          }}
+          onClick={() => {
+            setCurrent(record);
+            setVisible(true);
+          }}
+        >
+          <ReadOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          key={'action'}
+          type={'link'}
+          isPermission={true}
+          style={{ padding: 0 }}
+          tooltip={{
+            title: '查看',
+          }}
+        >
+          <SearchOutlined />
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <SearchComponent<NotifitionRecord>
+        field={columns}
+        target="notification-record"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<NotifitionRecord>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+      />
+      {visible && (
+        <Detail
+          close={() => {
+            actionRef.current?.reload();
+          }}
+          data={current}
+        />
+      )}
+    </PageContainer>
+  );
+};
+
+export default NotificationRecord;

+ 5 - 0
src/pages/account/NotificationRecord/typings.d.ts

@@ -0,0 +1,5 @@
+import type { BaseItem } from '@/utils/typings';
+
+type NotifitionRecord = {
+  type: string;
+} & BaseItem;

+ 179 - 0
src/pages/account/NotificationSubscription/index.tsx

@@ -0,0 +1,179 @@
+import { useIntl } from '@/.umi/plugin-locale/localeExports';
+import PermissionButton from '@/components/PermissionButton';
+import SearchComponent from '@/components/SearchComponent';
+import BaseService from '@/utils/BaseService';
+import {
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+  StopOutlined,
+} from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-layout';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { useRef, useState } from 'react';
+import type { NotifitionSubscriptionItem } from './typings';
+import Save from './save';
+
+export const service = new BaseService<NotifitionSubscriptionItem>('network/certificate');
+
+const NotificationSubscription = () => {
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<NotifitionSubscriptionItem | undefined>(undefined);
+
+  const Tools = (record: any) => {
+    return [
+      <PermissionButton
+        key={'update'}
+        type={'link'}
+        isPermission={true}
+        style={{ padding: 0 }}
+        tooltip={{
+          title: intl.formatMessage({
+            id: 'pages.data.option.edit',
+            defaultMessage: '编辑',
+          }),
+        }}
+        onClick={() => {
+          setVisible(true);
+          setCurrent(record);
+        }}
+      >
+        <EditOutlined />
+      </PermissionButton>,
+      <PermissionButton
+        key={'action'}
+        type={'link'}
+        style={{ padding: 0 }}
+        isPermission={true}
+        popConfirm={{
+          title: intl.formatMessage({
+            id: `pages.data.option.${
+              record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'
+            }.tips`,
+            defaultMessage: '确认禁用?',
+          }),
+          onConfirm: async () => {
+            // const resp =
+            //   record?.state?.value !== 'disabled'
+            //     ? await service._disable(record.id)
+            //     : await service._enable(record.id);
+            // if (resp.status === 200) {
+            //   message.success('操作成功!');
+            //   actionRef.current?.reload?.();
+            // } else {
+            //   message.error('操作失败!');
+            // }
+          },
+        }}
+        tooltip={{
+          title: intl.formatMessage({
+            id: `pages.data.option.${record?.state?.value !== 'disabled' ? 'disabled' : 'enabled'}`,
+            defaultMessage: '启用',
+          }),
+        }}
+      >
+        {record?.state?.value !== 'disabled' ? <StopOutlined /> : <PlayCircleOutlined />}
+      </PermissionButton>,
+      <PermissionButton
+        key={'delete'}
+        type={'link'}
+        isPermission={true}
+        style={{ padding: 0 }}
+        popConfirm={{
+          title: '确认删除?',
+          onConfirm: () => {},
+        }}
+        tooltip={{
+          title: '删除',
+        }}
+      >
+        <DeleteOutlined />
+      </PermissionButton>,
+    ];
+  };
+
+  const columns: ProColumns<NotifitionSubscriptionItem>[] = [
+    {
+      dataIndex: 'instance',
+      title: '名称',
+    },
+    {
+      dataIndex: 'name',
+      title: '类型',
+      hideInSearch: true,
+    },
+    {
+      dataIndex: 'description',
+      title: '告警规则',
+      hideInSearch: true,
+    },
+    {
+      dataIndex: 'state',
+      title: '状态',
+      hideInSearch: true,
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [Tools(record)],
+    },
+  ];
+  return (
+    <PageContainer>
+      <SearchComponent<NotifitionSubscriptionItem>
+        field={columns}
+        target="notification-subscription"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<NotifitionSubscriptionItem>
+        actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+        headerTitle={[
+          <PermissionButton
+            onClick={() => {
+              setVisible(true);
+              setCurrent(undefined);
+            }}
+            isPermission={true}
+            style={{ marginRight: 12 }}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>,
+        ]}
+      />
+      {visible && (
+        <Save
+          close={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+          data={current}
+        />
+      )}
+    </PageContainer>
+  );
+};
+
+export default NotificationSubscription;

+ 158 - 0
src/pages/account/NotificationSubscription/save/index.tsx

@@ -0,0 +1,158 @@
+import { Modal } from 'antd';
+import type { AccessLogItem } from '@/pages/Log/Access/typings';
+import { useEffect, useMemo, useState } from 'react';
+import { Form, FormGrid, FormItem, Input, Select, Checkbox } from '@formily/antd';
+import { createForm } from '@formily/core';
+import type { ISchema } from '@formily/react';
+import { createSchemaField } from '@formily/react';
+
+interface Props {
+  data: Partial<AccessLogItem>;
+  close: () => void;
+}
+
+const Save = (props: Props) => {
+  const [data, setDada] = useState<Partial<AccessLogItem>>(props.data || {});
+
+  useEffect(() => {
+    setDada(props.data);
+  }, [props.data]);
+
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+        initialValues: data,
+        effects() {
+          //
+        },
+      }),
+    [],
+  );
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      grid: {
+        type: 'void',
+        'x-component': 'FormGrid',
+        'x-component-props': {
+          maxColumns: 2,
+          minColumns: 1,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            'x-component': 'Input',
+            'x-decorator': 'FormItem',
+            required: true,
+            'x-decorator-props': {
+              gridSpan: 2,
+              labelAlign: 'left',
+              layout: 'vertical',
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+          },
+          type: {
+            title: '类型',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            required: true,
+            'x-decorator-props': {
+              gridSpan: 1,
+              labelAlign: 'left',
+              layout: 'vertical',
+            },
+            enum: [
+              {
+                label: '设备告警',
+                value: 'device',
+              },
+            ],
+            'x-component-props': {
+              placeholder: '请选择类型',
+            },
+          },
+          rule: {
+            title: '告警规则',
+            'x-component': 'Select',
+            'x-decorator': 'FormItem',
+            required: true,
+            'x-decorator-props': {
+              gridSpan: 1,
+              labelAlign: 'left',
+              layout: 'vertical',
+            },
+            enum: [],
+            'x-component-props': {
+              placeholder: '请选择告警规则',
+            },
+          },
+          notice: {
+            title: '通知方式',
+            type: 'array',
+            required: true,
+            enum: [
+              {
+                label: '站内通知',
+                value: 1,
+              },
+              {
+                label: '邮件通知',
+                value: 2,
+              },
+              {
+                label: '短信通知',
+                value: 3,
+              },
+            ],
+            'x-decorator': 'FormItem',
+            'x-component': 'Checkbox.Group',
+            'x-decorator-props': {
+              gridSpan: 2,
+              labelAlign: 'left',
+              layout: 'vertical',
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      FormGrid,
+      Input,
+      Select,
+      Checkbox,
+    },
+  });
+
+  return (
+    <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={'45vw'}>
+      <Form form={form} layout="vertical">
+        <SchemaField
+          schema={schema}
+          scope={
+            {
+              // useAsyncDataSource,
+              // queryRegionsList,
+              // queryProductList,
+              // queryAliyunProductList,
+            }
+          }
+        />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 5 - 0
src/pages/account/NotificationSubscription/typings.d.ts

@@ -0,0 +1,5 @@
+import type { BaseItem } from '@/utils/typings';
+
+type NotifitionSubscriptionItem = {
+  type: string;
+} & BaseItem;

+ 68 - 0
src/pages/link/Certificate/Detail/components/CertificateFile/index.tsx

@@ -0,0 +1,68 @@
+import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
+import { UploadOutlined } from '@ant-design/icons';
+import { Button, Input, message, Upload } from 'antd';
+import type { UploadChangeParam } from 'antd/lib/upload';
+import { useEffect, useState } from 'react';
+
+interface Props {
+  value?: string;
+  onChange?: (type: any) => void;
+}
+
+const CertificateFile = (props: Props) => {
+  const [keystoreBase64, setKeystoreBase64] = useState<string>('');
+
+  useEffect(() => {
+    setKeystoreBase64(props?.value || '');
+  }, [props.value]);
+
+  const handleChange = (info: UploadChangeParam) => {
+    if (info.file.status === 'done') {
+      const {
+        file: { response },
+      } = info;
+      if (response.status === 200) {
+        message.success('上传成功');
+        setKeystoreBase64(response.result);
+        if (props.onChange) {
+          props.onChange(response.result);
+        }
+      }
+    } else if (info.file.status === 'error') {
+      message.error(`${info.file.name} file upload failed.`);
+    }
+  };
+
+  return (
+    <div>
+      <Input.TextArea
+        onChange={(e) => {
+          setKeystoreBase64(e.target.value);
+          if (props.onChange) {
+            props.onChange(e.target.value);
+          }
+        }}
+        value={keystoreBase64}
+        rows={4}
+        placeholder='证书格式以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾。'
+      />
+      <Upload
+        accept=".pem"
+        listType={'text'}
+        action={`/${SystemConst.API_BASE}/network/certificate/upload`}
+        headers={{
+          'X-Access-Token': Token.get(),
+        }}
+        onChange={handleChange}
+        showUploadList={false}
+      >
+        <Button style={{ marginTop: 10 }} icon={<UploadOutlined />}>
+          上传文件
+        </Button>
+      </Upload>
+    </div>
+  );
+};
+
+export default CertificateFile;

+ 38 - 0
src/pages/link/Certificate/Detail/components/Standard/index.tsx

@@ -0,0 +1,38 @@
+import { useEffect, useState } from 'react';
+
+interface Props {
+  value?: string;
+  onChange?: (type: any) => void;
+}
+
+const Standard = (props: Props) => {
+  const imgMap = new Map<any, any>();
+  imgMap.set('common', require('/public/images/certificate.png'));
+
+  const [type, setType] = useState(props.value || '');
+
+  useEffect(() => {
+    setType(props.value || '');
+  }, [props.value]);
+
+  return (
+    <div>
+      {['common'].map((i) => (
+        <div
+          onClick={() => {
+            setType(i);
+            if (props.onChange) {
+              props.onChange(i);
+            }
+          }}
+          style={{ width: 150, border: type === i ? '1px solid #2F54EB' : '' }}
+          key={i}
+        >
+          <img style={{ width: '100%' }} src={imgMap.get(i)} />
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default Standard;

+ 143 - 0
src/pages/link/Certificate/Detail/index.tsx

@@ -0,0 +1,143 @@
+import PermissionButton from '@/components/PermissionButton';
+import usePermissions from '@/hooks/permission';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Form, FormButtonGroup, FormItem } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { Card, Col, Input, message, Row } from 'antd';
+import { createSchemaField, observer } from '@formily/react';
+import { useEffect, useMemo } from 'react';
+import { createForm } from '@formily/core';
+import CertificateFile from './components/CertificateFile';
+import Standard from './components/Standard';
+import { service } from '@/pages/link/Certificate';
+import { useParams } from 'umi';
+
+const Detail = observer(() => {
+  const params = useParams<{ id: string }>();
+  const form = useMemo(
+    () =>
+      createForm({
+        validateFirst: true,
+      }),
+    [],
+  );
+
+  useEffect(() => {
+    if (params.id && params.id !== ':id') {
+      service.detail(params.id).then((resp) => {
+        if (resp.status === 200) {
+          form.setValues(resp.result);
+        }
+      });
+    }
+  }, [params.id]);
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      CertificateFile,
+      Standard,
+    },
+  });
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      type: {
+        type: 'string',
+        title: '证书标准',
+        required: true,
+        default: 'common',
+        'x-decorator': 'FormItem',
+        'x-component': 'Standard',
+      },
+      name: {
+        type: 'string',
+        title: '证书名称',
+        required: true,
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-component-props': {
+          placeholder: '请输入证书名称',
+        },
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+        ],
+      },
+      'configs.cert': {
+        title: '证书文件',
+        'x-component': 'CertificateFile',
+        'x-decorator': 'FormItem',
+        required: true,
+        'x-component-props': {
+          rows: 3,
+          placeholder:
+            '证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。',
+        },
+      },
+      'configs.key': {
+        title: '证书私钥',
+        'x-component': 'Input.TextArea',
+        'x-decorator': 'FormItem',
+        required: true,
+        'x-component-props': {
+          rows: 3,
+          placeholder:
+            '证书私钥格式以"-----BEGIN (RSA|EC) PRIVATE KEY-----"开头,以"-----END(RSA|EC) PRIVATE KEY-----"结尾。',
+        },
+      },
+      description: {
+        title: '说明',
+        'x-component': 'Input.TextArea',
+        'x-decorator': 'FormItem',
+        'x-component-props': {
+          rows: 3,
+          showCount: true,
+          maxLength: 200,
+          placeholder: '请输入说明',
+        },
+      },
+    },
+  };
+
+  const { getOtherPermission } = usePermissions('link/Certificate');
+
+  return (
+    <PageContainer>
+      <Card>
+        <Row gutter={24}>
+          <Col span={12}>
+            <Form form={form} layout="vertical">
+              <SchemaField schema={schema} />
+              <FormButtonGroup.Sticky>
+                <FormButtonGroup.FormItem>
+                  <PermissionButton
+                    type="primary"
+                    isPermission={getOtherPermission(['add', 'update'])}
+                    onClick={async () => {
+                      const data: any = await form.submit();
+                      const response: any = data.id
+                        ? await service.update(data)
+                        : await service.save(data);
+                      if (response.status === 200) {
+                        message.success('操作成功');
+                        history.back();
+                      }
+                    }}
+                  >
+                    保存
+                  </PermissionButton>
+                </FormButtonGroup.FormItem>
+              </FormButtonGroup.Sticky>
+            </Form>
+          </Col>
+        </Row>
+      </Card>
+    </PageContainer>
+  );
+});
+
+export default Detail;

+ 79 - 151
src/pages/link/Certificate/index.tsx

@@ -1,46 +1,37 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import BaseService from '@/utils/BaseService';
-import type { CertificateItem } from '@/pages/link/Certificate/typings';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
-import { message, Popconfirm, Tooltip } from 'antd';
-import { EditOutlined, MinusOutlined } from '@ant-design/icons';
-import BaseCrud from '@/components/BaseCrud';
+import { message } from 'antd';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import type { ISchema } from '@formily/json-schema';
-import { CurdModel } from '@/components/BaseCrud/model';
+import SearchComponent from '@/components/SearchComponent';
+import ProTable from '@jetlinks/pro-table';
+import PermissionButton from '@/components/PermissionButton';
+import usePermissions from '@/hooks/permission';
+import { getMenuPathByParams, MENUS_CODE } from '@/utils/menu';
+import { history } from 'umi';
+import Service from '../service';
+
+export const service = new Service('network/certificate');
 
-export const service = new BaseService<CertificateItem>('network/certificate');
 const Certificate = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const { permission } = usePermissions('link/Certificate');
 
   const columns: ProColumns<CertificateItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
+      dataIndex: 'type',
+      title: '证书标准',
     },
     {
       dataIndex: 'name',
-      title: intl.formatMessage({
-        id: 'pages.table.name',
-        defaultMessage: '名称',
-      }),
-    },
-    {
-      dataIndex: 'instance',
-      title: intl.formatMessage({
-        id: 'pages.link.type',
-        defaultMessage: '类型',
-      }),
+      title: '证书名称',
     },
     {
       dataIndex: 'description',
-      title: intl.formatMessage({
-        id: 'pages.table.describe',
-        defaultMessage: '描述',
-      }),
+      title: '说明',
     },
     {
       title: intl.formatMessage({
@@ -51,29 +42,29 @@ const Certificate = () => {
       align: 'center',
       width: 200,
       render: (text, record) => [
-        <a
-          key="edit"
+        <PermissionButton
+          key={'update'}
+          type={'link'}
+          style={{ padding: 0 }}
+          isPermission={permission.update}
+          tooltip={{
+            title: '编辑',
+          }}
           onClick={() => {
-            CurdModel.update(record);
-            CurdModel.model = 'edit';
+            const url = `${getMenuPathByParams(MENUS_CODE['link/Certificate/Detail'], record.id)}`;
+            history.push(url);
           }}
         >
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
-          </Tooltip>
-        </a>,
-        <a key="delete">
-          <Popconfirm
-            title={intl.formatMessage({
-              id: 'pages.data.option.remove.tips',
-              defaultMessage: '确认删除?',
-            })}
-            onConfirm={async () => {
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          key={'delete'}
+          type={'link'}
+          style={{ padding: 0 }}
+          isPermission={permission.delete}
+          popConfirm={{
+            title: '确认删除?',
+            onConfirm: async () => {
               await service.remove(record.id);
               message.success(
                 intl.formatMessage({
@@ -82,117 +73,54 @@ const Certificate = () => {
                 }),
               );
               actionRef.current?.reload();
-            }}
-          >
-            <Tooltip
-              title={intl.formatMessage({
-                id: 'pages.data.option.remove',
-                defaultMessage: '删除',
-              })}
-            >
-              <MinusOutlined />
-            </Tooltip>
-          </Popconfirm>
-        </a>,
+            },
+          }}
+          tooltip={{
+            title: '删除',
+          }}
+        >
+          <DeleteOutlined />
+        </PermissionButton>,
       ],
     },
   ];
 
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      name: {
-        title: '名称',
-        'x-component': 'Input',
-        'x-decorator': 'FormItem',
-      },
-      instance: {
-        title: '类型',
-        'x-component': 'Select',
-        'x-decorator': 'FormItem',
-        default: 'PEM',
-        enum: [
-          { label: 'PFX', value: 'PFX' },
-          { label: 'JKS', value: 'JKS' },
-          { label: 'PEM', value: 'PEM' },
-        ],
-      },
-      configs: {
-        type: 'object',
-        properties: {
-          '{url:keystoreBase64}': {
-            title: '密钥库',
-            'x-component': 'FUpload',
-            'x-decorator': 'FormItem',
-            'x-component-props': {
-              type: 'file',
-            },
-          },
-          keystorePwd: {
-            title: '密钥库密码',
-            'x-component': 'Password',
-            'x-decorator': 'FormItem',
-            'x-visible': false,
-            'x-component-props': {
-              style: {
-                width: '100%',
-              },
-            },
-            'x-reactions': {
-              dependencies: ['..instance'],
-              fulfill: {
-                state: {
-                  visible: '{{["JKS","PFX"].includes($deps[0])}}',
-                },
-              },
-            },
-          },
-          '{url:trustKeyStoreBase64}': {
-            title: '信任库',
-            'x-component': 'FUpload',
-            'x-decorator': 'FormItem',
-            'x-component-props': {
-              style: {
-                width: '100px',
-              },
-              type: 'file',
-            },
-          },
-          trustKeyStorePwd: {
-            title: '信任库密码',
-            'x-visible': false,
-            'x-decorator': 'FormItem',
-            'x-component': 'Password',
-            'x-reactions': {
-              dependencies: ['..instance'],
-              fulfill: {
-                state: {
-                  visible: '{{["JKS","PFX"].includes($deps[0])}}',
-                },
-              },
-            },
-          },
-        },
-      },
-      description: {
-        title: '说明',
-        'x-component': 'Input.TextArea',
-        'x-decorator': 'FormItem',
-      },
-    },
-  };
-
   return (
     <PageContainer>
-      <BaseCrud
-        columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.link.certificate',
-          defaultMessage: '证书管理',
-        })}
-        schema={schema}
+      <SearchComponent<CertificateItem>
+        field={columns}
+        target="certificate"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<CertificateItem>
         actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        headerTitle={
+          <PermissionButton
+            onClick={() => {
+              const url = `${getMenuPathByParams(MENUS_CODE['link/Certificate/Detail'])}`;
+              history.push(url);
+            }}
+            isPermission={permission.add}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>
+        }
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
       />
     </PageContainer>
   );

+ 14 - 0
src/pages/link/Certificate/service.ts

@@ -0,0 +1,14 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+
+class Service extends BaseService<CertificateItem> {
+  // 上传证书并返回证书BASE64
+  public uploadCertificate = (data?: any) =>
+    request(`/${SystemConst.API_BASE}/network/certificate/upload`, {
+      method: 'POST',
+      data,
+    });
+}
+
+export default Service;

+ 7 - 6
src/pages/link/Certificate/typings.d.ts

@@ -1,7 +1,8 @@
-import type { BaseItem } from '@/utils/typings';
-
 type CertificateItem = {
-  instance: string;
-  description: string;
-  configs?: Record<string, any>;
-} & BaseItem;
+  id: string;
+  type: string;
+  name: string;
+  cert: string;
+  key: string;
+  description?: string;
+};

+ 10 - 0
src/utils/menu/index.ts

@@ -60,6 +60,16 @@ export const extraRouteArr = [
         url: '/account/center/bind',
         hideInMenu: true,
       },
+      {
+        code: 'account/NotificationSubscription',
+        name: '通知订阅',
+        url: '/account/NotificationSubscription',
+      },
+      {
+        code: 'account/NotificationRecord',
+        name: '通知记录',
+        url: '/account/NotificationRecord',
+      },
     ],
   },
 ];

+ 4 - 0
src/utils/menu/router.ts

@@ -33,6 +33,7 @@ export enum MENUS_CODE {
   'edge/Device' = 'edge/Device',
   'edge/Product' = 'edge/Product',
   'link/Certificate' = 'link/Certificate',
+  'link/Certificate/Detail' = 'link/Certificate/Detail',
   'link/Gateway' = 'link/Gateway',
   'link/Opcua' = 'link/Opcua',
   'link/Channal/Opcua' = 'link/Channal/Opcua',
@@ -111,6 +112,8 @@ export enum MENUS_CODE {
   'system/Department/Detail' = 'system/Department/Detail',
   'link/Type/Detail' = 'link/Type/Detail',
   'account/Center' = 'account/Center',
+  'account/NotificationSubscription' = 'account/NotificationSubscription',
+  'account/NotificationRecord' = 'account/NotificationRecord',
   'account/Center/bind' = 'account/Center/bind',
   'Northbound/DuerOS' = 'Northbound/DuerOS',
   'Northbound/DuerOS/Detail' = 'Northbound/DuerOS/Detail',
@@ -157,4 +160,5 @@ export const getDetailNameByCode = {
   'media/Stream/Detail': '流媒体详情',
   'rule-engine/Alarm/Log/Detail': '告警日志',
   'Northbound/AliCloud/Detail': '阿里云详情',
+  'link/Certificate/Detail': '证书详情',
 };