Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 2072797: Add duration to top consumers card #683

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions locales/en/plugin__kubevirt-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
"<0>The descheduler can be used to evict a running pod to allow the pod to be rescheduled onto a more suitable node.</0><1></1><2>Note: if VirtualMachine have LiveMigration=False condition, edit is disabled.</2>": "<0>The descheduler can be used to evict a running pod to allow the pod to be rescheduled onto a more suitable node.</0><1></1><2>Note: if VirtualMachine have LiveMigration=False condition, edit is disabled.</2>",
"<0>This is a Windows VirtualMachine but no Service for the RDP (Remote Desktop Protocol) can be found.</0><1></1><2>For better experience accessing Windows console, it is recommended to use the RDP. To do so, create a service:<1><0>exposing the <2>{DEFAULT_RDP_PORT}/tcp</2> port of the VirtualMachine</0><1>using selector: <2>{TEMPLATE_VM_NAME_LABEL}: {name}</2></1><2>Example: virtctl expose VirtualMachine {name} --name {name}-rdp --port [UNIQUE_PORT] --target-port {DEFAULT_RDP_PORT} --type NodePort</2></1>Make sure, the VirtualMachine object has <3>spec.template.metadata.labels</3> set to <6>{TEMPLATE_VM_NAME_LABEL}: {name}</6></2>": "<0>This is a Windows VirtualMachine but no Service for the RDP (Remote Desktop Protocol) can be found.</0><1></1><2>For better experience accessing Windows console, it is recommended to use the RDP. To do so, create a service:<1><0>exposing the <2>{DEFAULT_RDP_PORT}/tcp</2> port of the VirtualMachine</0><1>using selector: <2>{TEMPLATE_VM_NAME_LABEL}: {name}</2></1><2>Example: virtctl expose VirtualMachine {name} --name {name}-rdp --port [UNIQUE_PORT] --target-port {DEFAULT_RDP_PORT} --type NodePort</2></1>Make sure, the VirtualMachine object has <3>spec.template.metadata.labels</3> set to <6>{TEMPLATE_VM_NAME_LABEL}: {name}</6></2>",
"<0>Unattend.xml</0><1>Unattend can be used to configure windows setup and can be picked up several times during windows setup/configuration.</1><2><0>{t('Learn more')}</0></2>": "<0>Unattend.xml</0><1>Unattend can be used to configure windows setup and can be picked up several times during windows setup/configuration.</1><2><0>{t('Learn more')}</0></2>",
"1 day": "1 day",
"1 hour": "1 hour",
"24 hours": "24 hours",
"4 hours": "4 hours",
"5 min": "5 min",
"5 minutes": "5 minutes",
"6 hours": "6 hours",
Expand Down
19 changes: 19 additions & 0 deletions src/utils/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,22 @@
// t('plugin__kubevirt-plugin~Templates')
// t('plugin__kubevirt-plugin~Overview')
// t('plugin__kubevirt-plugin~MTV')
// t('plugin__kubevirt-plugin~5 minutes')
// t('plugin__kubevirt-plugin~1 hour')
// t('plugin__kubevirt-plugin~4 hours')
// t('plugin__kubevirt-plugin~1 day')
// t('plugin__kubevirt-plugin~By CPU')
// t('plugin__kubevirt-plugin~CPU')
// t('plugin__kubevirt-plugin~By memory')
// t('plugin__kubevirt-plugin~Memory')
// t('plugin__kubevirt-plugin~By memory swap traffic')
// t('plugin__kubevirt-plugin~Memory swap traffic')
// t('plugin__kubevirt-plugin~By vCPU wait')
// t('plugin__kubevirt-plugin~vCPU wait')
// t('plugin__kubevirt-plugin~By throughput')
// t('plugin__kubevirt-plugin~Storage throughput')
// t('plugin__kubevirt-plugin~By IOPS')
// t('plugin__kubevirt-plugin~Storage IOPS')
// t('plugin__kubevirt-plugin~Project')
// t('plugin__kubevirt-plugin~VM')
// t('plugin__kubevirt-plugin~Node')
18 changes: 18 additions & 0 deletions src/utils/utils/dropdownEnum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ObjectEnum } from '@kubevirt-utils/utils/ObjectEnum';
Copy link
Contributor

