Browse Source

feat: resource chart

wanghongkai 5 years ago
parent
commit
249c864357

+ 1 - 1
.umirc.ts

@@ -3,7 +3,7 @@ import { defineConfig } from 'umi';
 export default defineConfig({
   proxy: {
     '/api': {
-      'target': 'http://172.19.240.15:8080/',
+      'target': 'http://paas.seec.seecoder.cn/',
       'changeOrigin': true,
     },
   },

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "@umijs/hooks": "^1.9.3",
     "@umijs/preset-react": "1.x",
     "@umijs/test": "^3.2.24",
+    "bizcharts": "^4.1.5",
     "lint-staged": "^10.0.7",
     "prettier": "^1.19.1",
     "react": "^16.12.0",

+ 199 - 0
src/components/MetricsCard.js

@@ -0,0 +1,199 @@
+import React, { PureComponent } from 'react';
+import styles from './MetricsCard.less';
+import { connect, history } from 'umi';
+import { PageHeader, Statistic, Radio } from 'antd';
+import { Chart, Line, Point, Tooltip, Axis } from "bizcharts";
+import moment from 'moment';
+
+const types = ['cpu', 'memory'];
+const sourceNames = {
+  'cluster': '集群',
+  'node': '节点',
+  'pod': 'POD',
+  'environment': '环境',
+  'application': '应用'
+}
+const typeDivideFactor = {
+  cpu: 1000000000,
+  memory: 1000,
+}
+const typePoint = {
+  cpu: 3,
+  memory: 2,
+}
+const axisName = {
+  cpu: '核数',
+  memory: 'MB',
+}
+const typeName = {
+  cpu: 'CPU',
+  memory: '内存'
+}
+@connect(({ metrics }) => ({
+  metrics
+}))
+export default class MetricsCard extends PureComponent {
+  state = {
+    types: ['cpu', 'memory'],
+    currentData: {},
+    data: {},
+    interval: {},
+  }
+  componentDidMount() {
+    if (this.state.currentData['cpu'] == null || this.state.currentData['cpu'].length) {
+      this.fetchCurrent(this.props.metricsSourceValue);
+    }
+    this.state.types.forEach(type => {
+      if (this.state.data[type] == null || this.state.data[type].length) {
+        this.fetchBetween(type, this.props.metricsSourceValue);
+      }
+    });
+    this.currentTimeout = setInterval(() => {
+      this.fetchCurrent(this.props.metricsSourceValue);
+    }, 60000);
+    this.state.types.forEach(type => {
+      this.fetchBetween(type, this.props.metricsSourceValue);
+      if (!this.timeout) {
+        this.timeout = {}
+      }
+      this.timeout[type] = setInterval(() => {
+        this.fetchBetween(type, this.props.metricsSourceValue);
+      }, 60000);
+    });
+  }
+  fetchCurrent(source) {
+    if (source == null || source === 'undefined') {
+      return;
+    }
+    this.props.dispatch({
+      type: 'metrics/getCurrent',
+      payload: {
+        metricsSource: this.props.metricsSource,
+        metricsSourceValue: source,
+        callback: (response) => {
+          if (response && response.code == 0) {
+            let currentData = {};
+            this.state.types.forEach(type => {
+              let metric = response.result.filter(item => item.metricsType == type)[0];
+              currentData[type] = metric.metricsTypeValue;
+            });
+            this.setState({
+              currentData
+            });
+          }
+        }
+      }
+    });
+  }
+  fetchBetween(type, source) {
+    if (source == null || source === 'undefined') {
+      return;
+    }
+    let now = moment();
+    this.props.dispatch({
+      type: 'metrics/getBetween',
+      payload: {
+        metricsSource: this.props.metricsSource,
+        metricsSourceValue: source,
+        metricsType: type,
+        startTime: now.clone().subtract(this.state.interval[type] ? +this.state.interval[type] : 10, 'm').valueOf(),
+        endTime: now.valueOf(),
+        callback: (response) => {
+          if (response && response.code == 0) {
+            let data = response.result.map(item => {
+              return {
+                ...item,
+                time: item.metricsTime,
+                value: ((+item.metricsTypeValue) / typeDivideFactor[type]).toFixed(typePoint[type]),
+              }
+            });
+            this.setState({
+              data: {
+                ...this.state.data,
+                [type]: data,
+              }
+            });
+          }
+        }
+      }
+    });
+  }
+  componentWillReceiveProps(props) {
+    if (this.props.metricsSourceValue != props.metricsSourceValue) {
+      console.log(props.metricsSourceValue)
+      this.fetchCurrent(props.metricsSourceValue);
+      this.state.types.forEach(type => {
+        this.fetchBetween(type, props.metricsSourceValue);
+      });
+    }
+  }
+  componentWillUnmount() {
+    types.forEach(type => {
+      clearInterval(this.timeout[type]);
+    });
+    clearInterval(this.currentTimeout);
+  }
+  changeInterval(type, e) {
+    this.setState({
+      interval: {
+        ...this.state.interval,
+        [type]: e.target.value,
+      }
+    }, () => {
+      this.fetchBetween(type, this.props.metricsSourceValue);
+    });
+  }
+  render() {
+    return (
+      <PageHeader
+        title={this.props.showTitle ? this.props.metricsSourceValue : ''}
+        subTitle={this.props.showTitle ? sourceNames[this.props.metricsSource] : ''}
+      >
+        <div className={styles.cardBody}>
+          <div className={styles.statistic}>
+            <Statistic title="CPU" suffix="core" value={((+this.state.currentData['cpu'])/1000000000).toFixed(3)}></Statistic>
+            <Statistic title="内存" suffix="MB" value={((+this.state.currentData['memory'])/1000).toFixed(2)}></Statistic>
+          </div>
+          <div className={styles.charts}>
+              {types.map(type => {
+                const scale = {
+                  time: {
+                    type: 'time',
+                    alias: '时间',
+                    mask: 'HH:mm',
+                  },
+                  value: {
+                    type: 'linear',
+                    alias: axisName[type],
+                  }
+                }
+                return (
+                <div className={styles.chart} key={type}>
+                  <div className={styles.head}>{`${typeName[type]}使用情况`}</div>
+                  <div className={styles.radio}>
+                    <Radio.Group className={styles.radio} value={this.state.interval[type] ? this.state.interval[type] : "10"} onChange={(e) => this.changeInterval(type, e)} defaultValue="10">
+                      <Radio.Button value="10">10min</Radio.Button>
+                      <Radio.Button value="30">30min</Radio.Button>
+                      <Radio.Button value="60">1h</Radio.Button>
+                      <Radio.Button value="240">4h</Radio.Button>
+                    </Radio.Group>
+                  </div>
+                  <Chart
+                    scale={scale}
+                    autoFit
+                    height={200}
+                    data={this.state.data[type]}
+                  >
+                    <Point position="time*value"  shape='circle'></Point>
+                    <Line shape="smooth" position="time*value" />
+                    <Tooltip shared showCrosshairs/>
+                    <Axis />
+                  </Chart>
+                </div>)
+              })}
+          </div>
+        </div>
+      </PageHeader>
+    );
+  }
+}

+ 22 - 0
src/components/MetricsCard.less

@@ -0,0 +1,22 @@
+.cardBody {
+  display: flex;
+  .statistic {
+    min-width: 200px;
+  }
+  .charts {
+    flex: 1;
+    display: flex;
+    .chart {
+      flex: 1;
+      margin-right: 6px;
+      .head {
+        color: grey;
+      }
+      .radio {
+        display: flex;
+        justify-content: flex-end;
+        margin-bottom: 10px;
+      }
+    }
+  }
+}

+ 2 - 1
src/layouts/index.tsx

@@ -1,4 +1,4 @@
-import { IRouteComponentProps, connect, history } from 'umi'
+import { IRouteComponentProps, connect, history, Link } from 'umi'
 import React, { PureComponent } from 'react';
 import { Layout, Select } from 'antd'
 import Login from '@/pages/login';
@@ -39,6 +39,7 @@ export default class GlobalLayout extends PureComponent {
                   })}
                 </Select>
               </div>
