[Infra UI] Link to node detail page from Metrics Explorer (#37136)

* [Infra UI] Link to Metrics Detail page from Metrics Explorer for compatible group bys

* Adding test for incompatible group bys

* renaming for clearity

* Changing out SourceQuery for SourceConfiguration

* Fixing tests to ensure link it validated

* Make tests more robust

* Update x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx

Co-Authored-By: Felix Stürmer <weltenwort@users.noreply.github.com>

* Adding definite assingment operattors

* Fixing flaky test
This commit is contained in:
Chris Cowan 2019-06-07 14:41:58 -07:00 committed by GitHub
parent 3c9182f4b1
commit d16c394028
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 165 additions and 70 deletions

View file

@ -5,81 +5,120 @@
*/
import React from 'react';
import { MetricsExplorerChartContextMenu } from './chart_context_menu';
import { MetricsExplorerChartContextMenu, createNodeDetailLink } from './chart_context_menu';
import { mountWithIntl } from '../../utils/enzyme_helpers';
import { options, source, timeRange } from '../../utils/fixtures/metrics_explorer';
import { InfraNodeType } from '../../graphql/types';
import DateMath from '@elastic/datemath';
import { ReactWrapper } from 'enzyme';
const series = { id: 'exmaple-01', rows: [], columns: [] };
const getTestSubject = (component: ReactWrapper, name: string) => {
return component.find(`[data-test-subj="${name}"]`).hostNodes();
};
describe('MetricsExplorerChartContextMenu', () => {
it('should just work', async () => {
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={options}
onFilter={onFilter}
/>
);
describe('component', () => {
it('should just work', async () => {
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={options}
onFilter={onFilter}
/>
);
component.find('button').simulate('click');
const menuItems = component.find('.euiContextMenuItem__text');
expect(menuItems.length).toBe(2);
expect(menuItems.at(0).text()).toBe('Add Filter');
expect(menuItems.at(1).text()).toBe('Open in Visualize');
component.find('button').simulate('click');
expect(getTestSubject(component, 'metricsExplorerAction-AddFilter').length).toBe(1);
expect(getTestSubject(component, 'metricsExplorerAction-OpenInTSVB').length).toBe(1);
expect(getTestSubject(component, 'metricsExplorerAction-ViewNodeMetrics').length).toBe(1);
});
it('should not display View metrics for incompatible groupBy', async () => {
const customOptions = { ...options, groupBy: 'system.network.name' };
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={customOptions}
onFilter={onFilter}
/>
);
component.find('button').simulate('click');
expect(getTestSubject(component, 'metricsExplorerAction-ViewNodeMetrics').length).toBe(0);
});
it('should not display "Add Filter" without onFilter', async () => {
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={options}
/>
);
component.find('button').simulate('click');
expect(getTestSubject(component, 'metricsExplorerAction-AddFilter').length).toBe(0);
});
it('should not display "Add Filter" without options.groupBy', async () => {
const customOptions = { ...options, groupBy: void 0 };
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={customOptions}
onFilter={onFilter}
/>
);
component.find('button').simulate('click');
expect(getTestSubject(component, 'metricsExplorerAction-AddFilter').length).toBe(0);
});
it('should disable "Open in Visualize" when options.metrics is empty', async () => {
const customOptions = { ...options, metrics: [] };
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={customOptions}
/>
);
component.find('button').simulate('click');
expect(
getTestSubject(component, 'metricsExplorerAction-OpenInTSVB').prop('disabled')
).toBeTruthy();
});
});
it('should not display "Add Filter" without onFilter', async () => {
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={options}
/>
);
component.find('button').simulate('click');
const menuItems = component.find('.euiContextMenuItem__text');
expect(menuItems.length).toBe(1);
expect(menuItems.at(0).text()).toBe('Open in Visualize');
});
it('should not display "Add Filter" without options.groupBy', async () => {
const customOptions = { ...options, groupBy: void 0 };
const onFilter = jest.fn().mockImplementation((query: string) => void 0);
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={customOptions}
onFilter={onFilter}
/>
);
component.find('button').simulate('click');
const menuItems = component.find('.euiContextMenuItem__text');
expect(menuItems.length).toBe(1);
expect(menuItems.at(0).text()).toBe('Open in Visualize');
});
it('should disable "Open in Visualize" when options.metrics is empty', async () => {
const customOptions = { ...options, metrics: [] };
const component = mountWithIntl(
<MetricsExplorerChartContextMenu
timeRange={timeRange}
source={source}
series={series}
options={customOptions}
/>
);
component.find('button').simulate('click');
const menuItems = component.find('button.euiContextMenuItem');
expect(menuItems.length).toBe(1);
expect(menuItems.at(0).prop('disabled')).toBeTruthy();
describe('helpers', () => {
test('createNodeDetailLink()', () => {
const fromDateStrig = '2019-01-01T11:00:00Z';
const toDateStrig = '2019-01-01T12:00:00Z';
const to = DateMath.parse(toDateStrig, { roundUp: true })!;
const from = DateMath.parse(fromDateStrig)!;
const link = createNodeDetailLink(
InfraNodeType.host,
'example-01',
fromDateStrig,
toDateStrig
);
expect(link).toBe(
`#/link-to/host-detail/example-01?to=${to.valueOf()}&from=${from.valueOf()}`
);
});
});
});

