You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@ozone.apache.org by GitBox <gi...@apache.org> on 2021/08/12 20:25:23 UTC

[GitHub] [ozone] yuangu002 opened a new pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

yuangu002 opened a new pull request #2530:
URL: https://github.com/apache/ozone/pull/2530


   ## What changes were proposed in this pull request?
   
   Add a new tab to the Recon Web UI about Namespace Summary.
   
   [Other fixes]
   1. Fixed NPE bugs in `NSSummaryEndpoint` when bucket or directory is empty.
   2. Added a isKey field in DU's subpath in order for the UI to differentiate directories from keys.
   
   ## What is the link to the Apache JIRA
   
   https://issues.apache.org/jira/browse/HDDS-5377
   
   ## How was this patch tested?
   Frontend mocking: `pnpm run dev`
   Demo: https://www.youtube.com/watch?v=Q6naW-BxpuQ
   
   On docker:
   Configs:
   ```
   ozone sh volume create /vol1
   ozone sh volume create /vol2
   ozone sh bucket create /vol1/bucket11
   ozone sh bucket create /vol1/bucket12
   ozone sh bucket create /vol2/bucket21
   ozone sh bucket create /vol2/bucket22
   ozone sh key put --replication=THREE /vol1/bucket11/key1 README.md
   ozone sh key put --replication=THREE /vol1/bucket11/key2s README.md
   ozone sh key put --replication=THREE /vol1/bucket11/dir1/key3 README.md
   ozone sh key put /vol2/bucket21/key4 CONTRIBUTING.md
   ozone sh key put /vol2/bucket21/key5 CONTRIBUTING.md
   ozone sh key put /vol2/bucket22/key6 README.md
   ozone sh key put --replication=THREE /vol2/bucket22/dir22/key7 LICENSE.txt
   ozone sh key put /vol1/bucket12/dir1-1/key8 README.md
   ozone sh key put /vol1/bucket12/dir1-2/key9 CONTRIBUTING.md
   ozone sh key put --replication=THREE /vol1/bucket12/directKey README.md
   ```
   Demo: https://www.youtube.com/watch?v=J7n-2kbNu-4
   
   [Other]
   I also use a shell script to quickly run docker-up with FSO enabled
   
   ```
   cd hadoop-ozone/dist/target/ozone-*-SNAPSHOT/compose/ozone
   echo 'OZONE-SITE.XML_ozone.om.enable.filesystem.paths=true;' >> docker-config
   echo 'OZONE-SITE.XML_ozone.om.metadata.layout=PREFIX;' >> docker-config
   sed -i '' 's/-SIMPLE/-PREFIX/g' docker-compose.yaml
   sed -i '' 's/-false/-true/g' docker-compose.yaml
   docker-compose up -d --scale datanode=3
   ```


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] yuangu002 commented on pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
yuangu002 commented on pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#issuecomment-901209148


   @smengcl @vivekratnavel would you take another look?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] smengcl commented on a change in pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
smengcl commented on a change in pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#discussion_r690044979



