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

[Metrics-UI] Fix toolbar popover for metrics table row #56796

Merged
merged 8 commits into from
Feb 7, 2020
250 changes: 125 additions & 125 deletions x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,24 @@ import { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } fro
import { i18n } from '@kbn/i18n';

import { last } from 'lodash';
import React from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import { createWaffleMapNode } from '../../containers/waffle/nodes_to_wafflemap';
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib';
import { fieldToName } from '../waffle/lib/field_to_display_name';
import { NodeContextMenu } from '../waffle/node_context_menu';
import { InventoryItemType } from '../../../common/inventory_models/types';
import { SnapshotNode, SnapshotNodePath } from '../../../common/http_api/snapshot_api';
import { ROOT_ELEMENT_ID } from '../../app';

interface Props {
nodes: SnapshotNode[];
nodeType: InventoryItemType;
options: InfraWaffleMapOptions;
formatter: (subject: string | number) => string;
currentTime: number;
formatter: (subject: string | number) => string;
onFilter: (filter: string) => void;
}

const initialState = {
isPopoverOpen: [] as string[],
};

type State = Readonly<typeof initialState>;

const getGroupPaths = (path: SnapshotNodePath[]) => {
switch (path.length) {
case 3:
Expand All @@ -42,126 +37,131 @@ const getGroupPaths = (path: SnapshotNodePath[]) => {
}
};

export const TableView = class extends React.PureComponent<Props, State> {
public readonly state: State = initialState;
public render() {
const { nodes, options, formatter, currentTime, nodeType } = this.props;
const columns: Array<EuiBasicTableColumn<typeof items[number]>> = [
{
field: 'name',
name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }),
sortable: true,
truncateText: true,
textOnly: true,
render: (value: string, item: { node: InfraWaffleMapNode }) => {
const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`;
// For the table we need to create a UniqueID that takes into to account the groupings
// as well as the node name. There is the possibility that a node can be present in two
// different groups and be on the screen at the same time.
const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':');
return (
<NodeContextMenu
node={item.node}
nodeType={nodeType}
closePopover={this.closePopoverFor(uniqueID)}
currentTime={currentTime}
isPopoverOpen={this.state.isPopoverOpen.includes(uniqueID)}
options={options}
popoverPosition="rightCenter"
>
<EuiToolTip content={tooltipText}>
<EuiButtonEmpty onClick={this.openPopoverFor(uniqueID)}>{value}</EuiButtonEmpty>
</EuiToolTip>
</NodeContextMenu>
);
},
},
...options.groupBy.map((grouping, index) => ({
field: `group_${index}`,
name: fieldToName((grouping && grouping.field) || ''),
sortable: true,
truncateText: true,
textOnly: true,
render: (value: string) => {
const handleClick = () => this.props.onFilter(`${grouping.field}:"${value}"`);
return (
<EuiToolTip content="Set Filter">
<EuiButtonEmpty onClick={handleClick}>{value}</EuiButtonEmpty>
export const TableView = (props: Props) => {
const { nodes, options, formatter, currentTime, nodeType } = props;
const [openPopovers, setOpenPopovers] = useState<string[]>([]);
const openPopoverFor = useCallback(
(id: string) => () => {
setOpenPopovers([...openPopovers, id]);
},
[openPopovers]
);

const closePopoverFor = useCallback(
(id: string) => () => {
if (openPopovers.includes(id)) {
setOpenPopovers(openPopovers.filter(subject => subject !== id));
}
},
[openPopovers]
);

useEffect(() => {
if (openPopovers.length > 0) {
document.getElementById(ROOT_ELEMENT_ID)!.style.overflowY = 'hidden';
} else {
document.getElementById(ROOT_ELEMENT_ID)!.style.overflowY = 'auto';
}
}, [openPopovers]);

const columns: Array<EuiBasicTableColumn<typeof items[number]>> = [
{
field: 'name',
name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }),
sortable: true,
truncateText: true,
textOnly: true,
render: (value: string, item: { node: InfraWaffleMapNode }) => {
const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`;
// For the table we need to create a UniqueID that takes into to account the groupings
// as well as the node name. There is the possibility that a node can be present in two
// different groups and be on the screen at the same time.
const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':');
return (
<NodeContextMenu
node={item.node}
nodeType={nodeType}
closePopover={closePopoverFor(uniqueID)}
currentTime={currentTime}
isPopoverOpen={openPopovers.includes(uniqueID)}
options={options}
popoverPosition="rightCenter"
>
<EuiToolTip content={tooltipText}>
<EuiButtonEmpty onClick={openPopoverFor(uniqueID)}>{value}</EuiButtonEmpty>
</EuiToolTip>
);
},
})),
{
field: 'value',
name: i18n.translate('xpack.infra.tableView.columnName.last1m', {
defaultMessage: 'Last 1m',
}),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
{
field: 'avg',
name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
</NodeContextMenu>
);
},
{
field: 'max',
name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
...options.groupBy.map((grouping, index) => ({
field: `group_${index}`,
name: fieldToName((grouping && grouping.field) || ''),
sortable: true,
truncateText: true,
textOnly: true,
render: (value: string) => {
const handleClick = () => props.onFilter(`${grouping.field}:"${value}"`);
return (
<EuiToolTip content="Set Filter">
<EuiButtonEmpty onClick={handleClick}>{value}</EuiButtonEmpty>
</EuiToolTip>
);
},
];
const items = nodes.map(node => {
const name = last(node.path);
return {
name: (name && name.label) || 'unknown',
...getGroupPaths(node.path).reduce(
(acc, path, index) => ({
...acc,
[`group_${index}`]: path.label,
}),
{}
),
value: node.metric.value,
avg: node.metric.avg,
max: node.metric.max,
node: createWaffleMapNode(node),
};
});
const initialSorting = {
sort: {
field: 'value',
direction: 'desc',
},
} as const;
return (
<EuiInMemoryTable
pagination={true}
sorting={initialSorting}
items={items}
columns={columns}
/>
);
}
})),
{
field: 'value',
name: i18n.translate('xpack.infra.tableView.columnName.last1m', {
defaultMessage: 'Last 1m',
}),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
{
field: 'avg',
name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
{
field: 'max',
name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }),
sortable: true,
truncateText: true,
dataType: 'number',
render: (value: number) => <span>{formatter(value)}</span>,
},
];