@glekner glekner Jul 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pcbailey I feel like using ObjectEnum is a pain from the past that we should try to avoid at all cost.
I do understand that you have portions of your feature relying on it, so IMO its ok to use it for porting features like the Virtualization dashboard. But I strongly recommend to move over to plain objects and hooks when implementing new features from now on.


export type DropdownProps = {
dropdownLabel: string;
};

abstract class DropdownEnum<T> extends ObjectEnum<T> {
protected readonly dropdownLabel: string;

protected constructor(value: T, { dropdownLabel }) {
super(value);
this.dropdownLabel = dropdownLabel;
}

getDropdownLabel = (): string => this.dropdownLabel;
}

export default DropdownEnum;
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
border-bottom: var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100);
}

.kv-top-consumers-card__dropdown {
display: inline-block;
.kv-top-consumers-card__dropdown--duration {
margin-left: var(--pf-global--spacer--lg);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
SelectVariant,
} from '@patternfly/react-core';

import { SHOW_TOP_5_ITEMS, TOP_CONSUMER_NUM_ITEMS_KEY } from './utils/constants';
import {
SHOW_TOP_5_ITEMS,
TOP_CONSUMERS_DURATION_KEY,
TOP_CONSUMERS_NUM_ITEMS_KEY,
} from './utils/constants';
import DurationDropdown from './utils/DurationDropdown';
import DurationOption from './utils/DurationOption';
import FormPFSelect from './utils/FormPFSelect';
import TopConsumersGridRow from './utils/TopConsumersGridRow';
import { topAmountSelectOptions } from './utils/utils';
Expand All @@ -23,39 +29,50 @@ import './TopConsumersCard.scss';
const TopConsumersCard: React.FC = () => {
const { t } = useKubevirtTranslation();
const [numItemsToShow, setNumItemsToShow] = useLocalStorage(
TOP_CONSUMER_NUM_ITEMS_KEY,
TOP_CONSUMERS_NUM_ITEMS_KEY,
SHOW_TOP_5_ITEMS,
);
const [duration, setDuration] = useLocalStorage(
TOP_CONSUMERS_DURATION_KEY,
DurationOption.FIVE_MIN.toString(),
);

const onTopAmountSelect = (value) => setNumItemsToShow(value);
const onNumItemsSelect = (value) => setNumItemsToShow(value);
const onDurationSelect = (value: string) =>
setDuration(DurationOption.fromDropdownLabel(value).toString());

return (
<Card data-test="kv-top-consumers-card">
<CardHeader className="kv-top-consumers-card__header">
<CardTitle>{t('Top consumers')} </CardTitle>
<CardActions className="co-overview-card__actions">
<Link to="/monitoring/dashboards/grafana-dashboard-kubevirt-top-consumers?period=4h">
{t('View virtualization dashboard')}
</Link>
<div className="kv-top-consumers-card__dropdown">
<FormPFSelect
toggleId="kv-top-consumers-card-amount-select"
variant={SelectVariant.single}
selections={numItemsToShow}
onSelect={(e, value) => onTopAmountSelect(value)}
>
{topAmountSelectOptions(t).map((opt) => (
<SelectOption key={opt.key} value={opt.value} />
))}
</FormPFSelect>
</div>
</CardActions>
</CardHeader>
<CardBody className="kv-top-consumers-card__body">
<TopConsumersGridRow rowNumber={1} topGrid />
<TopConsumersGridRow rowNumber={2} />
</CardBody>
</Card>
<div className="kv-top-consumers-card">
<Card data-test="kv-top-consumers-card">
<CardHeader className="kv-top-consumers-card__header">
<CardTitle>{t('Top consumers')} </CardTitle>
<CardActions className="co-overview-card__actions">
<Link to="/monitoring/dashboards/grafana-dashboard-kubevirt-top-consumers?period=4h">
{t('View virtualization dashboard')}
</Link>
<div className="kv-top-consumers-card__dropdown--duration">
<DurationDropdown selectedDuration={duration} selectHandler={onDurationSelect} />
</div>
<div className="kv-top-consumers-card__dropdown--num-items">
<FormPFSelect
toggleId="kv-top-consumers-card-amount-select"
variant={SelectVariant.single}
selections={numItemsToShow}
onSelect={(e, value) => onNumItemsSelect(value)}
>
{topAmountSelectOptions(t).map((opt) => (
<SelectOption key={opt.key} value={opt.value} />
))}
</FormPFSelect>
</div>
</CardActions>
</CardHeader>
<CardBody className="kv-top-consumers-card__body">
<TopConsumersGridRow rowNumber={1} topGrid />
<TopConsumersGridRow rowNumber={2} />
</CardBody>
</Card>
</div>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';

import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';

import DurationOption from './DurationOption';

export type DurationDropdownProps = {
selectedDuration: string;
selectHandler: (value: string) => void;
};

const DurationDropdown: React.FC<DurationDropdownProps> = ({ selectedDuration, selectHandler }) => {
const [isOpen, setIsOpen] = React.useState(false);

const onSelect = (event, durationOption: string) => {
selectHandler(durationOption);
setIsOpen(false);
};

return (
<Select
data-test-id="duration-select-dropdown"
isOpen={isOpen}
onToggle={() => setIsOpen(!isOpen)}
onSelect={onSelect}
selections={DurationOption.fromString(selectedDuration)?.getDropdownLabel()}
variant={SelectVariant.single}
>
{DurationOption.getAll().map((durationOption) => {
const dropdownLabel = durationOption.getDropdownLabel();
const durationValue = durationOption.getValue();

return (
<SelectOption data-test-id={durationValue} key={durationValue} value={dropdownLabel}>
{dropdownLabel}
</SelectOption>
);
})}
</Select>
);
};

export default DurationDropdown;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import DropdownEnum from '@kubevirt-utils/utils/dropdownEnum';
import { ObjectEnum } from '@kubevirt-utils/utils/ObjectEnum';

class DurationOption extends DropdownEnum<string> {
static readonly FIVE_MIN = new DurationOption('5m', {
dropdownLabel: '5 minutes',
});

static readonly ONE_HOUR = new DurationOption('1h', {
dropdownLabel: '1 hour',
});

static readonly FOUR_HOURS = new DurationOption('4h', {
dropdownLabel: '4 hours',
});

static readonly ONE_DAY = new DurationOption('1d', {
dropdownLabel: '1 day',
});

private static readonly ALL = Object.freeze(
ObjectEnum.getAllClassEnumProperties<DurationOption>(DurationOption),
);

static getAll = () => DurationOption.ALL;

private static readonly stringMapper = DurationOption.ALL.reduce(
(accumulator, durationOption: DurationOption) => ({
...accumulator,
[durationOption.value]: durationOption,
}),
{},
);

private static readonly dropdownLabelMapper = DurationOption.ALL.reduce(
(accumulator, durationOption: DurationOption) => ({
...accumulator,
[durationOption.dropdownLabel]: durationOption,
}),
{},
);

static fromString = (source: string): DurationOption => DurationOption.stringMapper[source];

static fromDropdownLabel = (dropdownLabel: string): DurationOption =>
DurationOption.dropdownLabelMapper[dropdownLabel];
}

export default DurationOption;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTransla
import useLocalStorage from '@kubevirt-utils/hooks/useLocalStorage';
import { Card, SelectOption, SelectVariant } from '@patternfly/react-core';

import { SHOW_TOP_5_ITEMS, TOP_CONSUMER_NUM_ITEMS_KEY } from './constants';
import FormPFSelect from './FormPFSelect';
import { TopConsumerMetric } from './topConsumerMetric';
import { TopConsumersChartList } from './TopConsumersChartList';
Expand All @@ -19,11 +18,7 @@ type TopConsumersMetricCard = {

const TopConsumerCard: React.FC<TopConsumersMetricCard> = ({ cardID }) => {
const { t } = useKubevirtTranslation();
const [numItemsLabel] = useLocalStorage(TOP_CONSUMER_NUM_ITEMS_KEY);
const numItemsToShow = React.useMemo(
() => (numItemsLabel === SHOW_TOP_5_ITEMS ? 5 : 10),
[numItemsLabel],
);

const [metricKey, setMetricKey] = useLocalStorage(
`${cardID}-metric-value`,
initialTopConsumerCardSettings[cardID]?.metric.toString(),
Expand Down Expand Up @@ -75,7 +70,6 @@ const TopConsumerCard: React.FC<TopConsumersMetricCard> = ({ cardID }) => {
<div>{t('Usage')}</div>
</div>
<TopConsumersChartList
numItems={numItemsToShow}
metric={TopConsumerMetric.fromString(metricKey)}
scope={TopConsumerScope.fromString(scopeKey)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,47 @@
import * as React from 'react';

import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import useLocalStorage from '@kubevirt-utils/hooks/useLocalStorage';
import { PrometheusEndpoint, usePrometheusPoll } from '@openshift-console/dynamic-plugin-sdk';
import { CardBody } from '@patternfly/react-core';

import {
SHOW_TOP_5_ITEMS,
TOP_CONSUMERS_DURATION_KEY,
TOP_CONSUMERS_NUM_ITEMS_KEY,
} from './constants';
import NoDataAvailableMessage from './NoDataAvailableMessage';
import { getTopConsumerQuery } from './queries';
import { TopConsumerMetric } from './topConsumerMetric';
import { TopConsumerScope } from './topConsumerScope';
import TopConsumersProgressChart from './TopConsumersProgressChart';
import { getHumanizedValue, getValue } from './utils';
import { getChartTitle, getHumanizedValue, getValue } from './utils';

import './TopConsumersChartList.scss';

const getChartTitle = (scope, queryData) => {
let title = '';
const metricData = queryData?.metric;
switch (scope) {
case TopConsumerScope.NODE:
title = metricData?.node;
break;
case TopConsumerScope.PROJECT:
title = metricData?.namespace;
break;
case TopConsumerScope.VM:
default:
title =
metricData?.name || metricData?.label_vm_kubevirt_io_name || `VMI (${metricData.pod})`;
break;
}

return title;
};

type TopConsumersChartListProps = {
numItems: number;
metric: TopConsumerMetric;
scope: TopConsumerScope;
};

export const TopConsumersChartList: React.FC<TopConsumersChartListProps> = ({
numItems,
metric,
scope,
}) => {
export const TopConsumersChartList: React.FC<TopConsumersChartListProps> = ({ metric, scope }) => {
const { t } = useKubevirtTranslation();
const [duration] = useLocalStorage(TOP_CONSUMERS_DURATION_KEY);
const [numItemsLabel] = useLocalStorage(TOP_CONSUMERS_NUM_ITEMS_KEY);
const numItemsToShow = React.useMemo(
() => (numItemsLabel === SHOW_TOP_5_ITEMS ? 5 : 10),
[numItemsLabel],
);

const [query] = usePrometheusPoll({
endpoint: PrometheusEndpoint.QUERY,
query: getTopConsumerQuery(metric.getValue(), scope.getValue(), numItems),
query: getTopConsumerQuery(metric.getValue(), scope.getValue(), numItemsToShow, duration),
endTime: Date.now(),
});
const numQueryResults = query?.data?.result?.length;

const ChartList = React.useMemo(() => {
const numLinesToShow = numQueryResults >= numItems ? numItems : numQueryResults;
const numLinesToShow = numQueryResults >= numItemsToShow ? numItemsToShow : numQueryResults;
const max = getValue(query?.data?.result[0]?.value?.[1]);
const charts = [];

Expand All @@ -75,7 +63,7 @@ export const TopConsumersChartList: React.FC<TopConsumersChartListProps> = ({
);
}
return charts;
}, [query, metric, scope, numItems, numQueryResults]);
}, [query, metric, scope, numItemsToShow, numQueryResults]);

const showNoDataMessage = numQueryResults === 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const NON_VCPU_LINK =
export const vCPU_LINK =
'https://access.redhat.com/documentation/fr-fr/openshift_container_platform/4.8/html/openshift_virtualization/logging-events-and-monitoring#virt-promql-vcpu-metrics_virt-prometheus-queries';

export const TOP_CONSUMER_NUM_ITEMS_KEY = 'top-consumer-card-num-items';
export const TOP_CONSUMERS_NUM_ITEMS_KEY = 'top-consumers-card-num-items';
export const TOP_CONSUMERS_DURATION_KEY = 'top-consumers-card-duration';

export const SHOW_TOP_5_ITEMS = 'Show top 5';
Loading