[Metrics UI] Add sorting for name and value to Inventory View (#66644)
* [Metrics UI] Add sorting for name and value to Inventory View * Fixing saved views * Fixing overlooked i18n translations * Fixing type issue * Fixing i18n paths Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
89402acfae
commit
a7c2db73c5
|
@ -7,6 +7,7 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { WaffleSortControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls';
|
||||
import { ToolbarProps } from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { WaffleMetricControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/metric_control';
|
||||
|
@ -58,6 +59,11 @@ export const MetricsAndGroupByToolbarItems = (props: Props) => {
|
|||
customOptions={props.customOptions}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{props.view === 'map' && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<WaffleSortControls sort={props.sort} onChange={props.changeSort} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,6 +21,16 @@ export const inventoryViewSavedObjectType: SavedObjectsType = {
|
|||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
sort: {
|
||||
properties: {
|
||||
by: {
|
||||
type: 'keyword',
|
||||
},
|
||||
direction: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
metric: {
|
||||
properties: {
|
||||
type: {
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
SnapshotNodeMetric,
|
||||
SnapshotNodePath,
|
||||
} from '../../common/http_api/snapshot_api';
|
||||
import { WaffleSortOption } from '../pages/metrics/inventory_view/hooks/use_waffle_options';
|
||||
|
||||
export interface InfraFrontendLibs {
|
||||
apolloClient: InfraApolloClient;
|
||||
|
@ -163,6 +164,7 @@ export interface InfraWaffleMapOptions {
|
|||
metric: SnapshotMetricInput;
|
||||
groupBy: SnapshotGroupBy;
|
||||
legend: InfraWaffleMapLegend;
|
||||
sort: WaffleSortOption;
|
||||
}
|
||||
|
||||
export interface InfraOptions {
|
||||
|
|
|
@ -32,6 +32,7 @@ export const Layout = () => {
|
|||
const {
|
||||
metric,
|
||||
groupBy,
|
||||
sort,
|
||||
nodeType,
|
||||
accountId,
|
||||
region,
|
||||
|
@ -64,6 +65,7 @@ export const Layout = () => {
|
|||
],
|
||||
} as InfraWaffleMapGradientLegend,
|
||||
metric,
|
||||
sort,
|
||||
fields: source?.configuration?.fields,
|
||||
groupBy,
|
||||
};
|
||||
|
|
|
@ -19,17 +19,17 @@ import { ToolbarWrapper } from './toolbar_wrapper';
|
|||
import { InfraGroupByOptions } from '../../../../../lib/lib';
|
||||
import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public';
|
||||
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
|
||||
import { WaffleOptionsState } from '../../hooks/use_waffle_options';
|
||||
import { WaffleOptionsState, WaffleSortOption } from '../../hooks/use_waffle_options';
|
||||
import { useInventoryMeta } from '../../hooks/use_inventory_meta';
|
||||
|
||||
export interface ToolbarProps
|
||||
extends Omit<WaffleOptionsState, 'view' | 'boundsOverride' | 'autoBounds'> {
|
||||
export interface ToolbarProps extends Omit<WaffleOptionsState, 'boundsOverride' | 'autoBounds'> {
|
||||
createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern;
|
||||
changeMetric: (payload: SnapshotMetricInput) => void;
|
||||
changeGroupBy: (payload: SnapshotGroupBy) => void;
|
||||
changeCustomOptions: (payload: InfraGroupByOptions[]) => void;
|
||||
changeAccount: (id: string) => void;
|
||||
changeRegion: (name: string) => void;
|
||||
changeSort: (sort: WaffleSortOption) => void;
|
||||
accounts: InventoryCloudAccount[];
|
||||
regions: string[];
|
||||
changeCustomMetrics: (payload: SnapshotCustomMetricInput[]) => void;
|
||||
|
|
|
@ -25,12 +25,15 @@ export const ToolbarWrapper = (props: Props) => {
|
|||
changeCustomOptions,
|
||||
changeAccount,
|
||||
changeRegion,
|
||||
changeSort,
|
||||
customOptions,
|
||||
groupBy,
|
||||
metric,
|
||||
nodeType,
|
||||
accountId,
|
||||
view,
|
||||
region,
|
||||
sort,
|
||||
customMetrics,
|
||||
changeCustomMetrics,
|
||||
} = useWaffleOptionsContext();
|
||||
|
@ -47,8 +50,11 @@ export const ToolbarWrapper = (props: Props) => {
|
|||
changeAccount,
|
||||
changeRegion,
|
||||
changeCustomOptions,
|
||||
changeSort,
|
||||
customOptions,
|
||||
groupBy,
|
||||
sort,
|
||||
view,
|
||||
metric,
|
||||
nodeType,
|
||||
region,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { GroupOfNodes } from './group_of_nodes';
|
|||
import { applyWaffleMapLayout } from '../../lib/apply_wafflemap_layout';
|
||||
import { SnapshotNode } from '../../../../../../common/http_api/snapshot_api';
|
||||
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
|
||||
import { sortNodes } from '../../lib/sort_nodes';
|
||||
|
||||
interface Props {
|
||||
nodes: SnapshotNode[];
|
||||
|
@ -37,7 +38,8 @@ export const Map: React.FC<Props> = ({
|
|||
nodeType,
|
||||
dataBounds,
|
||||
}) => {
|
||||
const map = nodesToWaffleMap(nodes);
|
||||
const sortedNodes = sortNodes(options.sort, nodes);
|
||||
const map = nodesToWaffleMap(sortedNodes);
|
||||
return (
|
||||
<AutoSizer content>
|
||||
{({ measureRef, content: { width = 0, height = 0 } }) => {
|
||||
|
|
|
@ -132,7 +132,10 @@ export const WaffleMetricControls = ({
|
|||
}
|
||||
|
||||
const button = (
|
||||
<DropdownButton onClick={handleToggle} label="Metric">
|
||||
<DropdownButton
|
||||
onClick={handleToggle}
|
||||
label={i18n.translate('xpack.infra.waffle.metriclabel', { defaultMessage: 'Metric' })}
|
||||
>
|
||||
{currentLabel}
|
||||
</DropdownButton>
|
||||
);
|
||||
|
|
|
@ -58,7 +58,10 @@ export const WaffleAccountsControls = (props: Props) => {
|
|||
);
|
||||
|
||||
const button = (
|
||||
<DropdownButton label="Account" onClick={showPopover}>
|
||||
<DropdownButton
|
||||
label={i18n.translate('xpack.infra.waffle.accountLabel', { defaultMessage: 'Account' })}
|
||||
onClick={showPopover}
|
||||
>
|
||||
{currentLabel
|
||||
? currentLabel.name
|
||||
: i18n.translate('xpack.infra.waffle.accountAllTitle', {
|
||||
|
|
|
@ -130,7 +130,10 @@ export const WaffleGroupByControls = class extends React.PureComponent<Props, St
|
|||
);
|
||||
|
||||
const button = (
|
||||
<DropdownButton label="Group By" onClick={this.handleToggle}>
|
||||
<DropdownButton
|
||||
label={i18n.translate('xpack.infra.waffle.groupByLabel', { defaultMessage: 'Group by' })}
|
||||
onClick={this.handleToggle}
|
||||
>
|
||||
{buttonBody}
|
||||
</DropdownButton>
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { EuiPopover, EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui';
|
||||
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { findInventoryModel } from '../../../../../../common/inventory_models';
|
||||
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
|
||||
import { useWaffleOptionsContext } from '../../hooks/use_waffle_options';
|
||||
|
@ -115,7 +116,10 @@ export const WaffleInventorySwitcher: React.FC = () => {
|
|||
}, [nodeType]);
|
||||
|
||||
const button = (
|
||||
<DropdownButton onClick={openPopover} label="Show">
|
||||
<DropdownButton
|
||||
onClick={openPopover}
|
||||
label={i18n.translate('xpack.infra.waffle.showLabel', { defaultMessage: 'Show' })}
|
||||
>
|
||||
{selectedText}
|
||||
</DropdownButton>
|
||||
);
|
||||
|
|
|
@ -57,7 +57,10 @@ export const WaffleRegionControls = (props: Props) => {
|
|||
);
|
||||
|
||||
const button = (
|
||||
<DropdownButton onClick={showPopover} label="Region">
|
||||
<DropdownButton
|
||||
onClick={showPopover}
|
||||
label={i18n.translate('xpack.infra.waffle.regionLabel', { defaultMessage: 'Region' })}
|
||||
>
|
||||
{currentLabel ||
|
||||
i18n.translate('xpack.infra.waffle.region', {
|
||||
defaultMessage: 'All',
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState, ReactNode } from 'react';
|
||||
import { EuiSwitch, EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiTheme, withTheme } from '../../../../../../../observability/public';
|
||||
import { WaffleSortOption } from '../../hooks/use_waffle_options';
|
||||
import { DropdownButton } from '../dropdown_button';
|
||||
|
||||
interface Props {
|
||||
sort: WaffleSortOption;
|
||||
onChange: (sort: WaffleSortOption) => void;
|
||||
}
|
||||
|
||||
const LABELS = {
|
||||
name: i18n.translate('xpack.infra.waffle.sortNameLabel', { defaultMessage: 'Name' }),
|
||||
value: i18n.translate('xpack.infra.waffle.sort.valueLabel', { defaultMessage: 'Metric value' }),
|
||||
};
|
||||
|
||||
export const WaffleSortControls = ({ sort, onChange }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const showPopover = useCallback(() => {
|
||||
setIsOpen(true);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const label = LABELS[sort.by];
|
||||
|
||||
const button = (
|
||||
<DropdownButton
|
||||
label={i18n.translate('xpack.infra.waffle.sortLabel', { defaultMessage: 'Sort by' })}
|
||||
onClick={showPopover}
|
||||
>
|
||||
{label}
|
||||
</DropdownButton>
|
||||
);
|
||||
|
||||
const selectName = useCallback(() => {
|
||||
onChange({ ...sort, by: 'name' });
|
||||
closePopover();
|
||||
}, [closePopover, onChange, sort]);
|
||||
|
||||
const selectValue = useCallback(() => {
|
||||
onChange({ ...sort, by: 'value' });
|
||||
closePopover();
|
||||
}, [closePopover, onChange, sort]);
|
||||
|
||||
const toggleSort = useCallback(() => {
|
||||
onChange({
|
||||
...sort,
|
||||
direction: sort.direction === 'asc' ? 'desc' : 'asc',
|
||||
});
|
||||
}, [sort, onChange]);
|
||||
|
||||
const panels = useMemo<EuiContextMenuPanelDescriptor[]>(
|
||||
() => [
|
||||
{
|
||||
id: 0,
|
||||
title: '',
|
||||
items: [
|
||||
{
|
||||
name: LABELS.name,
|
||||
icon: sort.by === 'name' ? 'check' : 'empty',
|
||||
onClick: selectName,
|
||||
},
|
||||
{
|
||||
name: LABELS.value,
|
||||
icon: sort.by === 'value' ? 'check' : 'empty',
|
||||
onClick: selectValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[sort.by, selectName, selectValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
isOpen={isOpen}
|
||||
id="sortPopover"
|
||||
button={button}
|
||||
anchorPosition="downLeft"
|
||||
panelPaddingSize="none"
|
||||
closePopover={closePopover}
|
||||
>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
<SwitchContainer>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.infra.waffle.sortDirectionLabel', {
|
||||
defaultMessage: 'Reverse direction',
|
||||
})}
|
||||
checked={sort.direction === 'desc'}
|
||||
onChange={toggleSort}
|
||||
/>
|
||||
</SwitchContainer>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
interface SwitchContainerProps {
|
||||
theme: EuiTheme;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const SwitchContainer = withTheme(({ children, theme }: SwitchContainerProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: theme.eui.paddingSizes.m,
|
||||
borderTop: `1px solid ${theme.eui.euiBorderColor}`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -32,6 +32,7 @@ export const DEFAULT_WAFFLE_OPTIONS_STATE: WaffleOptionsState = {
|
|||
accountId: '',
|
||||
region: '',
|
||||
customMetrics: [],
|
||||
sort: { by: 'name', direction: 'desc' },
|
||||
};
|
||||
|
||||
export const useWaffleOptions = () => {
|
||||
|
@ -99,7 +100,15 @@ export const useWaffleOptions = () => {
|
|||
[setState]
|
||||
);
|
||||
|
||||
const changeSort = useCallback(
|
||||
(sort: WaffleSortOption) => {
|
||||
setState(previous => ({ ...previous, sort }));
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
return {
|
||||
...DEFAULT_WAFFLE_OPTIONS_STATE,
|
||||
...state,
|
||||
changeMetric,
|
||||
changeGroupBy,
|
||||
|
@ -111,10 +120,16 @@ export const useWaffleOptions = () => {
|
|||
changeAccount,
|
||||
changeRegion,
|
||||
changeCustomMetrics,
|
||||
changeSort,
|
||||
setWaffleOptionsState: setState,
|
||||
};
|
||||
};
|
||||
|
||||
export const WaffleSortOptionRT = rt.type({
|
||||
by: rt.keyof({ name: null, value: null }),
|
||||
direction: rt.keyof({ asc: null, desc: null }),
|
||||
});
|
||||
|
||||
export const WaffleOptionsStateRT = rt.type({
|
||||
metric: SnapshotMetricInputRT,
|
||||
groupBy: SnapshotGroupByRT,
|
||||
|
@ -134,8 +149,10 @@ export const WaffleOptionsStateRT = rt.type({
|
|||
accountId: rt.string,
|
||||
region: rt.string,
|
||||
customMetrics: rt.array(SnapshotCustomMetricInputRT),
|
||||
sort: WaffleSortOptionRT,
|
||||
});
|
||||
|
||||
export type WaffleSortOption = rt.TypeOf<typeof WaffleSortOptionRT>;
|
||||
export type WaffleOptionsState = rt.TypeOf<typeof WaffleOptionsStateRT>;
|
||||
const encodeUrlState = (state: WaffleOptionsState) => {
|
||||
return WaffleOptionsStateRT.encode(state);
|
||||
|
|
|
@ -28,6 +28,7 @@ export const useWaffleViewState = () => {
|
|||
autoBounds,
|
||||
accountId,
|
||||
region,
|
||||
sort,
|
||||
setWaffleOptionsState,
|
||||
} = useWaffleOptionsContext();
|
||||
const { currentTime, isAutoReloading, setWaffleTimeState } = useWaffleTimeContext();
|
||||
|
@ -35,6 +36,7 @@ export const useWaffleViewState = () => {
|
|||
|
||||
const viewState: WaffleViewState = {
|
||||
metric,
|
||||
sort,
|
||||
groupBy,
|
||||
nodeType,
|
||||
view,
|
||||
|
@ -59,6 +61,7 @@ export const useWaffleViewState = () => {
|
|||
const onViewChange = useCallback(
|
||||
(newState: WaffleViewState) => {
|
||||
setWaffleOptionsState({
|
||||
sort: newState.sort,
|
||||
metric: newState.metric,
|
||||
groupBy: newState.groupBy,
|
||||
nodeType: newState.nodeType,
|
||||
|
|
|
@ -21,6 +21,7 @@ const options: InfraWaffleMapOptions = {
|
|||
formatTemplate: '{{value}}',
|
||||
metric: { type: 'cpu' },
|
||||
groupBy: [],
|
||||
sort: { by: 'name', direction: 'asc' },
|
||||
legend: {
|
||||
type: 'gradient',
|
||||
rules: [],
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { sortBy, last } from 'lodash';
|
||||
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
|
||||
import { WaffleSortOption } from '../hooks/use_waffle_options';
|
||||
|
||||
const SORT_PATHS = {
|
||||
name: (node: SnapshotNode) => last(node.path),
|
||||
value: 'metric.value',
|
||||
};
|
||||
|
||||
export const sortNodes = (sort: WaffleSortOption, nodes: SnapshotNode[]) => {
|
||||
const sortPath = SORT_PATHS[sort.by];
|
||||
const sortedNodes = sortBy(nodes, sortPath);
|
||||
if (sort.direction === 'desc') {
|
||||
return sortedNodes.reverse();
|
||||
}
|
||||
return sortedNodes;
|
||||
};
|
Loading…
Reference in a new issue