private openPopoverFor = (id: string) => () => {
this.setState(prevState => ({ isPopoverOpen: [...prevState.isPopoverOpen, id] }));
};
const items = nodes.map(node => {
const name = last(node.path);
return {
name: (name && name.label) || 'unknown',
...getGroupPaths(node.path).reduce(
(acc, path, index) => ({
...acc,
[`group_${index}`]: path.label,
}),
{}
),
value: node.metric.value,
avg: node.metric.avg,
max: node.metric.max,
node: createWaffleMapNode(node),
};
});
const initialSorting = {
sort: {
field: 'value',
direction: 'desc',
},
} as const;

private closePopoverFor = (id: string) => () => {
if (this.state.isPopoverOpen.includes(id)) {
this.setState(prevState => {
return {
isPopoverOpen: prevState.isPopoverOpen.filter(subject => subject !== id),
};
});
}
};
return (
<EuiInMemoryTable pagination={true} sorting={initialSorting} items={items} columns={columns} />
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,14 @@ import {
interface Props {
options: InfraWaffleMapOptions;
currentTime: number;
children: any;
node: InfraWaffleMapNode;
nodeType: InventoryItemType;
isPopoverOpen: boolean;
closePopover: () => void;
popoverPosition: EuiPopoverProps['anchorPosition'];
}

export const NodeContextMenu = ({
export const NodeContextMenu: React.FC<Props> = ({
options,
currentTime,
children,
Expand All @@ -45,7 +44,7 @@ export const NodeContextMenu = ({
closePopover,
nodeType,
popoverPosition,
}: Props) => {
}) => {
const uiCapabilities = useKibana().services.application?.capabilities;
const inventoryModel = findInventoryModel(nodeType);
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
Expand Down Expand Up @@ -132,7 +131,7 @@ export const NodeContextMenu = ({
closePopover={closePopover}
id={`${node.pathId}-popover`}
isOpen={isPopoverOpen}
button={children}
button={children!}
anchorPosition={popoverPosition}
>
<div style={{ maxWidth: 300 }} data-test-subj="nodeContextMenu">
Expand Down