Skip to content

Commit dd3ea9c

Browse files
authored
[7.x] [Metrics UI] Add Process tab to Enhanced Node Details (#83477) (#83668)
1 parent 5d0917f commit dd3ea9c

File tree

20 files changed

+1163
-48
lines changed

20 files changed

+1163
-48
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 * from './process_list';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 * as rt from 'io-ts';
8+
import { MetricsAPITimerangeRT, MetricsAPISeriesRT } from '../metrics_api';
9+
10+
export const ProcessListAPIRequestRT = rt.type({
11+
hostTerm: rt.record(rt.string, rt.string),
12+
timerange: MetricsAPITimerangeRT,
13+
indexPattern: rt.string,
14+
});
15+
16+
export const ProcessListAPIResponseRT = rt.array(MetricsAPISeriesRT);
17+
18+
export type ProcessListAPIRequest = rt.TypeOf<typeof ProcessListAPIRequestRT>;
19+
20+
export type ProcessListAPIResponse = rt.TypeOf<typeof ProcessListAPIResponseRT>;

x-pack/plugins/infra/common/http_api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './metrics_explorer';
1111
export * from './metrics_api';
1212
export * from './log_alerts';
1313
export * from './snapshot_api';
14+
export * from './host_details';

x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export const BottomDrawer: React.FC<{
3838
<BottomActionContainer ref={isOpen ? measureRef : null} isOpen={isOpen}>
3939
<BottomActionTopBar ref={isOpen ? null : measureRef}>
4040
<EuiFlexItem grow={false}>
41-
<ShowHideButton iconType={isOpen ? 'arrowDown' : 'arrowRight'} onClick={onClick}>
41+
<ShowHideButton
42+
aria-expanded={isOpen}
43+
iconType={isOpen ? 'arrowDown' : 'arrowRight'}
44+
onClick={onClick}
45+
>
4246
{isOpen ? hideHistory : showHistory}
4347
</ShowHideButton>
4448
</EuiFlexItem>

x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { EuiTabbedContent } from '@elastic/eui';
7+
import { EuiPortal, EuiTabs, EuiTab, EuiPanel, EuiTitle } from '@elastic/eui';
88
import { FormattedMessage } from '@kbn/i18n/react';
9-
import { EuiPanel } from '@elastic/eui';
10-
import React, { CSSProperties, useMemo } from 'react';
11-
import { EuiText } from '@elastic/eui';
9+
import React, { CSSProperties, useMemo, useState } from 'react';
1210
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
1311
import { euiStyled } from '../../../../../../../observability/public';
1412
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib';
@@ -17,6 +15,7 @@ import { MetricsTab } from './tabs/metrics';
1715
import { LogsTab } from './tabs/logs';
1816
import { ProcessesTab } from './tabs/processes';
1917
import { PropertiesTab } from './tabs/properties';
18+
import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN, OVERLAY_HEADER_SIZE } from './tabs/shared';
2019

2120
interface Props {
2221
isOpen: boolean;
@@ -48,46 +47,63 @@ export const NodeContextPopover = ({
4847
});
4948
}, [tabConfigs, node, nodeType, currentTime, options]);
5049

50+
const [selectedTab, setSelectedTab] = useState(0);
51+
5152
if (!isOpen) {
5253
return null;
5354
}
5455

5556
return (
56-
<EuiPanel hasShadow={true} paddingSize={'none'} style={panelStyle}>
57-
<OverlayHeader>
58-
<EuiFlexGroup alignItems={'center'}>
59-
<EuiFlexItem grow={true}>
60-
<EuiText>
61-
<h4>{node.name}</h4>
62-
</EuiText>
63-
</EuiFlexItem>
64-
<EuiFlexItem grow={false}>
65-
<EuiButtonEmpty onClick={onClose} iconType={'cross'}>
66-
<FormattedMessage id="xpack.infra.infra.nodeDetails.close" defaultMessage="Close" />
67-
</EuiButtonEmpty>
68-
</EuiFlexItem>
69-
</EuiFlexGroup>
70-
</OverlayHeader>
71-
<EuiTabbedContent tabs={tabs} />
72-
</EuiPanel>
57+
<EuiPortal>
58+
<EuiPanel hasShadow={true} paddingSize={'none'} style={panelStyle}>
59+
<OverlayHeader>
60+
<OverlayHeaderTitleWrapper>
61+
<EuiFlexItem grow={true}>
62+
<EuiTitle size="s">
63+
<h4>{node.name}</h4>
64+
</EuiTitle>
65+
</EuiFlexItem>
66+
<EuiFlexItem grow={false}>
67+
<EuiButtonEmpty onClick={onClose} iconType={'cross'}>
68+
<FormattedMessage id="xpack.infra.infra.nodeDetails.close" defaultMessage="Close" />
69+
</EuiButtonEmpty>
70+
</EuiFlexItem>
71+
</OverlayHeaderTitleWrapper>
72+
<EuiTabs>
73+
{tabs.map((tab, i) => (
74+
<EuiTab key={tab.id} isSelected={i === selectedTab} onClick={() => setSelectedTab(i)}>
75+
{tab.name}
76+
</EuiTab>
77+
))}
78+
</EuiTabs>
79+
</OverlayHeader>
80+
{tabs[selectedTab].content}
81+
</EuiPanel>
82+
</EuiPortal>
7383
);
7484
};
7585

