ivandika3 commented on code in PR #4755: URL: https://github.com/apache/ozone/pull/4755#discussion_r1223948676
########## hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/heatMap/heatMapConfiguration.tsx: ########## @@ -0,0 +1,149 @@ +/* + * 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, { Component } from 'react'; +import { AgChartsReact } from 'ag-charts-react'; + +export default class HeatMapConfiguration extends Component { + constructor(props: {} | Readonly<{}>) { + super(props); + const { data } = this.props; + + const colorRange1 = [ + '#ffff99', //yellow start 80% + '#ffff80', //75% + '#ffff66', //70% + '#ffff4d', //yellow 65% + '#ffd24d', //dark Mustered yellow start 65% + '#ffbf00', //dark Mustard yellow end 50% + '#b38600', //Dark Mustard yellow 35% + '#ffb366', //orange start 70% + '#ff9933', //orange 60% + '#ff8c1a', //55% + '#e67300', //45% + '#994d00', //orange end 30% + '#ff6633', //Red start 60% + '#ff4000', // Red 50% + '#cc3300', //40% + '#992600', //30% + '#802000', //25% + '#661a00', //20% + '#4d1300', // 15% + '#330000', //dark Maroon + '#330d00', //10 % Last Red + ]; + + this.state = { + // Tree Map Options Start + options: { + data, + series: [ + { + type: 'treemap', + labelKey: 'label',// the name of the key to fetch the label value from + sizeKey: 'normalizedSize',// the name of the key to fetch the value that will determine tile size + colorkey: 'color', + fontSize: 35, + title: { color: 'white', fontSize: 18, fontFamily:'Courier New' }, + subtitle: { color: 'white', fontSize: 15, fontFamily:'Courier New' }, + tooltip: { + renderer: (params) => { + return { + content: this.tooltipContent(params) + }; + } + }, + formatter: ({ highlighted}) => { + const stroke = highlighted ? 'yellow' : 'white'; + return { stroke }; + }, + labels: { + color: 'white', + fontWeight: 'bold', + fontSize: 12 + }, + tileStroke: 'white', + tileStrokeWidth: 1, + colorDomain: [0.000, 0.050, 0.100, 0.150, 0.200, 0.250, 0.300, 0.350, 0.400, 0.450, 0.500, 0.550, 0.600, 0.650, 0.700, 0.750, 0.800, 0.850, 0.900, 0.950, 1.000], + colorRange: [...colorRange1], + groupFill: 'black', + nodePadding: 1, //Disatnce between two nodes + labelShadow: { enabled: false }, //labels shadow + highlightStyle: { + text: { + color: 'yellow', + }, + item:{ + fill: undefined, + }, + }, + listeners: { + nodeClick: (event) => { + var data = event.datum; + if (data.path) { + this.props.onClick(data.path); + } + }, + }, + }], + title: { text: 'Volumes and Buckets'}, + } + // Tree Map Options End + } + }; + + byteToSize = (bytes: number, decimals: number) => { + 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 isNaN(i) ? `Not Defined`:`${Number.parseFloat((bytes / (k ** i)).toFixed(dm))} ${sizes[i]}`; + }; Review Comment: This is almost duplicated with method with the same name in `diskUsage.tsx`. Can we refactor this and export from `common.tsx` instead? ########## hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/heatMap/heatMap.tsx: ########## @@ -0,0 +1,342 @@ +/* + * 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 { Row, Icon, Button, Input, Dropdown, Menu, DatePicker } from 'antd'; +import {DownOutlined} from '@ant-design/icons'; +import moment from 'moment'; +import {showDataFetchError} from 'utils/common'; +import './heatmap.less'; +import HeatMapConfiguration from './heatMapConfiguration'; + +interface ITreeResponse { + label: string; + maxAccessCount: number; + minAccessCount: number; + size: number; + children: Ichildren[]; +} + +interface Ichildren { Review Comment: Nit: `IChildren` ########## hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/heatMap/heatMap.tsx: ########## @@ -0,0 +1,342 @@ +/* + * 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 { Row, Icon, Button, Input, Dropdown, Menu, DatePicker } from 'antd'; +import {DownOutlined} from '@ant-design/icons'; +import moment from 'moment'; +import {showDataFetchError} from 'utils/common'; +import './heatmap.less'; +import HeatMapConfiguration from './heatMapConfiguration'; + +interface ITreeResponse { + label: string; + maxAccessCount: number; + minAccessCount: number; + size: number; + children: Ichildren[]; +} + +interface Ichildren { + label: string; + path: string; + size: number; + accessCount: number; + color: number; + children: Ichildren[]; +} + +interface ITreeState { + isLoading: boolean; + treeResponse: ITreeResponse[]; + showPanel: boolean; + inputRadio: number; + inputPath: string; + entityType: string; + date: string; +} + +let minSize = Infinity; +let maxSize = 0; + +export class HeatMap extends React.Component<Record<string, object>, ITreeState> { + constructor(props = {}) { + super(props); + this.state = { + isLoading: false, + treeResponse: [], + showPanel: false, + entityType: 'key', + inputPath: '/', + date: '24H' + }; + } + + handleCalenderChange = (e: any) => { + if (e.key === '24H' || e.key === '7D' || e.key === '90D') { + this.setState((prevState, newState) => ({ + date: e.key, + inputPath: prevState.inputPath, + entityType: prevState.entityType + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType, this.state.date) + }); + } + }; + + handleMenuChange = (e: any) => { + if (e.key === 'volume' || e.key === 'bucket' || e.key === 'key') + { + minSize=Infinity; + maxSize = 0; + this.setState((prevState, newState) => ({ + entityType: e.key, + date: prevState.date, + inputPath : prevState.inputPath + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + } + }; + + handleChange = (e: any) => { + const value = e.target.value; + let validExpression; + // Only allow letters, numbers,underscores and forward slashes + const regex = /^[a-zA-Z0-9_/]*$/; + if (regex.test(value)) { + validExpression = value; + } + else { + alert("Please Enter Valid Input Path."); + validExpression = '/'; + } + this.setState({ + inputPath: validExpression + }) + }; + + handleSubmit = (_e: any) => { + // Avoid empty request trigger 400 response + this.updateTreeMap(this.state.inputPath, this.state.entityType, this.state.date); + }; + + updateTreeMap = (path: string, entityType:string, date:string) => { + this.setState({ + isLoading: true + }); + + const treeEndpoint = `/api/v1/heatmap/readaccess?startDate=${date}&path=${path}&entityType=${entityType}`; + axios.get(treeEndpoint).then(response => { + minSize = this.minmax(response.data)[0]; + maxSize = this.minmax(response.data)[1]; + let treeResponse: ITreeResponse = this.updateSize(response.data); + this.setState({ + isLoading: false, + showPanel: false, + treeResponse + }); + }).catch(error => { + this.setState({ + isLoading: false, + inputPath: '', + entityType: '', + date:'' + }); + showDataFetchError(error.toString()); + }); + }; + + updateTreemapParent = (path: any) => { + this.setState({ + isLoading: true, + inputPath: path + },() => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + }; + + componentDidMount(): void { + this.setState({ + isLoading: true + }); + // By default render treemap for default path entity type and date + this.updateTreeMap('/',this.state.entityType,this.state.date); + } + + onChange = (date: any[]) => { + + this.setState(prevState => ({ + date: moment(date).unix(), + entityType: prevState.entityType, + inputPath : prevState.inputPath + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + } + + disabledDate(current: any) { + return current > moment() || current < moment().subtract(90, 'day'); + } + + resetInputpath = (e:any, path:string) => { + if (!path || path === '/') { + return; + } + else { + this.setState({ + inputPath: '/' + }) + } + }; + + minmax=(obj :any) => + { + if (obj.hasOwnProperty('children')) { + obj.children.forEach((child: any) => this.minmax(child)) + }; + if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) { + minSize = Math.min(minSize, obj.size); + maxSize = Math.max(maxSize, obj.size); + } + return [minSize, maxSize]; + }; + + updateSize = (obj: any) => { + //Normalize Size so other blocks also get visualized if size is large in bytes minimize and if size is too small make it big + if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) { + let newSize = this.normalize(minSize, maxSize, obj.size); + obj['normalizedSize'] = newSize; + } + if (obj.hasOwnProperty('children')) { + obj.children.forEach((child: any) => this.updateSize(child)); + } + return obj; + }; + + normalize = (min: number, max: number, size: number) => { + //Normaized Size using Deviation and mid Point + var mean = (max + min) / 2; + var highMean = (max+mean)/2; + var lowMean1 = (min+mean)/2; + var lowMean2 = (lowMean1 + min) / 2; + + if(size>highMean){ + var newsize= highMean+(size*0.1); + return(newsize); + } + // lowmean2= 100 value=10, diff= + // min= 10 ,max=100, mean=55, lowmean1=32.5,lowmean2=22, value= 15, diff=7, diff/2=3.5, newsize= 22-3.5=18.5 + if(size<lowMean2){ + var diff = (lowMean2-size)/2; + var newSize= lowMean2-diff; + return(newSize); + } + return size; + } + + render() { + const { treeResponse, isLoading, inputPath, date} = this.state; + const menuCalender = ( Review Comment: Nit: `menuCalendar` ########## hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/heatMap/heatMap.tsx: ########## @@ -0,0 +1,342 @@ +/* + * 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 { Row, Icon, Button, Input, Dropdown, Menu, DatePicker } from 'antd'; +import {DownOutlined} from '@ant-design/icons'; +import moment from 'moment'; +import {showDataFetchError} from 'utils/common'; +import './heatmap.less'; +import HeatMapConfiguration from './heatMapConfiguration'; + +interface ITreeResponse { + label: string; + maxAccessCount: number; + minAccessCount: number; + size: number; + children: Ichildren[]; +} + +interface Ichildren { + label: string; + path: string; + size: number; + accessCount: number; + color: number; + children: Ichildren[]; +} + +interface ITreeState { + isLoading: boolean; + treeResponse: ITreeResponse[]; + showPanel: boolean; + inputRadio: number; + inputPath: string; + entityType: string; + date: string; +} + +let minSize = Infinity; +let maxSize = 0; + +export class HeatMap extends React.Component<Record<string, object>, ITreeState> { + constructor(props = {}) { + super(props); + this.state = { + isLoading: false, + treeResponse: [], + showPanel: false, + entityType: 'key', + inputPath: '/', + date: '24H' + }; + } + + handleCalenderChange = (e: any) => { + if (e.key === '24H' || e.key === '7D' || e.key === '90D') { + this.setState((prevState, newState) => ({ + date: e.key, + inputPath: prevState.inputPath, + entityType: prevState.entityType + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType, this.state.date) + }); + } + }; + + handleMenuChange = (e: any) => { + if (e.key === 'volume' || e.key === 'bucket' || e.key === 'key') + { + minSize=Infinity; + maxSize = 0; + this.setState((prevState, newState) => ({ + entityType: e.key, + date: prevState.date, + inputPath : prevState.inputPath + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + } + }; + + handleChange = (e: any) => { + const value = e.target.value; + let validExpression; + // Only allow letters, numbers,underscores and forward slashes + const regex = /^[a-zA-Z0-9_/]*$/; + if (regex.test(value)) { + validExpression = value; + } + else { + alert("Please Enter Valid Input Path."); + validExpression = '/'; + } + this.setState({ + inputPath: validExpression + }) + }; + + handleSubmit = (_e: any) => { + // Avoid empty request trigger 400 response + this.updateTreeMap(this.state.inputPath, this.state.entityType, this.state.date); + }; + + updateTreeMap = (path: string, entityType:string, date:string) => { + this.setState({ + isLoading: true + }); + + const treeEndpoint = `/api/v1/heatmap/readaccess?startDate=${date}&path=${path}&entityType=${entityType}`; + axios.get(treeEndpoint).then(response => { + minSize = this.minmax(response.data)[0]; + maxSize = this.minmax(response.data)[1]; + let treeResponse: ITreeResponse = this.updateSize(response.data); + this.setState({ + isLoading: false, + showPanel: false, + treeResponse + }); + }).catch(error => { + this.setState({ + isLoading: false, + inputPath: '', + entityType: '', + date:'' + }); + showDataFetchError(error.toString()); + }); + }; + + updateTreemapParent = (path: any) => { + this.setState({ + isLoading: true, + inputPath: path + },() => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + }; + + componentDidMount(): void { + this.setState({ + isLoading: true + }); + // By default render treemap for default path entity type and date + this.updateTreeMap('/',this.state.entityType,this.state.date); + } + + onChange = (date: any[]) => { + + this.setState(prevState => ({ + date: moment(date).unix(), + entityType: prevState.entityType, + inputPath : prevState.inputPath + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + } + + disabledDate(current: any) { + return current > moment() || current < moment().subtract(90, 'day'); + } + + resetInputpath = (e:any, path:string) => { + if (!path || path === '/') { + return; + } + else { + this.setState({ + inputPath: '/' + }) + } + }; + + minmax=(obj :any) => + { + if (obj.hasOwnProperty('children')) { + obj.children.forEach((child: any) => this.minmax(child)) + }; + if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) { + minSize = Math.min(minSize, obj.size); + maxSize = Math.max(maxSize, obj.size); + } + return [minSize, maxSize]; + }; + + updateSize = (obj: any) => { + //Normalize Size so other blocks also get visualized if size is large in bytes minimize and if size is too small make it big + if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) { + let newSize = this.normalize(minSize, maxSize, obj.size); + obj['normalizedSize'] = newSize; + } + if (obj.hasOwnProperty('children')) { + obj.children.forEach((child: any) => this.updateSize(child)); + } + return obj; + }; + + normalize = (min: number, max: number, size: number) => { + //Normaized Size using Deviation and mid Point + var mean = (max + min) / 2; + var highMean = (max+mean)/2; + var lowMean1 = (min+mean)/2; + var lowMean2 = (lowMean1 + min) / 2; + + if(size>highMean){ + var newsize= highMean+(size*0.1); + return(newsize); + } + // lowmean2= 100 value=10, diff= + // min= 10 ,max=100, mean=55, lowmean1=32.5,lowmean2=22, value= 15, diff=7, diff/2=3.5, newsize= 22-3.5=18.5 + if(size<lowMean2){ + var diff = (lowMean2-size)/2; + var newSize= lowMean2-diff; + return(newSize); + } + return size; + } + + render() { + const { treeResponse, isLoading, inputPath, date} = this.state; + const menuCalender = ( + <Menu + defaultSelectedKeys={[date]} + onClick={e => this.handleCalenderChange(e)}> + <Menu.Item key='24H'> + 24 Hour + </Menu.Item> + <Menu.Item key='7D'> + 7 Days + </Menu.Item> + <Menu.Item key='90D'> + 90 Days + </Menu.Item> + <Menu.SubMenu title="Custom Select Last 90 Days"> + <Menu.Item> + <DatePicker + format="YYYY-MM-DD" + onChange={this.onChange} + disabledDate={this.disabledDate} + /> + </Menu.Item> + </Menu.SubMenu> + </Menu> + ); + + const entityTypeMenu = ( + <Menu + defaultSelectedKeys={[this.state.entityType]} + onClick={e => this.handleMenuChange(e)}> + <Menu.Item key='volume'> + Volume + </Menu.Item> + <Menu.Item key='bucket'> + Bucket + </Menu.Item> + <Menu.Item key='key'> + Key + </Menu.Item> + </Menu> + ); + + return ( + <div className='heatmap-container'> + <div className='page-header'> + Tree Map for Entities + </div> + <div className='content-div'> + { isLoading ? <span><Icon type='loading'/> Loading...</span> : ( + <div> + {(Object.keys(treeResponse).length > 0 && (treeResponse.minAccessCount > 0 || treeResponse.maxAccessCount > 0)) ? + (treeResponse.size === 0)? <div style={{ height: 800 }} className='heatmapinformation'><br />Failed to Load Heatmap.{' '}<br/></div> + : + <div> + <Row> + <div className='go-back-button'> + <Button type='primary' onClick={e => this.resetInputpath(e, inputPath)}><Icon type='undo'/></Button> + </div> + <div className='input-bar'> + <h4>Path</h4> + <form className='input' autoComplete="off" id='input-form' onSubmit={this.handleSubmit}> + <Input placeholder='/' name="inputPath" value={inputPath} onChange={this.handleChange} /> + </form> + </div> + <div className='entity-dropdown-button'> + <Dropdown overlay={entityTypeMenu} placement='bottomCenter'> + <Button>Entity Type: {this.state.entityType }<DownOutlined/></Button> + </Dropdown> + </div> + <div className='date-dropdown-button'> + <Dropdown overlay={menuCalender} placement='bottomLeft'> + <Button>Last {date > 100 ? new Date(date*1000).toLocaleString() : date }<DownOutlined/></Button> + </Dropdown> + </div> + </Row> + <br/><br/><br/><br/><br/> + <div style={{display:"flex", alignItems: "right"}}> + <div style={{ display: "flex", alignItems: "center",marginLeft:"30px"}}> + <div style={{ width: "13px", height: "13px", backgroundColor: "yellow", marginRight: "5px" }}> </div> + <span>Less Accessed</span> + </div> + <div style={{ display: "flex", alignItems: "center",marginLeft:"50px" }}> + <div style={{ width: "13px", height: "13px", backgroundColor: "orange", marginRight: "5px" }}> </div> + <span>Moderate Accessed</span> + </div> + <div style={{ display: "flex", alignItems: "center",marginLeft:"50px" }}> + <div style={{ width: "13px", height: "13px", backgroundColor: "maroon", marginRight: "5px" }}> </div> + <span>Most Accessed</span> + </div> + </div> + <div style={{ height:750}}> + <HeatMapConfiguration data={treeResponse} onClick={this.updateTreemapParent}></HeatMapConfiguration> + </div> Review Comment: Let's move the CSS styles to `heatmap.less` if possible ########## hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/heatMap/heatMap.tsx: ########## @@ -0,0 +1,342 @@ +/* + * 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 { Row, Icon, Button, Input, Dropdown, Menu, DatePicker } from 'antd'; +import {DownOutlined} from '@ant-design/icons'; +import moment from 'moment'; +import {showDataFetchError} from 'utils/common'; +import './heatmap.less'; +import HeatMapConfiguration from './heatMapConfiguration'; + +interface ITreeResponse { + label: string; + maxAccessCount: number; + minAccessCount: number; + size: number; + children: Ichildren[]; +} + +interface Ichildren { + label: string; + path: string; + size: number; + accessCount: number; + color: number; + children: Ichildren[]; +} + +interface ITreeState { + isLoading: boolean; + treeResponse: ITreeResponse[]; + showPanel: boolean; + inputRadio: number; + inputPath: string; + entityType: string; + date: string; +} + +let minSize = Infinity; +let maxSize = 0; + +export class HeatMap extends React.Component<Record<string, object>, ITreeState> { + constructor(props = {}) { + super(props); + this.state = { + isLoading: false, + treeResponse: [], + showPanel: false, + entityType: 'key', + inputPath: '/', + date: '24H' + }; + } + + handleCalenderChange = (e: any) => { + if (e.key === '24H' || e.key === '7D' || e.key === '90D') { + this.setState((prevState, newState) => ({ + date: e.key, + inputPath: prevState.inputPath, + entityType: prevState.entityType + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType, this.state.date) + }); + } + }; + + handleMenuChange = (e: any) => { + if (e.key === 'volume' || e.key === 'bucket' || e.key === 'key') + { + minSize=Infinity; + maxSize = 0; + this.setState((prevState, newState) => ({ + entityType: e.key, + date: prevState.date, + inputPath : prevState.inputPath + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + } + }; + + handleChange = (e: any) => { + const value = e.target.value; + let validExpression; + // Only allow letters, numbers,underscores and forward slashes + const regex = /^[a-zA-Z0-9_/]*$/; + if (regex.test(value)) { + validExpression = value; + } + else { + alert("Please Enter Valid Input Path."); + validExpression = '/'; + } + this.setState({ + inputPath: validExpression + }) + }; + + handleSubmit = (_e: any) => { + // Avoid empty request trigger 400 response + this.updateTreeMap(this.state.inputPath, this.state.entityType, this.state.date); + }; + + updateTreeMap = (path: string, entityType:string, date:string) => { + this.setState({ + isLoading: true + }); + + const treeEndpoint = `/api/v1/heatmap/readaccess?startDate=${date}&path=${path}&entityType=${entityType}`; + axios.get(treeEndpoint).then(response => { + minSize = this.minmax(response.data)[0]; + maxSize = this.minmax(response.data)[1]; + let treeResponse: ITreeResponse = this.updateSize(response.data); + this.setState({ + isLoading: false, + showPanel: false, + treeResponse + }); + }).catch(error => { + this.setState({ + isLoading: false, + inputPath: '', + entityType: '', + date:'' + }); + showDataFetchError(error.toString()); + }); + }; + + updateTreemapParent = (path: any) => { + this.setState({ + isLoading: true, + inputPath: path + },() => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + }; + + componentDidMount(): void { + this.setState({ + isLoading: true + }); + // By default render treemap for default path entity type and date + this.updateTreeMap('/',this.state.entityType,this.state.date); + } + + onChange = (date: any[]) => { + + this.setState(prevState => ({ + date: moment(date).unix(), + entityType: prevState.entityType, + inputPath : prevState.inputPath + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + } + + disabledDate(current: any) { + return current > moment() || current < moment().subtract(90, 'day'); + } + + resetInputpath = (e:any, path:string) => { + if (!path || path === '/') { + return; + } + else { + this.setState({ + inputPath: '/' + }) + } + }; + + minmax=(obj :any) => + { + if (obj.hasOwnProperty('children')) { + obj.children.forEach((child: any) => this.minmax(child)) + }; + if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) { + minSize = Math.min(minSize, obj.size); + maxSize = Math.max(maxSize, obj.size); + } + return [minSize, maxSize]; + }; + + updateSize = (obj: any) => { + //Normalize Size so other blocks also get visualized if size is large in bytes minimize and if size is too small make it big + if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) { + let newSize = this.normalize(minSize, maxSize, obj.size); + obj['normalizedSize'] = newSize; + } + if (obj.hasOwnProperty('children')) { + obj.children.forEach((child: any) => this.updateSize(child)); + } + return obj; + }; + + normalize = (min: number, max: number, size: number) => { + //Normaized Size using Deviation and mid Point + var mean = (max + min) / 2; + var highMean = (max+mean)/2; + var lowMean1 = (min+mean)/2; + var lowMean2 = (lowMean1 + min) / 2; + + if(size>highMean){ + var newsize= highMean+(size*0.1); + return(newsize); + } + // lowmean2= 100 value=10, diff= + // min= 10 ,max=100, mean=55, lowmean1=32.5,lowmean2=22, value= 15, diff=7, diff/2=3.5, newsize= 22-3.5=18.5 + if(size<lowMean2){ + var diff = (lowMean2-size)/2; + var newSize= lowMean2-diff; + return(newSize); + } + return size; + } + + render() { + const { treeResponse, isLoading, inputPath, date} = this.state; + const menuCalender = ( + <Menu + defaultSelectedKeys={[date]} + onClick={e => this.handleCalenderChange(e)}> + <Menu.Item key='24H'> + 24 Hour + </Menu.Item> + <Menu.Item key='7D'> + 7 Days + </Menu.Item> + <Menu.Item key='90D'> + 90 Days + </Menu.Item> + <Menu.SubMenu title="Custom Select Last 90 Days"> + <Menu.Item> + <DatePicker + format="YYYY-MM-DD" + onChange={this.onChange} + disabledDate={this.disabledDate} + /> + </Menu.Item> + </Menu.SubMenu> + </Menu> + ); + + const entityTypeMenu = ( + <Menu + defaultSelectedKeys={[this.state.entityType]} + onClick={e => this.handleMenuChange(e)}> + <Menu.Item key='volume'> + Volume + </Menu.Item> + <Menu.Item key='bucket'> + Bucket + </Menu.Item> + <Menu.Item key='key'> + Key + </Menu.Item> + </Menu> + ); + + return ( + <div className='heatmap-container'> + <div className='page-header'> + Tree Map for Entities + </div> + <div className='content-div'> + { isLoading ? <span><Icon type='loading'/> Loading...</span> : ( + <div> + {(Object.keys(treeResponse).length > 0 && (treeResponse.minAccessCount > 0 || treeResponse.maxAccessCount > 0)) ? + (treeResponse.size === 0)? <div style={{ height: 800 }} className='heatmapinformation'><br />Failed to Load Heatmap.{' '}<br/></div> Review Comment: Shall we move the `style={{ height: 800}}` to the `heatmapinformation` class in the `heatmap.less`? ########## hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/heatMap/heatMapConfiguration.tsx: ########## @@ -0,0 +1,149 @@ +/* + * 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, { Component } from 'react'; +import { AgChartsReact } from 'ag-charts-react'; + +export default class HeatMapConfiguration extends Component { + constructor(props: {} | Readonly<{}>) { + super(props); + const { data } = this.props; + + const colorRange1 = [ + '#ffff99', //yellow start 80% + '#ffff80', //75% + '#ffff66', //70% + '#ffff4d', //yellow 65% + '#ffd24d', //dark Mustered yellow start 65% + '#ffbf00', //dark Mustard yellow end 50% + '#b38600', //Dark Mustard yellow 35% + '#ffb366', //orange start 70% + '#ff9933', //orange 60% + '#ff8c1a', //55% + '#e67300', //45% + '#994d00', //orange end 30% + '#ff6633', //Red start 60% + '#ff4000', // Red 50% + '#cc3300', //40% + '#992600', //30% + '#802000', //25% + '#661a00', //20% + '#4d1300', // 15% + '#330000', //dark Maroon + '#330d00', //10 % Last Red + ]; + + this.state = { + // Tree Map Options Start + options: { + data, + series: [ + { + type: 'treemap', + labelKey: 'label',// the name of the key to fetch the label value from + sizeKey: 'normalizedSize',// the name of the key to fetch the value that will determine tile size + colorkey: 'color', + fontSize: 35, + title: { color: 'white', fontSize: 18, fontFamily:'Courier New' }, + subtitle: { color: 'white', fontSize: 15, fontFamily:'Courier New' }, + tooltip: { + renderer: (params) => { + return { + content: this.tooltipContent(params) + }; + } + }, + formatter: ({ highlighted}) => { + const stroke = highlighted ? 'yellow' : 'white'; + return { stroke }; + }, Review Comment: <img width="262" alt="image" src="https://github.com/apache/ozone/assets/36403683/6264611f-7e52-4bdc-8ef3-21d712fe403e"> The bottom of the title and subtitles are truncated since the borders are also white. It also doesn't demarcate well with the white background. Maybe we can modify some of the colorings and distance between the border and the text? I think the example in the website https://www.ag-grid.com/react-charts/gallery/market-index-treemap/ looks fine. ########## hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/heatMap/heatMap.tsx: ########## @@ -0,0 +1,342 @@ +/* + * 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 { Row, Icon, Button, Input, Dropdown, Menu, DatePicker } from 'antd'; +import {DownOutlined} from '@ant-design/icons'; +import moment from 'moment'; +import {showDataFetchError} from 'utils/common'; +import './heatmap.less'; +import HeatMapConfiguration from './heatMapConfiguration'; + +interface ITreeResponse { + label: string; + maxAccessCount: number; + minAccessCount: number; + size: number; + children: Ichildren[]; +} + +interface Ichildren { + label: string; + path: string; + size: number; + accessCount: number; + color: number; + children: Ichildren[]; +} + +interface ITreeState { + isLoading: boolean; + treeResponse: ITreeResponse[]; + showPanel: boolean; + inputRadio: number; + inputPath: string; + entityType: string; + date: string; +} + +let minSize = Infinity; +let maxSize = 0; + +export class HeatMap extends React.Component<Record<string, object>, ITreeState> { + constructor(props = {}) { + super(props); + this.state = { + isLoading: false, + treeResponse: [], + showPanel: false, + entityType: 'key', + inputPath: '/', + date: '24H' + }; + } + + handleCalenderChange = (e: any) => { Review Comment: Nit: `handleCalendarChange` ########## hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/views/heatMap/heatMap.tsx: ########## @@ -0,0 +1,342 @@ +/* + * 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 { Row, Icon, Button, Input, Dropdown, Menu, DatePicker } from 'antd'; +import {DownOutlined} from '@ant-design/icons'; +import moment from 'moment'; +import {showDataFetchError} from 'utils/common'; +import './heatmap.less'; +import HeatMapConfiguration from './heatMapConfiguration'; + +interface ITreeResponse { + label: string; + maxAccessCount: number; + minAccessCount: number; + size: number; + children: Ichildren[]; +} + +interface Ichildren { + label: string; + path: string; + size: number; + accessCount: number; + color: number; + children: Ichildren[]; +} + +interface ITreeState { + isLoading: boolean; + treeResponse: ITreeResponse[]; + showPanel: boolean; + inputRadio: number; + inputPath: string; + entityType: string; + date: string; +} + +let minSize = Infinity; +let maxSize = 0; + +export class HeatMap extends React.Component<Record<string, object>, ITreeState> { + constructor(props = {}) { + super(props); + this.state = { + isLoading: false, + treeResponse: [], + showPanel: false, + entityType: 'key', + inputPath: '/', + date: '24H' + }; + } + + handleCalenderChange = (e: any) => { + if (e.key === '24H' || e.key === '7D' || e.key === '90D') { + this.setState((prevState, newState) => ({ + date: e.key, + inputPath: prevState.inputPath, + entityType: prevState.entityType + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType, this.state.date) + }); + } + }; + + handleMenuChange = (e: any) => { + if (e.key === 'volume' || e.key === 'bucket' || e.key === 'key') + { + minSize=Infinity; + maxSize = 0; + this.setState((prevState, newState) => ({ + entityType: e.key, + date: prevState.date, + inputPath : prevState.inputPath + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + } + }; + + handleChange = (e: any) => { + const value = e.target.value; + let validExpression; + // Only allow letters, numbers,underscores and forward slashes + const regex = /^[a-zA-Z0-9_/]*$/; + if (regex.test(value)) { + validExpression = value; + } + else { + alert("Please Enter Valid Input Path."); + validExpression = '/'; + } + this.setState({ + inputPath: validExpression + }) + }; + + handleSubmit = (_e: any) => { + // Avoid empty request trigger 400 response + this.updateTreeMap(this.state.inputPath, this.state.entityType, this.state.date); + }; + + updateTreeMap = (path: string, entityType:string, date:string) => { + this.setState({ + isLoading: true + }); + + const treeEndpoint = `/api/v1/heatmap/readaccess?startDate=${date}&path=${path}&entityType=${entityType}`; + axios.get(treeEndpoint).then(response => { + minSize = this.minmax(response.data)[0]; + maxSize = this.minmax(response.data)[1]; + let treeResponse: ITreeResponse = this.updateSize(response.data); + this.setState({ + isLoading: false, + showPanel: false, + treeResponse + }); + }).catch(error => { + this.setState({ + isLoading: false, + inputPath: '', + entityType: '', + date:'' + }); + showDataFetchError(error.toString()); + }); + }; + + updateTreemapParent = (path: any) => { + this.setState({ + isLoading: true, + inputPath: path + },() => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + }; + + componentDidMount(): void { + this.setState({ + isLoading: true + }); + // By default render treemap for default path entity type and date + this.updateTreeMap('/',this.state.entityType,this.state.date); + } + + onChange = (date: any[]) => { + + this.setState(prevState => ({ + date: moment(date).unix(), + entityType: prevState.entityType, + inputPath : prevState.inputPath + }), () => { + this.updateTreeMap(this.state.inputPath, this.state.entityType,this.state.date) + }); + } + + disabledDate(current: any) { + return current > moment() || current < moment().subtract(90, 'day'); + } + + resetInputpath = (e:any, path:string) => { + if (!path || path === '/') { + return; + } + else { + this.setState({ + inputPath: '/' + }) + } + }; + + minmax=(obj :any) => + { + if (obj.hasOwnProperty('children')) { + obj.children.forEach((child: any) => this.minmax(child)) + }; + if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) { + minSize = Math.min(minSize, obj.size); + maxSize = Math.max(maxSize, obj.size); + } + return [minSize, maxSize]; + }; + + updateSize = (obj: any) => { + //Normalize Size so other blocks also get visualized if size is large in bytes minimize and if size is too small make it big + if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) { + let newSize = this.normalize(minSize, maxSize, obj.size); + obj['normalizedSize'] = newSize; + } + if (obj.hasOwnProperty('children')) { + obj.children.forEach((child: any) => this.updateSize(child)); + } + return obj; + }; + + normalize = (min: number, max: number, size: number) => { + //Normaized Size using Deviation and mid Point + var mean = (max + min) / 2; + var highMean = (max+mean)/2; + var lowMean1 = (min+mean)/2; + var lowMean2 = (lowMean1 + min) / 2; + + if(size>highMean){ + var newsize= highMean+(size*0.1); + return(newsize); + } + // lowmean2= 100 value=10, diff= + // min= 10 ,max=100, mean=55, lowmean1=32.5,lowmean2=22, value= 15, diff=7, diff/2=3.5, newsize= 22-3.5=18.5 + if(size<lowMean2){ + var diff = (lowMean2-size)/2; + var newSize= lowMean2-diff; + return(newSize); + } + return size; + } + + render() { + const { treeResponse, isLoading, inputPath, date} = this.state; + const menuCalender = ( + <Menu + defaultSelectedKeys={[date]} + onClick={e => this.handleCalenderChange(e)}> + <Menu.Item key='24H'> + 24 Hour + </Menu.Item> + <Menu.Item key='7D'> + 7 Days + </Menu.Item> + <Menu.Item key='90D'> + 90 Days + </Menu.Item> + <Menu.SubMenu title="Custom Select Last 90 Days"> + <Menu.Item> + <DatePicker + format="YYYY-MM-DD" + onChange={this.onChange} + disabledDate={this.disabledDate} + /> + </Menu.Item> + </Menu.SubMenu> + </Menu> + ); + + const entityTypeMenu = ( + <Menu + defaultSelectedKeys={[this.state.entityType]} + onClick={e => this.handleMenuChange(e)}> + <Menu.Item key='volume'> + Volume + </Menu.Item> + <Menu.Item key='bucket'> + Bucket + </Menu.Item> + <Menu.Item key='key'> + Key + </Menu.Item> + </Menu> + ); + + return ( + <div className='heatmap-container'> + <div className='page-header'> + Tree Map for Entities + </div> + <div className='content-div'> + { isLoading ? <span><Icon type='loading'/> Loading...</span> : ( + <div> + {(Object.keys(treeResponse).length > 0 && (treeResponse.minAccessCount > 0 || treeResponse.maxAccessCount > 0)) ? + (treeResponse.size === 0)? <div style={{ height: 800 }} className='heatmapinformation'><br />Failed to Load Heatmap.{' '}<br/></div> + : + <div> + <Row> + <div className='go-back-button'> + <Button type='primary' onClick={e => this.resetInputpath(e, inputPath)}><Icon type='undo'/></Button> + </div> + <div className='input-bar'> + <h4>Path</h4> + <form className='input' autoComplete="off" id='input-form' onSubmit={this.handleSubmit}> + <Input placeholder='/' name="inputPath" value={inputPath} onChange={this.handleChange} /> + </form> + </div> + <div className='entity-dropdown-button'> + <Dropdown overlay={entityTypeMenu} placement='bottomCenter'> + <Button>Entity Type: {this.state.entityType }<DownOutlined/></Button> + </Dropdown> + </div> + <div className='date-dropdown-button'> + <Dropdown overlay={menuCalender} placement='bottomLeft'> + <Button>Last {date > 100 ? new Date(date*1000).toLocaleString() : date }<DownOutlined/></Button> + </Dropdown> + </div> + </Row> + <br/><br/><br/><br/><br/> + <div style={{display:"flex", alignItems: "right"}}> + <div style={{ display: "flex", alignItems: "center",marginLeft:"30px"}}> + <div style={{ width: "13px", height: "13px", backgroundColor: "yellow", marginRight: "5px" }}> </div> + <span>Less Accessed</span> + </div> + <div style={{ display: "flex", alignItems: "center",marginLeft:"50px" }}> + <div style={{ width: "13px", height: "13px", backgroundColor: "orange", marginRight: "5px" }}> </div> + <span>Moderate Accessed</span> + </div> + <div style={{ display: "flex", alignItems: "center",marginLeft:"50px" }}> + <div style={{ width: "13px", height: "13px", backgroundColor: "maroon", marginRight: "5px" }}> </div> + <span>Most Accessed</span> + </div> + </div> + <div style={{ height:750}}> + <HeatMapConfiguration data={treeResponse} onClick={this.updateTreemapParent}></HeatMapConfiguration> + </div> + </div> + : + <div style={{ height: 800 }} className='heatmapinformation'><br /> Review Comment: > Shall we move the style={{ height: 800}} to the heatmapinformation class in the heatmap.less? Same here -- 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: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
