Skip to content

Commit 03cef22

Browse files
authored
[Logs UI] Add flyout action menu with uptime link (#36721)
* 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
1 parent fe2e248 commit 03cef22

File tree

6 files changed

+417
-133
lines changed

6 files changed

+417
-133
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { LogEntryFlyout } from './log_entry_flyout';
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import testSubject from '@kbn/test-subj-selector';
8+
import React from 'react';
9+
import { act } from 'react-dom/test-utils';
10+
11+
import { mountWithIntl } from '../../../utils/enzyme_helpers';
12+
import { LogEntryActionsMenu } from './log_entry_actions_menu';
13+
14+
describe('LogEntryActionsMenu component', () => {
15+
describe('uptime link', () => {
16+
it('renders with a host ip filter when present in log entry', () => {
17+
const elementWrapper = mountWithIntl(
18+
<LogEntryActionsMenu
19+
logItem={{
20+
fields: [{ field: 'host.ip', value: 'HOST_IP' }],
21+
id: 'ITEM_ID',
22+
index: 'INDEX',
23+
key: {
24+
time: 0,
25+
tiebreaker: 0,
26+
},
27+
}}
28+
/>
29+
);
30+
31+
act(() => {
32+
elementWrapper
33+
.find(`button${testSubject('logEntryActionsMenuButton')}`)
34+
.last()
35+
.simulate('click');
36+
});
37+
elementWrapper.update();
38+
39+
expect(
40+
elementWrapper.find(`a${testSubject('uptimeLogEntryActionsMenuItem')}`).prop('href')
41+
).toMatchInlineSnapshot(`"/app/uptime#/?search=(host.ip:HOST_IP)"`);
42+
});
43+
44+
it('renders with a container id filter when present in log entry', () => {
45+
const elementWrapper = mountWithIntl(
46+
<LogEntryActionsMenu
47+
logItem={{
48+
fields: [{ field: 'container.id', value: 'CONTAINER_ID' }],
49+
id: 'ITEM_ID',
50+
index: 'INDEX',
51+
key: {
52+
time: 0,
53+
tiebreaker: 0,
54+
},
55+
}}
56+
/>
57+
);
58+
59+
act(() => {
60+
elementWrapper
61+
.find(`button${testSubject('logEntryActionsMenuButton')}`)
62+
.last()
63+
.simulate('click');
64+
});
65+
elementWrapper.update();
66+
67+
expect(
68+
elementWrapper.find(`a${testSubject('uptimeLogEntryActionsMenuItem')}`).prop('href')
69+
).toMatchInlineSnapshot(`"/app/uptime#/?search=(container.id:CONTAINER_ID)"`);
70+
});
71+
72+
it('renders with a pod uid filter when present in log entry', () => {
73+
const elementWrapper = mountWithIntl(
74+
<LogEntryActionsMenu
75+
logItem={{
76+
fields: [{ field: 'kubernetes.pod.uid', value: 'POD_UID' }],
77+
id: 'ITEM_ID',
78+
index: 'INDEX',
79+
key: {
80+
time: 0,
81+
tiebreaker: 0,
82+
},
83+
}}
84+
/>
85+
);
86+
87+
act(() => {
88+
elementWrapper
89+
.find(`button${testSubject('logEntryActionsMenuButton')}`)
90+
.last()
91+
.simulate('click');
92+
});
93+
elementWrapper.update();
94+
95+
expect(
96+
elementWrapper.find(`a${testSubject('uptimeLogEntryActionsMenuItem')}`).prop('href')
97+
).toMatchInlineSnapshot(`"/app/uptime#/?search=(kubernetes.pod.uid:POD_UID)"`);
98+
});
99+
100+
it('renders with a disjunction of filters when multiple present in log entry', () => {
101+
const elementWrapper = mountWithIntl(
102+
<LogEntryActionsMenu
103+
logItem={{
104+
fields: [
105+
{ field: 'container.id', value: 'CONTAINER_ID' },
106+
{ field: 'host.ip', value: 'HOST_IP' },
107+
{ field: 'kubernetes.pod.uid', value: 'POD_UID' },
108+
],
109+
id: 'ITEM_ID',
110+
index: 'INDEX',
111+
key: {
112+
time: 0,
113+
tiebreaker: 0,
114+
},
115+
}}
116+
/>
117+
);
118+
119+
act(() => {
120+
elementWrapper
121+
.find(`button${testSubject('logEntryActionsMenuButton')}`)
122+
.last()
123+
.simulate('click');
124+
});
125+
elementWrapper.update();
126+
127+
expect(
128+
elementWrapper.find(`a${testSubject('uptimeLogEntryActionsMenuItem')}`).prop('href')
129+
).toMatchInlineSnapshot(
130+
`"/app/uptime#/?search=(container.id:CONTAINER_ID OR host.ip:HOST_IP OR kubernetes.pod.uid:POD_UID)"`
131+
);
132+
});
133+
134+
it('renders as disabled when no supported field is present in log entry', () => {
135+
const elementWrapper = mountWithIntl(
136+
<LogEntryActionsMenu
137+
logItem={{
138+
fields: [],
139+
id: 'ITEM_ID',
140+
index: 'INDEX',
141+
key: {
142+
time: 0,
143+
tiebreaker: 0,
144+
},
145+
}}
146+
/>
147+
);
148+
149+
act(() => {
150+
elementWrapper
151+
.find(`button${testSubject('logEntryActionsMenuButton')}`)
152+
.last()
153+
.simulate('click');
154+
});
155+
elementWrapper.update();
156+
157+
expect(
158+
elementWrapper
159+
.find(`button${testSubject('uptimeLogEntryActionsMenuItem')}`)
160+
.prop('disabled')
161+
).toEqual(true);
162+
});
163+
});
164+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
8+
import { FormattedMessage } from '@kbn/i18n/react';
9+
import React, { useCallback, useMemo, useState } from 'react';
10+
import url from 'url';
11+
12+
import chrome from 'ui/chrome';
13+
import { InfraLogItem } from '../../../graphql/types';
14+
15+
const UPTIME_FIELDS = ['container.id', 'host.ip', 'kubernetes.pod.uid'];
16+
17+
export const LogEntryActionsMenu: React.FunctionComponent<{
18+
logItem: InfraLogItem;
19+
}> = ({ logItem }) => {
20+
const { hide, isVisible, show } = useVisibility();
21+
22+
const uptimeLink = useMemo(() => getUptimeLink(logItem), [logItem]);
23+
24+
const menuItems = useMemo(
25+
() => [
26+
<EuiContextMenuItem
27+
data-test-subj="logEntryActionsMenuItem uptimeLogEntryActionsMenuItem"
28+
disabled={!uptimeLink}
29+
href={uptimeLink}
30+
icon="uptimeApp"
31+
key="uptimeLink"
32+
>
33+
<FormattedMessage
34+
id="xpack.infra.logEntryActionsMenu.uptimeActionLabel"
35+
defaultMessage="View monitor status"
36+
/>
37+
</EuiContextMenuItem>,
38+
],
39+
[uptimeLink]
40+
);
41+
42+
const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]);
43+
44+
return (
45+
<EuiPopover
46+
anchorPosition="downRight"
47+
button={
48+
<EuiButtonEmpty
49+
data-test-subj="logEntryActionsMenuButton"
50+
disabled={!hasMenuItems}
51+
iconSide="right"
52+
iconType="arrowDown"
53+
onClick={show}
54+
>
55+
<FormattedMessage
56+
id="xpack.infra.logEntryActionsMenu.buttonLabel"
57+
defaultMessage="Actions"
58+
/>
59+
</EuiButtonEmpty>
60+
}
61+
closePopover={hide}
62+
id="logEntryActionsMenu"
63+
isOpen={isVisible}
64+
panelPaddingSize="none"
65+
>
66+
<EuiContextMenuPanel items={menuItems} />
67+
</EuiPopover>
68+
);
69+
};
70+
71+
const useVisibility = (initialVisibility: boolean = false) => {
72+
const [isVisible, setIsVisible] = useState(initialVisibility);
73+
74+
const hide = useCallback(() => setIsVisible(false), [setIsVisible]);
75+
const show = useCallback(() => setIsVisible(true), [setIsVisible]);
76+
77+
return { hide, isVisible, show };
78+
};
79+
80+
const getUptimeLink = (logItem: InfraLogItem) => {
81+
const searchExpressions = logItem.fields
82+
.filter(({ field, value }) => value != null && UPTIME_FIELDS.includes(field))
83+
.map(({ field, value }) => `${field}:${value}`);
84+
85+
if (searchExpressions.length === 0) {
86+
return undefined;
87+
}
88+
89+
return url.format({
90+
pathname: chrome.addBasePath('/app/uptime'),
91+
hash: `/?search=(${searchExpressions.join(' OR ')})`,
92+
});
93+
};

0 commit comments

Comments
 (0)