##########
File path: hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/diskUsage/diskUsage.tsx
##########
@@ -0,0 +1,387 @@
+/*
+ * 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 Plot from 'react-plotly.js';
+import {Row, Col, Icon, Button, Input} from 'antd';
+import {DetailPanel} from 'components/rightDrawer/rightDrawer';
+import * as Plotly from 'plotly.js';
+import {showDataFetchError} from 'utils/common';
+import './diskUsage.less';
+
+const DISPLAY_LIMIT = 20;
+const OTHER_PATH_NAME = 'Other Objects';
+
+interface IDUSubpath {
+  path: string;
+  size: number;
+  sizeWithReplica: number;
+  isKey: boolean;
+}
+
+interface IDUResponse {
+  status: string;
+  path: string;
+  subPathCount: number;
+  size: number;
+  sizeWithReplica: number;
+  subPaths: IDUSubpath[];
+  sizeDirectKey: number;
+}
+
+interface IDUState {
+  isLoading: boolean;
+  duResponse: IDUResponse[];
+  plotData: Plotly.Data[];
+  showPanel: boolean;
+  panelKeys: string[];
+  panelValues: string[];
+  returnPath: string;
+  inputPath: string;
+}
+
+export class DiskUsage extends React.Component<Record<string, object>, IDUState> {
+  constructor(props = {}) {
+    super(props);
+    this.state = {
+      isLoading: false,
+      duResponse: [],
+      plotData: [],
+      showPanel: false,
+      panelKeys: [],
+      panelValues: [],
+      returnPath: '/',
+      inputPath: '/'
+    };
+  }
+
+  byteToSize = (bytes, decimals) => {
+    if (bytes === 0) {
+      return '0 Bytes';
+    }
+
+    const k = 1024;
+    const dm = decimals < 0 ? 0 : decimals;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+    return `${Number.parseFloat((bytes / (k ** i)).toFixed(dm))} ${sizes[i]}`;
+  };
+
+  handleChange = e => {
+    this.setState({inputPath: e.target.value, showPanel: false});
+  };
+
+  handleSubmit = _e => {
+    // Avoid empty request trigger 400 response
+    if (!this.state.inputPath) {
+      this.updatePieChart('/');
+      return;
+    }
+
+    this.updatePieChart(this.state.inputPath);
+  };
+
+  // The returned path is passed in, which should have been
+  // normalized by the backend
+  goBack = (e, path) => {
+    if (!path || path === '/') {
+      return;
+    }
+
+    const arr = path.split('/');
+    let parentPath = arr.slice(0, -1).join('/');
+    if (parentPath.length === 0) {
+      parentPath = '/';
+    }
+
+    this.updatePieChart(parentPath);
+  };
+
+  // Take the request path, make a DU request, inject response
+  // into the pie chart
+  updatePieChart = (path: string) => {
+    this.setState({
+      isLoading: true
+    });
+    const duEndpoint = `/api/v1/namespace/du?path=${path}&files=true`;
+    axios.get(duEndpoint).then(response => {
+      const duResponse: IDUResponse[] = response.data;
+      const status = duResponse.status;
+      if (status !== 'OK') {
+        this.setState({isLoading: false});
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      const dataSize = duResponse.size;
+      let subpaths: IDUSubpath[] = duResponse.subPaths;
+
+      subpaths.sort((a, b) => (a.size < b.size) ? 1 : -1);
+
+      // Only show 20 blocks with the most DU,
+      // other blocks are merged as a single block
+      if (subpaths.length > DISPLAY_LIMIT) {
+        subpaths = subpaths.slice(0, DISPLAY_LIMIT);
+        let topSize = 0;
+        for (let i = 0; i < DISPLAY_LIMIT; ++i) {
+          topSize += subpaths[i].size;
+        }
+
+        const otherSize = dataSize - topSize;
+        const other: IDUSubpath = {path: OTHER_PATH_NAME, size: otherSize};
+        subpaths.push(other);
+      }
+
+      const pathLabels = subpaths.map(subpath => {
+        // The return subPath must be normalized in a format with
+        // a leading slash and without trailing slash
+        const pieces = subpath.path.split('/');
+        // Differentiate key without trailing slash
+        return (subpath.isKey) ? pieces[pieces.length - 1] : pieces[pieces.length - 1] + '/';
+      });
+
+      const percentage = subpaths.map(subpath => {
+        return subpath.size / dataSize;
+      });
+
+      const sizeStr = subpaths.map(subpath => {
+        return this.byteToSize(subpath.size, 1);
+      });
+      this.setState({
+        // Normalized path
+        isLoading: false,
+        showPanel: false,
+        inputPath: duResponse.path,
+        returnPath: duResponse.path,
+        duResponse,
+        plotData: [{
+          type: 'pie',
+          hole: 0.2,
+          values: percentage,
+          labels: pathLabels,
+          text: sizeStr,
+          textinfo: 'label+percent',
+          hovertemplate: 'Total Data Size: %{text}<extra></extra>'
+        }]
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false
+      });
+      showDataFetchError(error.toString());
+    });
+  };
+
+  componentDidMount(): void {
+    this.setState({
+      isLoading: true
+    });
+    // By default render the DU for root path
+    this.updatePieChart('/');
+  }
+
+  clickPieSection(e, curPath: string): void {
+    const subPath: string = e.points[0].label;
+    const path = (curPath === '/') ? `${curPath}${subPath}` : `${curPath}/${subPath}`;
+    if (path === OTHER_PATH_NAME) {
+      return;
+    }
+
+    this.updatePieChart(path);
+  }
+
+  // Show the right side panel that display metadata details of path
+  showMetadataDetails(e, path: string): void {
+    const summaryEndpoint = `/api/v1/namespace/summary?path=${path}`;
+    const keys = [];
+    const values = [];
+    axios.get(summaryEndpoint).then(response => {
+      const summaryResponse = response.data;
+      keys.push('Entity Type');
+      values.push(summaryResponse.type);
+
+      if (summaryResponse.type === 'KEY') {
+        const keyEndpoint = `/api/v1/namespace/du?path=${path}&replica=true`;
+        axios.get(keyEndpoint).then(response => {
+          keys.push('File Size');
+          values.push(this.byteToSize(response.data.size, 3));
+          keys.push('File Size With Replication');
+          values.push(this.byteToSize(response.data.sizeWithReplica, 3));
+          console.log(values);
+
+          this.setState({
+            showPanel: true,
+            panelKeys: keys,
+            panelValues: values
+          });
+        }).catch(error => {
+          this.setState({
+            isLoading: false,
+            showPanel: false
+          });
+          showDataFetchError(error.toString());
+        });
+        return;
+      }
+
+      if (summaryResponse.status !== 'OK') {
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      if (summaryResponse.numVolume !== -1) {
+        keys.push('Volumes');
+        values.push(summaryResponse.numVolume);
+      }
+
+      if (summaryResponse.numBucket !== -1) {
+        keys.push('Buckets');
+        values.push(summaryResponse.numBucket);
+      }
+
+      if (summaryResponse.numDir !== -1) {
+        keys.push('Total Directories');
+        values.push(summaryResponse.numDir);
+      }
+
+      if (summaryResponse.numKey !== -1) {
+        keys.push('Total Keys');
+        values.push(summaryResponse.numKey);
+      }
+
+      // Show the right drawer
+      this.setState({
+        showPanel: true,
+        panelKeys: keys,
+        panelValues: values
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false,
+        showPanel: false
+      });
+      showDataFetchError(error.toString());
+    });
+
+    const quotaEndpoint = `/api/v1/namespace/quota?path=${path}`;
+    axios.get(quotaEndpoint).then(response => {
+      const quotaResponse = response.data;
+
+      if (quotaResponse.status === 'PATH_NOT_FOUND') {
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      // If quota request not applicable for this path, silently return
+      if (quotaResponse.status === 'TYPE_NOT_APPLICABLE') {
+        return;
+      }
+
+      // Append quota information
+      // In case the object's quota isn't set
+      if (quotaResponse.allowed !== -1) {
+        keys.push('Quota Allowed');
+        values.push(this.byteToSize(quotaResponse.allowed, 3));
+      }
+
+      keys.push('Quota Used');
+      values.push(this.byteToSize(quotaResponse.used, 3));
+      this.setState({
+        showPanel: true,
+        panelKeys: keys,
+        panelValues: values
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false,
+        showPanel: false
+      });
+      showDataFetchError(error.toString());
+    });
+  }
+
+  render() {
+    const {plotData, duResponse, returnPath, panelKeys, panelValues, showPanel, isLoading, inputPath} = this.state;
+    return (
+      <div className='du-container'>
+        <div className='page-header'>
+          Disk Usage
+        </div>
+        <div className='content-div'>
+          {isLoading ? <span><Icon type='loading'/> Loading...</span> : (
+            <div>
+              <Row>
+                <Col>
+                  <div className='go-back-button'>
+                    <Button type='primary' onClick={e => this.goBack(e, returnPath)}><Icon type='left'/>Back</Button>
+                  </div>
+                  <div className='input-bar'>
+                    <h3>Path</h3>
+                    <form className='input' id='input-form' onSubmit={this.handleSubmit}>
+                      <Input placeholder='/' value={inputPath} onChange={this.handleChange}/>
+                    </form>
+                  </div>
+                  <div className='metadata-button'>
+                    <Button type='primary' onClick={e => this.showMetadataDetails(e, returnPath)}>
+                      <b>
+                        Show Metadata for Current Path
+                      </b>
+                    </Button>
+                  </div>
+                </Col>
+              </Row>
+              <Row>
+                {(duResponse.size > 0) ?
+                  ((duResponse.size > 0 && duResponse.subPathCount === 0) ?
+                    <div style={{height: 800}}>
+                      <br/> {' '}
+                      <h3>This object is a key with a file size of {this.byteToSize(duResponse.size, 1)}.<br/> {' '}
+                        You can also view its metadata details by clicking the top right button.
+                      </h3>
+                    </div> :
+                    <Plot
+                      data={plotData}
+                      layout={
+                        {
+                          width: 800,
+                          height: 750,
+                          font: {
+                            family: 'Arial',
+                            size: 18

Review comment:
       Try a smaller font size.
   ```suggestion
                               size: 14
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] smengcl commented on a change in pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
smengcl commented on a change in pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#discussion_r690044979



##########
File path: hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/diskUsage/diskUsage.tsx
##########
@@ -0,0 +1,387 @@
+/*
+ * 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 Plot from 'react-plotly.js';
+import {Row, Col, Icon, Button, Input} from 'antd';
+import {DetailPanel} from 'components/rightDrawer/rightDrawer';
+import * as Plotly from 'plotly.js';
+import {showDataFetchError} from 'utils/common';
+import './diskUsage.less';
+
+const DISPLAY_LIMIT = 20;
+const OTHER_PATH_NAME = 'Other Objects';
+
+interface IDUSubpath {
+  path: string;
+  size: number;
+  sizeWithReplica: number;
+  isKey: boolean;
+}
+
+interface IDUResponse {
+  status: string;
+  path: string;
+  subPathCount: number;
+  size: number;
+  sizeWithReplica: number;
+  subPaths: IDUSubpath[];
+  sizeDirectKey: number;
+}
+
+interface IDUState {
+  isLoading: boolean;
+  duResponse: IDUResponse[];
+  plotData: Plotly.Data[];
+  showPanel: boolean;
+  panelKeys: string[];
+  panelValues: string[];
+  returnPath: string;
+  inputPath: string;
+}
+
+export class DiskUsage extends React.Component<Record<string, object>, IDUState> {
+  constructor(props = {}) {
+    super(props);
+    this.state = {
+      isLoading: false,
+      duResponse: [],
+      plotData: [],
+      showPanel: false,
+      panelKeys: [],
+      panelValues: [],
+      returnPath: '/',
+      inputPath: '/'
+    };
+  }
+
+  byteToSize = (bytes, decimals) => {
+    if (bytes === 0) {
+      return '0 Bytes';
+    }
+
+    const k = 1024;
+    const dm = decimals < 0 ? 0 : decimals;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+    return `${Number.parseFloat((bytes / (k ** i)).toFixed(dm))} ${sizes[i]}`;
+  };
+
+  handleChange = e => {
+    this.setState({inputPath: e.target.value, showPanel: false});
+  };
+
+  handleSubmit = _e => {
+    // Avoid empty request trigger 400 response
+    if (!this.state.inputPath) {
+      this.updatePieChart('/');
+      return;
+    }
+
+    this.updatePieChart(this.state.inputPath);
+  };
+
+  // The returned path is passed in, which should have been
+  // normalized by the backend
+  goBack = (e, path) => {
+    if (!path || path === '/') {
+      return;
+    }
+
+    const arr = path.split('/');
+    let parentPath = arr.slice(0, -1).join('/');
+    if (parentPath.length === 0) {
+      parentPath = '/';
+    }
+
+    this.updatePieChart(parentPath);
+  };
+
+  // Take the request path, make a DU request, inject response
+  // into the pie chart
+  updatePieChart = (path: string) => {
+    this.setState({
+      isLoading: true
+    });
+    const duEndpoint = `/api/v1/namespace/du?path=${path}&files=true`;
+    axios.get(duEndpoint).then(response => {
+      const duResponse: IDUResponse[] = response.data;
+      const status = duResponse.status;
+      if (status !== 'OK') {
+        this.setState({isLoading: false});
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      const dataSize = duResponse.size;
+      let subpaths: IDUSubpath[] = duResponse.subPaths;
+
+      subpaths.sort((a, b) => (a.size < b.size) ? 1 : -1);
+
+      // Only show 20 blocks with the most DU,
+      // other blocks are merged as a single block
+      if (subpaths.length > DISPLAY_LIMIT) {
+        subpaths = subpaths.slice(0, DISPLAY_LIMIT);
+        let topSize = 0;
+        for (let i = 0; i < DISPLAY_LIMIT; ++i) {
+          topSize += subpaths[i].size;
+        }
+
+        const otherSize = dataSize - topSize;
+        const other: IDUSubpath = {path: OTHER_PATH_NAME, size: otherSize};
+        subpaths.push(other);
+      }
+
+      const pathLabels = subpaths.map(subpath => {
+        // The return subPath must be normalized in a format with
+        // a leading slash and without trailing slash
+        const pieces = subpath.path.split('/');
+        // Differentiate key without trailing slash
+        return (subpath.isKey) ? pieces[pieces.length - 1] : pieces[pieces.length - 1] + '/';
+      });
+
+      const percentage = subpaths.map(subpath => {
+        return subpath.size / dataSize;
+      });
+
+      const sizeStr = subpaths.map(subpath => {
+        return this.byteToSize(subpath.size, 1);
+      });
+      this.setState({
+        // Normalized path
+        isLoading: false,
+        showPanel: false,
+        inputPath: duResponse.path,
+        returnPath: duResponse.path,
+        duResponse,
+        plotData: [{
+          type: 'pie',
+          hole: 0.2,
+          values: percentage,
+          labels: pathLabels,
+          text: sizeStr,
+          textinfo: 'label+percent',
+          hovertemplate: 'Total Data Size: %{text}<extra></extra>'
+        }]
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false
+      });
+      showDataFetchError(error.toString());
+    });
+  };
+
+  componentDidMount(): void {
+    this.setState({
+      isLoading: true
+    });
+    // By default render the DU for root path
+    this.updatePieChart('/');
+  }
+
+  clickPieSection(e, curPath: string): void {
+    const subPath: string = e.points[0].label;
+    const path = (curPath === '/') ? `${curPath}${subPath}` : `${curPath}/${subPath}`;
+    if (path === OTHER_PATH_NAME) {
+      return;
+    }
+
+    this.updatePieChart(path);
+  }
+
+  // Show the right side panel that display metadata details of path
+  showMetadataDetails(e, path: string): void {
+    const summaryEndpoint = `/api/v1/namespace/summary?path=${path}`;
+    const keys = [];
+    const values = [];
+    axios.get(summaryEndpoint).then(response => {
+      const summaryResponse = response.data;
+      keys.push('Entity Type');
+      values.push(summaryResponse.type);
+
+      if (summaryResponse.type === 'KEY') {
+        const keyEndpoint = `/api/v1/namespace/du?path=${path}&replica=true`;
+        axios.get(keyEndpoint).then(response => {
+          keys.push('File Size');
+          values.push(this.byteToSize(response.data.size, 3));
+          keys.push('File Size With Replication');
+          values.push(this.byteToSize(response.data.sizeWithReplica, 3));
+          console.log(values);
+
+          this.setState({
+            showPanel: true,
+            panelKeys: keys,
+            panelValues: values
+          });
+        }).catch(error => {
+          this.setState({
+            isLoading: false,
+            showPanel: false
+          });
+          showDataFetchError(error.toString());
+        });
+        return;
+      }
+
+      if (summaryResponse.status !== 'OK') {
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      if (summaryResponse.numVolume !== -1) {
+        keys.push('Volumes');
+        values.push(summaryResponse.numVolume);
+      }
+
+      if (summaryResponse.numBucket !== -1) {
+        keys.push('Buckets');
+        values.push(summaryResponse.numBucket);
+      }
+
+      if (summaryResponse.numDir !== -1) {
+        keys.push('Total Directories');
+        values.push(summaryResponse.numDir);
+      }
+
+      if (summaryResponse.numKey !== -1) {
+        keys.push('Total Keys');
+        values.push(summaryResponse.numKey);
+      }
+
+      // Show the right drawer
+      this.setState({
+        showPanel: true,
+        panelKeys: keys,
+        panelValues: values
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false,
+        showPanel: false
+      });
+      showDataFetchError(error.toString());
+    });
+
+    const quotaEndpoint = `/api/v1/namespace/quota?path=${path}`;
+    axios.get(quotaEndpoint).then(response => {
+      const quotaResponse = response.data;
+
+      if (quotaResponse.status === 'PATH_NOT_FOUND') {
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      // If quota request not applicable for this path, silently return
+      if (quotaResponse.status === 'TYPE_NOT_APPLICABLE') {
+        return;
+      }
+
+      // Append quota information
+      // In case the object's quota isn't set
+      if (quotaResponse.allowed !== -1) {
+        keys.push('Quota Allowed');
+        values.push(this.byteToSize(quotaResponse.allowed, 3));
+      }
+
+      keys.push('Quota Used');
+      values.push(this.byteToSize(quotaResponse.used, 3));
+      this.setState({
+        showPanel: true,
+        panelKeys: keys,
+        panelValues: values
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false,
+        showPanel: false
+      });
+      showDataFetchError(error.toString());
+    });
+  }
+
+  render() {
+    const {plotData, duResponse, returnPath, panelKeys, panelValues, showPanel, isLoading, inputPath} = this.state;
+    return (
+      <div className='du-container'>
+        <div className='page-header'>
+          Disk Usage
+        </div>
+        <div className='content-div'>
+          {isLoading ? <span><Icon type='loading'/> Loading...</span> : (
+            <div>
+              <Row>
+                <Col>
+                  <div className='go-back-button'>
+                    <Button type='primary' onClick={e => this.goBack(e, returnPath)}><Icon type='left'/>Back</Button>
+                  </div>
+                  <div className='input-bar'>
+                    <h3>Path</h3>
+                    <form className='input' id='input-form' onSubmit={this.handleSubmit}>
+                      <Input placeholder='/' value={inputPath} onChange={this.handleChange}/>
+                    </form>
+                  </div>
+                  <div className='metadata-button'>
+                    <Button type='primary' onClick={e => this.showMetadataDetails(e, returnPath)}>
+                      <b>
+                        Show Metadata for Current Path
+                      </b>
+                    </Button>
+                  </div>
+                </Col>
+              </Row>
+              <Row>
+                {(duResponse.size > 0) ?
+                  ((duResponse.size > 0 && duResponse.subPathCount === 0) ?
+                    <div style={{height: 800}}>
+                      <br/> {' '}
+                      <h3>This object is a key with a file size of {this.byteToSize(duResponse.size, 1)}.<br/> {' '}
+                        You can also view its metadata details by clicking the top right button.
+                      </h3>
+                    </div> :
+                    <Plot
+                      data={plotData}
+                      layout={
+                        {
+                          width: 800,
+                          height: 750,
+                          font: {
+                            family: 'Arial',
+                            size: 18

Review comment:
       Try a smaller font size.
   ```suggestion
                               size: 14
   ```

##########
File path: hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/diskUsage/diskUsage.tsx
##########
@@ -0,0 +1,387 @@
+/*
+ * 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 Plot from 'react-plotly.js';
+import {Row, Col, Icon, Button, Input} from 'antd';
+import {DetailPanel} from 'components/rightDrawer/rightDrawer';
+import * as Plotly from 'plotly.js';
+import {showDataFetchError} from 'utils/common';
+import './diskUsage.less';
+
+const DISPLAY_LIMIT = 20;

Review comment:
       Let's set this to `10` by default here?
   ```suggestion
   const DISPLAY_LIMIT = 10;
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] smengcl commented on a change in pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
smengcl commented on a change in pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#discussion_r691514251



##########
File path: hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/diskUsage/diskUsage.tsx
##########
@@ -0,0 +1,439 @@
+/*
+ * 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 Plot from 'react-plotly.js';
+import {Row, Col, Icon, Button, Input, Menu, Dropdown} from 'antd';
+import {DetailPanel} from 'components/rightDrawer/rightDrawer';
+import * as Plotly from 'plotly.js';
+import {showDataFetchError} from 'utils/common';
+import './diskUsage.less';
+
+const DEFAULT_DISPLAY_LIMIT = 10;
+const OTHER_PATH_NAME = 'Other Objects';
+
+interface IDUSubpath {
+  path: string;
+  size: number;
+  sizeWithReplica: number;
+  isKey: boolean;
+}
+
+interface IDUResponse {
+  status: string;
+  path: string;
+  subPathCount: number;
+  size: number;
+  sizeWithReplica: number;
+  subPaths: IDUSubpath[];
+  sizeDirectKey: number;
+}
+
+interface IDUState {
+  isLoading: boolean;
+  duResponse: IDUResponse[];
+  plotData: Plotly.Data[];
+  showPanel: boolean;
+  panelKeys: string[];
+  panelValues: string[];
+  returnPath: string;
+  inputPath: string;
+  displayLimit: number;
+}
+
+export class DiskUsage extends React.Component<Record<string, object>, IDUState> {
+  constructor(props = {}) {
+    super(props);
+    this.state = {
+      isLoading: false,
+      duResponse: [],
+      plotData: [],
+      showPanel: false,
+      panelKeys: [],
+      panelValues: [],
+      returnPath: '/',
+      inputPath: '/',
+      displayLimit: DEFAULT_DISPLAY_LIMIT
+    };
+  }
+
+  byteToSize = (bytes, decimals) => {
+    if (bytes === 0) {
+      return '0 Bytes';
+    }
+
+    const k = 1024;
+    const dm = decimals < 0 ? 0 : decimals;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+    return `${Number.parseFloat((bytes / (k ** i)).toFixed(dm))} ${sizes[i]}`;
+  };
+
+  handleChange = e => {
+    this.setState({inputPath: e.target.value, showPanel: false});
+  };
+
+  handleSubmit = _e => {
+    // Avoid empty request trigger 400 response
+    if (!this.state.inputPath) {
+      this.updatePieChart('/', DEFAULT_DISPLAY_LIMIT);
+      return;
+    }
+
+    this.updatePieChart(this.state.inputPath, DEFAULT_DISPLAY_LIMIT);
+  };
+
+  // The returned path is passed in, which should have been
+  // normalized by the backend
+  goBack = (e, path) => {
+    if (!path || path === '/') {
+      return;
+    }
+
+    const arr = path.split('/');
+    let parentPath = arr.slice(0, -1).join('/');
+    if (parentPath.length === 0) {
+      parentPath = '/';
+    }
+
+    this.updatePieChart(parentPath, DEFAULT_DISPLAY_LIMIT);
+  };
+
+  // Take the request path, make a DU request, inject response
+  // into the pie chart
+  updatePieChart = (path: string, limit: number) => {
+    this.setState({
+      isLoading: true
+    });
+    const duEndpoint = `/api/v1/namespace/du?path=${path}&files=true`;
+    axios.get(duEndpoint).then(response => {
+      const duResponse: IDUResponse[] = response.data;
+      const status = duResponse.status;
+      if (status !== 'OK') {
+        this.setState({isLoading: false});
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      const dataSize = duResponse.size;
+      let subpaths: IDUSubpath[] = duResponse.subPaths;
+
+      subpaths.sort((a, b) => (a.size < b.size) ? 1 : -1);
+
+      // Only show top n blocks with the most DU,
+      // other blocks are merged as a single block
+      if (subpaths.length > limit) {
+        subpaths = subpaths.slice(0, limit);
+        let topSize = 0;
+        for (let i = 0; i < limit; ++i) {
+          topSize += subpaths[i].size;
+        }
+
+        const otherSize = dataSize - topSize;
+        const other: IDUSubpath = {path: OTHER_PATH_NAME, size: otherSize};
+        subpaths.push(other);
+      }
+
+      const pathLabels = subpaths.map(subpath => {
+        // The return subPath must be normalized in a format with
+        // a leading slash and without trailing slash
+        const pieces = subpath.path.split('/');
+        const subpathName = pieces[pieces.length - 1];
+        // Differentiate key without trailing slash
+        return (subpath.isKey || subpathName === OTHER_PATH_NAME) ? subpathName : subpathName + '/';
+      });
+
+      const percentage = subpaths.map(subpath => {
+        return subpath.size / dataSize;
+      });
+
+      const sizeStr = subpaths.map(subpath => {
+        return this.byteToSize(subpath.size, 1);
+      });
+      this.setState({
+        // Normalized path
+        isLoading: false,
+        showPanel: false,
+        inputPath: duResponse.path,
+        returnPath: duResponse.path,
+        displayLimit: limit,
+        duResponse,
+        plotData: [{
+          type: 'pie',
+          hole: 0.2,
+          values: percentage,
+          labels: pathLabels,
+          text: sizeStr,
+          textinfo: 'label+percent',
+          hovertemplate: 'Total Data Size: %{text}<extra></extra>'
+        }]
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false
+      });
+      showDataFetchError(error.toString());
+    });
+  };
+
+  componentDidMount(): void {
+    this.setState({
+      isLoading: true
+    });
+    // By default render the DU for root path
+    this.updatePieChart('/', DEFAULT_DISPLAY_LIMIT);
+  }
+
+  clickPieSection(e, curPath: string): void {
+    const subPath: string = e.points[0].label;
+    if (subPath === OTHER_PATH_NAME) {
+      return;
+    }
+
+    const path = (curPath === '/') ? `${curPath}${subPath}` : `${curPath}/${subPath}`;
+
+    // Reset to default everytime
+    this.updatePieChart(path, DEFAULT_DISPLAY_LIMIT);
+  }
+
+  refreshCurPath(e, path: string): void {
+    if (!path) {
+      return;
+    }
+
+    this.updatePieChart(path, this.state.displayLimit);
+  }
+
+  updateDisplayLimit(e): void {
+    let res = -1;
+    if (e.key === 'all') {
+      res = Number.MAX_VALUE;
+    } else {
+      res = Number.parseInt(e.key, 10);
+    }
+
+    this.updatePieChart(this.state.inputPath, res);
+  }
+
+  // Show the right side panel that display metadata details of path
+  showMetadataDetails(e, path: string): void {
+    const summaryEndpoint = `/api/v1/namespace/summary?path=${path}`;
+    const keys = [];
+    const values = [];
+    axios.get(summaryEndpoint).then(response => {
+      const summaryResponse = response.data;
+      keys.push('Entity Type');
+      values.push(summaryResponse.type);
+
+      if (summaryResponse.type === 'KEY') {
+        const keyEndpoint = `/api/v1/namespace/du?path=${path}&replica=true`;
+        axios.get(keyEndpoint).then(response => {
+          keys.push('File Size');
+          values.push(this.byteToSize(response.data.size, 3));
+          keys.push('File Size With Replication');
+          values.push(this.byteToSize(response.data.sizeWithReplica, 3));
+          console.log(values);
+
+          this.setState({
+            showPanel: true,
+            panelKeys: keys,
+            panelValues: values
+          });
+        }).catch(error => {
+          this.setState({
+            isLoading: false,
+            showPanel: false
+          });
+          showDataFetchError(error.toString());
+        });
+        return;
+      }
+
+      if (summaryResponse.status !== 'OK') {
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      if (summaryResponse.numVolume !== -1) {
+        keys.push('Volumes');
+        values.push(summaryResponse.numVolume);
+      }
+
+      if (summaryResponse.numBucket !== -1) {
+        keys.push('Buckets');
+        values.push(summaryResponse.numBucket);
+      }
+
+      if (summaryResponse.numDir !== -1) {
+        keys.push('Total Directories');
+        values.push(summaryResponse.numDir);
+      }
+
+      if (summaryResponse.numKey !== -1) {
+        keys.push('Total Keys');
+        values.push(summaryResponse.numKey);
+      }
+
+      // Show the right drawer
+      this.setState({
+        showPanel: true,
+        panelKeys: keys,
+        panelValues: values
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false,
+        showPanel: false
+      });
+      showDataFetchError(error.toString());
+    });
+
+    const quotaEndpoint = `/api/v1/namespace/quota?path=${path}`;
+    axios.get(quotaEndpoint).then(response => {
+      const quotaResponse = response.data;
+
+      if (quotaResponse.status === 'PATH_NOT_FOUND') {
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      // If quota request not applicable for this path, silently return
+      if (quotaResponse.status === 'TYPE_NOT_APPLICABLE') {
+        return;
+      }
+
+      // Append quota information
+      // In case the object's quota isn't set
+      if (quotaResponse.allowed !== -1) {
+        keys.push('Quota Allowed');
+        values.push(this.byteToSize(quotaResponse.allowed, 3));
+      }
+
+      keys.push('Quota Used');
+      values.push(this.byteToSize(quotaResponse.used, 3));
+      this.setState({
+        showPanel: true,
+        panelKeys: keys,
+        panelValues: values
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false,
+        showPanel: false
+      });
+      showDataFetchError(error.toString());
+    });
+  }
+
+  render() {
+    const {plotData, duResponse, returnPath, panelKeys, panelValues, showPanel, isLoading, inputPath, displayLimit} = this.state;
+    const menu = (
+      <Menu onClick={e => this.updateDisplayLimit(e)}>
+        <Menu.Item key='5'>
+          5
+        </Menu.Item>
+        <Menu.Item key='10'>
+          10
+        </Menu.Item>
+        <Menu.Item key='15'>
+          15
+        </Menu.Item>
+        <Menu.Item key='20'>
+          20
+        </Menu.Item>
+        <Menu.Item key='all'>
+          All
+        </Menu.Item>
+      </Menu>
+    );
+    return (
+      <div className='du-container'>
+        <div className='page-header'>
+          Disk Usage
+        </div>
+        <div className='content-div'>
+          {isLoading ? <span><Icon type='loading'/> Loading...</span> : (
+            <div>
+              <Row>
+                <Col>
+                  <div className='go-back-button'>
+                    <Button type='primary' onClick={e => this.goBack(e, returnPath)}><Icon type='left'/> </Button>
+                  </div>
+                  <div className='input-bar'>
+                    <h3>Path</h3>
+                    <form className='input' id='input-form' onSubmit={this.handleSubmit}>
+                      <Input placeholder='/' value={inputPath} onChange={this.handleChange}/>
+                    </form>
+                  </div>
+                  <div className='go-back-button'>
+                    <Button type='primary' onClick={e => this.refreshCurPath(e, returnPath)}><Icon type='redo'/> </Button>

Review comment:
       ```suggestion
                       <Button type='primary' onClick={e => this.refreshCurPath(e, returnPath)}><Icon type='redo'/></Button>
   ```

##########
File path: hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/diskUsage/diskUsage.tsx
##########
@@ -0,0 +1,439 @@
+/*
+ * 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 Plot from 'react-plotly.js';
+import {Row, Col, Icon, Button, Input, Menu, Dropdown} from 'antd';
+import {DetailPanel} from 'components/rightDrawer/rightDrawer';
+import * as Plotly from 'plotly.js';
+import {showDataFetchError} from 'utils/common';
+import './diskUsage.less';
+
+const DEFAULT_DISPLAY_LIMIT = 10;
+const OTHER_PATH_NAME = 'Other Objects';
+
+interface IDUSubpath {
+  path: string;
+  size: number;
+  sizeWithReplica: number;
+  isKey: boolean;
+}
+
+interface IDUResponse {
+  status: string;
+  path: string;
+  subPathCount: number;
+  size: number;
+  sizeWithReplica: number;
+  subPaths: IDUSubpath[];
+  sizeDirectKey: number;
+}
+
+interface IDUState {
+  isLoading: boolean;
+  duResponse: IDUResponse[];
+  plotData: Plotly.Data[];
+  showPanel: boolean;
+  panelKeys: string[];
+  panelValues: string[];
+  returnPath: string;
+  inputPath: string;
+  displayLimit: number;
+}
+
+export class DiskUsage extends React.Component<Record<string, object>, IDUState> {
+  constructor(props = {}) {
+    super(props);
+    this.state = {
+      isLoading: false,
+      duResponse: [],
+      plotData: [],
+      showPanel: false,
+      panelKeys: [],
+      panelValues: [],
+      returnPath: '/',
+      inputPath: '/',
+      displayLimit: DEFAULT_DISPLAY_LIMIT
+    };
+  }
+
+  byteToSize = (bytes, decimals) => {
+    if (bytes === 0) {
+      return '0 Bytes';
+    }
+
+    const k = 1024;
+    const dm = decimals < 0 ? 0 : decimals;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+    return `${Number.parseFloat((bytes / (k ** i)).toFixed(dm))} ${sizes[i]}`;
+  };
+
+  handleChange = e => {
+    this.setState({inputPath: e.target.value, showPanel: false});
+  };
+
+  handleSubmit = _e => {
+    // Avoid empty request trigger 400 response
+    if (!this.state.inputPath) {
+      this.updatePieChart('/', DEFAULT_DISPLAY_LIMIT);
+      return;
+    }
+
+    this.updatePieChart(this.state.inputPath, DEFAULT_DISPLAY_LIMIT);
+  };
+
+  // The returned path is passed in, which should have been
+  // normalized by the backend
+  goBack = (e, path) => {
+    if (!path || path === '/') {
+      return;
+    }
+
+    const arr = path.split('/');
+    let parentPath = arr.slice(0, -1).join('/');
+    if (parentPath.length === 0) {
+      parentPath = '/';
+    }
+
+    this.updatePieChart(parentPath, DEFAULT_DISPLAY_LIMIT);
+  };
+
+  // Take the request path, make a DU request, inject response
+  // into the pie chart
+  updatePieChart = (path: string, limit: number) => {
+    this.setState({
+      isLoading: true
+    });
+    const duEndpoint = `/api/v1/namespace/du?path=${path}&files=true`;
+    axios.get(duEndpoint).then(response => {
+      const duResponse: IDUResponse[] = response.data;
+      const status = duResponse.status;
+      if (status !== 'OK') {
+        this.setState({isLoading: false});
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      const dataSize = duResponse.size;
+      let subpaths: IDUSubpath[] = duResponse.subPaths;
+
+      subpaths.sort((a, b) => (a.size < b.size) ? 1 : -1);
+
+      // Only show top n blocks with the most DU,
+      // other blocks are merged as a single block
+      if (subpaths.length > limit) {
+        subpaths = subpaths.slice(0, limit);
+        let topSize = 0;
+        for (let i = 0; i < limit; ++i) {
+          topSize += subpaths[i].size;
+        }
+
+        const otherSize = dataSize - topSize;
+        const other: IDUSubpath = {path: OTHER_PATH_NAME, size: otherSize};
+        subpaths.push(other);
+      }
+
+      const pathLabels = subpaths.map(subpath => {
+        // The return subPath must be normalized in a format with
+        // a leading slash and without trailing slash
+        const pieces = subpath.path.split('/');
+        const subpathName = pieces[pieces.length - 1];
+        // Differentiate key without trailing slash
+        return (subpath.isKey || subpathName === OTHER_PATH_NAME) ? subpathName : subpathName + '/';
+      });
+
+      const percentage = subpaths.map(subpath => {
+        return subpath.size / dataSize;
+      });
+
+      const sizeStr = subpaths.map(subpath => {
+        return this.byteToSize(subpath.size, 1);
+      });
+      this.setState({
+        // Normalized path
+        isLoading: false,
+        showPanel: false,
+        inputPath: duResponse.path,
+        returnPath: duResponse.path,
+        displayLimit: limit,
+        duResponse,
+        plotData: [{
+          type: 'pie',
+          hole: 0.2,
+          values: percentage,
+          labels: pathLabels,
+          text: sizeStr,
+          textinfo: 'label+percent',
+          hovertemplate: 'Total Data Size: %{text}<extra></extra>'
+        }]
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false
+      });
+      showDataFetchError(error.toString());
+    });
+  };
+
+  componentDidMount(): void {
+    this.setState({
+      isLoading: true
+    });
+    // By default render the DU for root path
+    this.updatePieChart('/', DEFAULT_DISPLAY_LIMIT);
+  }
+
+  clickPieSection(e, curPath: string): void {
+    const subPath: string = e.points[0].label;
+    if (subPath === OTHER_PATH_NAME) {
+      return;
+    }
+
+    const path = (curPath === '/') ? `${curPath}${subPath}` : `${curPath}/${subPath}`;
+
+    // Reset to default everytime
+    this.updatePieChart(path, DEFAULT_DISPLAY_LIMIT);
+  }
+
+  refreshCurPath(e, path: string): void {
+    if (!path) {
+      return;
+    }
+
+    this.updatePieChart(path, this.state.displayLimit);
+  }
+
+  updateDisplayLimit(e): void {
+    let res = -1;
+    if (e.key === 'all') {
+      res = Number.MAX_VALUE;
+    } else {
+      res = Number.parseInt(e.key, 10);
+    }
+
+    this.updatePieChart(this.state.inputPath, res);
+  }
+
+  // Show the right side panel that display metadata details of path
+  showMetadataDetails(e, path: string): void {
+    const summaryEndpoint = `/api/v1/namespace/summary?path=${path}`;
+    const keys = [];
+    const values = [];
+    axios.get(summaryEndpoint).then(response => {
+      const summaryResponse = response.data;
+      keys.push('Entity Type');
+      values.push(summaryResponse.type);
+
+      if (summaryResponse.type === 'KEY') {
+        const keyEndpoint = `/api/v1/namespace/du?path=${path}&replica=true`;
+        axios.get(keyEndpoint).then(response => {
+          keys.push('File Size');
+          values.push(this.byteToSize(response.data.size, 3));
+          keys.push('File Size With Replication');
+          values.push(this.byteToSize(response.data.sizeWithReplica, 3));
+          console.log(values);
+
+          this.setState({
+            showPanel: true,
+            panelKeys: keys,
+            panelValues: values
+          });
+        }).catch(error => {
+          this.setState({
+            isLoading: false,
+            showPanel: false
+          });
+          showDataFetchError(error.toString());
+        });
+        return;
+      }
+
+      if (summaryResponse.status !== 'OK') {
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      if (summaryResponse.numVolume !== -1) {
+        keys.push('Volumes');
+        values.push(summaryResponse.numVolume);
+      }
+
+      if (summaryResponse.numBucket !== -1) {
+        keys.push('Buckets');
+        values.push(summaryResponse.numBucket);
+      }
+
+      if (summaryResponse.numDir !== -1) {
+        keys.push('Total Directories');
+        values.push(summaryResponse.numDir);
+      }
+
+      if (summaryResponse.numKey !== -1) {
+        keys.push('Total Keys');
+        values.push(summaryResponse.numKey);
+      }
+
+      // Show the right drawer
+      this.setState({
+        showPanel: true,
+        panelKeys: keys,
+        panelValues: values
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false,
+        showPanel: false
+      });
+      showDataFetchError(error.toString());
+    });
+
+    const quotaEndpoint = `/api/v1/namespace/quota?path=${path}`;
+    axios.get(quotaEndpoint).then(response => {
+      const quotaResponse = response.data;
+
+      if (quotaResponse.status === 'PATH_NOT_FOUND') {
+        showDataFetchError(`Invalid Path: ${path}`);
+        return;
+      }
+
+      // If quota request not applicable for this path, silently return
+      if (quotaResponse.status === 'TYPE_NOT_APPLICABLE') {
+        return;
+      }
+
+      // Append quota information
+      // In case the object's quota isn't set
+      if (quotaResponse.allowed !== -1) {
+        keys.push('Quota Allowed');
+        values.push(this.byteToSize(quotaResponse.allowed, 3));
+      }
+
+      keys.push('Quota Used');
+      values.push(this.byteToSize(quotaResponse.used, 3));
+      this.setState({
+        showPanel: true,
+        panelKeys: keys,
+        panelValues: values
+      });
+    }).catch(error => {
+      this.setState({
+        isLoading: false,
+        showPanel: false
+      });
+      showDataFetchError(error.toString());
+    });
+  }
+
+  render() {
+    const {plotData, duResponse, returnPath, panelKeys, panelValues, showPanel, isLoading, inputPath, displayLimit} = this.state;
+    const menu = (
+      <Menu onClick={e => this.updateDisplayLimit(e)}>
+        <Menu.Item key='5'>
+          5
+        </Menu.Item>
+        <Menu.Item key='10'>
+          10
+        </Menu.Item>
+        <Menu.Item key='15'>
+          15
+        </Menu.Item>
+        <Menu.Item key='20'>
+          20
+        </Menu.Item>
+        <Menu.Item key='all'>
+          All
+        </Menu.Item>
+      </Menu>
+    );
+    return (
+      <div className='du-container'>
+        <div className='page-header'>
+          Disk Usage
+        </div>
+        <div className='content-div'>
+          {isLoading ? <span><Icon type='loading'/> Loading...</span> : (
+            <div>
+              <Row>
+                <Col>
+                  <div className='go-back-button'>
+                    <Button type='primary' onClick={e => this.goBack(e, returnPath)}><Icon type='left'/> </Button>

Review comment:
       ```suggestion
                       <Button type='primary' onClick={e => this.goBack(e, returnPath)}><Icon type='left'/></Button>
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] vivekratnavel commented on pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
vivekratnavel commented on pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#issuecomment-899688874


   @yuangu002 Great job! Thanks for the demo video. Overall looks good to me. Just a couple of items to remember to improve the user experience:
   
   - Rename "< Back" button to just "<" or "< Previous" to avoid confusion with browser back button behavior and make it disabled at the first visit when there is no history.
   - Make the [display limit](https://github.com/apache/ozone/pull/2530/files#diff-4d9ddef7873bb3ee5f89fd7b50f68cc78eef723e37a764c16d6ef8c723ed2bcdR28) configurable in UI with a dropdown (5, 10, 15, 20, All). Then we should make the default be 10 since I think even 20 will be clunky.  
   
   Both of these items can be tracked separately.
    


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] yuangu002 commented on pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
yuangu002 commented on pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#issuecomment-900715134


   [Fixed]
   1. Add a dropdown list for configure display limit. Default value set to 10.
   (Every time a pie section is clicked, go-back button is clicked, or a new path entered, default display is reset)
   2. Make pie chart's title one line and all fonts 14 to avoid text overlapping.
   3. Add a refresh button to refresh the current path and the currently selected display limit.
   4. Add mock path "/clunky" to test display limit in frontend.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] yuangu002 commented on pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
yuangu002 commented on pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#issuecomment-900715134






-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] smengcl commented on pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
smengcl commented on pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#issuecomment-901611308


   Merged. Thanks @yuangu002 for the contribution. Thanks @vivekratnavel for reviewing this.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] smengcl commented on a change in pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
smengcl commented on a change in pull request #2530:
URL: https://github.com/apache/ozone/pull/2530#discussion_r690624893



##########
File path: hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/diskUsage/diskUsage.tsx
##########
@@ -0,0 +1,387 @@
+/*
+ * 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 Plot from 'react-plotly.js';
+import {Row, Col, Icon, Button, Input} from 'antd';
+import {DetailPanel} from 'components/rightDrawer/rightDrawer';
+import * as Plotly from 'plotly.js';
+import {showDataFetchError} from 'utils/common';
+import './diskUsage.less';
+
+const DISPLAY_LIMIT = 20;

Review comment:
       Let's set this to `10` by default here?
   ```suggestion
   const DISPLAY_LIMIT = 10;
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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


[GitHub] [ozone] smengcl merged pull request #2530: HDDS-5377. Add a Namespace Summary tab to Recon Web UI

Posted by GitBox <gi...@apache.org>.
smengcl merged pull request #2530:
URL: https://github.com/apache/ozone/pull/2530


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@ozone.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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