[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:
parent
3c9182f4b1
commit
d16c394028
|
@ -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()}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue