Skip to content
This repository was archived by the owner on Apr 28, 2020. It is now read-only.

Commit e59f281

Browse files
gnehapkrawagner
authored andcommitted
Added top consumer card for storage dashboard (#349)
1 parent a96be3a commit e59f281

File tree

10 files changed

+315
-0
lines changed

10 files changed

+315
-0
lines changed

sass/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
@import './components/CreateBaremetalHostDialog/create-baremetal-host-dialog';
3636
@import './components/StorageOverview/ocs-health';
3737
@import './components/StorageOverview/data-resiliency';
38+
@import './components/StorageOverview/top-consumer';
3839

3940
/*
4041
TODO: these styles should be backported to the corresponding PF-React package
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.kubevirt-top-consumer__line-chart {
2+
opacity: 1;
3+
}
4+
5+
.kubevirt-top-consumer__time-duration {
6+
float: right;
7+
padding-right: 1rem;
8+
font-size: 0.8em;
9+
}

src/components/StorageOverview/StorageOverview.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import EventsConnected from './Events/Events';
1414
import { UtilizationConnected } from './Utilization/Utilization';
1515
import { DataResiliencyConnected } from './DataResiliency/DataResiliency';
1616
import { AlertsConnected } from './Alerts/Alerts';
17+
import TopConsumersConnected from './TopConsumers/TopConsumers';
1718

1819
const MainCards = () => (
1920
<GridItem lg={6} md={12} sm={12}>
@@ -34,6 +35,9 @@ const MainCards = () => (
3435
<GridItem span={6}>
3536
<DataResiliencyConnected />
3637
</GridItem>
38+
<GridItem span={12}>
39+
<TopConsumersConnected />
40+
</GridItem>
3741
</Grid>
3842
</GridItem>
3943
);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { LineChart, Row, Col } from 'patternfly-react';
5+
6+
import { InlineLoading } from '../../Loading';
7+
import { formatToShortTime } from '../../../utils';
8+
9+
import {
10+
DashboardCard,
11+
DashboardCardBody,
12+
DashboardCardHeader,
13+
DashboardCardTitle,
14+
} from '../../Dashboard/DashboardCard';
15+
import { StorageOverviewContext } from '../StorageOverviewContext/StorageOverviewContext';
16+
import { getTopConsumerVectorStats } from '../../../selectors/prometheus/storage';
17+
18+
const TopConsumersBody = ({ topConsumerStats }) => {
19+
let results = 'No data available';
20+
21+
if (topConsumerStats.length) {
22+
const columnsConf = getTopConsumerVectorStats(topConsumerStats);
23+
const { columns, unit } = columnsConf;
24+
const formatTime = x => formatToShortTime(x);
25+
26+
results = (
27+
<React.Fragment>
28+
<Row>
29+
<Col className="kubevirt-top-consumer__time-duration">Last 6 hours</Col>
30+
</Row>
31+
<Row>
32+
<Col lg={12} md={12} sm={12} xs={12}>
33+
<LineChart
34+
className="kubevirt-top-consumer__line-chart"
35+
id="line-chart"
36+
data={{
37+
x: 'x',
38+
columns,
39+
type: 'line',
40+
}}
41+
axis={{
42+
y: {
43+
label: {
44+
text: `Used Capacity(${unit})`,
45+
position: 'outer-top',
46+
},
47+
min: 0,
48+
padding: {
49+
bottom: 3,
50+
},
51+
},
52+
x: {
53+
tick: {
54+
format: formatTime,
55+
fit: true,
56+
values: [...columns[0].slice(1)],
57+
},
58+
},
59+
}}
60+
/>
61+
</Col>
62+
</Row>
63+
</React.Fragment>
64+
);
65+
}
66+
67+
return <div>{results}</div>;
68+
};
69+
70+
export const TopConsumers = ({ topConsumerStats, topConsumerLoaded, LoadingComponent }) => (
71+
<DashboardCard>
72+
<DashboardCardHeader>
73+
<DashboardCardTitle>Top Projects by Used Capacity</DashboardCardTitle>
74+
</DashboardCardHeader>
75+
<DashboardCardBody isLoading={!topConsumerLoaded} LoadingComponent={LoadingComponent}>
76+
<TopConsumersBody topConsumerStats={topConsumerStats} />
77+
</DashboardCardBody>
78+
</DashboardCard>
79+
);
80+
81+
TopConsumersBody.propTypes = {
82+
topConsumerStats: PropTypes.array.isRequired,
83+
};
84+
85+
TopConsumers.defaultProps = {
86+
topConsumerStats: [],
87+
topConsumerLoaded: false,
88+
LoadingComponent: InlineLoading,
89+
};
90+
91+
TopConsumers.propTypes = {
92+
topConsumerStats: PropTypes.array,
93+
topConsumerLoaded: PropTypes.bool,
94+
LoadingComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
95+
};
96+
97+
const TopConsumersConnected = () => (
98+
<StorageOverviewContext.Consumer>{props => <TopConsumers {...props} />}</StorageOverviewContext.Consumer>
99+
);
100+
101+
export default TopConsumersConnected;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { TopConsumers } from '../TopConsumers';
2+
import { InlineLoading } from '../../../Loading';
3+
4+
export const TopConsumerStats = [
5+
{
6+
topConsumerStats: [],
7+
topConsumerLoaded: true,
8+
LoadingComponent: InlineLoading,
9+
},
10+
{
11+
topConsumerStats: [
12+
{
13+
metric: {
14+
namespace: 'openshift-namespace',
15+
},
16+
values: [
17+
[1554797100, '46161920'],
18+
[1554797400, '46161920'],
19+
[1554797700, '46161920'],
20+
[1554798000, '46161920'],
21+
[1554798300, '46161920'],
22+
[1554798600, '46161920'],
23+
],
24+
},
25+
],
26+
topConsumerLoaded: true,
27+
LoadingComponent: InlineLoading,
28+
},
29+
];
30+
31+
export default [
32+
{
33+
component: TopConsumers,
34+
props: { ...TopConsumerStats },
35+
},
36+
{
37+
component: TopConsumers,
38+
name: 'Loading top consumers',
39+
},
40+
];
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
import { render } from 'enzyme';
3+
4+
import { TopConsumers } from '../TopConsumers';
5+
import { TopConsumerStats } from '../fixtures/TopConsumers.fixture';
6+
7+
describe('<TopConsumers />', () => {
8+
it('no-data-renders-correctly', () => {
9+
const component = render(<TopConsumers {...TopConsumerStats[0]} />);
10+
expect(component).toMatchSnapshot();
11+
});
12+
it('data-renders-correctly', () => {
13+
const component = render(<TopConsumers {...TopConsumerStats[1]} />);
14+
expect(component).toMatchSnapshot();
15+
});
16+
it('render-correctly-on-loading', () => {
17+
const component = render(<TopConsumers />);
18+
expect(component).toMatchSnapshot();
19+
});
20+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`<TopConsumers /> data-renders-correctly 1`] = `
4+
<div
5+
class="card-pf kubevirt-dashboard__card"
6+
>
7+
<div
8+
class="card-pf-heading kubevirt-dashboard__card-heading"
9+
>
10+
<h2
11+
class="card-pf-title"
12+
>
13+
Top Projects by Used Capacity
14+
</h2>
15+
</div>
16+
<div
17+
class="card-pf-body"
18+
>
19+
<div>
20+
<div
21+
class="row"
22+
>
23+
<div
24+
class="kubevirt-top-consumer__time-duration"
25+
>
26+
Last 6 hours
27+
</div>
28+
</div>
29+
<div
30+
class="row"
31+
>
32+
<div
33+
class="col-lg-12 col-md-12 col-sm-12 col-xs-12"
34+
>
35+
<div
36+
class=" kubevirt-top-consumer__line-chart"
37+
/>
38+
</div>
39+
</div>
40+
</div>
41+
</div>
42+
</div>
43+
`;
44+
45+
exports[`<TopConsumers /> no-data-renders-correctly 1`] = `
46+
<div
47+
class="card-pf kubevirt-dashboard__card"
48+
>
49+
<div
50+
class="card-pf-heading kubevirt-dashboard__card-heading"
51+
>
52+
<h2
53+
class="card-pf-title"
54+
>
55+
Top Projects by Used Capacity
56+
</h2>
57+
</div>
58+
<div
59+
class="card-pf-body"
60+
>
61+
<div>
62+
No data available
63+
</div>
64+
</div>
65+
</div>
66+
`;
67+
68+
exports[`<TopConsumers /> render-correctly-on-loading 1`] = `
69+
<div
70+
class="card-pf kubevirt-dashboard__card"
71+
>
72+
<div
73+
class="card-pf-heading kubevirt-dashboard__card-heading"
74+
>
75+
<h2
76+
class="card-pf-title"
77+
>
78+
Top Projects by Used Capacity
79+
</h2>
80+
</div>
81+
<div
82+
class="card-pf-body"
83+
>
84+
<div>
85+
<div
86+
class="spinner spinner-md blank-slate-pf-icon"
87+
/>
88+
</div>
89+
</div>
90+
</div>
91+
`;

src/components/StorageOverview/fixtures/StorageOverview.fixture.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { persistentVolumes } from '../../../tests/mocks/persistentVolume';
1616
import { osdDisksCount } from '../../../tests/mocks/disks';
1717
import { cephDiskInaccessibleAlert, cephDataRecoveryAlert } from '../Alerts/fixtures/Alerts.fixture';
1818

19+
import { TopConsumerStats } from '../TopConsumers/fixtures/TopConsumers.fixture';
20+
1921
export const nodes = [localhostNode];
2022
export const pvcs = persistentVolumeClaims;
2123
export const pvs = persistentVolumes;
@@ -42,6 +44,7 @@ export default [
4244
...utilizationStats,
4345
...dataResiliencyData[0],
4446
alertsResponse: [cephDiskInaccessibleAlert, cephDataRecoveryAlert],
47+
...TopConsumerStats[1],
4548
},
4649
},
4750
{

src/selectors/prometheus/storage.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { flatMap, max } from 'lodash';
2+
3+
import { parseNumber, formatBytes } from '../../utils';
4+
5+
export const getTopConsumerVectorStats = result => {
6+
let maxVal = 0;
7+
8+
const namespaceValues = flatMap(result, namespace => namespace.values);
9+
const allBytes = flatMap(namespaceValues, value => parseNumber(value[1]));
10+
11+
maxVal = max(allBytes);
12+
13+
const maxCapacityConverted = { ...formatBytes(maxVal) };
14+
15+
const yAxisData = result.map(r => [
16+
r.metric.namespace,
17+
...r.values.map(array => formatBytes(array[1], maxCapacityConverted.unit, 2).value),
18+
]);
19+
20+
let largestLengthArray = 0;
21+
let largestLengthArrayIndex = 0;
22+
23+
// timestamps count and values are not same for all the namespaces. Hence, finding the largest length array and taking its timestamps value as x-axis point
24+
result.forEach((namespace, index) => {
25+
const len = namespace.values.length;
26+
if (len > largestLengthArray) {
27+
largestLengthArray = len;
28+
largestLengthArrayIndex = index;
29+
}
30+
});
31+
const xAxisData = ['x', ...result[largestLengthArrayIndex].values.map(array => new Date(array[0] * 1000))];
32+
33+
const stats = {
34+
columns: [xAxisData, ...yAxisData],
35+
unit: maxCapacityConverted.unit,
36+
};
37+
38+
return stats;
39+
};

src/utils/utils.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,10 @@ export const getDefaultNetworkBinding = networkType => {
138138
return NETWORK_BINDING_MASQUERADE;
139139
}
140140
};
141+
142+
export const formatToShortTime = timestamp => {
143+
const dt = new Date(timestamp);
144+
145+
// returns in HH:MM format
146+
return dt.toString().substring(16, 21);
147+
};

0 commit comments

Comments
 (0)