Skip to content

Commit

Permalink
Add additional metrics to PVC Page
Browse files Browse the repository at this point in the history
Introduces Donut Chart in Details Page
Introduces Used Capacity in List Page
  • Loading branch information
bipuladh committed Jul 2, 2020
1 parent 8389fd6 commit e741b00
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 41 deletions.
1 change: 1 addition & 0 deletions frontend/packages/console-shared/src/sorts/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './nodes';
export * from './pvc';
5 changes: 5 additions & 0 deletions frontend/packages/console-shared/src/sorts/pvc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as UIActions from '@console/internal/actions/ui';
import { K8sResourceCommon } from '@console/internal/module/k8s';

export const pvcUsed = (pvc: K8sResourceCommon): number =>
UIActions.getPVCMetric(pvc, 'usedCapacity');
13 changes: 13 additions & 0 deletions frontend/public/actions/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export enum ActionType {
SetPodMetrics = 'setPodMetrics',
SetNamespaceMetrics = 'setNamespaceMetrics',
SetNodeMetrics = 'setNodeMetrics',
SetPVCMetrics = 'setPVCMetrics',
SetPinnedResources = 'setPinnedResources',
}

Expand Down Expand Up @@ -93,6 +94,10 @@ export type NodeMetrics = {
totalStorage: MetricValuesByName;
};

export type PVCMetrics = {
usedCapacity: MetricValuesByName;
};

// URL routes that can be namespaced
export const namespacedResources = new Set();

Expand Down Expand Up @@ -125,6 +130,11 @@ export const getNodeMetric = (node: NodeKind, metric: string): number => {
return metrics?.[metric]?.[node.metadata.name] ?? 0;
};

export const getPVCMetric = (pvc: K8sResourceKind, metric: string): number => {
const metrics = store.getState().UI.getIn(['metrics', 'pvc']);
return metrics?.[metric]?.[pvc.metadata.namespace]?.[pvc.metadata.name] ?? 0;
};

