Browse Source

feat: 持久化

wanghongkai 6 years ago
parent
commit
721cb127a7

+ 52 - 1
src/components/ConfigModal.js

@@ -16,6 +16,7 @@ export default class ConfigModal extends PureComponent {
         mountsArr: Object.keys(config.config.mounts || {}).map(key => ({ key, value: config.config.mounts[key] })),
         ingressAnnotaionsArr: Object.keys(config.config.ingressAnnotaions || {}).map(key => ({ key, value: config.config.ingressAnnotaions[key] })),
         hostPathArr: Object.keys(config.config.hostPath || {}).map(key => ({ key, value: config.config.hostPath[key] })),
+        pvcPathsArr: Object.keys(config.config.pvcPaths || {}).map(key => ({ key, value: config.config.pvcPaths[key] })),
       });
     }
   }
@@ -47,6 +48,10 @@ export default class ConfigModal extends PureComponent {
             obj[item.key] = item.value;
             return obj;
           }, {}),
+          pvcPaths: (values.pvcPathsArr || []).reduce((obj, item) => {
+            obj[item.key] = item.value;
+            return obj;
+          }, {}),
           ingressAnnotaions: (values.ingressAnnotaionsArr || []).reduce((obj, item) => {
             obj[item.key] = item.value;
             return obj;
@@ -89,13 +94,20 @@ export default class ConfigModal extends PureComponent {
           >
             <Input placeholder='服务端口,多个用英文逗号隔开'></Input>
           </Form.Item >
+          <Form.Item 
+            name="ingressPort"
+            label="暴露端口"
+            tooltip="暴露端口"
+          >
+            <Input placeholder='暴露端口'></Input>
+          </Form.Item >
           <Form.Item 
             name="replicas"
             label="实例数"
             tooltip="服务部署的实例数目,即pod数量,默认为1"
           >
             <Input placeholder="实例数"></Input>
-          </Form.Item >
+          </Form.Item>
           <Form.Item 
             name="runArgs"
             label="运行参数"
@@ -188,6 +200,45 @@ export default class ConfigModal extends PureComponent {
               )}
             </Form.List>
           </div>
+          <Divider plain>持久卷挂载配置</Divider>
+          <div className={styles.list}>
+            <Form.List name="pvcPathsArr">
+              {(fields, {add, remove}) => (
+                  <>
+                    {fields.map(field => (
+                        <div key={`pvc-${field.fieldKey}`} className={styles.field}>
+                          <Form.Item
+                            {...field}
+                            key={`pvc-key-${field.fieldKey}`}
+                            label="持久卷名称"
+                            name={[field.name, 'key']}
+                            fieldKey={[field.fieldKey, 'key']}
+                            rules={[{ required: true, message: '持久卷名称不能为空' }]}
+                          >
+                            <Input placeholder="持久卷名称" />
+                          </Form.Item>
+                          <Form.Item
+                            {...field}
+                            label="挂载路径"
+                            key={`pvc-value-${field.fieldKey}`}
+                            style={{alignItems: 'center'}}
+                            name={[field.name, 'value']}
+                            fieldKey={[field.fieldKey, 'value']}
+                          >
+                            <Input placeholder="挂载路径" />
+                          </Form.Item>
+                          <MinusCircleOutlined style={{fontSize: 18, marginLeft: 10}} onClick={() => remove(field.name)} />
+                        </div>
+                    ))}
+                    <Form.Item className={styles.addBtn} >
+                      <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
+                        添加
+                      </Button>
+                    </Form.Item>
+                  </>
+              )}
+            </Form.List>
+          </div>
           <Divider plain>节点文件挂载</Divider>
           <div className={styles.list}>
             <Form.List name="hostPathArr">

+ 286 - 0
src/components/VolumeModal.js

@@ -0,0 +1,286 @@
+import React, { PureComponent } from 'react';
+import { PageHeader, Button, Descriptions, Modal, Card, Icon, Row, Col, Form, Popconfirm, Input, Space, Divider, Table } from 'antd';
+import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
+import { connect } from 'umi'
+import styles from './VolumeModal.less'
+
+const EditableCell = ({
+  editing,
+  dataIndex,
+  title,
+  inputType,
+  record,
+  index,
+  children,
+  ...restProps
+}) => {
+  return (
+    <td key={`${record?.name}-${dataIndex}`} {...restProps}>
+      {editing ? (
+        <Form.Item
+          key={`formitem-${record.name}-${dataIndex}`}
+          name={dataIndex}
+          style={{ margin: 0 }}
+          rules={[
+            {
+              required: true,
+              message: `该项不能为空!`,
+            },
+          ]}
+        >
+          <Input></Input>
+        </Form.Item>
+      ) : (
+        dataIndex == 'name' ? record.name.substring(record.name.indexOf('-') + 1) : children
+      )}
+    </td>
+  );
+};
+@connect(({app}) => ({
+  app
+}))
+export default class VolumeModal extends PureComponent {
+  tableFormRef = React.createRef();
+  createFormRef = React.createRef();
+  state = {
+    editingKey: null,
+    showCreateModal: false,
+    currentApp: null,
+  }
+
+  fetchData(id) {
+    this.props.dispatch({
+      type: 'app/get',
+      payload: {
+        id,
+        callback: (response) => {
+          if (response && response.code == 0) {
+            this.setState({
+              currentApp: response.result,
+            });
+          }
+        }
+      }
+    })
+  }
+
+  componentWillReceiveProps(props) {
+    if (props.visible && !this.props.visible) {
+      this.fetchData(props.appId);
+      this.setState({
+        editingKey: null,
+      });
+    }
+  }
+
+  save(record) {
+    this.tableFormRef.current.validateFields().then((values) => {
+      this.props.dispatch({
+        type: 'app/updateVolume',
+        payload: {
+          ...values,
+          name: record.name.substring(record.name.indexOf('-') + 1),
+          appId: this.props.appId,
+          callback: (response) => {
+            if (response && response.code == 0) {
+              this.fetchData(this.props.appId);
+              this.setState({
+                editingKey: null,
+              });
+            }
+          }
+        }
+      });
+    })
+  }
+
+  cancel() {
+    this.setState({
+      editingKey: null,
+    });
+  }
+
+  edit(record) {
+    this.setState({
+      editingKey: record.name,
+    });
+    this.tableFormRef.current.setFieldsValue({
+      ...record,
+      name: record.name.substring(record.name.indexOf('-') + 1)
+    });
+  }
+
+  deleteVolume(record) {
+    this.props.dispatch({
+      type: 'app/deleteVolume',
+      payload: {
+        ...record,
+        appId: this.props.appId,
+        callback: (response) => {
+          if (response && response.code == 0) {
+            this.fetchData(this.props.appId);
+          }
+        }
+      }
+    })
+  }
+  
+  create() {
+    this.createFormRef.current.validateFields().then(values => {
+      this.props.dispatch({
+        type: 'app/createVolume',
+        payload: {
+          ...values,
+          appId: this.props.appId,
+          callback: (response) => {
+            if (response && response.code == 0) {
+              this.fetchData(this.props.appId);
+              this.setState({
+                showCreateModal: false,
+              })
+            }
+          }
+        }
+      })
+    })
+  }
+
+  hideCreate() {
+    this.setState({
+      showCreateModal: false,
+    });
+  }
+
+  showCreate() {
+    this.setState({
+      showCreateModal: true,
+    }, () => {
+      this.createFormRef.current.setFieldsValue({
+        name: '',
+        quantity: ''
+      })
+    });
+  }
+
+  render() {
+    const columns = [
+      {
+        title: '名称',
+        dataIndex: 'name',
+        render: (text) => {
+          return <span>{text.substring(text.indexOf('-') + 1)}</span>;
+        }
+      },
+      {
+        title: '容量',
+        dataIndex: 'quantity',
+        editable: true,
+      },
+      {
+        title: '操作',
+        width: '10%',
+        dataIndex: 'operation',
+        render: (_, record) => {
+          const editable = record.name == this.state.editingKey;
+          return editable ? (
+            <span key={record.name}>
+              <a href="javascript:;" onClick={() => this.save(record)} style={{ marginRight: 8 }}>
+                保存
+              </a>
+              <a onClick={this.cancel.bind(this)}>取消</a>
+            </span>
+          ) : (
+            <span key={record.name}>
+              <a disabled={this.state.editingKey != null} onClick={() => this.edit(record)} style={{marginRight: 5}}>
+                编辑
+              </a>
+              <Popconfirm title="确认要删除吗?当前Volume策略为保留,请及时迁移数据以免被删除" onConfirm={() => this.deleteVolume(record)} okText="确定" cancelText="取消">
+                <a disabled={this.state.editingKey != null} >
+                  删除
+                </a>
+              </Popconfirm>
+            </span>
+          );
+        },
+      },
+    ];
+    const mergedColumns = columns.map(col => {
+      if (!col.editable) {
+        return col;
+      }
+      return {
+        ...col,
+        onCell: (record) => ({
+          record,
+          dataIndex: col.dataIndex,
+          title: col.title,
+          editing: record.name == this.state.editingKey,
+        }),
+      };
+    });
+    return (
+      <Modal
+        visible={this.props.visible}
+        forceRender
+        onCancel={() => this.props.onCancel()}
+        title={"更新配置"}
+        okText="确认"
+        cancelText="取消"
+        onOk={() => this.props.onOk()}
+        className={styles.modal}
+        width="80%"
+      >
+        <Button onClick={this.showCreate.bind(this)}>新建</Button>
+        <Modal
+          title="新建持久卷"
+          okText="确认"
+          cancelText="取消"
+          visible={this.state.showCreateModal}
+          onCancel={this.hideCreate.bind(this)}
+          onOk={this.create.bind(this)}
+        >
+          <Form
+            ref={this.createFormRef}
+            labelCol={{ span: 4 }}
+            wrapperCol={{ span: 20 }}
+            labelAlign="right"
+          >
+            <Form.Item 
+              name="name"
+              label="名称"
+              rules={[{required: true, message: '名称不能为空'}]}
+            >
+              <Input></Input>
+            </Form.Item >
+            <Form.Item 
+              name="quantity"
+              label="容量"
+              rules={[{required: true, message: '容量不能为空'}, {pattern:/[0-9][MBKG]i/, message: '格式错误!'}]}
+              tooltip="格式为[0-9][MBKG]i"
+            >
+              <Input></Input>
+            </Form.Item >
+          </Form>
+        </Modal>
+        <Form
+          ref={this.tableFormRef}
+        >
+          <Table
+            components={{
+              body: {
+                cell: EditableCell,
+              },
+            }}
+            bordered
+            dataSource={this.state.currentApp?.pvcs || []}
+            columns={mergedColumns}
+            rowClassName="editable-row"
+            pagination={{
+              onChange: this.cancel.bind(this),
+            }}
+          />
+        </Form>
+      </Modal>
+    )
+  }
+}

+ 19 - 0
src/components/VolumeModal.less

@@ -0,0 +1,19 @@
+.modal {
+  min-width: 1050px;
+  .list {
+    .field {
+      display: flex;
+      align-items: center;
+      margin-bottom: 16px;
+      :global(.ant-form-item) {
+        margin-bottom: 0px;
+        flex: 1;
+      }
+    }
+    .addBtn {
+      :global(.ant-col) {
+        max-width: 100%;
+      }
+    }
+  }
+}

+ 30 - 0
src/models/app.js

@@ -4,6 +4,9 @@ import {
   create,
   update,
   deleteApp,
+  createVolume,
+  updateVolume,
+  deleteVolume,
 } from '@/services/app'
 import { message } from 'antd'
 export default {
@@ -55,6 +58,33 @@ export default {
       if (payload && payload.callback) {
         yield call(payload.callback, response);
       }
+    },
+    *createVolume({payload}, {call, put}) {
+      const response = yield call(createVolume, payload);
+      if (response && response.code == 0) {
+        message.success('创建成功!');
+      }
+      if (payload && payload.callback) {
+        yield call(payload.callback, response);
+      }
+    },
+    *updateVolume({payload}, {call, put}) {
+      const response = yield call(updateVolume, payload);
+      if (response && response.code == 0) {
+        message.success('更新成功!');
+      }
+      if (payload && payload.callback) {
+        yield call(payload.callback, response);
+      }
+    },
+    *deleteVolume({payload}, {call, put}) {
+      const response = yield call(deleteVolume, payload);
+      if (response && response.code == 0) {
+        message.success('删除成功!');
+      }
+      if (payload && payload.callback) {
+        yield call(payload.callback, response);
+      }
     }
   },
   reducers: {

+ 48 - 4
src/pages/app/[id]/_layout.tsx

@@ -1,8 +1,9 @@
 import React, { PureComponent } from 'react';
 import { connect, history } from 'umi';
-import { Empty, PageHeader, Button, Descriptions, Menu, Divider } from 'antd';
+import { Empty, PageHeader, Button, Descriptions, Menu, Divider, Modal } from 'antd';
 import ConfigModal from '@/components/ConfigModal'
 import EnvironmentModal from '@/components/EnvironmentModal'
+import VolumeModal from '@/components/VolumeModal'
 import styles from './_layout.less'
 import {
   LoadingOutlined,
@@ -14,7 +15,9 @@ export default class AppDetailPage extends PureComponent {
   state = {
     showConfigModal: false,
     showEnvironmentModal: false,
+    showVolumeModal: false,
     currentApp: null,
+    fetching: false,
   }
   componentDidMount() {
     this.fetchData(this.props.match.params.id);
@@ -25,6 +28,9 @@ export default class AppDetailPage extends PureComponent {
     }
   }
   fetchData(id) {
+    this.setState({
+      fetching: true,
+    });
     this.props.dispatch({
       type: 'app/get',
       payload: {
@@ -51,7 +57,9 @@ export default class AppDetailPage extends PureComponent {
                         id,
                       }
                     });
-
+                    this.setState({
+                      fetching: false,
+                    });
                   }
                 }
               });
@@ -62,6 +70,9 @@ export default class AppDetailPage extends PureComponent {
                   id,
                 }
               });
+              this.setState({
+                fetching: false,
+              });
             }
             this.setState({
               currentApp: response.result,
@@ -76,6 +87,15 @@ export default class AppDetailPage extends PureComponent {
             if (!this.props.app.apps.length) {
               this.props.dispatch({
                 type: 'app/getAll',
+                callback: () => {
+                  this.setState({
+                    fetching: false,
+                  });
+                }
+              });
+            } else {
+              this.setState({
+                fetching: false,
               });
             }
           }
@@ -148,6 +168,19 @@ export default class AppDetailPage extends PureComponent {
   selectEnvironment({ item, key, keyPath, domEvent }) {
     history.push(`/app/${this.state.currentApp.id}/environment/${key}`)
   }
+  showVolume() {
+    this.setState({
+      showVolumeModal: true,
+    });
+  }
+  hideVolume() {
+    this.setState({
+      showVolumeModal: false,
+    });
+  }
+  saveVolume(values) {
+    console.log(values);
+  }
   render() {
     return (
       <PageHeader
@@ -158,11 +191,12 @@ export default class AppDetailPage extends PureComponent {
         extra={this.props.app.currentApp ? (
           <div>
             <Button type="primary" onClick={this.showEnvCreate.bind(this)} style={{marginRight: 10, background: '#74de37', borderColor: '#74de37'}}>创建环境</Button>
-            <Button type="primary" onClick={this.showAppConfig.bind(this)}>应用配置</Button>
+            <Button type="primary" onClick={this.showAppConfig.bind(this)} style={{marginRight: 10, background: '#74de37', borderColor: '#74de37'}}>应用配置</Button>
+            <Button type="primary" onClick={this.showVolume.bind(this)}>持久化卷配置</Button>
           </div>
         ) : null}
       >
-        {(this.props.loading.effects['app/getAll'] || this.props.loading.effects['app/get']) ? 
+        {(this.state.fetching) ? 
           <div style={{textAlign: 'center', marginTop: 30}}><LoadingOutlined style={{ fontSize: '48px'}} /></div> :
           (this.props.app.currentApp ? 
             <div className={styles.pageContent}>
@@ -197,6 +231,16 @@ export default class AppDetailPage extends PureComponent {
                 onCancel={this.hideEnvCreate.bind(this)}
               >
               </EnvironmentModal>
+              <VolumeModal
+                visible={this.state.showVolumeModal}
+                dataSource={(this.state.currentApp?.pvcs || [])}
+                appId={this.state.currentApp?.id}
+                onOk={this.hideVolume.bind(this)}
+                onCancel={this.hideVolume.bind(this)}
+              >
+              </VolumeModal>
+              <Modal>
+              </Modal>
             </div> : <Empty />   
           )
         }

+ 18 - 0
src/services/app.js

@@ -32,3 +32,21 @@ export async function deleteApp(params) {
   });
 }
 
+export async function createVolume(params) {
+  return request(`/api/application/volume`, {
+    method: 'post',
+    data: params,
+  });
+}
+export async function updateVolume(params) {
+  return request(`/api/application/volume`, {
+    method: 'put',
+    data: params,
+  });
+}
+export async function deleteVolume(params) {
+  return request(`/api/application/volume`, {
+    method: 'delete',
+    data: params,
+  });
+}