[Metrics UI] Design Refresh: Inventory View, Episode 1 (#64026)

* [Metrics UI] Design Refresh: Inventory View, Episode 1

* Removing unused i18n labels

* Removing obsolete code removed in previous PR

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Chris Cowan 2020-04-23 18:00:19 -07:00 committed by GitHub
parent e3ee02c687
commit f37185fd8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 493 additions and 516 deletions

View file

@ -0,0 +1,19 @@
/*
* 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 { EuiPanel } from '@elastic/eui';
import { euiStyled } from '../../../observability/public';
export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({
grow: false,
paddingSize: 'none',
}))`
border-top: none;
border-right: none;
border-left: none;
border-radius: 0;
padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`};
`;

View file

@ -0,0 +1,53 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
import React, { ReactNode } from 'react';
import { withTheme, EuiTheme } from '../../../../../../observability/public';
interface Props {
label: string;
onClick: () => void;
theme: EuiTheme;
children: ReactNode;
}
export const DropdownButton = withTheme(({ onClick, label, theme, children }: Props) => {
return (
<EuiFlexGroup
alignItems="center"
gutterSize="none"
style={{
border: theme.eui.euiFormInputGroupBorder,
boxShadow: `0px 3px 2px ${theme.eui.euiTableActionsBorderColor}, 0px 1px 1px ${theme.eui.euiTableActionsBorderColor}`,
}}
>
<EuiFlexItem
grow={false}
style={{
padding: 12,
background: theme.eui.euiFormInputGroupLabelBackground,
fontSize: '0.75em',
fontWeight: 600,
color: theme.eui.euiTitleColor,
}}
>
{label}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="text"
iconType="arrowDown"
onClick={onClick}
iconSide="right"
size="xs"
>
{children}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);
});

View file

@ -7,17 +7,13 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { Toolbar } from '../../../components/eui/toolbar';
import { WaffleTimeControls } from './components/waffle/waffle_time_controls';
import { WaffleInventorySwitcher } from './components/waffle/waffle_inventory_switcher';
import { SearchBar } from './components/search_bar';
import { WaffleTimeControls } from './waffle/waffle_time_controls';
import { SearchBar } from './search_bar';
import { ToolbarPanel } from '../../../../components/toolbar_panel';
export const SnapshotToolbar = () => (
<Toolbar>
export const FilterBar = () => (
<ToolbarPanel>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="m">
<EuiFlexItem grow={false}>
<WaffleInventorySwitcher />
</EuiFlexItem>
<EuiFlexItem>
<SearchBar />
</EuiFlexItem>
@ -25,5 +21,5 @@ export const SnapshotToolbar = () => (
<WaffleTimeControls />
</EuiFlexItem>
</EuiFlexGroup>
</Toolbar>
</ToolbarPanel>
);

View file

@ -4,20 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useCallback } from 'react';
import { useInterval } from 'react-use';
import { euiPaletteColorBlind } from '@elastic/eui';
import { NodesOverview } from './nodes_overview';
import { Toolbar } from './toolbars/toolbar';
import { euiPaletteColorBlind, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { convertIntervalToString } from '../../../../utils/convert_interval_to_string';
import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview';
import { PageContent } from '../../../../components/page';
import { useSnapshot } from '../hooks/use_snaphot';
import { useInventoryMeta } from '../hooks/use_inventory_meta';
import { useWaffleTimeContext } from '../hooks/use_waffle_time';
import { useWaffleFiltersContext } from '../hooks/use_waffle_filters';
import { useWaffleOptionsContext } from '../hooks/use_waffle_options';
import { useSourceContext } from '../../../../containers/source';
import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../../../lib/lib';
import { euiStyled } from '../../../../../../observability/public';
import { Toolbar } from './toolbars/toolbar';
import { ViewSwitcher } from './waffle/view_switcher';
import { SavedViews } from './saved_views';
import { IntervalLabel } from './waffle/interval_label';
import { Legend } from './waffle/legend';
import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter';
const euiVisColorPalette = euiPaletteColorBlind();
@ -34,7 +40,6 @@ export const Layout = () => {
autoBounds,
boundsOverride,
} = useWaffleOptionsContext();
const { accounts, regions } = useInventoryMeta(sourceId, nodeType);
const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext();
const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext();
const { loading, nodes, reload, interval } = useSnapshot(
@ -72,25 +77,75 @@ export const Layout = () => {
isAutoReloading ? 5000 : null
);
const intervalAsString = convertIntervalToString(interval);
const dataBounds = calculateBoundsFromNodes(nodes);
const bounds = autoBounds ? dataBounds : boundsOverride;
const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]);
return (
<>
<Toolbar accounts={accounts} regions={regions} nodeType={nodeType} />
<PageContent>
<NodesOverview
nodes={nodes}
options={options}
nodeType={nodeType}
loading={loading}
reload={reload}
onDrilldown={applyFilterQuery}
currentTime={currentTime}
onViewChange={changeView}
view={view}
autoBounds={autoBounds}
boundsOverride={boundsOverride}
interval={interval}
/>
<MainContainer>
<TopActionContainer>
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="m">
<Toolbar nodeType={nodeType} />
<EuiFlexItem grow={false}>
<ViewSwitcher view={view} onChange={changeView} />
</EuiFlexItem>
</EuiFlexGroup>
</TopActionContainer>
<NodesOverview
nodes={nodes}
options={options}
nodeType={nodeType}
loading={loading}
reload={reload}
onDrilldown={applyFilterQuery}
currentTime={currentTime}
view={view}
autoBounds={autoBounds}
boundsOverride={boundsOverride}
formatter={formatter}
/>
<BottomActionContainer>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<SavedViews />
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ position: 'relative', minWidth: 400 }}>
<Legend
formatter={formatter}
bounds={bounds}
dataBounds={dataBounds}
legend={options.legend}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<IntervalLabel intervalAsString={intervalAsString} />
</EuiFlexItem>
</EuiFlexGroup>
</BottomActionContainer>
</MainContainer>
</PageContent>
</>
);
};
const MainContainer = euiStyled.div`
position: relative;
flex: 1 1 auto;
`;
const TopActionContainer = euiStyled.div`
padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`};
`;
const BottomActionContainer = euiStyled.div`
background-color: ${props => props.theme.eui.euiPageBackgroundColor};
padding: ${props => props.theme.eui.paddingSizes.m} ${props =>
props.theme.eui.paddingSizes.m} ${props => props.theme.eui.paddingSizes.s};
position: absolute;
left: 0;
bottom: 4px;
right: 0;
`;

View file

@ -4,31 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { get, max, min } from 'lodash';
import React from 'react';
import { max, min } from 'lodash';
import React, { useCallback } from 'react';
import { InventoryItemType } from '../../../../../common/inventory_models/types';
import { euiStyled } from '../../../../../../observability/public';
import {
InfraFormatterType,
InfraWaffleMapBounds,
InfraWaffleMapOptions,
} from '../../../../lib/lib';
import { createFormatter } from '../../../../utils/formatters';
import { InfraWaffleMapBounds, InfraWaffleMapOptions, InfraFormatter } from '../../../../lib/lib';
import { NoData } from '../../../../components/empty_states';
import { InfraLoadingPanel } from '../../../../components/loading';
import { Map } from './waffle/map';
import { ViewSwitcher } from './waffle/view_switcher';
import { TableView } from './table_view';
import {
SnapshotNode,
SnapshotCustomMetricInputRT,
} from '../../../../../common/http_api/snapshot_api';
import { convertIntervalToString } from '../../../../utils/convert_interval_to_string';
import { InventoryItemType } from '../../../../../common/inventory_models/types';
import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric';
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
export interface KueryFilterQuery {
kind: 'kuery';
@ -43,74 +30,13 @@ interface Props {
reload: () => void;
onDrilldown: (filter: KueryFilterQuery) => void;
currentTime: number;
onViewChange: (view: string) => void;
view: string;
boundsOverride: InfraWaffleMapBounds;
autoBounds: boolean;
interval: string;
formatter: InfraFormatter;
}
interface MetricFormatter {
formatter: InfraFormatterType;
template: string;
bounds?: { min: number; max: number };
}
interface MetricFormatters {
[key: string]: MetricFormatter;
}
const METRIC_FORMATTERS: MetricFormatters = {
['count']: { formatter: InfraFormatterType.number, template: '{{value}}' },
['cpu']: {
formatter: InfraFormatterType.percent,
template: '{{value}}',
},
['memory']: {
formatter: InfraFormatterType.percent,
template: '{{value}}',
},
['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
['logRate']: {
formatter: InfraFormatterType.abbreviatedNumber,
template: '{{value}}/s',
},
['diskIOReadBytes']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}/s',
},
['diskIOWriteBytes']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}/s',
},
['s3BucketSize']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}',
},
['s3TotalRequests']: {
formatter: InfraFormatterType.abbreviatedNumber,
template: '{{value}}',
},
['s3NumberOfObjects']: {
formatter: InfraFormatterType.abbreviatedNumber,
template: '{{value}}',
},
['s3UploadBytes']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}',
},
['s3DownloadBytes']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}',
},
['sqsOldestMessage']: {
formatter: InfraFormatterType.number,
template: '{{value}} seconds',
},
};
const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => {
export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => {
const maxValues = nodes.map(node => node.metric.max);
const minValues = nodes.map(node => node.metric.value);
// if there is only one value then we need to set the bottom range to zero for min
@ -122,141 +48,97 @@ const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds =
return { min: min(minValues) || 0, max: max(maxValues) || 0 };
};
export const NodesOverview = class extends React.Component<Props, {}> {
public static displayName = 'Waffle';
public render() {
const {
autoBounds,
boundsOverride,
loading,
nodes,
nodeType,
reload,
view,
currentTime,
options,
interval,
} = this.props;
if (loading) {
return (
<InfraLoadingPanel
height="100%"
width="100%"
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
defaultMessage: 'Loading data',
})}
/>
);
} else if (!loading && nodes && nodes.length === 0) {
return (
<NoData
titleText={i18n.translate('xpack.infra.waffle.noDataTitle', {
defaultMessage: 'There is no data to display.',
})}
bodyText={i18n.translate('xpack.infra.waffle.noDataDescription', {
defaultMessage: 'Try adjusting your time or filter.',
})}
refetchText={i18n.translate('xpack.infra.waffle.checkNewDataButtonLabel', {
defaultMessage: 'Check for new data',
})}
onRefetch={() => {
reload();
}}
testString="noMetricsDataPrompt"
/>
);
}
const dataBounds = calculateBoundsFromNodes(nodes);
const bounds = autoBounds ? dataBounds : boundsOverride;
const intervalAsString = convertIntervalToString(interval);
export const NodesOverview = ({
autoBounds,
boundsOverride,
loading,
nodes,
nodeType,
reload,
view,
currentTime,
options,
formatter,
onDrilldown,
}: Props) => {
const handleDrilldown = useCallback(
(filter: string) => {
onDrilldown({
kind: 'kuery',
expression: filter,
});
return;
},
[onDrilldown]
);
const noData = !loading && nodes && nodes.length === 0;
if (loading) {
return (
<MainContainer>
<ViewSwitcherContainer>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<ViewSwitcher view={view} onChange={this.handleViewChange} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.infra.homePage.toolbar.showingLastOneMinuteDataText"
defaultMessage="Showing the last {duration} of data at the selected time"
values={{ duration: intervalAsString }}
/>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</ViewSwitcherContainer>
{view === 'table' ? (
<TableContainer>
<TableView
nodeType={nodeType}
nodes={nodes}
options={options}
formatter={this.formatter}
currentTime={currentTime}
onFilter={this.handleDrilldown}
/>
</TableContainer>
) : (
<MapContainer>
<Map
nodeType={nodeType}
nodes={nodes}
options={options}
formatter={this.formatter}
currentTime={currentTime}
onFilter={this.handleDrilldown}
bounds={bounds}
dataBounds={dataBounds}
/>
</MapContainer>
)}
</MainContainer>
<InfraLoadingPanel
height="100%"
width="100%"
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
defaultMessage: 'Loading data',
})}
/>
);
} else if (noData) {
return (
<NoData
titleText={i18n.translate('xpack.infra.waffle.noDataTitle', {
defaultMessage: 'There is no data to display.',
})}
bodyText={i18n.translate('xpack.infra.waffle.noDataDescription', {
defaultMessage: 'Try adjusting your time or filter.',
})}
refetchText={i18n.translate('xpack.infra.waffle.checkNewDataButtonLabel', {
defaultMessage: 'Check for new data',
})}
onRefetch={() => {
reload();
}}
testString="noMetricsDataPrompt"
/>
);
}
const dataBounds = calculateBoundsFromNodes(nodes);
const bounds = autoBounds ? dataBounds : boundsOverride;
private handleViewChange = (view: string) => this.props.onViewChange(view);
// TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example.
private formatter = (val: string | number) => {
const { metric } = this.props.options;
if (SnapshotCustomMetricInputRT.is(metric)) {
const formatter = createFormatterForMetric(metric);
return formatter(val);
}
const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count);
if (val == null) {
return '';
}
const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template);
return formatter(val);
};
private handleDrilldown = (filter: string) => {
this.props.onDrilldown({
kind: 'kuery',
expression: filter,
});
return;
};
if (view === 'table') {
return (
<TableContainer>
<TableView
nodeType={nodeType}
nodes={nodes}
options={options}
formatter={formatter}
currentTime={currentTime}
onFilter={handleDrilldown}
/>
</TableContainer>
);
}
return (
<MapContainer>
<Map
nodeType={nodeType}
nodes={nodes}
options={options}
formatter={formatter}
currentTime={currentTime}
onFilter={handleDrilldown}
bounds={bounds}
dataBounds={dataBounds}
/>
</MapContainer>
);
};
const MainContainer = euiStyled.div`
position: relative;
flex: 1 1 auto;
`;
const TableContainer = euiStyled.div`
padding: ${props => props.theme.eui.paddingSizes.l};
`;
const ViewSwitcherContainer = euiStyled.div`
padding: ${props => props.theme.eui.paddingSizes.l};
`;
const MapContainer = euiStyled.div`
position: absolute;
display: flex;

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { SavedViewsToolbarControls } from '../../../../../components/saved_views/toolbar_control';
import { inventoryViewSavedObjectType } from '../../../../../../common/saved_objects/inventory_view';
import { useWaffleViewState } from '../../hooks/use_waffle_view_state';
import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control';
import { inventoryViewSavedObjectType } from '../../../../../common/saved_objects/inventory_view';
import { useWaffleViewState } from '../hooks/use_waffle_view_state';
export const SavedViews = () => {
const { viewState, defaultViewState, onViewChange } = useWaffleViewState();

View file

@ -6,6 +6,7 @@
import React, { FunctionComponent } from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { useSourceContext } from '../../../../../containers/source';
import {
SnapshotMetricInput,
SnapshotGroupBy,
@ -19,7 +20,7 @@ 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 { SavedViews } from './save_views';
import { useInventoryMeta } from '../../hooks/use_inventory_meta';
export interface ToolbarProps
extends Omit<WaffleOptionsState, 'view' | 'boundsOverride' | 'autoBounds'> {
@ -45,9 +46,6 @@ const wrapToolbarItems = (
<>
<ToolbarItems {...props} accounts={accounts} regions={regions} />
<EuiFlexItem grow={true} />
<EuiFlexItem grow={false}>
<SavedViews />
</EuiFlexItem>
</>
)}
</ToolbarWrapper>
@ -56,10 +54,11 @@ const wrapToolbarItems = (
interface Props {
nodeType: InventoryItemType;
regions: string[];
accounts: InventoryCloudAccount[];
}
export const Toolbar = ({ nodeType, accounts, regions }: Props) => {
export const Toolbar = ({ nodeType }: Props) => {
const { sourceId } = useSourceContext();
const { accounts, regions } = useInventoryMeta(sourceId, nodeType);
const ToolbarItems = findToolbar(nodeType);
return wrapToolbarItems(ToolbarItems, accounts, regions);
};

View file

@ -5,14 +5,14 @@
*/
import React from 'react';
import { EuiFlexGroup } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SnapshotMetricType } from '../../../../../../common/inventory_models/types';
import { Toolbar } from '../../../../../components/eui/toolbar';
import { ToolbarProps } from './toolbar';
import { fieldToName } from '../../lib/field_to_display_name';
import { useSourceContext } from '../../../../../containers/source';
import { useWaffleOptionsContext } from '../../hooks/use_waffle_options';
import { WaffleInventorySwitcher } from '../waffle/waffle_inventory_switcher';
import { ToolbarProps } from './toolbar';
interface Props {
children: (props: Omit<ToolbarProps, 'accounts' | 'regions'>) => React.ReactElement;
@ -36,26 +36,27 @@ export const ToolbarWrapper = (props: Props) => {
} = useWaffleOptionsContext();
const { createDerivedIndexPattern } = useSourceContext();
return (
<Toolbar>
<EuiFlexGroup alignItems="center" gutterSize="m">
{props.children({
createDerivedIndexPattern,
changeMetric,
changeGroupBy,
changeAccount,
changeRegion,
changeCustomOptions,
customOptions,
groupBy,
metric,
nodeType,
region,
accountId,
customMetrics,
changeCustomMetrics,
})}
</EuiFlexGroup>
</Toolbar>
<>
<EuiFlexItem grow={false}>
<WaffleInventorySwitcher />
</EuiFlexItem>
{props.children({
createDerivedIndexPattern,
changeMetric,
changeGroupBy,
changeAccount,
changeRegion,
changeCustomOptions,
customOptions,
groupBy,
metric,
nodeType,
region,
accountId,
customMetrics,
changeCustomMetrics,
})}
</>
);
};