+              <Link to="/metrics">集群状态</Link>
               <div className={styles.user}></div>
             </div>
           </Header>

+ 25 - 0
src/models/metrics.js

@@ -0,0 +1,25 @@
+import {
+  getBetween,
+  getCurrent,
+} from '@/services/metrics'
+export default {
+  namespace: 'metrics',
+  state: {
+  },
+  effects: {
+    *getBetween({payload}, {call, put}) {
+      const response = yield call(getBetween, payload);
+      if (payload && payload.callback) {
+        yield call(payload.callback, response);
+      }
+    },
+    *getCurrent({payload}, {call, put}) {
+      const response = yield call(getCurrent, payload);
+      if (payload && payload.callback) {
+        yield call(payload.callback, response);
+      }
+    },
+  },
+  reducers: {
+  }
+}

+ 9 - 0
src/pages/app/[id]/_layout.tsx

@@ -5,6 +5,7 @@ import ConfigModal from '@/components/ConfigModal'
 import EnvironmentModal from '@/components/EnvironmentModal'
 import VolumeModal from '@/components/VolumeModal'
 import styles from './_layout.less'
+import MetricsCard from '@/components/MetricsCard';
 import {
   LoadingOutlined,
 } from '@ant-design/icons';
@@ -18,6 +19,7 @@ export default class AppDetailPage extends PureComponent {
     showVolumeModal: false,
     currentApp: null,
     fetching: false,
+    showResource: true,
   }
   componentDidMount() {
     this.fetchData(this.props.match.params.id);
@@ -181,6 +183,11 @@ export default class AppDetailPage extends PureComponent {
   saveVolume(values) {
     console.log(values);
   }
+  reverseShowResource() {
+    this.setState({
+      showResource: !this.state.showResource,
+    })
+  }
   render() {
     return (
       <PageHeader
@@ -190,6 +197,7 @@ export default class AppDetailPage extends PureComponent {
         className={styles.appHeader}
         extra={this.props.app.currentApp ? (
           <div>
+            <Button type="primary" onClick={this.reverseShowResource.bind(this)} style={{marginRight: 10}}>{`${this.state.showResource ? '收起' : '展开'}资源详情`}</Button>
             <Button type="primary" onClick={this.showEnvCreate.bind(this)} style={{marginRight: 10, background: '#74de37', borderColor: '#74de37'}}>创建环境</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>
@@ -204,6 +212,7 @@ export default class AppDetailPage extends PureComponent {
               >
                 <Descriptions.Item label="Git URL"><a target="_blank" href={this.state.currentApp?.gitUrl}>{this.state.currentApp?.gitUrl}</a></Descriptions.Item>
               </Descriptions>
+              {this.state.showResource ? <MetricsCard metricsSource="application" metricsSourceValue={this.state.currentApp?.id} showTitle={false}></MetricsCard> : null}
               <div><Divider orientation="left" style={{fontWeight: 600}}>环境列表</Divider></div>
               <div className={styles.environments}>
                 <Menu

+ 29 - 0
src/pages/app/[id]/environment/[environmentId].js

@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
 import { connect, history } from 'umi';
 import { Empty, PageHeader, Button, Popconfirm, Descriptions, Steps, Divider, Modal, Table, Input } from 'antd';
 import moment from 'moment';
+import MetricsCard from '@/components/MetricsCard';
 import {
   LoadingOutlined,
 } from '@ant-design/icons';
@@ -60,6 +61,8 @@ export default class AppEnvironment extends PureComponent {
     deployTimeOffset: null,
     log: '',
     requestFromInitial: false,
+    showResourceModalFlag: false,
+    resourcePodName: '',
   }
   fetchData(id) {
     this.setState({
@@ -369,6 +372,17 @@ export default class AppEnvironment extends PureComponent {
       }
     })
   }
+  showResourceModal(record) {
+    this.setState({
+      showResourceModalFlag: true,
+      resourcePodName: record.name
+    })
+  }
+  hideResourceModal() {
+    this.setState({
+      showResourceModalFlag: false,
+    })
+  }
   render() {
     const buildType = this.state.currentEnvironment?.buildType;
     const stepText = this.state.currentStep == 0 ? '构建' : '部署';
@@ -391,6 +405,7 @@ export default class AppEnvironment extends PureComponent {
       title: '操作',
       render: (text, record, index) => {
         return (<div key={`action-${record.name}`}>
+          <a key={`action-resource-${record.name}`} style={{marginRight: 5}} onClick={() => this.showResourceModal(record)}>查看资源</a>
           <a key={`action-podlog-${record.name}`} style={{marginRight: 5}} onClick={() => this.showLogModal('pod', record)}>查看日志</a>
           <Popconfirm title="确认要重启吗?" onConfirm={() => this.restartPod(record)} okText="确定" cancelText="取消">
             <a key={`action-restart-${record.name}`} style={{marginRight: 5}}>重新启动</a>
@@ -456,6 +471,20 @@ export default class AppEnvironment extends PureComponent {
               </Descriptions>
               <Divider style={{fontSize: 14, color: '#aaaaaad9', fontWeight: 600}}>实例维护</Divider>
               <Table dataSource={dataSource} columns={columns}></Table>
+              <Divider style={{fontSize: 14, color: '#aaaaaad9', fontWeight: 600}}>环境资源</Divider>
+              <MetricsCard metricsSource="environment" metricsSourceValue={this.state.currentEnvironment?.id} showTitle={false}></MetricsCard>
+              <Modal
+                visible={this.state.showResourceModalFlag}
+                onOk={this.hideResourceModal.bind(this)}
+                onCancel={this.hideResourceModal.bind(this)}
+                okText="确认"
+                cancelText="取消"
+                title="资源详情"
+                id="resourceModal"
+                width="90%"
+              >
+                <MetricsCard metricsSource="pod" metricsSourceValue={this.state.resourcePodName} showTitle={true}></MetricsCard>
+              </Modal>
               <Modal
                 visible={this.state.showLogModal}
                 onOk={this.hideLogModal.bind(this)}

+ 29 - 0
src/pages/metrics.js

@@ -0,0 +1,29 @@
+import React, { PureComponent } from 'react';
+import styles from './metrics.less';
+import { connect, history } from 'umi';
+import { Card } from 'antd';
+import MetricsCard from '@/components/MetricsCard';
+
+export default class Metrics extends PureComponent {
+  state = {
+    nodeList: ['172.19.51.10', '172.19.51.11', '172.19.47.92'], //暂时写死,后端还没有节点列表的请求
+  }
+  componentDidMount() {
+  }
+  render() {
+    return (
+      <div>
+        <Card className={styles.card} hoverable>
+          <MetricsCard metricsSource="cluster" metricsSourceValue="cluster" showTitle={true}></MetricsCard>
+        </Card>
+        {this.state.nodeList.map(node => {
+          return (
+            <Card className={styles.card} hoverable key={node}>
+              <MetricsCard metricsSource="node" metricsSourceValue={node} showTitle={true}></MetricsCard>
+            </Card>
+          )
+        })}
+      </div>
+    );
+  }
+}

+ 3 - 0
src/pages/metrics.less

@@ -0,0 +1,3 @@
+.card {
+  margin-top: 15px;
+}

+ 14 - 0
src/services/metrics.js

@@ -0,0 +1,14 @@
+import { request } from 'umi';
+
+export async function getBetween(params) {
+  return request(`/api/metrics?metricsSource=${params.metricsSource}&metricsSourceValue=${params.metricsSourceValue}&metricsType=${params.metricsType}&startTime=${params.startTime}&endTime=${params.endTime}`, {
+    method: 'get',
+  });
+}
+
+
+export async function getCurrent(params) {
+  return request(`/api/metrics/current?metricsSource=${params.metricsSource}&metricsSourceValue=${params.metricsSourceValue}`, {
+    method: 'get',
+  });
+}