Skip to content

Commit

Permalink
[Logs UI] Add flyout action menu with uptime link (#36721)
Browse files Browse the repository at this point in the history
* Move log entry flyout and memoize some values

In preparation to the addition of an action menu, the log entry flyout
now lives in a directory.

* Add log entry action menu with uptime link

* Add component tests

* Remove static reference from memoization key

* Improve uptime filter value check

* Use an object as useVisibility return value
  • Loading branch information
weltenwort authored May 21, 2019
1 parent fe2e248 commit 03cef22
Show file tree
Hide file tree
Showing 6 changed files with 417 additions and 133 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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.
*/

export { LogEntryFlyout } from './log_entry_flyout';
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* 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 testSubject from '@kbn/test-subj-selector';
import React from 'react';
import { act } from 'react-dom/test-utils';

import { mountWithIntl } from '../../../utils/enzyme_helpers';
import { LogEntryActionsMenu } from './log_entry_actions_menu';

describe('LogEntryActionsMenu component', () => {
describe('uptime link', () => {
it('renders with a host ip filter when present in log entry', () => {
const elementWrapper = mountWithIntl(
<LogEntryActionsMenu
logItem={{
fields: [{ field: 'host.ip', value: 'HOST_IP' }],
id: 'ITEM_ID',
index: 'INDEX',
key: {
time: 0,
tiebreaker: 0,
},
}}
/>
);

act(() => {
elementWrapper
.find(`button${testSubject('logEntryActionsMenuButton')}`)
.last()
.simulate('click');
});
elementWrapper.update();

expect(
elementWrapper.find(`a${testSubject('uptimeLogEntryActionsMenuItem')}`).prop('href')
).toMatchInlineSnapshot(`"/app/uptime#/?search=(host.ip:HOST_IP)"`);
});

it('renders with a container id filter when present in log entry', () => {
const elementWrapper = mountWithIntl(
<LogEntryActionsMenu
logItem={{
fields: [{ field: 'container.id', value: 'CONTAINER_ID' }],
id: 'ITEM_ID',
index: 'INDEX',
key: {
time: 0,
tiebreaker: 0,
},
}}
/>
);

act(() => {
elementWrapper
.find(`button${testSubject('logEntryActionsMenuButton')}`)
.last()
.simulate('click');
});
elementWrapper.update();

expect(
elementWrapper.find(`a${testSubject('uptimeLogEntryActionsMenuItem')}`).prop('href')
).toMatchInlineSnapshot(`"/app/uptime#/?search=(container.id:CONTAINER_ID)"`);
});

it('renders with a pod uid filter when present in log entry', () => {
const elementWrapper = mountWithIntl(
<LogEntryActionsMenu
logItem={{
fields: [{ field: 'kubernetes.pod.uid', value: 'POD_UID' }],
id: 'ITEM_ID',
index: 'INDEX',
key: {
time: 0,
tiebreaker: 0,
},
}}
/>
);

act(() => {
elementWrapper
.find(`button${testSubject('logEntryActionsMenuButton')}`)
.last()
.simulate('click');
});
elementWrapper.update();

expect(
elementWrapper.find(`a${testSubject('uptimeLogEntryActionsMenuItem')}`).prop('href')
).toMatchInlineSnapshot(`"/app/uptime#/?search=(kubernetes.pod.uid:POD_UID)"`);
});

it('renders with a disjunction of filters when multiple present in log entry', () => {
const elementWrapper = mountWithIntl(
<LogEntryActionsMenu
logItem={{
fields: [
{ field: 'container.id', value: 'CONTAINER_ID' },
{ field: 'host.ip', value: 'HOST_IP' },
{ field: 'kubernetes.pod.uid', value: 'POD_UID' },
],
id: 'ITEM_ID',
index: 'INDEX',
key: {
time: 0,
tiebreaker: 0,
},
}}
/>
);

act(() => {
elementWrapper
.find(`button${testSubject('logEntryActionsMenuButton')}`)
.last()
.simulate('click');
});
elementWrapper.update();

expect(
elementWrapper.find(`a${testSubject('uptimeLogEntryActionsMenuItem')}`).prop('href')
).toMatchInlineSnapshot(
`"/app/uptime#/?search=(container.id:CONTAINER_ID OR host.ip:HOST_IP OR kubernetes.pod.uid:POD_UID)"`
);
});

it('renders as disabled when no supported field is present in log entry', () => {
const elementWrapper = mountWithIntl(
<LogEntryActionsMenu
logItem={{
fields: [],
id: 'ITEM_ID',
index: 'INDEX',
key: {
time: 0,
tiebreaker: 0,
},
}}
/>
);

act(() => {
elementWrapper
.find(`button${testSubject('logEntryActionsMenuButton')}`)
.last()
.simulate('click');
});
elementWrapper.update();

expect(
elementWrapper
.find(`button${testSubject('uptimeLogEntryActionsMenuItem')}`)
.prop('disabled')
).toEqual(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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 { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useCallback, useMemo, useState } from 'react';
import url from 'url';

import chrome from 'ui/chrome';
import { InfraLogItem } from '../../../graphql/types';

const UPTIME_FIELDS = ['container.id', 'host.ip', 'kubernetes.pod.uid'];

export const LogEntryActionsMenu: React.FunctionComponent<{
logItem: InfraLogItem;
}> = ({ logItem }) => {
const { hide, isVisible, show } = useVisibility();

const uptimeLink = useMemo(() => getUptimeLink(logItem), [logItem]);

const menuItems = useMemo(
() => [
<EuiContextMenuItem
data-test-subj="logEntryActionsMenuItem uptimeLogEntryActionsMenuItem"
disabled={!uptimeLink}
href={uptimeLink}
icon="uptimeApp"
key="uptimeLink"
>
<FormattedMessage
id="xpack.infra.logEntryActionsMenu.uptimeActionLabel"
defaultMessage="View monitor status"
/>
</EuiContextMenuItem>,
],
[uptimeLink]
);

const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]);

return (
<EuiPopover
anchorPosition="downRight"
button={
<EuiButtonEmpty
data-test-subj="logEntryActionsMenuButton"
disabled={!hasMenuItems}
iconSide="right"
iconType="arrowDown"
onClick={show}
>
<FormattedMessage
id="xpack.infra.logEntryActionsMenu.buttonLabel"
defaultMessage="Actions"
/>
</EuiButtonEmpty>
}
closePopover={hide}
id="logEntryActionsMenu"
isOpen={isVisible}
panelPaddingSize="none"
>
<EuiContextMenuPanel items={menuItems} />
</EuiPopover>
);
};

const useVisibility = (initialVisibility: boolean = false) => {
const [isVisible, setIsVisible] = useState(initialVisibility);

const hide = useCallback(() => setIsVisible(false), [setIsVisible]);
const show = useCallback(() => setIsVisible(true), [setIsVisible]);

return { hide, isVisible, show };
};

const getUptimeLink = (logItem: InfraLogItem) => {
const searchExpressions = logItem.fields
.filter(({ field, value }) => value != null && UPTIME_FIELDS.includes(field))
.map(({ field, value }) => `${field}:${value}`);

if (searchExpressions.length === 0) {
return undefined;
}

return url.format({
pathname: chrome.addBasePath('/app/uptime'),
hash: `/?search=(${searchExpressions.join(' OR ')})`,
});
};
Loading

0 comments on commit 03cef22

Please sign in to comment.