7686
const OverlayHeader = euiStyled.div`
7787
border-color: ${(props) => props.theme.eui.euiBorderColor};
7888
border-bottom-width: ${(props) => props.theme.eui.euiBorderWidthThick};
79-
padding: ${(props) => props.theme.eui.euiSizeS};
8089
padding-bottom: 0;
8190
overflow: hidden;
91+
background-color: ${(props) => props.theme.eui.euiColorLightestShade};
92+
height: ${OVERLAY_HEADER_SIZE}px;
93+
`;
94+
95+
const OverlayHeaderTitleWrapper = euiStyled(EuiFlexGroup).attrs({ alignItems: 'center' })`
96+
padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) =>
97+
props.theme.eui.paddingSizes.m} 0;
8298
`;
8399

84100
const panelStyle: CSSProperties = {
85101
position: 'absolute',
86102
right: 10,
87-
top: -100,
103+
top: OVERLAY_Y_START,
88104
width: '50%',
89-
maxWidth: 600,
105+
maxWidth: 730,
90106
zIndex: 2,
91-
height: '50vh',
107+
height: `calc(100vh - ${OVERLAY_Y_START + OVERLAY_BOTTOM_MARGIN}px)`,
92108
overflow: 'hidden',
93109
};

x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 React, { useMemo, useState } from 'react';
8+
import { i18n } from '@kbn/i18n';
9+
import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton, Query } from '@elastic/eui';
10+
import { useProcessList } from '../../../../hooks/use_process_list';
11+
import { TabContent, TabProps } from '../shared';
12+
import { STATE_NAMES } from './states';
13+
import { SummaryTable } from './summary_table';
14+
import { ProcessesTable } from './processes_table';
15+
16+
const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => {
17+
const [searchFilter, setSearchFilter] = useState<Query>(EuiSearchBar.Query.MATCH_ALL);
18+
19+
const hostTerm = useMemo(() => {
20+
const field =
21+
options.fields && Reflect.has(options.fields, nodeType)
22+
? Reflect.get(options.fields, nodeType)
23+
: nodeType;
24+
return { [field]: node.name };
25+
}, [options, node, nodeType]);
26+
27+
const { loading, error, response, makeRequest: reload } = useProcessList(
28+
hostTerm,
29+
'metricbeat-*',
30+
options.fields!.timestamp,
31+
currentTime
32+
);
33+
34+
if (error) {
35+
return (
36+
<TabContent>
37+
<EuiEmptyPrompt
38+
iconType="tableDensityNormal"
39+
title={
40+
<h4>
41+
{i18n.translate('xpack.infra.metrics.nodeDetails.processListError', {
42+
defaultMessage: 'Unable to show process data',
43+
})}
44+
</h4>
45+
}
46+
actions={
47+
<EuiButton color="primary" fill onClick={reload}>
48+
{i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', {
49+
defaultMessage: 'Try again',
50+
})}
51+
</EuiButton>
52+
}
53+
/>
54+
</TabContent>
55+
);
56+
}
57+
58+
return (
59+
<TabContent>
60+
<SummaryTable isLoading={loading} processList={response ?? []} />
61+
<EuiSpacer size="m" />
62+
<EuiSearchBar
63+
query={searchFilter}
64+
onChange={({ query }) => setSearchFilter(query ?? EuiSearchBar.Query.MATCH_ALL)}
65+
box={{
66+
incremental: true,
67+
placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', {
68+
defaultMessage: 'Search for processes…',
69+
}),
70+
}}
71+
filters={[
72+
{
73+
type: 'field_value_selection',
74+
field: 'state',
75+
name: 'State',
76+
operator: 'exact',
77+
multiSelect: false,
78+
options: Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({
79+
value,
80+
view,
81+
})),
82+
},
83+
]}
84+
/>
85+
<EuiSpacer size="m" />
86+
<ProcessesTable
87+
currentTime={currentTime}
88+
isLoading={loading || !response}
89+
processList={response ?? []}
90+
searchFilter={searchFilter}
91+
/>
92+
</TabContent>
93+
);
94+
};
95+
96+
export const ProcessesTab = {
97+
id: 'processes',
98+
name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', {
99+
defaultMessage: 'Processes',
100+
}),
101+
content: TabComponent,
102+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 { ProcessListAPIResponse } from '../../../../../../../../common/http_api';
8+
import { Process } from './types';
9+
10+
export const parseProcessList = (processList: ProcessListAPIResponse) =>
11+
processList.map((process) => {
12+
const command = process.id;
13+
let mostRecentPoint;
14+
for (let i = process.rows.length - 1; i >= 0; i--) {
15+
const point = process.rows[i];
16+
if (point && Array.isArray(point.meta) && point.meta?.length) {
17+
mostRecentPoint = point;
18+
break;
19+
}
20+
}
21+
if (!mostRecentPoint) return { command, cpu: null, memory: null, startTime: null, state: null };
22+
23+
const { cpu, memory } = mostRecentPoint;
24+
const { system, process: processMeta, user } = (mostRecentPoint.meta as any[])[0];
25+
const startTime = system.process.cpu.start_time;
26+
const state = system.process.state;
27+
28+
const timeseries = {
29+
cpu: pickTimeseries(process.rows, 'cpu'),
30+
memory: pickTimeseries(process.rows, 'memory'),
31+
};
32+
33+
return {
34+
command,
35+
cpu,
36+
memory,
37+
startTime,
38+
state,
39+
pid: processMeta.pid,
40+
user: user.name,
41+
timeseries,
42+
} as Process;
43+
});
44+
45+
const pickTimeseries = (rows: any[], metricID: string) => ({
46+
rows: rows.map((row) => ({
47+
timestamp: row.timestamp,
48+
metric_0: row[metricID],
49+
})),
50+
columns: [
51+
{ name: 'timestamp', type: 'date' },
52+
{ name: 'metric_0', type: 'number' },
53+
],
54+
id: metricID,
55+
});

0 commit comments

Comments
 (0)