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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0e0d5e9bad HDDS-11163. Improve Heatmap page UI (#7420)
0e0d5e9bad is described below

commit 0e0d5e9bad3b2acfb1cbac260b47755cbdcd1c7f
Author: Abhishek Pal <[email protected]>
AuthorDate: Tue Nov 19 17:21:54 2024 +0530

    HDDS-11163. Improve Heatmap page UI (#7420)
---
 .../src/v2/components/navBar/navBar.tsx            |   2 +-
 .../src/v2/components/plots/heatmapPlot.tsx        | 151 ++++++++
 .../src/v2/constants/heatmap.constants.tsx         |  47 +++
 .../src/v2/pages/heatmap/heatmap.less              |  86 +++++
 .../src/v2/pages/heatmap/heatmap.tsx               | 388 +++++++++++++++++++++
 .../recon/ozone-recon-web/src/v2/routes-v2.tsx     |   5 +-
 .../ozone-recon-web/src/v2/types/heatmap.types.ts  |  51 +++
 7 files changed, 725 insertions(+), 5 deletions(-)

diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/navBar/navBar.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/navBar/navBar.tsx
index 3da4104634..1dd1ede48d 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/navBar/navBar.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/navBar/navBar.tsx
@@ -144,7 +144,7 @@ const NavBar: React.FC<NavBarProps> = ({
       <span>Heatmap</span>
       <Link to={{
         pathname: '/Heatmap',
-        state: { isHeatmapEnabled: true }
+        state: { isHeatmapEnabled: isHeatmapEnabled }
       }}
       />
     </Menu.Item>
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/plots/heatmapPlot.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/plots/heatmapPlot.tsx
new file mode 100644
index 0000000000..a58a7704da
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/plots/heatmapPlot.tsx
@@ -0,0 +1,151 @@
+/*
+ * 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 { AgChartsReact } from 'ag-charts-react';
+import { byteToSize } from '@/utils/common';
+import { HeatmapResponse } from '@/v2/types/heatmap.types';
+
+type HeatmapPlotProps = {
+  data: HeatmapResponse;
+  onClick: (arg0: string) => void;
+  colorScheme: string[];
+  entityType: string;
+};
+
+const capitalize = <T extends string>(str: T) => {
+  return str.charAt(0).toUpperCase() + str.slice(1) as Capitalize<typeof str>;
+}
+
+const HeatmapPlot: React.FC<HeatmapPlotProps> = ({
+  data,
+  onClick,
+  colorScheme,
+  entityType = ''
+}) => {
+
+  const tooltipContent = (params: any) => {
+    let tooltipContent = `<span>
+      <strong>Size</strong>:
+      ${byteToSize(params.datum.size, 1)}
+      `;
+    if (params.datum.accessCount !== undefined) {
+      tooltipContent += `<br/>
+        <strong>Access count</strong>:
+      ${params.datum.accessCount }
+    `;
+      }
+    else{
+        tooltipContent += `<br/>
+        <strong>Max Access Count</strong>:
+      ${params.datum.maxAccessCount}
+    `;}
+    if (params.datum.label !== '') {
+      tooltipContent += `<br/>
+        <strong>Entity Name</strong>:
+        ${params.datum.label ? params.datum.label.split('/').slice(-1) : ''}
+      `;
+    }
+    tooltipContent += '</span>';
+    return tooltipContent;
+  };
+
+  const heatmapConfig = {
+    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',
+    title: { color: '#424242', fontSize: 14, fontFamily: 'Roboto', fontWeight: 
'600' },
+    subtitle: { color: '#424242', fontSize: 12, fontFamily: 'Roboto', 
fontWeight: '400' },
+    tooltip: {
+      renderer: (params) => {
+        return {
+          content: tooltipContent(params)
+        };
+      }
+    },
+    formatter: ({ highlighted }: { highlighted: boolean }) => {
+      const stroke = highlighted ? '#CED4D9' : '#FFFFFF';
+      return { stroke };
+    },
+    labels: {
+      color: '#FFFFFF',
+      fontWeight: 'bold',
+      fontSize: 12
+    },
+    tileStroke: '#FFFFFF',
+    tileStrokeWidth: 1.4,
+    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: [...colorScheme],
+    groupFill: '#E6E6E6',
+    groupStroke: '#E1E2E6',
+    nodePadding: 3,
+    labelShadow: { enabled: false }, //labels shadow
+    gradient: false,
+    highlightStyle: {
+      text: {
+        color: '#424242',
+      },
+      item: {
+        fill: 'rgba(0, 0 ,0, 0.0)',
+      },
+    },
+    listeners: {
+        nodeClick: (event) => {
+        var data = event.datum;
+        // Leaf level box should not call API
+        if (!data.color)
+          if (data.path) {
+            onClick(data.path);
+          }
+        },
+      },
+    }
+  
+  const options = {
+    data,
+    series: [heatmapConfig],
+    title: { text: `${capitalize(entityType)} Heatmap`}
+  };
+
+  return <AgChartsReact options={options} />
+}
+
+export default HeatmapPlot;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/heatmap.constants.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/heatmap.constants.tsx
new file mode 100644
index 0000000000..63a8476648
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/heatmap.constants.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+export const colourScheme = {
+  amberAlert: [
+    '#FFCF88',
+    '#FFCA87',
+    '#FFC586',
+    '#FFC085',
+    '#FFBB83',
+    '#FFB682',
+    '#FFB181',
+    '#FFA676',
+    '#FF9F6F',
+    '#FF9869',
+    '#FF9262',
+    '#FF8B5B',
+    '#FF8455',
+    '#FF7D4E',
+    '#FF8282',
+    '#FF7776',
+    '#FF6D6A',
+    '#FF625F',
+    '#FF5753',
+    '#FF4D47',
+    '#FF423B'
+  ]
+};
+
+export const TIME_PERIODS: string[] = ['24H', '7D', '90D']
+export const ENTITY_TYPES: string[] = ['key', 'bucket', 'volume']
+export const ROOT_PATH = '/'
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/heatmap/heatmap.less
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/heatmap/heatmap.less
new file mode 100644
index 0000000000..57eaf8391d
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/heatmap/heatmap.less
@@ -0,0 +1,86 @@
+/*
+* 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.
+*/
+
+.content-div {
+  min-height: unset;
+  padding-bottom: 10px;
+
+  .heatmap-header-section {
+    display: flex;
+    justify-content: space-between;
+    align-items: stretch;
+
+    @media (max-width: 1680px) {
+      flex-direction: column;
+    }
+
+    .heatmap-filter-section {
+      font-size: 14px;
+      font-weight: normal;
+      display: flex;
+      column-gap: 12px;
+
+      .path-input-container {
+        display: inline-flex;
+        align-items: flex-start;
+        
+        .path-input-element {
+          margin: 0px 0px 0px 10px;
+          width: 20vw;
+        }
+      }
+
+      .entity-dropdown-button {
+        display: inline-block;
+      }
+
+      .date-dropdown-button {
+        display: inline-block;
+      }
+
+      .input-bar {
+        display: inline-block;
+        margin-left: 25px;
+      }
+
+      .input {
+        padding-left: 5px;
+        margin-right: 10px;
+        display: inline-block;
+        width: 400px;
+      }
+    }
+
+    .heatmap-legend-container {
+      display: flex !important;
+      align-self: flex-start;
+
+      .heatmap-legend-item {
+        display: flex;
+        align-items: center;
+        margin-left: 20px;
+      }
+    }
+  }
+
+  #heatmap-plot-container {
+    height: 600px;
+    width: 84vw;
+    margin-left: -12px;
+  }
+}
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/heatmap/heatmap.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/heatmap/heatmap.tsx
new file mode 100644
index 0000000000..5cf2d64bd5
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/heatmap/heatmap.tsx
@@ -0,0 +1,388 @@
+/*
+ * 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, { ChangeEvent, useRef, useState } from 'react';
+import moment, { Moment } from 'moment';
+import { Row, Button, Menu, Input, Dropdown, DatePicker, Form, Result, 
message, Spin } from 'antd';
+import { MenuProps } from 'antd/es/menu';
+import { DownOutlined, LoadingOutlined, UndoOutlined } from 
'@ant-design/icons';
+
+
+import { showDataFetchError } from '@/utils/common';
+import { AxiosGetHelper } from '@/utils/axiosRequestHelper';
+import * as CONSTANTS from '@/v2/constants/heatmap.constants';
+import { HeatmapChild, HeatmapResponse, HeatmapState, InputPathState, 
InputPathValidTypes, IResponseError } from '@/v2/types/heatmap.types';
+import HeatmapPlot from '@/v2/components/plots/heatmapPlot';
+
+import './heatmap.less';
+import { useLocation } from 'react-router-dom';
+import { AxiosResponse } from 'axios';
+
+let minSize = Infinity;
+let maxSize = 0;
+
+const Heatmap: React.FC<{}> = () => {
+
+  const [state, setState] = useState<HeatmapState>({
+    heatmapResponse: {
+      label: '',
+      path: '',
+      children: [],
+      size: 0,
+      maxAccessCount: 0,
+      minAccessCount: 0
+    },
+    entityType: CONSTANTS.ENTITY_TYPES[0],
+    date: CONSTANTS.TIME_PERIODS[0]
+  });
+
+  const [inputPathState, setInputPathState] = useState<InputPathState>({
+    inputPath: CONSTANTS.ROOT_PATH,
+    isInputPathValid: undefined,
+    helpMessage: ''
+  });
+
+  const [isLoading, setLoading] = useState<boolean>(false);
+  const [treeEndpointFailed, setTreeEndpointFailed] = useState<boolean>(false);
+
+  const location = useLocation();
+  const cancelSignal = useRef<AbortController>();
+  const cancelDisabledFeatureSignal = useRef<AbortController>();
+
+  const [isHeatmapEnabled, setIsHeatmapEnabled] = 
useState<boolean>(location?.state?.isHeatmapEnabled);
+
+
+  function handleChange(e: ChangeEvent<HTMLInputElement>) {
+    const value = e.target.value;
+    // Only allow letters, numbers,underscores and forward slashes and hyphen
+    const regex = /^[a-zA-Z0-9_/-]*$/;
+
+    let inputValid = undefined;
+    let helpMessage = '';
+    if (!regex.test(value)) {
+      helpMessage = 'Please enter valid path';
+      inputValid = 'error';
+    }
+    setInputPathState({
+      inputPath: value,
+      isInputPathValid: inputValid as InputPathValidTypes,
+      helpMessage: helpMessage
+    });
+  }
+
+  function handleSubmit() {
+    updateHeatmap(inputPathState.inputPath, state.entityType, state.date);
+  }
+
+  const normalize = (min: number, max: number, size: number) => {
+    // Since there can be a huge difference between the largest entity size
+    // and the smallest entity size, it might cause some blocks to render 
smaller
+    // we are normalizing the size to ensure all entities are visible
+    //Normaized Size using Deviation and mid Point
+    const mean = (max + min) / 2;
+    const highMean = (max + mean) / 2;
+    const lowMean1 = (min + mean) / 2;
+    const lowMean2 = (lowMean1 + min) / 2;
+
+    if (size > highMean) {
+      const newsize = highMean + (size * 0.1);
+      return (newsize);
+    }
+    if (size < lowMean2) {
+      const diff = (lowMean2 - size) / 2;
+      const newSize = lowMean2 - diff;
+      return (newSize);
+    }
+
+    return size;
+  };
+
+  const updateSize = (obj: HeatmapResponse | HeatmapChild) => {
+    //Normalize Size so other blocks also get visualized if size is large in 
bytes minimize and if size is too small make it big
+    // it will only apply on leaf level as checking color property
+    if (obj.hasOwnProperty('size') && obj.hasOwnProperty('color')) {
+
+      // hide block at key,volume,bucket level if size accessCount and 
maxAccessCount are zero apply normalized size only for leaf level
+      if ((obj as HeatmapChild)?.size === 0 && (obj as 
HeatmapChild)?.accessCount === 0) {
+        obj['normalizedSize'] = 0;
+      } else if ((obj as HeatmapResponse)?.size === 0 && (obj as 
HeatmapResponse)?.maxAccessCount === 0) {
+        obj['normalizedSize'] = 0;
+      }
+      else if (obj?.size === 0 && ((obj as HeatmapChild)?.accessCount >= 0 || 
(obj as HeatmapResponse).maxAccessCount >= 0)) {
+        obj['normalizedSize'] = 1;
+        obj.size = 0;
+      }
+      else {
+        const newSize = normalize(minSize, maxSize, obj.size);
+        obj['normalizedSize'] = newSize;
+      }
+    }
+
+    if (obj.hasOwnProperty('children')) {
+      (obj as HeatmapResponse)?.children.forEach(child => updateSize(child));
+    }
+    return obj as HeatmapResponse;
+  };
+
+  const updateHeatmap = (path: string, entityType: string, date: string | 
number) => {
+    // Only perform requests if the heatmap is enabled
+    if (isHeatmapEnabled) {
+      setLoading(true);
+      // We want to ensure these are not empty as they will be passed as path 
params
+      if (date && path && entityType) {
+        const { request, controller } = AxiosGetHelper(
+          
`/api/v1/heatmap/readaccess?startDate=${date}&path=${path}&entityType=${entityType}`,
+          cancelSignal.current
+        );
+        cancelSignal.current = controller;
+
+        request.then(response => {
+          if (response?.status === 200) {
+            minSize = response.data.minAccessCount;
+            maxSize = response.data.maxAccessCount;
+            const heatmapResponse: HeatmapResponse = updateSize(response.data);
+            setLoading(false);
+            setState(prevState => ({
+              ...prevState,
+              heatmapResponse: heatmapResponse
+            }));
+          } else {
+            const error = new Error((response.status).toString()) as 
IResponseError;
+            error.status = response.status;
+            error.message = `Failed to fetch Heatmap Response with status 
${error.status}`
+            throw error;
+          }
+        }).catch(error => {
+          setLoading(false);
+          setInputPathState(prevState => ({
+            ...prevState,
+            inputPath: CONSTANTS.ROOT_PATH
+          }));
+          setTreeEndpointFailed(true);
+          if (error.response.status !== 404) {
+            showDataFetchError(error.message.toString());
+          }
+        });
+      } else {
+        setLoading(false);
+      }
+
+    }
+  }
+
+  const updateHeatmapParent = (path: string) => {
+    setInputPathState(prevState => ({
+      ...prevState,
+      inputPath: path
+    }));
+  }
+
+  function isDateDisabled(current: Moment) {
+    return current > moment() || current < moment().subtract(90, 'day');
+  }
+
+  function getIsHeatmapEnabled() {
+    const disabledfeaturesEndpoint = `/api/v1/features/disabledFeatures`;
+    const { request, controller } = AxiosGetHelper(
+      disabledfeaturesEndpoint,
+      cancelDisabledFeatureSignal.current
+    )
+    cancelDisabledFeatureSignal.current = controller;
+    request.then(response => {
+      setIsHeatmapEnabled(!response?.data?.includes('HEATMAP'));
+    }).catch(error => {
+      showDataFetchError((error as Error).toString());
+    });
+  }
+
+  React.useEffect(() => {
+    // We do not know if heatmap is enabled or not, so set it
+    if (isHeatmapEnabled === undefined) {
+      getIsHeatmapEnabled();
+    }
+    updateHeatmap(inputPathState.inputPath, state.entityType, state.date);
+
+    return (() => {
+      cancelSignal.current && cancelSignal.current.abort();
+    })
+  }, [isHeatmapEnabled, state.entityType, state.date]);
+
+  const handleDatePickerChange = (date: moment.MomentInput) => {
+    setState(prevState => ({
+      ...prevState,
+      date: moment(date).unix()
+    }));
+  };
+
+  const handleMenuChange: MenuProps["onClick"] = (e) => {
+    if (CONSTANTS.ENTITY_TYPES.includes(e.key as string)) {
+      minSize = Infinity;
+      maxSize = 0;
+      setState(prevState => ({
+        ...prevState,
+        entityType: e.key as string,
+      }));
+    }
+  };
+
+  const handleCalendarChange: MenuProps["onClick"] = (e) => {
+    if (CONSTANTS.TIME_PERIODS.includes(e.key as string)) {
+      setState(prevState => ({
+        ...prevState,
+        date: e.key
+      }));
+    }
+  };
+
+  const { date, entityType, heatmapResponse } = state;
+  const { inputPath, helpMessage, isInputPathValid } = inputPathState;
+
+  const menuCalendar = (
+    <Menu
+      defaultSelectedKeys={[date as string]}
+      onClick={handleCalendarChange}
+      selectable={true}>
+      <Menu.Item key={CONSTANTS.TIME_PERIODS[0]}>
+        24 Hour
+      </Menu.Item>
+      <Menu.Item key={CONSTANTS.TIME_PERIODS[1]}>
+        7 Days
+      </Menu.Item>
+      <Menu.Item key={CONSTANTS.TIME_PERIODS[2]}>
+        90 Days
+      </Menu.Item>
+      <Menu.SubMenu title='Custom Select Last 90 Days'>
+        <Menu.Item key='heatmapDatePicker'>
+          <DatePicker
+            format="YYYY-MM-DD"
+            onChange={handleDatePickerChange}
+            onClick={(e) => { e.stopPropagation() }}
+            disabledDate={isDateDisabled} />
+        </Menu.Item>
+      </Menu.SubMenu>
+    </Menu>
+  );
+
+  const entityTypeMenu = (
+    <Menu
+      defaultSelectedKeys={[entityType]}
+      onClick={handleMenuChange}
+      selectable={true}>
+      <Menu.Item key={CONSTANTS.ENTITY_TYPES[2]}>
+        Volume
+      </Menu.Item>
+      <Menu.Item key={CONSTANTS.ENTITY_TYPES[1]}>
+        Bucket
+      </Menu.Item>
+      <Menu.Item key={CONSTANTS.ENTITY_TYPES[0]}>
+        Key
+      </Menu.Item>
+    </Menu>
+  );
+
+  function getErrorContent() {
+    if (!isHeatmapEnabled) {
+      return <Result
+        status='error'
+        title='Heatmap Not Available'
+        subTitle='Please ensure Heatmap is enabled in the configs and you have 
sufficient permissions' />
+    }
+
+    if (treeEndpointFailed) {
+      return <Result
+        status='error'
+        title='Failed to fetch Heatmap'
+        subTitle='Check for any failed requests for more information' />
+    }
+  }
+
+  return (
+    <>
+      <div className='page-header-v2'>
+        Heatmap
+      </div>
+      <div className='data-container'>
+        {
+          (!isHeatmapEnabled || treeEndpointFailed)
+            ? getErrorContent()
+            : <div className='content-div'>
+              <div className='heatmap-header-section'>
+                <div className='heatmap-filter-section'>
+                  <div className='path-input-container'>
+                    <h4 style={{ paddingTop: '2%' }}>Path</h4>
+                    <Form.Item className='path-input-element' 
validateStatus={isInputPathValid} help={helpMessage}>
+                      <Input.Search
+                        allowClear
+                        placeholder={CONSTANTS.ROOT_PATH}
+                        name="inputPath"
+                        value={inputPath}
+                        onChange={handleChange}
+                        onSearch={handleSubmit} />
+                    </Form.Item>
+                  </div>
+                  <div className='entity-dropdown-button'>
+                    <Dropdown
+                      overlay={entityTypeMenu}
+                      placement='bottomCenter'>
+                      <Button>Entity Type:&nbsp;{entityType}<DownOutlined 
/></Button>
+                    </Dropdown>
+                  </div>
+                  <div className='date-dropdown-button'>
+                    <Dropdown
+                      overlay={menuCalendar}
+                      placement='bottomLeft'>
+                      <Button>Last &nbsp;{(date as number) > 100 ? new 
Date((date as number) * 1000).toLocaleString() : date}<DownOutlined /></Button>
+                    </Dropdown>
+                  </div>
+                </div>
+                <div className='heatmap-legend-container'>
+                  <div className='heatmap-legend-item'>
+                    <div style={{ width: "13px", height: "13px", 
backgroundColor: `${CONSTANTS.colourScheme["amberAlert"][0]}`, marginRight: 
"5px" }}> </div>
+                    <span>Less Accessed</span>
+                  </div>
+                  <div className='heatmap-legend-item'>
+                    <div style={{ width: "13px", height: "13px", 
backgroundColor: `${CONSTANTS.colourScheme["amberAlert"][8]}`, marginRight: 
"5px" }}> </div>
+                    <span>Moderate Accessed</span>
+                  </div>
+                  <div className='heatmap-legend-item'>
+                    <div style={{ width: "13px", height: "13px", 
backgroundColor: `${CONSTANTS.colourScheme["amberAlert"][20]}`, marginRight: 
"5px" }}> </div>
+                    <span>Most Accessed</span>
+                  </div>
+                </div>
+              </div>
+              {isLoading
+                ? <Spin size='large' />
+                : (Object.keys(heatmapResponse).length > 0 && 
(heatmapResponse.label !== null || heatmapResponse.path !== null))
+                  ? <div id="heatmap-plot-container">
+                    <HeatmapPlot
+                      data={heatmapResponse}
+                      onClick={updateHeatmapParent}
+                      colorScheme={CONSTANTS.colourScheme['amberAlert']}
+                      entityType={entityType} />
+                  </div>
+                  : <Result
+                    status='warning'
+                    title='No Data available' />
+              }
+            </div>
+        }
+      </div>
+    </>
+  );
+}
+
+export default Heatmap;
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
index c3ff1b97e3..fb2dc0b9c4 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
@@ -17,9 +17,6 @@
  */
 import { lazy } from 'react';
 
-import { Heatmap } from '@/views/heatMap/heatmap';
-import NotFound from '@/v2/pages/notFound/notFound';
-
 const Overview = lazy(() => import('@/v2/pages/overview/overview'));
 const Volumes = lazy(() => import('@/v2/pages/volumes/volumes'))
 const Buckets = lazy(() => import('@/v2/pages/buckets/buckets'));
@@ -29,6 +26,7 @@ const DiskUsage = lazy(() => 
import('@/v2/pages/diskUsage/diskUsage'));
 const Containers = lazy(() => import('@/v2/pages/containers/containers'));
 const Insights = lazy(() => import('@/v2/pages/insights/insights'));
 const OMDBInsights = lazy(() => import('@/v2/pages/insights/omInsights'));
+const Heatmap = lazy(() => import('@/v2/pages/heatmap/heatmap'));
 
 
 export const routesV2 = [
@@ -68,7 +66,6 @@ export const routesV2 = [
     path: '/Om',
     component: OMDBInsights
   },
-  // TODO: Replace with V2 heatmap once rea
   {
     path: '/Heatmap',
     component: Heatmap
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/heatmap.types.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/heatmap.types.ts
new file mode 100644
index 0000000000..a76db22a6f
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/heatmap.types.ts
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+export type InputPathValidTypes = 'error' | 'success' | 'warning' | 
'validating' | undefined;
+
+export type HeatmapChild = {
+  label: string;
+  size: number;
+  accessCount: number;
+  color: number;
+}
+
+export type InputPathState = {
+  inputPath: string;
+  isInputPathValid: InputPathValidTypes;
+  helpMessage: string;
+}
+
+export type HeatmapResponse =  {
+  label: string;
+  path: string;
+  maxAccessCount: number;
+  minAccessCount: number;
+  size: number;
+  children: HeatmapChild[];
+}
+
+export type HeatmapState = {
+  heatmapResponse: HeatmapResponse;
+  entityType: string;
+  date: string | number;
+}
+
+export interface IResponseError extends Error {
+  status?: number;
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to