View file

@ -11,23 +11,58 @@ import {
EuiContextMenuPanelDescriptor,
EuiPopover,
} from '@elastic/eui';
import DateMath from '@elastic/datemath';
import { MetricsExplorerSeries } from '../../../server/routes/metrics_explorer/types';
import {
MetricsExplorerOptions,
MetricsExplorerTimeOptions,
} from '../../containers/metrics_explorer/use_metrics_explorer_options';
import { createTSVBLink } from './helpers/create_tsvb_link';
import { SourceQuery } from '../../graphql/types';
import { InfraNodeType } from '../../graphql/types';
import { getNodeDetailUrl } from '../../pages/link_to/redirect_to_node_detail';
import { SourceConfiguration } from '../../utils/source_configuration';
interface Props {
intl: InjectedIntl;
options: MetricsExplorerOptions;
onFilter?: (query: string) => void;
series: MetricsExplorerSeries;
source: SourceQuery.Query['source']['configuration'] | undefined;
source?: SourceConfiguration;
timeRange: MetricsExplorerTimeOptions;
}
const fieldToNodeType = (source: SourceConfiguration, field: string): InfraNodeType | undefined => {
if (source.fields.host === field) {
return InfraNodeType.host;
}
if (source.fields.pod === field) {
return InfraNodeType.pod;
}
if (source.fields.container === field) {
return InfraNodeType.container;
}
};
const dateMathExpressionToEpoch = (dateMathExpression: string, roundUp = false): number => {
const dateObj = DateMath.parse(dateMathExpression, { roundUp });
if (!dateObj) throw new Error(`"${dateMathExpression}" is not a valid time string`);
return dateObj.valueOf();
};
export const createNodeDetailLink = (
nodeType: InfraNodeType,
nodeId: string,
from: string,
to: string
) => {
return getNodeDetailUrl({
nodeType,
nodeId,
from: dateMathExpressionToEpoch(from),
to: dateMathExpressionToEpoch(to, true),
});
};
export const MetricsExplorerChartContextMenu = injectI18n(
({ intl, onFilter, options, series, source, timeRange }: Props) => {
const [isPopoverOpen, setPopoverState] = useState(false);
@ -52,10 +87,29 @@ export const MetricsExplorerChartContextMenu = injectI18n(
{
name: intl.formatMessage({
id: 'xpack.infra.metricsExplorer.filterByLabel',
defaultMessage: 'Add Filter',
defaultMessage: 'Add filter',
}),
icon: 'infraApp',
onClick: handleFilter,
'data-test-subj': 'metricsExplorerAction-AddFilter',
},
]
: [];
const nodeType = source && options.groupBy && fieldToNodeType(source, options.groupBy);
const viewNodeDetail = nodeType
? [
{
name: intl.formatMessage(
{
id: 'xpack.infra.metricsExplorer.viewNodeDetail',
defaultMessage: 'View metrics for {name}',
},
{ name: nodeType }
),
icon: 'infraApp',
href: createNodeDetailLink(nodeType, series.id, timeRange.from, timeRange.to),
'data-test-subj': 'metricsExplorerAction-ViewNodeMetrics',
},
]
: [];
@ -74,7 +128,9 @@ export const MetricsExplorerChartContextMenu = injectI18n(
href: tsvbUrl,
icon: 'visualizeApp',
disabled: options.metrics.length === 0,
'data-test-subj': 'metricsExplorerAction-OpenInTSVB',
},
...viewNodeDetail,
],
},
];