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
251 changes: 126 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,132 @@ 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 [isPopoverOpen, setIsPopoverOpen] = useState<string[]>([]);
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] I see this was the previous name, but I'd rename this to openPopovers or similar. My first reaction with the current name is that:

  • There's only one popover
  • The variable is a boolean

const openPopoverFor = useCallback(
(id: string) => () => {
setIsPopoverOpen([...isPopoverOpen, id]);
},
[isPopoverOpen]
);

const closePopoverFor = useCallback(
(id: string) => () => {
setIsPopoverOpen([...isPopoverOpen, id]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can drop this line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch

if (isPopoverOpen.includes(id)) {
setIsPopoverOpen(isPopoverOpen.filter(subject => subject !== id));
}
},
[isPopoverOpen]
);

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

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={isPopoverOpen.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