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/03/12 10:36:56 UTC

[incubator-apisix-dashboard] branch next updated: SSL List Page (#151)

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 9019923  SSL List Page (#151)
9019923 is described below

commit 9019923f6f587b4b4f4145962d5ff82f6238b226
Author: 琚致远 <ju...@apache.org>
AuthorDate: Thu Mar 12 18:36:49 2020 +0800

    SSL List Page (#151)
    
    * feat: fake fetchCurrentUser & rewrite dev api
    
    * feat: added SSL model
    
    * feat: added transformFetchListData
    
    * feat: added SSL service
    
    * feat: added SSLModelState to connect.d
    
    * feat: added transformFetchItemData
    
    * feat: added SSL List page
    
    * feat: remove ListTableList sample
    
    * feat: remove Admin sample
    
    * feat: transform key
    
    * feat: added i18n for SSLModule list
    
    * fix: i18n
    
    * feat: added SSL page
---
 config/config.ts                                  |  20 +-
 config/proxy.ts                                   |   4 +-
 public/favicon.png                                | Bin 2849 -> 85376 bytes
 src/layouts/BasicLayout.tsx                       |   2 +-
 src/locales/en-US/component.ts                    |   9 +
 src/locales/en-US/menu.ts                         |   3 +
 src/locales/zh-CN/component.ts                    |   9 +
 src/locales/zh-CN/menu.ts                         |   3 +
 src/models/connect.d.ts                           |   2 +
 src/models/ssl.ts                                 |  23 +++
 src/pages/Admin.tsx                               |  31 ---
 src/pages/ListTableList/_mock.ts                  | 154 --------------
 src/pages/ListTableList/components/CreateForm.tsx |  25 ---
 src/pages/ListTableList/components/UpdateForm.tsx | 215 -------------------
 src/pages/ListTableList/data.d.ts                 |  35 ----
 src/pages/ListTableList/index.tsx                 | 238 ----------------------
 src/pages/ListTableList/service.ts                |  38 ----
 src/pages/SSLModule/list/index.tsx                |  77 +++++++
 src/services/ssl.ts                               |  20 ++
 src/services/user.ts                              |   7 +-
 src/transforms/global.ts                          |  68 +++++++
 src/utils/request.ts                              |   9 +-
 22 files changed, 232 insertions(+), 760 deletions(-)

diff --git a/config/config.ts b/config/config.ts
index 8ae887c..687b505 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -119,28 +119,18 @@ export default {
               component: './Welcome',
             },
             {
-              path: '/admin',
-              name: 'admin',
+              name: 'ssl',
+              path: '/ssl',
               icon: 'crown',
-              component: './Admin',
-              authority: ['admin'],
               routes: [
                 {
-                  path: '/admin/sub-page',
-                  name: 'sub-page',
-                  icon: 'smile',
-                  component: './Welcome',
-                  authority: ['admin'],
+                  path: '/ssl/list',
+                  name: 'list',
+                  component: './SSLModule/list',
                 },
               ],
             },
             {
-              name: 'list.table-list',
-              icon: 'table',
-              path: '/list',
-              component: './ListTableList',
-            },
-            {
               component: './404',
             },
           ],
diff --git a/config/proxy.ts b/config/proxy.ts
index 3fa70dd..e189b0f 100644
--- a/config/proxy.ts
+++ b/config/proxy.ts
@@ -8,9 +8,9 @@
 export default {
   dev: {
     '/api/': {
-      target: 'https://preview.pro.ant.design',
+      target: 'https://apisix.iresty.com/apisix/admin/',
       changeOrigin: true,
-      pathRewrite: { '^': '' },
+      pathRewrite: { '^/api': '' },
     },
   },
   test: {
diff --git a/public/favicon.png b/public/favicon.png
index ece59ce..381ab08 100644
Binary files a/public/favicon.png and b/public/favicon.png differ
diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx
index 256570c..b1e19b7 100644
--- a/src/layouts/BasicLayout.tsx
+++ b/src/layouts/BasicLayout.tsx
@@ -156,7 +156,7 @@ const BasicLayout: React.FC<BasicLayoutProps> = props => {
       breadcrumbRender={(routers = []) => [
         {
           path: '/',
-          breadcrumbName: '首页',
+          breadcrumbName: formatMessage({ id: 'menu.home' }),
         },
         ...routers,
       ]}
diff --git a/src/locales/en-US/component.ts b/src/locales/en-US/component.ts
index 3ba7eed..b3bc39e 100644
--- a/src/locales/en-US/component.ts
+++ b/src/locales/en-US/component.ts
@@ -2,4 +2,13 @@ export default {
   'component.tagSelect.expand': 'Expand',
   'component.tagSelect.collapse': 'Collapse',
   'component.tagSelect.all': 'All',
+  'component.global.remove': 'Remove',
+  'component.global.cancel': 'Cancel',
+  'component.global.edit': 'Edit',
+  'component.global.action': 'Action',
+  // SSL Module
+  'component.ssl.removeSSLItemModalContent': 'You are going to remove this item!',
+  'component.ssl.removeSSLItemModalTitle': 'SSL Remove Alert',
+  'component.ssl.fetchSSLListSuccess': 'fetch SSL list successfully',
+  'component.ssl.removeSSLSuccess': 'remove target SSL successfully',
 };
diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts
index a737e69..cc8862a 100644
--- a/src/locales/en-US/menu.ts
+++ b/src/locales/en-US/menu.ts
@@ -49,4 +49,7 @@ export default {
   'menu.editor.flow': 'Flow Editor',
   'menu.editor.mind': 'Mind Editor',
   'menu.editor.koni': 'Koni Editor',
+  'menu.ssl': 'SSL',
+  'menu.ssl.list': 'SSL List',
+  'menu.ssl.edit': 'Edit',
 };
diff --git a/src/locales/zh-CN/component.ts b/src/locales/zh-CN/component.ts
index 1f1fead..b8c505e 100644
--- a/src/locales/zh-CN/component.ts
+++ b/src/locales/zh-CN/component.ts
@@ -2,4 +2,13 @@ export default {
   'component.tagSelect.expand': '展开',
   'component.tagSelect.collapse': '收起',
   'component.tagSelect.all': '全部',
+  'component.global.remove': '移除',
+  'component.global.cancel': '取消',
+  'component.global.edit': '编辑',
+  'component.global.action': '操作',
+  // SSL 模块
+  'component.ssl.removeSSLItemModalContent': '确定要移除该项吗?',
+  'component.ssl.removeSSLItemModalTitle': '移除 SSL',
+  'component.ssl.fetchSSLListSuccess': '获取 SSL 列表成功',
+  'component.ssl.removeSSLSuccess': '移除 SSL 成功',
 };
diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts
index 985b516..1b0aa04 100644
--- a/src/locales/zh-CN/menu.ts
+++ b/src/locales/zh-CN/menu.ts
@@ -49,4 +49,7 @@ export default {
   'menu.editor.flow': '流程编辑器',
   'menu.editor.mind': '脑图编辑器',
   'menu.editor.koni': '拓扑编辑器',
+  'menu.ssl': 'SSL',
+  'menu.ssl.list': 'SSL 列表',
+  'menu.ssl.edit': '编辑',
 };
diff --git a/src/models/connect.d.ts b/src/models/connect.d.ts
index 3fc53f6..6ef39fb 100644
--- a/src/models/connect.d.ts
+++ b/src/models/connect.d.ts
@@ -5,6 +5,7 @@ import { GlobalModelState } from './global';
 import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
 import { UserModelState } from './user';
 import { StateType } from './login';
+import { ModelState as SSLModelState } from './ssl';
 
 export { GlobalModelState, SettingModelState, UserModelState };
 
@@ -26,6 +27,7 @@ export interface ConnectState {
   settings: SettingModelState;
   user: UserModelState;
   login: StateType;
+  ssl: SSLModelState;
 }
 
 export interface Route extends MenuDataItem {
diff --git a/src/models/ssl.ts b/src/models/ssl.ts
new file mode 100644
index 0000000..47d956a
--- /dev/null
+++ b/src/models/ssl.ts
@@ -0,0 +1,23 @@
+export interface SSL {
+  sni: string;
+  cert: string;
+  key: string;
+}
+
+export interface ModelState {}
+
+export interface ModelType {
+  namespace: string;
+  state: ModelState;
+  effects: {};
+  reducers: {};
+}
+
+const model: ModelType = {
+  namespace: 'ssl',
+  state: {},
+  effects: {},
+  reducers: {},
+};
+
+export default model;
diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx
deleted file mode 100644
index 9c343ad..0000000
--- a/src/pages/Admin.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
-import { Card, Typography, Alert } from 'antd';
-import { PageHeaderWrapper } from '@ant-design/pro-layout';
-
-export default (): React.ReactNode => (
-  <PageHeaderWrapper content=" 这个页面只有 admin 权限才能查看">
-    <Card>
-      <Alert
-        message="umi ui 现已发布,欢迎使用 npm run ui 启动体验。"
-        type="success"
-        showIcon
-        banner
-        style={{
-          margin: -12,
-          marginBottom: 48,
-        }}
-      />
-      <Typography.Title level={2} style={{ textAlign: 'center' }}>
-        <SmileTwoTone /> Ant Design Pro <HeartTwoTone twoToneColor="#eb2f96" /> You
-      </Typography.Title>
-    </Card>
-    <p style={{ textAlign: 'center', marginTop: 24 }}>
-      Want to add more pages? Please refer to{' '}
-      <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
-        use block
-      </a>
-      。
-    </p>
-  </PageHeaderWrapper>
-);
diff --git a/src/pages/ListTableList/_mock.ts b/src/pages/ListTableList/_mock.ts
deleted file mode 100644
index 33b74a9..0000000
--- a/src/pages/ListTableList/_mock.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import { Request, Response } from 'express';
-import { parse } from 'url';
-import { TableListItem, TableListParams } from './data.d';
-
-// mock tableListDataSource
-const genList = (current: number, pageSize: number) => {
-  const tableListDataSource: TableListItem[] = [];
-
-  for (let i = 0; i < pageSize; i += 1) {
-    const index = (current - 1) * 10 + i;
-    tableListDataSource.push({
-      key: index,
-      disabled: i % 6 === 0,
-      href: 'https://ant.design',
-      avatar: [
-        'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
-        'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
-      ][i % 2],
-      name: `TradeCode ${index}`,
-      owner: '曲丽丽',
-      desc: '这是一段描述',
-      callNo: Math.floor(Math.random() * 1000),
-      status: Math.floor(Math.random() * 10) % 4,
-      updatedAt: new Date(),
-      createdAt: new Date(),
-      progress: Math.ceil(Math.random() * 100),
-    });
-  }
-  tableListDataSource.reverse();
-  return tableListDataSource;
-};
-
-let tableListDataSource = genList(1, 100);
-
-function getRule(req: Request, res: Response, u: string) {
-  let url = u;
-  if (!url || Object.prototype.toString.call(url) !== '[object String]') {
-    // eslint-disable-next-line prefer-destructuring
-    url = req.url;
-  }
-  const { current = 1, pageSize = 10 } = req.query;
-  const params = (parse(url, true).query as unknown) as TableListParams;
-
-  let dataSource = [...tableListDataSource].slice((current - 1) * pageSize, current * pageSize);
-  if (params.sorter) {
-    const s = params.sorter.split('_');
-    dataSource = dataSource.sort((prev, next) => {
-      if (s[1] === 'descend') {
-        return next[s[0]] - prev[s[0]];
-      }
-      return prev[s[0]] - next[s[0]];
-    });
-  }
-
-  if (params.status) {
-    const status = params.status.split(',');
-    let filterDataSource: TableListItem[] = [];
-    status.forEach((s: string) => {
-      filterDataSource = filterDataSource.concat(
-        dataSource.filter(item => {
-          if (parseInt(`${item.status}`, 10) === parseInt(s.split('')[0], 10)) {
-            return true;
-          }
-          return false;
-        }),
-      );
-    });
-    dataSource = filterDataSource;
-  }
-
-  if (params.name) {
-    dataSource = dataSource.filter(data => data.name.includes(params.name || ''));
-  }
-  const result = {
-    data: dataSource,
-    total: tableListDataSource.length,
-    success: true,
-    pageSize,
-    current: parseInt(`${params.currentPage}`, 10) || 1,
-  };
-
-  return res.json(result);
-}
-
-function postRule(req: Request, res: Response, u: string, b: Request) {
-  let url = u;
-  if (!url || Object.prototype.toString.call(url) !== '[object String]') {
-    // eslint-disable-next-line prefer-destructuring
-    url = req.url;
-  }
-
-  const body = (b && b.body) || req.body;
-  const { method, name, desc, key } = body;
-
-  switch (method) {
-    /* eslint no-case-declarations:0 */
-    case 'delete':
-      tableListDataSource = tableListDataSource.filter(item => key.indexOf(item.key) === -1);
-      break;
-    case 'post':
-      (() => {
-        const i = Math.ceil(Math.random() * 10000);
-        const newRule = {
-          key: tableListDataSource.length,
-          href: 'https://ant.design',
-          avatar: [
-            'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
-            'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
-          ][i % 2],
-          name,
-          owner: '曲丽丽',
-          desc,
-          callNo: Math.floor(Math.random() * 1000),
-          status: Math.floor(Math.random() * 10) % 2,
-          updatedAt: new Date(),
-          createdAt: new Date(),
-          progress: Math.ceil(Math.random() * 100),
-        };
-        tableListDataSource.unshift(newRule);
-        return res.json(newRule);
-      })();
-      return;
-
-    case 'update':
-      (() => {
-        let newRule = {};
-        tableListDataSource = tableListDataSource.map(item => {
-          if (item.key === key) {
-            newRule = { ...item, desc, name };
-            return { ...item, desc, name };
-          }
-          return item;
-        });
-        return res.json(newRule);
-      })();
-      return;
-    default:
-      break;
-  }
-
-  const result = {
-    list: tableListDataSource,
-    pagination: {
-      total: tableListDataSource.length,
-    },
-  };
-
-  res.json(result);
-}
-
-export default {
-  'GET /api/rule': getRule,
-  'POST /api/rule': postRule,
-};
diff --git a/src/pages/ListTableList/components/CreateForm.tsx b/src/pages/ListTableList/components/CreateForm.tsx
deleted file mode 100644
index 817922a..0000000
--- a/src/pages/ListTableList/components/CreateForm.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-import { Modal } from 'antd';
-
-interface CreateFormProps {
-  modalVisible: boolean;
-  onCancel: () => void;
-}
-
-const CreateForm: React.FC<CreateFormProps> = props => {
-  const { modalVisible, onCancel } = props;
-
-  return (
-    <Modal
-      destroyOnClose
-      title="新建规则"
-      visible={modalVisible}
-      onCancel={() => onCancel()}
-      footer={null}
-    >
-      {props.children}
-    </Modal>
-  );
-};
-
-export default CreateForm;
diff --git a/src/pages/ListTableList/components/UpdateForm.tsx b/src/pages/ListTableList/components/UpdateForm.tsx
deleted file mode 100644
index 2c20df3..0000000
--- a/src/pages/ListTableList/components/UpdateForm.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-import React, { useState } from 'react';
-import { Form, Button, DatePicker, Input, Modal, Radio, Select, Steps } from 'antd';
-
-import { TableListItem } from '../data.d';
-
-export interface FormValueType extends Partial<TableListItem> {
-  target?: string;
-  template?: string;
-  type?: string;
-  time?: string;
-  frequency?: string;
-}
-
-export interface UpdateFormProps {
-  onCancel: (flag?: boolean, formVals?: FormValueType) => void;
-  onSubmit: (values: FormValueType) => void;
-  updateModalVisible: boolean;
-  values: Partial<TableListItem>;
-}
-const FormItem = Form.Item;
-const { Step } = Steps;
-const { TextArea } = Input;
-const { Option } = Select;
-const RadioGroup = Radio.Group;
-
-export interface UpdateFormState {
-  formVals: FormValueType;
-  currentStep: number;
-}
-
-const formLayout = {
-  labelCol: { span: 7 },
-  wrapperCol: { span: 13 },
-};
-
-const UpdateForm: React.FC<UpdateFormProps> = props => {
-  const [formVals, setFormVals] = useState<FormValueType>({
-    name: props.values.name,
-    desc: props.values.desc,
-    key: props.values.key,
-    target: '0',
-    template: '0',
-    type: '1',
-    time: '',
-    frequency: 'month',
-  });
-
-  const [currentStep, setCurrentStep] = useState<number>(0);
-
-  const [form] = Form.useForm();
-
-  const {
-    onSubmit: handleUpdate,
-    onCancel: handleUpdateModalVisible,
-    updateModalVisible,
-    values,
-  } = props;
-
-  const forward = () => setCurrentStep(currentStep + 1);
-
-  const backward = () => setCurrentStep(currentStep - 1);
-
-  const handleNext = async () => {
-    const fieldsValue = await form.validateFields();
-
-    setFormVals({ ...formVals, ...fieldsValue });
-
-    if (currentStep < 2) {
-      forward();
-    } else {
-      handleUpdate(formVals);
-    }
-  };
-
-  const renderContent = () => {
-    if (currentStep === 1) {
-      return (
-        <>
-          <FormItem name="target" label="监控对象">
-            <Select style={{ width: '100%' }}>
-              <Option value="0">表一</Option>
-              <Option value="1">表二</Option>
-            </Select>
-          </FormItem>
-          <FormItem name="template" label="规则模板">
-            <Select style={{ width: '100%' }}>
-              <Option value="0">规则模板一</Option>
-              <Option value="1">规则模板二</Option>
-            </Select>
-          </FormItem>
-          <FormItem name="type" label="规则类型">
-            <RadioGroup>
-              <Radio value="0">强</Radio>
-              <Radio value="1">弱</Radio>
-            </RadioGroup>
-          </FormItem>
-        </>
-      );
-    }
-    if (currentStep === 2) {
-      return (
-        <>
-          <FormItem
-            name="time"
-            label="开始时间"
-            rules={[{ required: true, message: '请选择开始时间!' }]}
-          >
-            <DatePicker
-              style={{ width: '100%' }}
-              showTime
-              format="YYYY-MM-DD HH:mm:ss"
-              placeholder="选择开始时间"
-            />
-          </FormItem>
-          <FormItem name="frequency" label="调度周期">
-            <Select style={{ width: '100%' }}>
-              <Option value="month">月</Option>
-              <Option value="week">周</Option>
-            </Select>
-          </FormItem>
-        </>
-      );
-    }
-    return (
-      <>
-        <FormItem
-          name="name"
-          label="规则名称"
-          rules={[{ required: true, message: '请输入规则名称!' }]}
-        >
-          <Input placeholder="请输入" />
-        </FormItem>
-        <FormItem
-          name="desc"
-          label="规则描述"
-          rules={[{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 }]}
-        >
-          <TextArea rows={4} placeholder="请输入至少五个字符" />
-        </FormItem>
-      </>
-    );
-  };
-
-  const renderFooter = () => {
-    if (currentStep === 1) {
-      return (
-        <>
-          <Button style={{ float: 'left' }} onClick={backward}>
-            上一步
-          </Button>
-          <Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
-          <Button type="primary" onClick={() => handleNext()}>
-            下一步
-          </Button>
-        </>
-      );
-    }
-    if (currentStep === 2) {
-      return (
-        <>
-          <Button style={{ float: 'left' }} onClick={backward}>
-            上一步
-          </Button>
-          <Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
-          <Button type="primary" onClick={() => handleNext()}>
-            完成
-          </Button>
-        </>
-      );
-    }
-    return (
-      <>
-        <Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
-        <Button type="primary" onClick={() => handleNext()}>
-          下一步
-        </Button>
-      </>
-    );
-  };
-
-  return (
-    <Modal
-      width={640}
-      bodyStyle={{ padding: '32px 40px 48px' }}
-      destroyOnClose
-      title="规则配置"
-      visible={updateModalVisible}
-      footer={renderFooter()}
-      onCancel={() => handleUpdateModalVisible(false, values)}
-      afterClose={() => handleUpdateModalVisible()}
-    >
-      <Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
-        <Step title="基本信息" />
-        <Step title="配置规则属性" />
-        <Step title="设定调度周期" />
-      </Steps>
-      <Form
-        {...formLayout}
-        form={form}
-        initialValues={{
-          target: formVals.target,
-          template: formVals.template,
-          type: formVals.type,
-          frequency: formVals.frequency,
-          name: formVals.name,
-          desc: formVals.desc,
-        }}
-      >
-        {renderContent()}
-      </Form>
-    </Modal>
-  );
-};
-
-export default UpdateForm;
diff --git a/src/pages/ListTableList/data.d.ts b/src/pages/ListTableList/data.d.ts
deleted file mode 100644
index 04096c7..0000000
--- a/src/pages/ListTableList/data.d.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-export interface TableListItem {
-  key: number;
-  disabled?: boolean;
-  href: string;
-  avatar: string;
-  name: string;
-  owner: string;
-  desc: string;
-  callNo: number;
-  status: number;
-  updatedAt: Date;
-  createdAt: Date;
-  progress: number;
-}
-
-export interface TableListPagination {
-  total: number;
-  pageSize: number;
-  current: number;
-}
-
-export interface TableListData {
-  list: TableListItem[];
-  pagination: Partial<TableListPagination>;
-}
-
-export interface TableListParams {
-  sorter?: string;
-  status?: string;
-  name?: string;
-  desc?: string;
-  key?: number;
-  pageSize?: number;
-  currentPage?: number;
-}
diff --git a/src/pages/ListTableList/index.tsx b/src/pages/ListTableList/index.tsx
deleted file mode 100644
index 22e15b6..0000000
--- a/src/pages/ListTableList/index.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import { DownOutlined, PlusOutlined } from '@ant-design/icons';
-import { Button, Divider, Dropdown, Menu, message } from 'antd';
-import React, { useState, useRef } from 'react';
-import { PageHeaderWrapper } from '@ant-design/pro-layout';
-import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
-import { SorterResult } from 'antd/es/table/interface';
-
-import CreateForm from './components/CreateForm';
-import UpdateForm, { FormValueType } from './components/UpdateForm';
-import { TableListItem } from './data.d';
-import { queryRule, updateRule, addRule, removeRule } from './service';
-
-/**
- * 添加节点
- * @param fields
- */
-const handleAdd = async (fields: TableListItem) => {
-  const hide = message.loading('正在添加');
-  try {
-    await addRule({ ...fields });
-    hide();
-    message.success('添加成功');
-    return true;
-  } catch (error) {
-    hide();
-    message.error('添加失败请重试!');
-    return false;
-  }
-};
-
-/**
- * 更新节点
- * @param fields
- */
-const handleUpdate = async (fields: FormValueType) => {
-  const hide = message.loading('正在配置');
-  try {
-    await updateRule({
-      name: fields.name,
-      desc: fields.desc,
-      key: fields.key,
-    });
-    hide();
-
-    message.success('配置成功');
-    return true;
-  } catch (error) {
-    hide();
-    message.error('配置失败请重试!');
-    return false;
-  }
-};
-
-/**
- *  删除节点
- * @param selectedRows
- */
-const handleRemove = async (selectedRows: TableListItem[]) => {
-  const hide = message.loading('正在删除');
-  if (!selectedRows) return true;
-  try {
-    await removeRule({
-      key: selectedRows.map(row => row.key),
-    });
-    hide();
-    message.success('删除成功,即将刷新');
-    return true;
-  } catch (error) {
-    hide();
-    message.error('删除失败,请重试');
-    return false;
-  }
-};
-
-const TableList: React.FC<{}> = () => {
-  const [sorter, setSorter] = useState<string>('');
-  const [createModalVisible, handleModalVisible] = useState<boolean>(false);
-  const [updateModalVisible, handleUpdateModalVisible] = useState<boolean>(false);
-  const [stepFormValues, setStepFormValues] = useState({});
-  const actionRef = useRef<ActionType>();
-  const columns: ProColumns<TableListItem>[] = [
-    {
-      title: '规则名称',
-      dataIndex: 'name',
-      rules: [
-        {
-          required: true,
-          message: '规则名称为必填项',
-        },
-      ],
-    },
-    {
-      title: '描述',
-      dataIndex: 'desc',
-      valueType: 'textarea',
-    },
-    {
-      title: '服务调用次数',
-      dataIndex: 'callNo',
-      sorter: true,
-      hideInForm: true,
-      renderText: (val: string) => `${val} 万`,
-    },
-    {
-      title: '状态',
-      dataIndex: 'status',
-      hideInForm: true,
-      valueEnum: {
-        0: { text: '关闭', status: 'Default' },
-        1: { text: '运行中', status: 'Processing' },
-        2: { text: '已上线', status: 'Success' },
-        3: { text: '异常', status: 'Error' },
-      },
-    },
-    {
-      title: '上次调度时间',
-      dataIndex: 'updatedAt',
-      sorter: true,
-      valueType: 'dateTime',
-      hideInForm: true,
-    },
-    {
-      title: '操作',
-      dataIndex: 'option',
-      valueType: 'option',
-      render: (_, record) => (
-        <>
-          <a
-            onClick={() => {
-              handleUpdateModalVisible(true);
-              setStepFormValues(record);
-            }}
-          >
-            配置
-          </a>
-          <Divider type="vertical" />
-          <a href="">订阅警报</a>
-        </>
-      ),
-    },
-  ];
-
-  return (
-    <PageHeaderWrapper>
-      <ProTable<TableListItem>
-        headerTitle="查询表格"
-        actionRef={actionRef}
-        rowKey="key"
-        onChange={(_, _filter, _sorter) => {
-          const sorterResult = _sorter as SorterResult<TableListItem>;
-          if (sorterResult.field) {
-            setSorter(`${sorterResult.field}_${sorterResult.order}`);
-          }
-        }}
-        params={{
-          sorter,
-        }}
-        toolBarRender={(action, { selectedRows }) => [
-          <Button type="primary" onClick={() => handleModalVisible(true)}>
-            <PlusOutlined /> 新建
-          </Button>,
-          selectedRows && selectedRows.length > 0 && (
-            <Dropdown
-              overlay={
-                <Menu
-                  onClick={async e => {
-                    if (e.key === 'remove') {
-                      await handleRemove(selectedRows);
-                      action.reload();
-                    }
-                  }}
-                  selectedKeys={[]}
-                >
-                  <Menu.Item key="remove">批量删除</Menu.Item>
-                  <Menu.Item key="approval">批量审批</Menu.Item>
-                </Menu>
-              }
-            >
-              <Button>
-                批量操作 <DownOutlined />
-              </Button>
-            </Dropdown>
-          ),
-        ]}
-        tableAlertRender={(selectedRowKeys, selectedRows) => (
-          <div>
-            已选择 <a style={{ fontWeight: 600 }}>{selectedRowKeys.length}</a> 项&nbsp;&nbsp;
-            <span>
-              服务调用次数总计 {selectedRows.reduce((pre, item) => pre + item.callNo, 0)} 万
-            </span>
-          </div>
-        )}
-        request={params => queryRule(params)}
-        columns={columns}
-        rowSelection={{}}
-      />
-      <CreateForm onCancel={() => handleModalVisible(false)} modalVisible={createModalVisible}>
-        <ProTable<TableListItem, TableListItem>
-          onSubmit={async value => {
-            const success = await handleAdd(value);
-            if (success) {
-              handleModalVisible(false);
-              if (actionRef.current) {
-                actionRef.current.reload();
-              }
-            }
-          }}
-          rowKey="key"
-          type="form"
-          columns={columns}
-          rowSelection={{}}
-        />
-      </CreateForm>
-      {stepFormValues && Object.keys(stepFormValues).length ? (
-        <UpdateForm
-          onSubmit={async value => {
-            const success = await handleUpdate(value);
-            if (success) {
-              handleModalVisible(false);
-              setStepFormValues({});
-              if (actionRef.current) {
-                actionRef.current.reload();
-              }
-            }
-          }}
-          onCancel={() => {
-            handleUpdateModalVisible(false);
-            setStepFormValues({});
-          }}
-          updateModalVisible={updateModalVisible}
-          values={stepFormValues}
-        />
-      ) : null}
-    </PageHeaderWrapper>
-  );
-};
-
-export default TableList;
diff --git a/src/pages/ListTableList/service.ts b/src/pages/ListTableList/service.ts
deleted file mode 100644
index e008a6d..0000000
--- a/src/pages/ListTableList/service.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import request from '@/utils/request';
-import { TableListParams, TableListItem } from './data.d';
-
-export async function queryRule(params?: TableListParams) {
-  return request('/api/rule', {
-    params,
-  });
-}
-
-export async function removeRule(params: { key: number[] }) {
-  return request('/api/rule', {
-    method: 'POST',
-    data: {
-      ...params,
-      method: 'delete',
-    },
-  });
-}
-
-export async function addRule(params: TableListItem) {
-  return request('/api/rule', {
-    method: 'POST',
-    data: {
-      ...params,
-      method: 'post',
-    },
-  });
-}
-
-export async function updateRule(params: TableListParams) {
-  return request('/api/rule', {
-    method: 'POST',
-    data: {
-      ...params,
-      method: 'update',
-    },
-  });
-}
diff --git a/src/pages/SSLModule/list/index.tsx b/src/pages/SSLModule/list/index.tsx
new file mode 100644
index 0000000..f06f424
--- /dev/null
+++ b/src/pages/SSLModule/list/index.tsx
@@ -0,0 +1,77 @@
+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 { router } from 'umi';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+import { fetchList as fetchSSLList, remove as removeSSL } from '@/services/ssl';
+import { SSL } from '@/models/ssl';
+import { ListItem } from '@/transforms/global';
+
+const List: React.FC = () => {
+  const tableRef = useRef<ActionType>();
+
+  const onRemove = (key: string) => {
+    Modal.confirm({
+      title: formatMessage({ id: 'component.ssl.removeSSLItemModalTitle' }),
+      content: formatMessage({ id: 'component.ssl.removeSSLItemModalContent' }),
+      okText: formatMessage({ id: 'component.global.remove' }),
+      cancelText: formatMessage({ id: 'component.global.cancel' }),
+      okButtonProps: {
+        type: 'danger',
+      },
+      /* eslint-disable no-unused-expressions */
+      onOk: () =>
+        removeSSL(key).then(() => {
+          notification.success({
+            message: formatMessage({ id: 'component.ssl.removeSSLSuccess' }),
+          });
+          tableRef.current?.reload();
+        }),
+      /* eslint-enable no-unused-expressions */
+    });
+  };
+
+  const columns: ProColumns<ListItem<SSL>>[] = [
+    {
+      title: 'ID',
+      dataIndex: 'displayKey',
+    },
+    {
+      title: 'SNI',
+      dataIndex: ['value', 'sni'],
+    },
+    {
+      title: formatMessage({ id: 'component.global.action' }),
+      valueType: 'option',
+      render: (_, record) => (
+        <>
+          <Button
+            type="primary"
+            style={{ marginRight: '10px' }}
+            onClick={() => router.push(`/ssl/${record.key}/edit`)}
+          >
+            {formatMessage({ id: 'component.global.edit' })}
+          </Button>
+          <Button type="danger" onClick={() => onRemove(record.key)}>
+            {formatMessage({ id: 'component.global.remove' })}
+          </Button>
+        </>
+      ),
+    },
+  ];
+
+  return (
+    <PageHeaderWrapper>
+      <ProTable<ListItem<SSL>>
+        request={() => fetchSSLList()}
+        search={false}
+        columns={columns}
+        actionRef={tableRef}
+      />
+    </PageHeaderWrapper>
+  );
+};
+
+export default List;
diff --git a/src/services/ssl.ts b/src/services/ssl.ts
new file mode 100644
index 0000000..cd51a25
--- /dev/null
+++ b/src/services/ssl.ts
@@ -0,0 +1,20 @@
+import request from '@/utils/request';
+import { transformFetchListData, transformFetchItemData } from '@/transforms/global';
+import { SSL } from '@/models/ssl';
+
+export const fetchList = () => request('/api/ssl').then(data => transformFetchListData<SSL>(data));
+
+export const fetchItem = (id: number) =>
+  request(`/api/ssl/${id}`).then(data => transformFetchItemData<SSL>(data));
+
+export const remove = (key: string) => request.delete(`/api/ssl/${key}`);
+
+export const create = (data: SSL) =>
+  request.post('/api/ssl', {
+    data,
+  });
+
+export const update = (id: number, data: SSL) =>
+  request.put(`/api/ssl/${id}`, {
+    data,
+  });
diff --git a/src/services/user.ts b/src/services/user.ts
index 1988721..1e5fec8 100644
--- a/src/services/user.ts
+++ b/src/services/user.ts
@@ -5,7 +5,12 @@ export async function query(): Promise<any> {
 }
 
 export async function queryCurrent(): Promise<any> {
-  return request('/api/currentUser');
+  // NOTE: APISIX doesn‘t support user login currently, we return fake data directly.
+  return {
+    name: 'APISIX User',
+    avatar: 'favicon.png',
+    userid: '00000001',
+  };
 }
 
 export async function queryNotices(): Promise<any> {
diff --git a/src/transforms/global.ts b/src/transforms/global.ts
new file mode 100644
index 0000000..4d8e3fd
--- /dev/null
+++ b/src/transforms/global.ts
@@ -0,0 +1,68 @@
+import { RequestData } from '@ant-design/pro-table';
+
+type ActionType = 'get' | 'set' | 'delete' | 'create';
+
+interface Node<T> {
+  createdIndex: number;
+  key: string;
+  modifiedIndex: number;
+  value: T;
+}
+
+interface ListData<T> {
+  action: ActionType;
+  node: {
+    modifiedIndex: number;
+    createdIndex: number;
+    key: string;
+    dir: boolean;
+    nodes: Node<T>[];
+  };
+}
+
+export interface ListItem<T> extends Node<T> {
+  displayKey: string;
+}
+
+const key2id = (key: string) => parseInt(key.replace(/^(0+)/, ''), 10);
+
+/**
+ * Transform data from fetch list api.
+ */
+export const transformFetchListData = <T>(data: ListData<T>): RequestData<ListItem<T>> => {
+  const results = (data.node.nodes || [])
+    .map(node => {
+      const result = node.key.match(/\/([0-9]+)/);
+      let displayKey = '';
+
+      if (result) {
+        const [, key] = result;
+        displayKey = key2id(key).toString();
+        /* eslint no-param-reassign: ["error", { "props": false }] */
+        node.key = key;
+      }
+      return {
+        ...node,
+        displayKey,
+      };
+    })
+    .filter(item => item.displayKey);
+
+  return {
+    data: results,
+    total: results.length,
+  };
+};
+
+/**
+ * Transform data from fetch target item.
+ */
+export const transformFetchItemData = <T>(data: { node: Node<T>; action: ActionType }) => {
+  const result = data.node.key.match(/\/([0-9]+)/);
+  if (result) {
+    const [, key] = result;
+    /* eslint no-param-reassign: ["error", { "props": false }] */
+    data.node.key = key;
+  }
+  return data.node;
+};
diff --git a/src/utils/request.ts b/src/utils/request.ts
index 270dfad..849bf1c 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -26,14 +26,13 @@ const codeMessage = {
 /**
  * 异常处理程序
  */
-const errorHandler = (error: { response: Response }): Response => {
+const errorHandler = (error: { response: Response; data: any }): Promise<Response> => {
   const { response } = error;
   if (response && response.status) {
-    const errorText = codeMessage[response.status] || response.statusText;
-    const { status, url } = response;
+    const errorText = error.data.message || codeMessage[response.status];
 
     notification.error({
-      message: `请求错误 ${status}: ${url}`,
+      message: `请求错误,错误码: ${error.data.errorCode}`,
       description: errorText,
     });
   } else if (!response) {
@@ -42,7 +41,7 @@ const errorHandler = (error: { response: Response }): Response => {
       message: '网络异常',
     });
   }
-  return response;
+  return Promise.reject(response);
 };
 
 /**