You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ozone.apache.org by el...@apache.org on 2020/01/28 12:32:27 UTC

[hadoop-ozone] branch master updated: HDDS-1335. Add basic UI for showing missing containers and keys

This is an automated email from the ASF dual-hosted git repository.

elek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hadoop-ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new ef79f33  HDDS-1335. Add basic UI for showing missing containers and keys
ef79f33 is described below

commit ef79f3332dde071da7299dc76077dc594dfa3264
Author: Vivek Ratnavel Subramanian <vi...@gmail.com>
AuthorDate: Tue Jan 28 13:31:24 2020 +0100

    HDDS-1335. Add basic UI for showing missing containers and keys
    
    Closes #492
---
 .../webapps/recon/ozone-recon-web/api/db.json      |  85 ++++++++
 .../webapps/recon/ozone-recon-web/api/routes.json  |   3 +-
 .../webapps/recon/ozone-recon-web/src/App.less     |   4 +-
 .../src/components/OverviewCard/OverviewCard.less  |   4 +
 .../src/components/OverviewCard/OverviewCard.tsx   |   9 +-
 .../src/constants/breadcrumbs.constants.tsx        |   3 +-
 .../webapps/recon/ozone-recon-web/src/routes.tsx   |   5 +
 .../MissingContainers.less}                        |  10 -
 .../views/MissingContainers/MissingContainers.tsx  | 218 +++++++++++++++++++++
 .../src/views/Overview/Overview.less               |   3 +
 .../src/views/Overview/Overview.tsx                |  35 +++-
 11 files changed, 354 insertions(+), 25 deletions(-)

diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json
index 578e280..3e3d413 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/db.json
@@ -271,5 +271,90 @@
         "leaderElections": 5
       }
     ]
+  },
+  "missingContainers": {
+    "totalCount": 2,
+    "containers": [
+      {
+        "id": 1,
+        "keys": 3876,
+        "datanodes": [
+          "localhost1.storage.enterprise.com",
+          "localhost3.storage.enterprise.com",
+          "localhost5.storage.enterprise.com"
+        ]
+      },
+      {
+        "id": 2,
+        "keys": 5943,
+        "datanodes": [
+          "localhost1.storage.enterprise.com",
+          "localhost3.storage.enterprise.com",
+          "localhost5.storage.enterprise.com"
+        ]
+      }
+    ]
+  },
+  "keys": {
+    "totalCount": 534,
+    "keys": [
+      {
+        "Volume": "vol-0-20448",
+        "Bucket": "bucket-0-12811",
+        "Key": "key-0-77505",
+        "DataSize": 10240,
+        "Versions": [
+          0
+        ],
+        "Blocks": {
+          "0": [
+            {
+              "containerID": 1,
+              "localID": 103206297511460860
+            }
+          ]
+        },
+        "CreationTime": "2019-11-26T21:18:43.688Z",
+        "ModificationTime": "2019-11-26T21:18:46.062Z"
+      },
+      {
+        "Volume": "vol-0-20448",
+        "Bucket": "bucket-0-12811",
+        "Key": "key-21-64511",
+        "DataSize": 5692407,
+        "Versions": [
+          0
+        ],
+        "Blocks": {
+          "0": [
+            {
+              "containerID": 1,
+              "localID": 103206299949795380
+            }
+          ]
+        },
+        "CreationTime": "2019-11-26T21:19:20.855Z",
+        "ModificationTime": "2019-11-26T21:19:20.991Z"
+      },
+      {
+        "Volume": "vol-0-20448",
+        "Bucket": "bucket-0-12811",
+        "Key": "key-22-69104",
+        "DataSize": 189407,
+        "Versions": [
+          0
+        ],
+        "Blocks": {
+          "0": [
+            {
+              "containerID": 1,
+              "localID": 103206300033091740
+            }
+          ]
+        },
+        "CreationTime": "2019-11-26T21:19:22.126Z",
+        "ModificationTime": "2019-11-26T21:19:22.251Z"
+      }
+    ]
   }
 }
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json
index 028ffc3..df5cec3 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/api/routes.json
@@ -1,3 +1,4 @@
 {
-  "/api/v1/*": "/$1"
+  "/api/v1/*": "/$1",
+  "/containers/:id/keys": "/keys"
 }
\ No newline at end of file
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/App.less b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/App.less
index 2896a82..5de722c 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/App.less
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/App.less
@@ -51,10 +51,10 @@
 
         .ant-table-tbody {
           tr.ant-table-row:hover {
-            background: rgba(60, 90, 100, 0.04);
+            background: rgba(60, 90, 100, 0.01);
 
             & > td {
-              background: rgba(60, 90, 100, 0.04);
+              background: rgba(60, 90, 100, 0.01);
             }
           }
         }
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/OverviewCard/OverviewCard.less b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/OverviewCard/OverviewCard.less
index 774c791..536b1b4 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/OverviewCard/OverviewCard.less
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/OverviewCard/OverviewCard.less
@@ -36,3 +36,7 @@
     }
   }
 }