export const formatNamespaceRoute = (activeNamespace, originalPath, location?) => {
let path = originalPath.substr(window.SERVER_FLAGS.basePath.length);

Expand Down Expand Up @@ -348,6 +358,8 @@ export const setNamespaceMetrics = (namespaceMetrics: NamespaceMetrics) =>
action(ActionType.SetNamespaceMetrics, { namespaceMetrics });
export const setNodeMetrics = (nodeMetrics: NodeMetrics) =>
action(ActionType.SetNodeMetrics, { nodeMetrics });
export const setPVCMetrics = (pvcMetrics: PVCMetrics) =>
action(ActionType.SetPVCMetrics, { pvcMetrics });

// TODO(alecmerdler): Implement all actions using `typesafe-actions` and add them to this export
const uiActions = {
Expand Down Expand Up @@ -396,6 +408,7 @@ const uiActions = {
setPodMetrics,
setNamespaceMetrics,
setNodeMetrics,
setPVCMetrics,
notificationDrawerToggleExpanded,
notificationDrawerToggleRead,
setPinnedResources,
Expand Down
5 changes: 5 additions & 0 deletions frontend/public/components/_persistent-volume-claim.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.co-pvc-donut {
max-width: 130px;
max-height: 130px;
margin-bottom: 20px;
}
2 changes: 2 additions & 0 deletions frontend/public/components/factory/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
nodeCPU,
nodeFS,
nodePods,
pvcUsed,
} from '@console/shared';
import * as UIActions from '../../actions/ui';
import {
Expand Down Expand Up @@ -143,6 +144,7 @@ const sorts = {
nodeFS: (node: NodeKind): number => nodeFS(node),
machinePhase: (machine: MachineKind): string => getMachinePhase(machine),
nodePods: (node: NodeKind): number => nodePods(node),
pvcUsed: (pvc: K8sResourceKind): number => pvcUsed(pvc),
};

const stateToProps = (
Expand Down
180 changes: 139 additions & 41 deletions frontend/public/components/persistent-volume-claim.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as React from 'react';
import * as _ from 'lodash-es';
import { sortable } from '@patternfly/react-table';
import * as classNames from 'classnames';

import { Status, FLAGS } from '@console/shared';
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
import { useDispatch, connect } from 'react-redux';
import { sortable } from '@patternfly/react-table';
import { ChartDonut } from '@patternfly/react-charts';
import { Status, FLAGS, calculateRadius, getNamespace, getName } from '@console/shared';
import { connectToFlags } from '../reducers/features';
import { Conditions } from './conditions';
import { DetailsPage, ListPage, Table, TableRow, TableData } from './factory';
Expand All @@ -15,9 +18,14 @@ import {
ResourceLink,
ResourceSummary,
Selector,
humanizeBinaryBytes,
convertToBaseValue,
} from './utils';
import { ResourceEventStream } from './events';
import { PersistentVolumeClaimModel } from '../models';
import { setPVCMetrics } from '../actions/ui';
import { PrometheusEndpoint } from './graphs/helpers';
import { usePrometheusPoll } from './graphs/prometheus-poll-hook';

const { common, ExpandPVC } = Kebab.factory;
const menuActions = [
Expand All @@ -30,12 +38,18 @@ const PVCStatus = ({ pvc }) => (
<Status status={pvc.metadata.deletionTimestamp ? 'Terminating' : pvc.status.phase} />
);

const getQuery = (name) => {
const query = _.template('kubelet_volume_stats_used_bytes${name}');
return name ? query({ name: `{persistentvolumeclaim='${name}'}` }) : query();
};

const tableColumnClasses = [
'', // name
'', // namespace
classNames('pf-m-hidden', 'pf-m-visible-on-lg'), // status
classNames('pf-m-hidden', 'pf-m-visible-on-xl'), // persistence volume
classNames('pf-m-hidden', 'pf-m-visible-on-xl'), // capacity
classNames('pf-m-hidden', 'pf-m-visible-on-2xl'), // used capacity
classNames('pf-m-hidden', 'pf-m-visible-on-2xl'), // storage class
Kebab.columnClass,
];
Expand Down Expand Up @@ -72,39 +86,44 @@ const PVCTableHeader = () => {
transforms: [sortable],
props: { className: tableColumnClasses[4] },
},
{
title: 'Used',
sortFunc: 'pvcUsed',
transforms: [sortable],
props: { className: tableColumnClasses[5] },
},
{
title: 'Storage Class',
sortField: 'spec.storageClassName',
transforms: [sortable],
props: { className: tableColumnClasses[5] },
props: { className: tableColumnClasses[6] },
},
{
title: '',
props: { className: tableColumnClasses[6] },
props: { className: tableColumnClasses[7] },
},
];
};
PVCTableHeader.displayName = 'PVCTableHeader';

const kind = 'PersistentVolumeClaim';

const PVCTableRow = ({ obj, index, key, style }) => {
const mapStateToProps = ({ UI }, { obj }) => ({
metrics: UI.getIn(['metrics', 'pvc', 'usedCapacity', getNamespace(obj), getName(obj)]),
});

const PVCTableRow = connect(mapStateToProps)(({ obj, index, key, style, metrics }) => {
const [name, namespace] = [getName(obj), getNamespace(obj)];
const totalCapacityMetric = convertToBaseValue(obj?.status?.capacity?.storage);
const totalCapcityHumanized = humanizeBinaryBytes(totalCapacityMetric);
const usedCapacity = humanizeBinaryBytes(metrics);
return (
<TableRow id={obj.metadata.uid} index={index} trKey={key} style={style}>
<TableData className={tableColumnClasses[0]}>
<ResourceLink
kind={kind}
name={obj.metadata.name}
namespace={obj.metadata.namespace}
title={obj.metadata.name}
/>
<ResourceLink kind={kind} name={name} namespace={namespace} title={name} />
</TableData>
<TableData className={classNames(tableColumnClasses[1], 'co-break-word')}>
<ResourceLink
kind="Namespace"
name={obj.metadata.namespace}
title={obj.metadata.namespace}
/>
<ResourceLink kind="Namespace" name={namespace} title={namespace} />
</TableData>
<TableData className={tableColumnClasses[2]}>
<PVCStatus pvc={obj} />
Expand All @@ -121,9 +140,10 @@ const PVCTableRow = ({ obj, index, key, style }) => {
)}
</TableData>
<TableData className={tableColumnClasses[4]}>
{_.get(obj, 'status.capacity.storage', '-')}
{totalCapacityMetric ? totalCapcityHumanized.string : '-'}
</TableData>
<TableData className={classNames(tableColumnClasses[5])}>
<TableData className={tableColumnClasses[5]}>{metrics ? usedCapacity.string : '-'}</TableData>
<TableData className={classNames(tableColumnClasses[6])}>
{obj?.spec?.storageClassName ? (
<ResourceLink
kind="StorageClass"
Expand All @@ -134,36 +154,82 @@ const PVCTableRow = ({ obj, index, key, style }) => {
'-'
)}
</TableData>
<TableData className={tableColumnClasses[6]}>
<TableData className={tableColumnClasses[7]}>
<ResourceKebab actions={menuActions} kind={kind} resource={obj} />
</TableData>
</TableRow>
);
};
});

const Details_ = ({ flags, obj: pvc }) => {
const canListPV = flags[FLAGS.CAN_LIST_PV];
const labelSelector = _.get(pvc, 'spec.selector');
const storageClassName = _.get(pvc, 'spec.storageClassName');
const volumeName = _.get(pvc, 'spec.volumeName');
const storage = _.get(pvc, 'status.capacity.storage');
const accessModes = _.get(pvc, 'status.accessModes');
const volumeMode = _.get(pvc, 'spec.volumeMode');
const conditions = _.get(pvc, 'status.conditions');

const labelSelector = pvc?.spec?.selector;
const storageClassName = pvc?.spec?.storageClassName;
const volumeName = pvc?.spec?.volumeName;
const storage = pvc?.status?.capacity?.storage;
const accessModes = pvc?.status?.accessModes;
const volumeMode = pvc?.spec?.volumeMode;
const conditions = pvc?.status?.conditions;

const [response, loadError, loading] = usePrometheusPoll({
endpoint: PrometheusEndpoint.QUERY,
namespace: pvc.metadata.namespace,
query: getQuery(pvc.metadata.name),
});

const totalCapacityMetric = convertToBaseValue(storage);
const usedMetrics = response?.data?.result?.[0]?.value?.[1];
const availableMetrics = usedMetrics ? totalCapacityMetric - usedMetrics : null;
const totalCapacity = humanizeBinaryBytes(totalCapacityMetric);
const availableCapacity = humanizeBinaryBytes(availableMetrics, undefined, totalCapacity.unit);
const usedCapacity = humanizeBinaryBytes(usedMetrics, undefined, totalCapacity.unit);
const { podStatusInnerRadius: innerRadius, podStatusOuterRadius: radius } = calculateRadius(130);
const availableCapacityString = `${Number(availableCapacity.value.toFixed(1))} ${
availableCapacity.unit
}`;
const totalCapacityString = `${Number(totalCapacity.value.toFixed(1))} ${totalCapacity.unit}`;

const donutData = [];
if (usedMetrics) {
donutData.push({ x: 'Used', y: usedCapacity.value });
donutData.push({ x: 'Available', y: availableCapacity.value });
} else {
donutData.push({ x: 'Total', y: totalCapacity.value });
}

return (
<>
<div className="co-m-pane__body">
<SectionHeading text="PersistentVolumeClaim Details" />
<SectionHeading text="Persistent Volume Claim Details" />
{totalCapacityMetric && !loading && (
<div className="co-pvc-donut">
<ChartDonut
ariaDesc={availableMetrics ? 'Available versus Used Capacity' : 'Total Capacity'}
ariaTitle={availableMetrics ? 'Available versus Used Capacity' : 'Total Capacity'}
height={130}
width={130}
size={130}
innerRadius={innerRadius}
radius={radius}
data={donutData}
labels={({ datum }) => `${datum.y} ${totalCapacity.unit} ${datum.x}`}
subTitle={availableMetrics ? 'Available' : 'Total'}
title={availableMetrics ? availableCapacityString : totalCapacityString}
constrainToVisibleArea={true}
/>
</div>
)}
<div className="row">
<div className="col-sm-6">
<div className="col-md-4 col-xl-5">
<ResourceSummary resource={pvc}>
<dt>Label Selector</dt>
<dd data-test-id="pvc-name">
<Selector selector={labelSelector} kind="PersistentVolume" />
</dd>
</ResourceSummary>
</div>
<div className="col-sm-6">
<div className="col-md-4 col-xl-5">
<dl>
<dt>Status</dt>
<dd data-test-id="pvc-status">
Expand All @@ -172,7 +238,13 @@ const Details_ = ({ flags, obj: pvc }) => {
{storage && (
<>
<dt>Capacity</dt>
<dd data-test-id="pvc-capacity">{storage}</dd>
<dd data-test-id="pvc-capacity">{totalCapacity.string}</dd>
</>
)}
{usedMetrics && _.isEmpty(loadError) && !loading && (
<>
<dt>Used</dt>
<dd>{humanizeBinaryBytes(usedMetrics).string}</dd>
</>
)}
{!_.isEmpty(accessModes) && (
Expand Down Expand Up @@ -226,16 +298,41 @@ const filters = [
},
];

export const PersistentVolumeClaimsList = (props) => (
<Table
{...props}
aria-label="Persistent Volume Claims"
Header={PVCTableHeader}
Row={PVCTableRow}
virtualize
/>
);
export const PersistentVolumeClaimsList = (props) => {
const Row = React.useCallback((rowProps) => <PVCTableRow {...rowProps} />, []);
return (
<Table
{...props}
aria-label="Persistent Volume Claims"
Header={PVCTableHeader}
Row={Row}
virtualize
/>
);
};

export const PersistentVolumeClaimsPage = (props) => {
const { namespace = undefined } = props;
const query = getQuery();
const dispatch = useDispatch();
const [response, loadError, loading] = usePrometheusPoll({
endpoint: PrometheusEndpoint.QUERY,
namespace,
query,
});
const pvcMetrics =
_.isEmpty(loadError) && !loading
? response?.data?.result?.reduce((acc, item) => {
_.set(
acc,
['usedCapacity', item?.metric?.namespace, item?.metric?.persistentvolumeclaim],
Number(item?.value?.[1]),
);
return acc;
}, {})
: {};
dispatch(setPVCMetrics(pvcMetrics));

const createProps = {
to: `/k8s/ns/${props.namespace || 'default'}/persistentvolumeclaims/~new/form`,
};
Expand All @@ -247,6 +344,7 @@ export const PersistentVolumeClaimsPage = (props) => {
canCreate={true}
rowFilters={filters}
createProps={createProps}
customData={pvcMetrics}
/>
);
};
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/reducers/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ export default (state: UIState, action: UIAction): UIState => {
return state.setIn(['metrics', 'namespace'], action.payload.namespaceMetrics);
case ActionType.SetNodeMetrics:
return state.setIn(['metrics', 'node'], action.payload.nodeMetrics);
case ActionType.SetPVCMetrics:
return state.setIn(['metrics', 'pvc'], action.payload.pvcMetrics);

case ActionType.SetPinnedResources: {
const pinnedResources = { ...state.get('pinnedResources') };
Expand Down
1 change: 1 addition & 0 deletions frontend/public/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
@import 'components/filter-toolbar';
@import 'components/autocomplete';
@import 'components/conditions';
@import 'components/persistent-volume-claim';

@import 'components/dashboard/dashboards-page/cluster-dashboard/activity-card';
@import 'components/dashboard/dashboards-page/cluster-dashboard/status-card';
Expand Down

0 comments on commit e741b00

Please sign in to comment.