View file

@ -0,0 +1,31 @@
/*
* 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 from 'react';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
interface Props {
intervalAsString?: string;
}
export const IntervalLabel = ({ intervalAsString }: Props) => {
if (!intervalAsString) {
return null;
}
return (
<EuiText color="subdued" size="s">
<p>
<FormattedMessage
id="xpack.infra.homePage.toolbar.showingLastOneMinuteDataText"
defaultMessage="Last {duration} of data for the selected time"
values={{ duration: intervalAsString }}
/>
</p>
</EuiText>
);
};

View file

@ -53,7 +53,7 @@ export const Legend: React.FC<Props> = ({ dataBounds, legend, bounds, formatter
const LegendContainer = euiStyled.div`
position: absolute;
bottom: 10px;
bottom: 0px;
left: 10px;
right: 10px;
`;

View file

@ -40,7 +40,7 @@ export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBound
const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop
const buttonComponent = (
<EuiButtonIcon
iconType="gear"
iconType="controlsHorizontal"
color="text"
aria-label={i18n.translate('xpack.infra.legendControls.buttonLabel', {
defaultMessage: 'configure legend',

View file

@ -12,7 +12,6 @@ import { InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../../../../lib/
import { AutoSizer } from '../../../../../components/auto_sizer';
import { GroupOfGroups } from './group_of_groups';
import { GroupOfNodes } from './group_of_nodes';
import { Legend } from './legend';
import { applyWaffleMapLayout } from '../../lib/apply_wafflemap_layout';
import { SnapshotNode } from '../../../../../../common/http_api/snapshot_api';
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
@ -78,12 +77,6 @@ export const Map: React.FC<Props> = ({
}
})}
</WaffleMapInnerContainer>
<Legend
formatter={formatter}
bounds={bounds}
dataBounds={dataBounds}
legend={options.legend}
/>
</WaffleMapOuterContainer>
);
}}

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui';
import { EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState, useCallback } from 'react';
import { IFieldType } from 'src/plugins/data/public';
import {
@ -21,6 +20,7 @@ import { ModeSwitcher } from './mode_switcher';
import { MetricsEditMode } from './metrics_edit_mode';
import { CustomMetricMode } from './types';
import { SnapshotMetricType } from '../../../../../../../common/inventory_models/types';
import { DropdownButton } from '../../dropdown_button';
interface Props {
options: Array<{ text: string; value: string }>;
@ -132,17 +132,13 @@ export const WaffleMetricControls = ({
}
const button = (
<EuiFilterButton iconType="arrowDown" onClick={handleToggle}>
<FormattedMessage
id="xpack.infra.waffle.metricButtonLabel"
defaultMessage="Metric: {selectedMetric}"
values={{ selectedMetric: currentLabel }}
/>
</EuiFilterButton>
<DropdownButton onClick={handleToggle} label="Metric">
{currentLabel}
</DropdownButton>
);
return (
<EuiFilterGroup>
<>
<EuiPopover
isOpen={isPopoverOpen}
id="metricsPanel"
@ -194,6 +190,6 @@ export const WaffleMetricControls = ({
customMetrics={customMetrics}
/>
</EuiPopover>
</EuiFilterGroup>
</>
);
};

View file

@ -28,7 +28,7 @@ export const ViewSwitcher = ({ view, onChange }: Props) => {
label: i18n.translate('xpack.infra.viewSwitcher.tableViewLabel', {
defaultMessage: 'Table view',
}),
iconType: 'editorUnorderedList',
iconType: 'visTable',
},
];
return (
@ -37,9 +37,11 @@ export const ViewSwitcher = ({ view, onChange }: Props) => {
defaultMessage: 'Switch between table and map view',
})}
options={buttons}
color="primary"
color="text"
buttonSize="m"
idSelected={view}
onChange={onChange}
isIconOnly
/>
);
};

View file

@ -4,17 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiContextMenuPanelDescriptor,
EuiFilterButton,
EuiFilterGroup,
EuiPopover,
EuiContextMenu,
} from '@elastic/eui';
import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui';
import React, { useCallback, useState, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { InventoryCloudAccount } from '../../../../../../common/http_api/inventory_meta_api';
import { DropdownButton } from '../dropdown_button';
interface Props {
accountId: string;
@ -63,32 +57,26 @@ export const WaffleAccountsControls = (props: Props) => {
[options, accountId, changeAccount]
);
const button = (
<DropdownButton label="Account" onClick={showPopover}>
{currentLabel
? currentLabel.name
: i18n.translate('xpack.infra.waffle.accountAllTitle', {
defaultMessage: 'All',
})}
</DropdownButton>
);
return (
<EuiFilterGroup>
<EuiPopover
isOpen={isOpen}
id="accontPopOver"
button={
<EuiFilterButton iconType="arrowDown" onClick={showPopover}>
<FormattedMessage
id="xpack.infra.waffle.accountLabel"
defaultMessage="Account: {selectedAccount}"
values={{
selectedAccount: currentLabel
? currentLabel.name
: i18n.translate('xpack.infra.waffle.accountAllTitle', {
defaultMessage: 'All',
}),
}}
/>
</EuiFilterButton>
}
anchorPosition="downLeft"
panelPaddingSize="none"
closePopover={closePopover}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFilterGroup>
<EuiPopover
isOpen={isOpen}
id="accontPopOver"
button={button}
anchorPosition="downLeft"
panelPaddingSize="none"
closePopover={closePopover}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
};

View file

@ -9,8 +9,6 @@ import {
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
EuiFilterButton,
EuiFilterGroup,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -22,6 +20,7 @@ import { CustomFieldPanel } from './custom_field_panel';
import { euiStyled } from '../../../../../../../observability/public';
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
import { SnapshotGroupBy } from '../../../../../../common/http_api/snapshot_api';
import { DropdownButton } from '../dropdown_button';
interface Props {
options: Array<{ text: string; field: string; toolTipContent?: string }>;
@ -121,29 +120,31 @@ export const WaffleGroupByControls = class extends React.PureComponent<Props, St
.filter(o => o != null)
// In this map the `o && o.field` is totally unnecessary but Typescript is
// too stupid to realize that the filter above prevents the next map from being null
.map(o => <EuiBadge key={o && o.field}>{o && o.text}</EuiBadge>)
.map(o => (
<EuiBadge color="hollow" key={o && o.field}>
{o && o.text}
</EuiBadge>
))
) : (
<FormattedMessage id="xpack.infra.waffle.groupByAllTitle" defaultMessage="All" />
);
const button = (
<EuiFilterButton iconType="arrowDown" onClick={this.handleToggle}>
<FormattedMessage id="xpack.infra.waffle.groupByButtonLabel" defaultMessage="Group By: " />
<DropdownButton label="Group By" onClick={this.handleToggle}>
{buttonBody}
</EuiFilterButton>
</DropdownButton>
);
return (
<EuiFilterGroup>
<EuiPopover
isOpen={this.state.isPopoverOpen}
id="groupByPanel"
button={button}
panelPaddingSize="none"
closePopover={this.handleClose}
>
<StyledContextMenu initialPanelId="firstPanel" panels={panels} />
</EuiPopover>
</EuiFilterGroup>
<EuiPopover
isOpen={this.state.isPopoverOpen}
id="groupByPanel"
button={button}
panelPaddingSize="none"
closePopover={this.handleClose}
>
<StyledContextMenu initialPanelId="firstPanel" panels={panels} />
</EuiPopover>
);
}

View file

@ -4,19 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiPopover,
EuiContextMenu,
EuiFilterButton,
EuiFilterGroup,
EuiContextMenuPanelDescriptor,
} from '@elastic/eui';
import { EuiPopover, EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui';
import React, { useCallback, useState, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { findInventoryModel } from '../../../../../../common/inventory_models';
import { InventoryItemType } from '../../../../../../common/inventory_models/types';
import { useWaffleOptionsContext } from '../../hooks/use_waffle_options';
import { DropdownButton } from '../dropdown_button';
const getDisplayNameForType = (type: InventoryItemType) => {
const inventoryModel = findInventoryModel(type);
@ -120,27 +114,23 @@ export const WaffleInventorySwitcher: React.FC = () => {
return getDisplayNameForType(nodeType);
}, [nodeType]);
const button = (
<DropdownButton onClick={openPopover} label="Show">
{selectedText}
</DropdownButton>
);
return (
<EuiFilterGroup>
<EuiPopover
id="contextMenu"
button={
<EuiFilterButton iconType="arrowDown" onClick={openPopover}>
<FormattedMessage
id="xpack.infra.waffle.inventoryButtonLabel"
defaultMessage="View: {selectedText}"
values={{ selectedText }}
/>
</EuiFilterButton>
}
isOpen={isOpen}
closePopover={closePopover}
panelPaddingSize="none"
withTitle
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId="firstPanel" panels={panels} />
</EuiPopover>
</EuiFilterGroup>
<EuiPopover
id="contextMenu"
button={button}
isOpen={isOpen}
closePopover={closePopover}
panelPaddingSize="none"
withTitle
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId="firstPanel" panels={panels} />
</EuiPopover>
);
};

View file

@ -4,16 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiContextMenuPanelDescriptor,
EuiFilterButton,
EuiFilterGroup,
EuiPopover,
EuiContextMenu,
} from '@elastic/eui';
import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui';
import React, { useCallback, useState, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { DropdownButton } from '../dropdown_button';
interface Props {
region?: string;
@ -62,32 +56,25 @@ export const WaffleRegionControls = (props: Props) => {
[changeRegion, options, region]
);
const button = (
<DropdownButton onClick={showPopover} label="Region">
{currentLabel ||
i18n.translate('xpack.infra.waffle.region', {
defaultMessage: 'All',
})}
</DropdownButton>
);
return (
<EuiFilterGroup>
<EuiPopover
isOpen={isOpen}
id="regionPanel"
button={
<EuiFilterButton iconType="arrowDown" onClick={showPopover}>
<FormattedMessage
id="xpack.infra.waffle.regionLabel"
defaultMessage="Region: {selectedRegion}"
values={{
selectedRegion:
currentLabel ||
i18n.translate('xpack.infra.waffle.region', {
defaultMessage: 'All',
}),
}}
/>
</EuiFilterButton>
}
anchorPosition="downLeft"
panelPaddingSize="none"
closePopover={closePopover}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFilterGroup>
<EuiPopover
isOpen={isOpen}
id="regionPanel"
button={button}
anchorPosition="downLeft"
panelPaddingSize="none"
closePopover={closePopover}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
};

View file

@ -8,7 +8,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useContext } from 'react';
import { SnapshotToolbar } from './toolbar';
import { FilterBar } from './components/filter_bar';
import { DocumentTitle } from '../../../components/document_title';
import { NoIndices } from '../../../components/empty_states/no_indices';
@ -56,7 +56,7 @@ export const SnapshotPage = () => {
<SourceLoadingPage />
) : metricIndicesExist ? (
<>
<SnapshotToolbar />
<FilterBar />
<Layout />
</>
) : hasFailedLoadingSource ? (

View file

@ -0,0 +1,89 @@
/*
* 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 { get } from 'lodash';
import { createFormatter } from '../../../../utils/formatters';
import { InfraFormatterType } from '../../../../lib/lib';
import {
SnapshotMetricInput,
SnapshotCustomMetricInputRT,
} from '../../../../../common/http_api/snapshot_api';
import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric';
interface MetricFormatter {
formatter: InfraFormatterType;
template: string;
bounds?: { min: number; max: number };
}
interface MetricFormatters {
[key: string]: MetricFormatter;
}
const METRIC_FORMATTERS: MetricFormatters = {
['count']: { formatter: InfraFormatterType.number, template: '{{value}}' },
['cpu']: {
formatter: InfraFormatterType.percent,
template: '{{value}}',
},
['memory']: {
formatter: InfraFormatterType.percent,
template: '{{value}}',
},
['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' },
['logRate']: {
formatter: InfraFormatterType.abbreviatedNumber,
template: '{{value}}/s',
},
['diskIOReadBytes']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}/s',
},
['diskIOWriteBytes']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}/s',
},
['s3BucketSize']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}',
},
['s3TotalRequests']: {
formatter: InfraFormatterType.abbreviatedNumber,
template: '{{value}}',
},
['s3NumberOfObjects']: {
formatter: InfraFormatterType.abbreviatedNumber,
template: '{{value}}',
},
['s3UploadBytes']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}',
},
['s3DownloadBytes']: {
formatter: InfraFormatterType.bytes,
template: '{{value}}',
},
['sqsOldestMessage']: {
formatter: InfraFormatterType.number,
template: '{{value}} seconds',
},
};
export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => (
val: string | number
) => {
if (SnapshotCustomMetricInputRT.is(metric)) {
const formatter = createFormatterForMetric(metric);
return formatter(val);
}
const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count);
if (val == null) {
return '';
}
const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template);
return formatter(val);
};

View file

@ -17,7 +17,6 @@ import {
MetricsExplorerTimeOptions,
MetricsExplorerChartOptions,
} from '../hooks/use_metrics_explorer_options';
import { Toolbar } from '../../../../components/eui/toolbar';
import { MetricsExplorerKueryBar } from './kuery_bar';
import { MetricsExplorerMetrics } from './metrics';
import { MetricsExplorerGroupBy } from './group_by';
@ -28,6 +27,7 @@ import { MetricExplorerViewState } from '../hooks/use_metric_explorer_state';
import { metricsExplorerViewSavedObjectType } from '../../../../../common/saved_objects/metrics_explorer_view';
import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting';
import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges';
import { ToolbarPanel } from '../../../../components/toolbar_panel';
interface Props {
derivedIndexPattern: IIndexPattern;
@ -65,7 +65,7 @@ export const MetricsExplorerToolbar = ({
const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges);
return (
<Toolbar>
<ToolbarPanel>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={options.aggregation === 'count' ? 2 : false}>
<MetricsExplorerAggregationPicker
@ -143,6 +143,6 @@ export const MetricsExplorerToolbar = ({
/>
</EuiFlexItem>
</EuiFlexGroup>
</Toolbar>
</ToolbarPanel>
);
};

View file

@ -1,65 +0,0 @@
/*
* 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 { isDisplayable } from './is_displayable';
describe('isDisplayable()', () => {
test('field that is not displayable', () => {
const field = {
name: 'some.field',
type: 'number',
displayable: false,
};
expect(isDisplayable(field)).toBe(false);
});
test('field that is displayable', () => {
const field = {
name: 'some.field',
type: 'number',
displayable: true,
};
expect(isDisplayable(field)).toBe(true);
});
test('field that an ecs field', () => {
const field = {
name: '@timestamp',
type: 'date',
displayable: true,
};
expect(isDisplayable(field)).toBe(true);
});
test('field that matches same prefix', () => {
const field = {
name: 'system.network.name',
type: 'string',
displayable: true,
};
expect(isDisplayable(field, ['system.network'])).toBe(true);
});
test('field that does not matches same prefix', () => {
const field = {
name: 'system.load.1',
type: 'number',
displayable: true,
};
expect(isDisplayable(field, ['system.network'])).toBe(false);
});
test('field that is an K8s allowed field but does not match prefix', () => {
const field = {
name: 'kubernetes.namespace',
type: 'string',
displayable: true,
};
expect(isDisplayable(field, ['kubernetes.pod'])).toBe(true);
});
test('field that is a Prometheus allowed field but does not match prefix', () => {
const field = {
name: 'prometheus.labels.foo.bar',
type: 'string',
displayable: true,
};
expect(isDisplayable(field, ['prometheus.metrics'])).toBe(true);
});
});

View file

@ -1,30 +0,0 @@
/*
* 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 { IFieldType } from 'src/plugins/data/public';
import { startsWith, uniq } from 'lodash';
import { getAllowedListForPrefix } from '../../common/ecs_allowed_list';
interface DisplayableFieldType extends IFieldType {
displayable?: boolean;
}
const fieldStartsWith = (field: DisplayableFieldType) => (name: string) =>
startsWith(field.name, name);
export const isDisplayable = (field: DisplayableFieldType, additionalPrefixes: string[] = []) => {
// We need to start with at least one prefix, even if it's empty
const prefixes = additionalPrefixes && additionalPrefixes.length ? additionalPrefixes : [''];
// Create a set of allowed list based on the prefixes
const allowedList = prefixes.reduce((acc, prefix) => {
return uniq([...acc, ...getAllowedListForPrefix(prefix)]);
}, [] as string[]);
// If the field is displayable and part of the allowed list or covered by the prefix
return (
(field.displayable && prefixes.some(fieldStartsWith(field))) ||
allowedList.some(fieldStartsWith(field))
);
};

View file

@ -8165,7 +8165,6 @@
"xpack.infra.viewSwitcher.mapViewLabel": "マップビュー",
"xpack.infra.viewSwitcher.tableViewLabel": "表ビュー",
"xpack.infra.waffle.accountAllTitle": "すべて",
"xpack.infra.waffle.accountLabel": "アカウント: {selectedAccount}",
"xpack.infra.waffle.aggregationNames.avg": "{field} の平均",
"xpack.infra.waffle.aggregationNames.max": "{field} の最大値",
"xpack.infra.waffle.aggregationNames.min": "{field} の最小値",
@ -8201,11 +8200,8 @@
"xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "カスタムメトリックの変更を保存",
"xpack.infra.waffle.customMetrics.submitLabel": "保存",
"xpack.infra.waffle.groupByAllTitle": "すべて",
"xpack.infra.waffle.groupByButtonLabel": "グループ分けの条件: ",
"xpack.infra.waffle.inventoryButtonLabel": "ビュー: {selectedText}",
"xpack.infra.waffle.loadingDataText": "データを読み込み中",
"xpack.infra.waffle.maxGroupByTooltip": "一度に選択できるグループは 2 つのみです",
"xpack.infra.waffle.metricButtonLabel": "メトリック: {selectedMetric}",
"xpack.infra.waffle.metricOptions.countText": "カウント",
"xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用状況",
"xpack.infra.waffle.metricOptions.diskIOReadBytes": "ディスク読み取り",
@ -8232,7 +8228,6 @@
"xpack.infra.waffle.noDataDescription": "期間またはフィルターを調整してみてください。",
"xpack.infra.waffle.noDataTitle": "表示するデータがありません。",
"xpack.infra.waffle.region": "すべて",
"xpack.infra.waffle.regionLabel": "地域: {selectedRegion}",
"xpack.infra.waffle.savedView.createHeader": "ビューを保存",
"xpack.infra.waffle.savedViews.cancel": "キャンセル",
"xpack.infra.waffle.savedViews.cancelButton": "キャンセル",

View file

@ -8168,7 +8168,6 @@
"xpack.infra.viewSwitcher.mapViewLabel": "地图视图",
"xpack.infra.viewSwitcher.tableViewLabel": "表视图",
"xpack.infra.waffle.accountAllTitle": "全部",
"xpack.infra.waffle.accountLabel": "帐户:{selectedAccount}",
"xpack.infra.waffle.aggregationNames.avg": "“{field}”的平均值",
"xpack.infra.waffle.aggregationNames.max": "“{field}”的最大值",
"xpack.infra.waffle.aggregationNames.min": "“{field}”的最小值",
@ -8204,11 +8203,8 @@
"xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "保存定制指标的更改",
"xpack.infra.waffle.customMetrics.submitLabel": "保存",
"xpack.infra.waffle.groupByAllTitle": "全部",
"xpack.infra.waffle.groupByButtonLabel": "分组依据: ",
"xpack.infra.waffle.inventoryButtonLabel": "视图:{selectedText}",
"xpack.infra.waffle.loadingDataText": "正在加载数据",
"xpack.infra.waffle.maxGroupByTooltip": "一次只能选择两个分组",
"xpack.infra.waffle.metricButtonLabel": "指标:{selectedMetric}",
"xpack.infra.waffle.metricOptions.countText": "计数",
"xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用",
"xpack.infra.waffle.metricOptions.diskIOReadBytes": "磁盘读取",
@ -8235,7 +8231,6 @@
"xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。",
"xpack.infra.waffle.noDataTitle": "没有可显示的数据。",
"xpack.infra.waffle.region": "全部",
"xpack.infra.waffle.regionLabel": "地区:{selectedRegion}",
"xpack.infra.waffle.savedView.createHeader": "保存视图",
"xpack.infra.waffle.savedViews.cancel": "取消",
"xpack.infra.waffle.savedViews.cancelButton": "取消",