blob: 19f00821f9093f6b7aebc14fff430e015f8a82f2 [file] [log] [blame]
/*
* 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 {Icon, Row, Col} from 'antd';
import filesize from 'filesize';
import {showDataFetchError} from 'utils/common';
import Plot from 'react-plotly.js';
import * as Plotly from 'plotly.js';
import {MultiSelect, IOption} from 'components/multiSelect/multiSelect';
import {ActionMeta, ValueType} from 'react-select';
import './insights.less';
const size = filesize.partial({standard: 'iec'});
interface IFileCountResponse {
volume: string;
bucket: string;
fileSize: number;
count: number;
}
interface IInsightsState {
isLoading: boolean;
fileCountsResponse: IFileCountResponse[];
plotData: Plotly.Data[];
volumeBucketMap: Map<string, Set<string>>;
selectedVolumes: IOption[];
selectedBuckets: IOption[];
bucketOptions: IOption[];
volumeOptions: IOption[];
isBucketSelectionDisabled: boolean;
}
const allVolumesOption: IOption = {
label: 'All Volumes',
value: '*'
};
const allBucketsOption: IOption = {
label: 'All Buckets',
value: '*'
};
export class Insights extends React.Component<Record<string, object>, IInsightsState> {
constructor(props = {}) {
super(props);
this.state = {
isLoading: false,
fileCountsResponse: [],
plotData: [],
volumeBucketMap: new Map<string, Set<string>>(),
selectedBuckets: [],
selectedVolumes: [],
bucketOptions: [],
volumeOptions: [],
isBucketSelectionDisabled: false
};
}
handleVolumeChange = (selected: ValueType<IOption>, _action: ActionMeta<IOption>) => {
const {volumeBucketMap} = this.state;
const selectedVolumes = (selected as IOption[]);
// Disable bucket selection dropdown if more than one volume is selected
// If there is only one volume, bucket selection dropdown should not be disabled.
const isBucketSelectionDisabled = !selectedVolumes ||
(selectedVolumes &&
(selectedVolumes.length > 2 &&
(volumeBucketMap.size !== 1)));
let bucketOptions: IOption[] = [];
// When volume is changed and more than one volume is selected,
// selected buckets value should be reset to all buckets
let selectedBuckets = [allBucketsOption];
// Update bucket options only if one volume is selected
if (selectedVolumes && selectedVolumes.length === 1) {
const selectedVolume = selectedVolumes[0].value;
if (volumeBucketMap.has(selectedVolume) && volumeBucketMap.get(selectedVolume)) {
bucketOptions = Array.from(volumeBucketMap.get(selectedVolume)!).map(bucket => ({
label: bucket,
value: bucket
}));
selectedBuckets = [...selectedBuckets, ...bucketOptions];
}
}
this.setState({
selectedVolumes,
selectedBuckets,
bucketOptions,
isBucketSelectionDisabled
}, this.updatePlotData);
};
handleBucketChange = (selected: ValueType<IOption>, _event: ActionMeta<IOption>) => {
const selectedBuckets = (selected as IOption[]);
this.setState({
selectedBuckets
}, this.updatePlotData);
};
updatePlotData = () => {
const {fileCountsResponse, selectedVolumes, selectedBuckets} = this.state;
// Aggregate counts across volumes & buckets
if (selectedVolumes && selectedBuckets) {
let filteredData = fileCountsResponse;
const selectedVolumeValues = new Set(selectedVolumes.map(option => option.value));
const selectedBucketValues = new Set(selectedBuckets.map(option => option.value));
if (selectedVolumes.length > 0 && !selectedVolumeValues.has(allVolumesOption.value)) {
// If not all volumes are selected, filter volumes based on the selection
filteredData = filteredData.filter(item => selectedVolumeValues.has(item.volume));
}
if (selectedBuckets.length > 0 && !selectedBucketValues.has(allBucketsOption.value)) {
// If not all buckets are selected, filter buckets based on the selection
filteredData = filteredData.filter(item => selectedBucketValues.has(item.bucket));
}
const xyMap: Map<number, number> = filteredData.reduce(
(map: Map<number, number>, current) => {
const fileSize = current.fileSize;
const oldCount = map.has(fileSize) ? map.get(fileSize)! : 0;
map.set(fileSize, oldCount + current.count);
return map;
}, new Map<number, number>());
// Calculate the previous power of 2 to find the lower bound of the range
// Ex: for 2048, the lower bound is 1024
const xValues = Array.from(xyMap.keys()).map(value => {
const upperbound = size(value);
const upperboundPower = Math.log2(value);
// For 1024 which is 2^10, the lowerbound is 0, since we start binning
// after 2^10
const lowerbound = upperboundPower > 10 ? size(2 ** (upperboundPower - 1)) : size(0);
return `${lowerbound} - ${upperbound}`;
});
this.setState({
plotData: [{
type: 'bar',
x: xValues,
y: Array.from(xyMap.values()),
name: 'file count'
}]
});
}
};
componentDidMount(): void {
// Fetch file size counts on component mount
this.setState({
isLoading: true
});
axios.get('/api/v1/utilization/fileCount').then(response => {
const fileCountsResponse: IFileCountResponse[] = response.data;
// Construct volume -> bucket[] map for populating filters
// Ex: vol1 -> [bucket1, bucket2], vol2 -> [bucket1]
const volumeBucketMap: Map<string, Set<string>> = fileCountsResponse.reduce(
(map: Map<string, Set<string>>, current) => {
const volume = current.volume;
const bucket = current.bucket;
if (map.has(volume)) {
const buckets = Array.from(map.get(volume)!);
map.set(volume, new Set([...buckets, bucket]));
} else {
map.set(volume, new Set().add(bucket));
}
return map;
}, new Map<string, Set<string>>());
// Set options for volume selection dropdown
const volumeOptions: IOption[] = Array.from(volumeBucketMap.keys()).map(k => ({
label: k,
value: k
}));
this.setState({
isLoading: false,
volumeBucketMap,
fileCountsResponse,
volumeOptions
}, () => {
this.updatePlotData();
// Select all volumes by default
this.handleVolumeChange([allVolumesOption, ...volumeOptions], {action: 'select-option'});
});
}).catch(error => {
this.setState({
isLoading: false
});
showDataFetchError(error.toString());
});
}
render() {
const {plotData, isLoading, selectedBuckets, volumeOptions,
selectedVolumes, fileCountsResponse, bucketOptions, isBucketSelectionDisabled} = this.state;
return (
<div className='insights-container'>
<div className='page-header'>
Insights
</div>
<div className='content-div'>
{isLoading ? <span><Icon type='loading'/> Loading...</span> :
((fileCountsResponse && fileCountsResponse.length > 0) ?
<div>
<Row>
<Col xs={24} xl={18}>
<Row>
<Col>
<div className='filter-block'>
<h4>Volumes</h4>
<MultiSelect
allowSelectAll
isMulti
className='multi-select-container'
options={volumeOptions}
closeMenuOnSelect={false}
hideSelectedOptions={false}
value={selectedVolumes}
allOption={allVolumesOption}
onChange={this.handleVolumeChange}
/>
</div>
<div className='filter-block'>
<h4>Buckets</h4>
<MultiSelect
allowSelectAll
isMulti
className='multi-select-container'
options={bucketOptions}
closeMenuOnSelect={false}
hideSelectedOptions={false}
value={selectedBuckets}
allOption={allBucketsOption}
isDisabled={isBucketSelectionDisabled}
onChange={this.handleBucketChange}
/>
</div>
</Col>
</Row>
</Col>
</Row>
<Row>
<Col>
<Plot
data={plotData}
layout={
{
width: 800,
height: 600,
title: 'File Size Distribution',
showlegend: true
}
}/>
</Col>
</Row>
</div> :
<div>No data to visualize file size distribution. Add files to Ozone to see a visualization on file size distribution.</div>)}
</div>
</div>
);
}
}