+
+.card-error {
+  border-color: #f5222d;
+}
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/OverviewCard/OverviewCard.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/OverviewCard/OverviewCard.tsx
index cb591d2..fcf3fd2 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/OverviewCard/OverviewCard.tsx
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/components/OverviewCard/OverviewCard.tsx
@@ -32,13 +32,15 @@ interface OverviewCardProps extends RouteComponentProps<any> {
   loading?: boolean;
   linkToUrl?: string;
   capacityPercent?: number;
+  error?: boolean;
 }
 
 const defaultProps = {
   hoverable: false,
   loading: false,
   capacityPercent: -1,
-  linkToUrl: ''
+  linkToUrl: '',
+  error: false
 };
 
 interface OverviewCardWrapperProps {
@@ -62,8 +64,9 @@ class OverviewCard extends React.Component<OverviewCardProps> {
   static defaultProps = defaultProps;
 
   render() {
-    let {icon, data, title, loading, hoverable, capacityPercent, linkToUrl} = this.props;
+    let {icon, data, title, loading, hoverable, capacityPercent, linkToUrl, error} = this.props;
     let meta = <Meta title={data} description={title}/>;
+    const errorClass = error ? 'card-error' : '';
     if (capacityPercent && capacityPercent > -1) {
       meta = <div className="ant-card-percentage">
         {meta}
@@ -74,7 +77,7 @@ class OverviewCard extends React.Component<OverviewCardProps> {
 
     return (
         <OverviewCardWrapper linkToUrl={linkToUrl}>
-          <Card className="overview-card" loading={loading} hoverable={hoverable}>
+          <Card className={`overview-card ${errorClass}`} loading={loading} hoverable={hoverable}>
             <Row type="flex" justify="space-between">
               <Col span={18}>
                 <Row>
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx
index 4cd1cac..93773b9 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/constants/breadcrumbs.constants.tsx
@@ -23,5 +23,6 @@ interface IBreadcrumbNameMap {
 export const breadcrumbNameMap: IBreadcrumbNameMap = {
   '/Overview': 'Overview',
   '/Datanodes': 'Datanodes',
-  '/Pipelines': 'Pipelines'
+  '/Pipelines': 'Pipelines',
+  '/MissingContainers': 'Missing Containers'
 };
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/routes.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/routes.tsx
index 37f4338..8d520db 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/routes.tsx
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/routes.tsx
@@ -21,6 +21,7 @@ import {Datanodes} from './views/Datanodes/Datanodes';
 import {Pipelines} from "./views/Pipelines/Pipelines";
 import {NotFound} from './views/NotFound/NotFound';
 import {IRoute} from "./routes.types";
+import {MissingContainers} from "./views/MissingContainers/MissingContainers";
 
 export const routes: IRoute[] = [
   {
@@ -36,6 +37,10 @@ export const routes: IRoute[] = [
     component: Pipelines
   },
   {
+    path: "/MissingContainers",
+    component: MissingContainers
+  },
+  {
     path: "/:NotFound",
     component: NotFound,
   }
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.less b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/MissingContainers/MissingContainers.less
similarity index 87%
copy from hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.less
copy to hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/MissingContainers/MissingContainers.less
index c8e74d2..5d5f1e3 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.less
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/MissingContainers/MissingContainers.less
@@ -15,13 +15,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-.overview-content {
-  margin: 20px 5px;
-  .icon-small {
-    font-size: 16px;
-  }
-  .meta {
-    font-size: 12px;
-  }
-}
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/MissingContainers/MissingContainers.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/MissingContainers/MissingContainers.tsx
new file mode 100644
index 0000000..f97ec5e
--- /dev/null
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/MissingContainers/MissingContainers.tsx
@@ -0,0 +1,218 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import axios from 'axios';
+import {Table} from 'antd';
+import './MissingContainers.less';
+import {PaginationConfig} from "antd/lib/pagination";
+import prettyBytes from "pretty-bytes";
+import moment from "moment";
+
+interface MissingContainerResponse {
+  id: number;
+  keys: number;
+  datanodes: string[];
+}
+
+interface MissingContainersResponse  {
+  totalCount: number;
+  containers: MissingContainerResponse[];
+}
+
+interface KeyResponse {
+  Volume: string;
+  Bucket: string;
+  Key: string;
+  DataSize: number;
+  Versions: number[];
+  Blocks: any;
+  CreationTime: string;
+  ModificationTime: string;
+}
+
+interface ContainerKeysResponse {
+  totalCount: number;
+  keys: KeyResponse[];
+}
+
+const COLUMNS = [
+  {
+    title: 'Container ID',
+    dataIndex: 'id',
+    key: 'id'
+  },
+  {
+    title: 'No. of Keys',
+    dataIndex: 'keys',
+    key: 'keys'
+  },
+  {
+    title: 'Datanodes',
+    dataIndex: 'datanodes',
+    key: 'datanodes',
+    render: (datanodes: string[]) => <div>{datanodes.map(datanode => <div key={datanode}>{datanode}</div>)}</div>
+  }
+];
+
+const KEY_TABLE_COLUMNS = [
+  {
+    title: 'Volume',
+    dataIndex: 'Volume',
+    key: 'Volume'
+  },
+  {
+    title: 'Bucket',
+    dataIndex: 'Bucket',
+    key: 'Bucket'
+  },
+  {
+    title: 'Key',
+    dataIndex: 'Key',
+    key: 'Key'
+  },
+  {
+    title: 'Size',
+    dataIndex: 'DataSize',
+    key: 'DataSize',
+    render: (dataSize: number) => <div>{prettyBytes(dataSize)}</div>
+  },
+  {
+    title: 'Date Created',
+    dataIndex: 'CreationTime',
+    key: 'CreationTime',
+    render: (date: number) => moment(date).format('lll')
+  },
+  {
+    title: 'Date Modified',
+    dataIndex: 'ModificationTime',
+    key: 'ModificationTime',
+    render: (date: number) => moment(date).format('lll')
+  }
+];
+
+interface ExpandedRow {
+  [key: number]: ExpandedRowState
+}
+
+interface ExpandedRowState {
+  containerId: number;
+  loading: boolean;
+  dataSource: KeyResponse[];
+  totalCount: number;
+}
+
+interface MissingContainersState {
+  loading: boolean;
+  dataSource: MissingContainerResponse[];
+  totalCount: number;
+  expandedRowData: ExpandedRow
+}
+
+export class MissingContainers extends React.Component<any, MissingContainersState> {
+
+  constructor(props: any) {
+    super(props);
+    this.state = {
+      loading: false,
+      dataSource: [],
+      totalCount: 0,
+      expandedRowData: {}
+    }
+  }
+
+  componentDidMount(): void {
+    // Fetch missing containers on component mount
+    this.setState({
+      loading: true
+    });
+    axios.get('/api/v1/missingContainers').then(response => {
+      const missingContainersResponse: MissingContainersResponse = response.data;
+      const totalCount = missingContainersResponse.totalCount;
+      const missingContainers: MissingContainerResponse[] = missingContainersResponse.containers;
+      this.setState({
+        loading: false,
+        dataSource: missingContainers,
+        totalCount: totalCount
+      });
+    });
+  }
+
+  onShowSizeChange = (current: number, pageSize: number) => {
+    // TODO: Implement this method once server side pagination is enabled
+    console.log(current, pageSize);
+  };
+
+  onRowExpandClick = (expanded: boolean, record: MissingContainerResponse) => {
+    if (expanded) {
+      this.setState(({expandedRowData}) => {
+        const expandedRowState: ExpandedRowState = expandedRowData[record.id] ?
+            Object.assign({}, expandedRowData[record.id], {loading: true}) :
+            {containerId: record.id, loading: true, dataSource: [], totalCount: 0};
+        return {
+          expandedRowData: Object.assign({}, expandedRowData, {[record.id]: expandedRowState})
+        }
+      });
+      axios.get(`/api/v1/containers/${record.id}/keys`).then(response => {
+        const containerKeysResponse: ContainerKeysResponse = response.data;
+        this.setState(({expandedRowData}) => {
+          const expandedRowState: ExpandedRowState =
+              Object.assign({}, expandedRowData[record.id],
+                  {loading: false, dataSource: containerKeysResponse.keys, totalCount: containerKeysResponse.totalCount});
+          return {
+            expandedRowData: Object.assign({}, expandedRowData, {[record.id]: expandedRowState})
+          }
+        });
+      });
+    }
+  };
+
+  expandedRowRender = (record: MissingContainerResponse) => {
+    const {expandedRowData} = this.state;
+    const containerId = record.id;
+    if (expandedRowData[containerId]) {
+      const containerKeys: ExpandedRowState = expandedRowData[containerId];
+      const paginationConfig: PaginationConfig = {
+        showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} keys`
+      };
+      return <Table loading={containerKeys.loading} dataSource={containerKeys.dataSource}
+                    columns={KEY_TABLE_COLUMNS} pagination={paginationConfig}/>
+    }
+    return <div>Loading...</div>;
+  };
+
+  render () {
+    const {dataSource, loading, totalCount} = this.state;
+    const paginationConfig: PaginationConfig = {
+      showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total} missing containers`,
+      showSizeChanger: true,
+      onShowSizeChange: this.onShowSizeChange
+    };
+    return (
+        <div className="missing-containers-container">
+          <div className="page-header">
+            Missing Containers ({totalCount})
+          </div>
+          <div className="content-div">
+            <Table dataSource={dataSource} columns={COLUMNS} loading={loading} pagination={paginationConfig}
+                   rowKey="id" expandedRowRender={this.expandedRowRender} expandRowByClick={true} onExpand={this.onRowExpandClick}/>
+          </div>
+        </div>
+    );
+  }
+}
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.less b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.less
index c8e74d2..5a8a069 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.less
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.less
@@ -24,4 +24,7 @@
   .meta {
     font-size: 12px;
   }
+  .padded-text {
+    padding-left: 5px;
+  }
 }
diff --git a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.tsx b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.tsx
index 0d4d31b..cf0b911 100644
--- a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.tsx
+++ b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/Overview/Overview.tsx
@@ -17,7 +17,7 @@
  */
 
 import React from 'react';
-import {Row, Col, Icon} from 'antd';
+import {Row, Col, Icon, Tooltip} from 'antd';
 import OverviewCard from 'components/OverviewCard/OverviewCard';
 import axios from 'axios';
 import prettyBytes from 'pretty-bytes';
@@ -34,6 +34,7 @@ interface OverviewState {
   volumes: string;
   buckets: string;
   keys: string;
+  missingContainersCount: number;
 }
 
 export class Overview extends React.Component<any, OverviewState> {
@@ -49,7 +50,8 @@ export class Overview extends React.Component<any, OverviewState> {
       containers: '',
       volumes: '',
       buckets: '',
-      keys: ''
+      keys: '',
+      missingContainersCount: 0
     }
   }
 
@@ -57,8 +59,12 @@ export class Overview extends React.Component<any, OverviewState> {
     this.setState({
       loading: true
     });
-    axios.get('/api/v1/stats').then(response => {
-      const stats = response.data;
+    axios.all([
+        axios.get('/api/v1/stats'),
+        axios.get('/api/v1/missingContainers')
+    ]).then(axios.spread((statsResponse, missingContainersResponse) => {
+      const stats = statsResponse.data;
+      const missingContainers = missingContainersResponse.data;
       const clusterUsedPercent = getCapacityPercent(stats.capacity.used, stats.capacity.total);
       this.setState({
         loading: false,
@@ -69,16 +75,28 @@ export class Overview extends React.Component<any, OverviewState> {
         containers: stats.containers,
         volumes: stats.volumes,
         buckets: stats.buckets,
-        keys: stats.keys
+        keys: stats.keys,
+        missingContainersCount: missingContainers.totalCount
       });
-    });
+    }));
   }
 
   render() {
-    const {loading, datanodes, pipelines, clusterCapacity, clusterUsedPercent, containers, volumes, buckets, keys} = this.state;
+    const {loading, datanodes, pipelines, clusterCapacity, clusterUsedPercent, containers, volumes, buckets,
+      keys, missingContainersCount} = this.state;
     const datanodesElement = <span>
       <Icon type="check-circle" theme="filled" className="icon-success icon-small"/> {datanodes} <span className="ant-card-meta-description meta">HEALTHY</span>
     </span>;
+    const containersTooltip = missingContainersCount === 1 ? "container is missing" : "containers are missing";
+    const containersLink = missingContainersCount > 0 ? '/MissingContainers' : '';
+    const containersElement = missingContainersCount > 0 ?
+        <span>
+          <Tooltip placement="bottom" title={`${missingContainersCount} ${containersTooltip}`}>
+            <Icon type="exclamation-circle" theme="filled" className="icon-failure icon-small"/>
+          </Tooltip>
+          <span className="padded-text">{containers}</span>
+        </span>
+        : containers;
     return (
         <div className="overview-content">
           <Row gutter={[25, 25]}>
@@ -95,7 +113,8 @@ export class Overview extends React.Component<any, OverviewState> {
                             capacityPercent={clusterUsedPercent}/>
             </Col>
             <Col xs={24} sm={18} md={12} lg={12} xl={6}>
-              <OverviewCard loading={loading} title={"Containers"} data={containers} icon={"container"}/>
+              <OverviewCard loading={loading} title={"Containers"} data={containersElement} icon={"container"}
+                            error={missingContainersCount > 0} linkToUrl={containersLink}/>
             </Col>
           </Row>
           <Row gutter={[25, 25]}>


---------------------------------------------------------------------
To unsubscribe, e-mail: ozone-commits-unsubscribe@hadoop.apache.org
For additional commands, e-mail: ozone-commits-help@hadoop.apache.org