[Infrastructure UI] Show processes on a single hosts (#153866)

Closes #150907

## Summary
This PR adds the Processes tab to the single host flyout. The component
is already implemented in Inventory so we can reuse it here

## Testing
- Go to hosts view
- Open the flyout for any host to see the single host details
- Click on the processes tab 
⚠️ If you want to see the processes summary on top (where the total
processes are displayed) you need inside your metricbeat modules yml
configuration to include the `process_summary` so your config should
include:
```
metricbeat.modules:
  - module: system
    metricsets:
      ..... other data  .......
      - process # Per process metrics
      - process_summary # Process summary
    ..... other data  .......
```
<img width="1913" alt="image"
src="https://user-images.githubusercontent.com/14139027/228534978-c38437e4-4279-4ad4-9fc8-5222cbd15c2e.png">

---------

Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
jennypavlova 2023-04-03 19:24:11 +02:00 committed by GitHub
parent c6b54d18c6
commit ef4afdd64d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 26828 additions and 70 deletions

View file

@ -5,39 +5,54 @@
* 2.0.
*/
import React, { useMemo, useState } from 'react';
import React from 'react';
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui';
import { MetadataTab } from './metadata/metadata';
import { useLazyRef } from '../../../../../hooks/use_lazy_ref';
import { metadataTab } from './metadata';
import type { InventoryItemType } from '../../../../../../common/inventory_models/types';
import type { HostNodeRow } from '../../hooks/use_hosts_table';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search';
import { processesTab } from './processes';
import { Metadata } from './metadata/metadata';
import { Processes } from './processes/processes';
import { FlyoutTabIds, useHostFlyoutOpen } from '../../hooks/use_host_flyout_open_url_state';
interface Props {
node: HostNodeRow;
closeFlyout: () => void;
}
const flyoutTabs = [MetadataTab];
const flyoutTabs = [metadataTab, processesTab];
const NODE_TYPE = 'host' as InventoryItemType;
export const Flyout = ({ node, closeFlyout }: Props) => {
const { getDateRangeAsTimestamp } = useUnifiedSearchContext();
const tabs = useMemo(() => {
const currentTimeRange = {
...getDateRangeAsTimestamp(),
interval: '1m',
};
const currentTimeRange = {
...getDateRangeAsTimestamp(),
interval: '1m',
};
return flyoutTabs.map((m) => {
const TabContent = m.content;
return {
...m,
content: <TabContent node={node} currentTimeRange={currentTimeRange} />,
};
});
}, [getDateRangeAsTimestamp, node]);
const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen();
const [selectedTab, setSelectedTab] = useState(0);
// This map allow to keep track of which tabs content have been rendered the first time.
// We need it in order to load a tab content only if it gets clicked, and then keep it in the DOM for performance improvement.
const renderedTabsSet = useLazyRef(() => new Set([hostFlyoutOpen.selectedTabId]));
const tabEntries = flyoutTabs.map((tab) => (
<EuiTab
{...tab}
key={tab.id}
onClick={() => {
renderedTabsSet.current.add(tab.id); // On a tab click, mark the tab content as allowed to be rendered
setHostFlyoutOpen({ selectedTabId: tab.id });
}}
isSelected={tab.id === hostFlyoutOpen.selectedTabId}
>
{tab.name}
</EuiTab>
));
return (
<EuiFlyout onClose={closeFlyout} ownFocus={false}>
@ -47,14 +62,21 @@ export const Flyout = ({ node, closeFlyout }: Props) => {
</EuiTitle>
<EuiSpacer size="s" />
<EuiTabs style={{ marginBottom: '-25px' }} size="s">
{tabs.map((tab, i) => (
<EuiTab key={tab.id} isSelected={i === selectedTab} onClick={() => setSelectedTab(i)}>
{tab.name}
</EuiTab>
))}
{tabEntries}
</EuiTabs>
</EuiFlyoutHeader>
<EuiFlyoutBody>{tabs[selectedTab].content}</EuiFlyoutBody>
<EuiFlyoutBody>
{renderedTabsSet.current.has(FlyoutTabIds.METADATA) && (
<div hidden={hostFlyoutOpen.selectedTabId !== FlyoutTabIds.METADATA}>
<Metadata currentTimeRange={currentTimeRange} node={node} nodeType={NODE_TYPE} />
</div>
)}
{renderedTabsSet.current.has(FlyoutTabIds.PROCESSES) && (
<div hidden={hostFlyoutOpen.selectedTabId !== FlyoutTabIds.PROCESSES}>
<Processes node={node} nodeType={NODE_TYPE} currentTime={currentTimeRange.to} />
</div>
)}
</EuiFlyoutBody>
</EuiFlyout>
);
};

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { FlyoutTabIds } from '../../../hooks/use_host_flyout_open_url_state';
export const metadataTab = {
id: FlyoutTabIds.METADATA,
name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', {
defaultMessage: 'Metadata',
}),
'data-test-subj': 'hostsView-flyout-tabs-metadata',
};

View file

@ -23,8 +23,9 @@ const metadataProps: TabProps = {
to: 1679585836087,
interval: '1m',
},
nodeType: 'host',
node: {
uuid: 'b9c06da0-b9b0-4c7e-830c-05128cb34396',
id: 'host-1-0',
name: 'host-1',
os: 'iOS',
title: {

View file

@ -19,22 +19,21 @@ import { getAllFields } from './utils';
import type { HostNodeRow } from '../../../hooks/use_hosts_table';
import type { MetricsTimeInput } from '../../../../metric_detail/hooks/use_metrics_time';
const NODE_TYPE = 'host' as InventoryItemType;
export interface TabProps {
currentTimeRange: MetricsTimeInput;
node: HostNodeRow;
nodeType: InventoryItemType;
}
export const Metadata = ({ node, currentTimeRange }: TabProps) => {
export const Metadata = ({ node, currentTimeRange, nodeType }: TabProps) => {
const nodeId = node.name;
const inventoryModel = findInventoryModel(NODE_TYPE);
const inventoryModel = findInventoryModel(nodeType);
const { sourceId } = useSourceContext();
const {
loading: metadataLoading,
error,
metadata,
} = useMetadata(nodeId, NODE_TYPE, inventoryModel.requiredMetrics, sourceId, currentTimeRange);
} = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, currentTimeRange);
const fields = useMemo(() => getAllFields(metadata), [metadata]);
@ -102,12 +101,3 @@ const LoadingPlaceholder = () => {
</div>
);
};
export const MetadataTab = {
id: 'metadata',
name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', {
defaultMessage: 'Metadata',
}),
content: Metadata,
'data-test-subj': 'hostsView-flyout-tabs-metadata',
};

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { FlyoutTabIds } from '../../../hooks/use_host_flyout_open_url_state';
export const processesTab = {
id: FlyoutTabIds.PROCESSES,
name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', {
defaultMessage: 'Processes',
}),
'data-test-subj': 'hostsView-flyout-tabs-processes',
};

View file

@ -0,0 +1,181 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo, useState, useCallback } from 'react';
import { debounce } from 'lodash';
import { i18n } from '@kbn/i18n';
import {
EuiSearchBar,
EuiSpacer,
EuiEmptyPrompt,
EuiButton,
EuiText,
EuiIconTip,
Query,
} from '@elastic/eui';
import type { InventoryItemType } from '../../../../../../../common/inventory_models/types';
import { getFieldByType } from '../../../../../../../common/inventory_models';
import { parseSearchString } from '../../../../inventory_view/components/node_details/tabs/processes/parse_search_string';
import { ProcessesTable } from '../../../../inventory_view/components/node_details/tabs/processes/processes_table';
import { STATE_NAMES } from '../../../../inventory_view/components/node_details/tabs/processes/states';
import { SummaryTable } from '../../../../inventory_view/components/node_details/tabs/processes/summary_table';
import { TabContent } from '../../../../inventory_view/components/node_details/tabs/shared';
import {
SortBy,
useProcessList,
ProcessListContextProvider,
} from '../../../../inventory_view/hooks/use_process_list';
import type { HostNodeRow } from '../../../hooks/use_hosts_table';
import { useHostFlyoutOpen } from '../../../hooks/use_host_flyout_open_url_state';
interface ProcessesProps {
node: HostNodeRow;
nodeType: InventoryItemType;
currentTime: number;
}
const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({
value,
view,
}));
export const Processes = ({ currentTime, node, nodeType }: ProcessesProps) => {
const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen();
const [searchBarState, setSearchBarState] = useState<Query>(() =>
hostFlyoutOpen.searchFilter ? Query.parse(hostFlyoutOpen.searchFilter) : Query.MATCH_ALL
);
const [sortBy, setSortBy] = useState<SortBy>({
name: 'cpu',
isAscending: false,
});
const hostTerm = useMemo(() => {
const field = getFieldByType(nodeType) ?? nodeType;
return { [field]: node.name };
}, [node, nodeType]);
const {
loading,
error,
response,
makeRequest: reload,
} = useProcessList(hostTerm, currentTime, sortBy, parseSearchString(hostFlyoutOpen.searchFilter));
const debouncedSearchOnChange = useMemo(
() =>
debounce<(queryText: string) => void>(
(queryText) => setHostFlyoutOpen({ searchFilter: queryText }),
500
),
[setHostFlyoutOpen]
);
const searchBarOnChange = useCallback(
({ query, queryText }) => {
setSearchBarState(query);
debouncedSearchOnChange(queryText);
},
[setSearchBarState, debouncedSearchOnChange]
);
const clearSearchBar = useCallback(() => {
setSearchBarState(Query.MATCH_ALL);
setHostFlyoutOpen({ searchFilter: '' });
}, [setHostFlyoutOpen]);
return (
<TabContent>
<ProcessListContextProvider hostTerm={hostTerm} to={currentTime}>
<SummaryTable
isLoading={loading}
processSummary={(!error ? response?.summary : null) ?? { total: 0 }}
/>
<EuiSpacer size="m" />
<EuiText>
<h4>
{i18n.translate('xpack.infra.metrics.nodeDetails.processesHeader', {
defaultMessage: 'Top processes',
})}{' '}
<EuiIconTip
aria-label={i18n.translate(
'xpack.infra.metrics.nodeDetails.processesHeader.tooltipLabel',
{
defaultMessage: 'More info',
}
)}
size="m"
type="iInCircle"
content={i18n.translate(
'xpack.infra.metrics.nodeDetails.processesHeader.tooltipBody',
{
defaultMessage:
'The table below aggregates the top CPU and top memory consuming processes. It does not display all processes.',
}
)}
/>
</h4>
</EuiText>
<EuiSpacer size="m" />
<EuiSearchBar
query={searchBarState}
onChange={searchBarOnChange}
box={{
incremental: true,
placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', {
defaultMessage: 'Search for processes…',
}),
}}
filters={[
{
type: 'field_value_selection',
field: 'state',
name: 'State',
operator: 'exact',
multiSelect: false,
options,
},
]}
/>
<EuiSpacer size="m" />
{!error ? (
<ProcessesTable
currentTime={currentTime}
isLoading={loading || !response}
processList={response?.processList ?? []}
sortBy={sortBy}
setSortBy={setSortBy}
clearSearchBar={clearSearchBar}
/>
) : (
<EuiEmptyPrompt
iconType="warning"
title={
<h4>
{i18n.translate('xpack.infra.metrics.nodeDetails.processListError', {
defaultMessage: 'Unable to load process data',
})}
</h4>
}
actions={
<EuiButton
data-test-subj="infraTabComponentTryAgainButton"
color="primary"
fill
onClick={reload}
>
{i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', {
defaultMessage: 'Try again',
})}
</EuiButton>
}
/>
)}
</ProcessListContextProvider>
</TabContent>
);
};

View file

@ -22,12 +22,10 @@ export const HostsTable = () => {
const { onSubmit, searchCriteria } = useUnifiedSearchContext();
const [properties, setProperties] = useTableProperties();
const { columns, items, isFlyoutOpen, closeFlyout, clickedItemUuid } = useHostsTable(hostNodes, {
const { columns, items, isFlyoutOpen, closeFlyout, clickedItem } = useHostsTable(hostNodes, {
time: searchCriteria.dateRange,
});
const clickedItem = items.find(({ uuid }) => uuid === clickedItemUuid);
const noData = items.length === 0;
const onTableChange = useCallback(

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as rt from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import { useUrlState } from '../../../../utils/use_url_state';
export enum FlyoutTabIds {
METADATA = 'metadata',
PROCESSES = 'processes',
}
export const GET_DEFAULT_TABLE_PROPERTIES = {
clickedItemId: '',
selectedTabId: FlyoutTabIds.METADATA,
searchFilter: '',
};
const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'hostFlyoutOpen';
type Action = rt.TypeOf<typeof ActionRT>;
type SetNewHostFlyoutOpen = (newProps: Action) => void;
export const useHostFlyoutOpen = (): [HostFlyoutOpen, SetNewHostFlyoutOpen] => {
const [urlState, setUrlState] = useUrlState<HostFlyoutOpen>({
defaultState: GET_DEFAULT_TABLE_PROPERTIES,
decodeUrlState,
encodeUrlState,
urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY,
});
const setHostFlyoutOpen = (newProps: Action) => setUrlState({ ...urlState, ...newProps });
return [urlState, setHostFlyoutOpen];
};
const FlyoutTabIdRT = rt.union([rt.literal('metadata'), rt.literal('processes')]);
const ClickedItemIdRT = rt.string;
const SearchFilterRT = rt.string;
const SetFlyoutTabId = rt.partial({
selectedTabId: FlyoutTabIdRT,
});
const SetClickedItemIdRT = rt.partial({
clickedItemId: ClickedItemIdRT,
});
const SetSearchFilterRT = rt.partial({
searchFilter: SearchFilterRT,
});
const ActionRT = rt.intersection([SetClickedItemIdRT, SetFlyoutTabId, SetSearchFilterRT]);
const HostFlyoutOpenRT = rt.type({
clickedItemId: ClickedItemIdRT,
selectedTabId: FlyoutTabIdRT,
searchFilter: SearchFilterRT,
});
type HostFlyoutOpen = rt.TypeOf<typeof HostFlyoutOpenRT>;
const encodeUrlState = HostFlyoutOpenRT.encode;
const decodeUrlState = (value: unknown) => {
return pipe(HostFlyoutOpenRT.decode(value), fold(constant(undefined), identity));
};

View file

@ -9,10 +9,6 @@ import { useHostsTable } from './use_hosts_table';
import { renderHook } from '@testing-library/react-hooks';
import { SnapshotNode } from '../../../../../common/http_api';
jest.mock('uuid', () => ({
v4: () => 'uuidv4',
}));
describe('useHostTable hook', () => {
it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => {
const nodes: SnapshotNode[] = [
@ -77,7 +73,7 @@ describe('useHostTable hook', () => {
{
name: 'host-0',
os: '-',
uuid: 'uuidv4',
id: 'host-0-0',
title: {
cloudProvider: 'aws',
name: 'host-0',
@ -107,7 +103,7 @@ describe('useHostTable hook', () => {
{
name: 'host-1',
os: 'macOS',
uuid: 'uuidv4',
id: 'host-1-1',
title: {
cloudProvider: null,
name: 'host-1',

View file

@ -5,11 +5,10 @@
* 2.0.
*/
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { EuiBasicTableColumn, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TimeRange } from '@kbn/es-query';
import { v4 as uuidv4 } from 'uuid';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter';
@ -19,6 +18,7 @@ import type {
SnapshotNodeMetric,
SnapshotMetricInput,
} from '../../../../../common/http_api';
import { useHostFlyoutOpen } from './use_host_flyout_open_url_state';
/**
* Columns and items types
@ -34,7 +34,7 @@ export interface HostNodeRow extends HostMetrics {
servicesOnHost?: number | null;
title: { name: string; cloudProvider?: CloudProvider | null };
name: string;
uuid: string;
id: string;
}
interface HostTableParams {
@ -49,8 +49,8 @@ const formatMetric = (type: SnapshotMetricInput['type'], value: number | undefin
};
const buildItemsList = (nodes: SnapshotNode[]) => {
return nodes.map(({ metrics, path, name }) => ({
uuid: uuidv4(),
return nodes.map(({ metrics, path, name }, index) => ({
id: `${name}-${index}`,
name,
os: path.at(-1)?.os ?? '-',
title: {
@ -123,10 +123,9 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams)
services: { telemetry },
} = useKibanaContextForPlugin();
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
const [clickedItemUuid, setClickedItemUuid] = useState(() => uuidv4());
const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen();
const closeFlyout = () => setIsFlyoutOpen(false);
const closeFlyout = () => setHostFlyoutOpen({ clickedItemId: '' });
const reportHostEntryClick = useCallback(
({ name, cloudProvider }: HostNodeRow['title']) => {
@ -139,25 +138,35 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams)
);
const items = useMemo(() => buildItemsList(nodes), [nodes]);
const clickedItem = useMemo(
() => items.find(({ id }) => id === hostFlyoutOpen.clickedItemId),
[hostFlyoutOpen.clickedItemId, items]
);
const columns: Array<EuiBasicTableColumn<HostNodeRow>> = useMemo(
() => [
{
name: '',
width: '40px',
field: 'uuid',
field: 'id',
actions: [
{
name: toggleDialogActionLabel,
description: toggleDialogActionLabel,
icon: ({ uuid }) => (isFlyoutOpen && uuid === clickedItemUuid ? 'minimize' : 'expand'),
icon: ({ id }) =>
hostFlyoutOpen.clickedItemId && id === hostFlyoutOpen.clickedItemId
? 'minimize'
: 'expand',
type: 'icon',
onClick: ({ uuid }) => {
setClickedItemUuid(uuid);
if (isFlyoutOpen && uuid === clickedItemUuid) {
setIsFlyoutOpen(false);
'data-test-subj': 'hostsView-flyout-button',
onClick: ({ id }) => {
setHostFlyoutOpen({
clickedItemId: id,
});
if (id === hostFlyoutOpen.clickedItemId) {
setHostFlyoutOpen({ clickedItemId: '' });
} else {
setIsFlyoutOpen(true);
setHostFlyoutOpen({ clickedItemId: id });
}
},
},
@ -233,8 +242,14 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams)
align: 'right',
},
],
[clickedItemUuid, isFlyoutOpen, reportHostEntryClick, time]
[hostFlyoutOpen.clickedItemId, reportHostEntryClick, setHostFlyoutOpen, time]
);
return { columns, items, isFlyoutOpen, closeFlyout, clickedItemUuid };
return {
columns,
items,
clickedItem,
isFlyoutOpen: !!hostFlyoutOpen.clickedItemId,
closeFlyout,
};
};

View file

@ -155,7 +155,7 @@ export const ProcessesTable = ({
return (
<>
<EuiTable responsive={false}>
<EuiTable data-test-subj="infraProcessesTable" responsive={false}>
<EuiTableHeader>
<EuiTableHeaderCell width={24} />
{columns.map((column) => (

View file

@ -44,8 +44,8 @@ export const SummaryTable = ({ processSummary, isLoading }: Props) => {
<>
<EuiFlexGroup gutterSize="m" responsive={false} wrap={true}>
{Object.entries(processCount).map(([field, value]) => (
<EuiFlexItem>
<EuiDescriptionList compressed={true}>
<EuiFlexItem key={field}>
<EuiDescriptionList data-test-subj="infraProcessesSummaryTableItem" compressed>
<ColumnTitle>{columnTitles[field as keyof SummaryRecord]}</ColumnTitle>
<EuiDescriptionListDescription>
{value === -1 ? <LoadingSpinner /> : value}

View file

@ -24,6 +24,8 @@ export const DATES = {
withoutData: '10/09/2018 10:00:00 PM',
min: '2018-10-17T19:42:21.208Z',
max: '2018-10-17T19:58:03.952Z',
processesDataStartDate: '2023-03-28T18:20:00.000Z',
processesDataEndDate: '2023-03-28T18:21:00.000Z',
},
stream: {
startWithData: '2018-10-17T19:42:22.000Z',

View file

@ -15,6 +15,8 @@ import { DATES, HOSTS_LINK_LOCAL_STORAGE_KEY, HOSTS_VIEW_PATH } from './constant
const START_DATE = moment.utc(DATES.metricsAndLogs.hosts.min);
const END_DATE = moment.utc(DATES.metricsAndLogs.hosts.max);
const START_HOST_PROCESSES_DATE = moment.utc(DATES.metricsAndLogs.hosts.processesDataStartDate);
const END_HOST_PROCESSES_DATE = moment.utc(DATES.metricsAndLogs.hosts.processesDataEndDate);
const timepickerFormat = 'MMM D, YYYY @ HH:mm:ss.SSS';
const tableEntries = [
@ -155,6 +157,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await Promise.all([
esArchiver.load('x-pack/test/functional/es_archives/infra/alerts'),
esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'),
esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_hosts_processes'),
kibanaServer.savedObjects.cleanStandardList(),
]);
});
@ -162,6 +165,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
after(() => {
esArchiver.unload('x-pack/test/functional/es_archives/infra/alerts');
esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_hosts_processes');
browser.removeLocalStorageItem(HOSTS_LINK_LOCAL_STORAGE_KEY);
});
@ -217,6 +221,67 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
describe('#Single host Flyout', () => {
before(async () => {
await setHostViewEnabled(true);
await loginWithReadOnlyUser();
await pageObjects.common.navigateToApp(HOSTS_VIEW_PATH);
await pageObjects.timePicker.setAbsoluteRange(
START_HOST_PROCESSES_DATE.format(timepickerFormat),
END_HOST_PROCESSES_DATE.format(timepickerFormat)
);
await pageObjects.infraHostsView.clickTableOpenFlyoutButton();
});
after(async () => {
await pageObjects.infraHostsView.clickCloseFlyoutButton();
await logoutAndDeleteReadOnlyUser();
});
it('should render metadata tab', async () => {
const metadataTab = await pageObjects.infraHostsView.getMetadataTabName();
expect(metadataTab).to.contain('Metadata');
});
describe('should render processes tab', async () => {
const processTitles = [
'Total processes',
'Running',
'Sleeping',
'Dead',
'Stopped',
'Idle',
'Zombie',
'Unknown',
];
processTitles.forEach((value, index) => {
it(`Render title: ${value}`, async () => {
await pageObjects.infraHostsView.clickProcessesFlyoutTab();
const processesTitleValue =
await pageObjects.infraHostsView.getProcessesTabContentTitle(index);
const processValue = await processesTitleValue.getVisibleText();
expect(processValue).to.eql(value);
});
});
it('should render processes total value', async () => {
await pageObjects.infraHostsView.clickProcessesFlyoutTab();
const processesTotalValue =
await pageObjects.infraHostsView.getProcessesTabContentTotalValue();
const processValue = await processesTotalValue.getVisibleText();
expect(processValue).to.eql('313');
});
it('should render processes table', async () => {
await pageObjects.infraHostsView.clickProcessesFlyoutTab();
await pageObjects.infraHostsView.getProcessesTable();
await pageObjects.infraHostsView.getProcessesTableBody();
await pageObjects.infraHostsView.clickProcessesTableExpandButton();
});
});
});
describe('#Page Content', () => {
before(async () => {
await setHostViewEnabled(true);

File diff suppressed because it is too large Load diff

View file

@ -21,11 +21,19 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
},
async clickTableOpenFlyoutButton() {
return await testSubjects.click('hostsView-flyout-button');
return testSubjects.click('hostsView-flyout-button');
},
async clickCloseFlyoutButton() {
return await testSubjects.click('euiFlyoutCloseButton');
return testSubjects.click('euiFlyoutCloseButton');
},
async clickProcessesFlyoutTab() {
return testSubjects.click('hostsView-flyout-tabs-processes');
},
async clickProcessesTableExpandButton() {
return testSubjects.click('infraProcessRowButton');
},
async getHostsLandingPageDisabled() {
@ -123,7 +131,26 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
async getMetadataTabName() {
const tabElement = await this.getMetadataTab();
const tabTitle = await tabElement.findByClassName('euiTab__content');
return await tabTitle.getVisibleText();
return tabTitle.getVisibleText();
},
async getProcessesTabContentTitle(index: number) {
const processesListElements = await testSubjects.findAll('infraProcessesSummaryTableItem');
return processesListElements[index].findByCssSelector('dt');
},
async getProcessesTabContentTotalValue() {
const processesListElements = await testSubjects.findAll('infraProcessesSummaryTableItem');
return processesListElements[0].findByCssSelector('dd');
},
getProcessesTable() {
return testSubjects.find('infraProcessesTable');
},
async getProcessesTableBody() {
const processesTable = await this.getProcessesTable();
return processesTable.findByCssSelector('tbody');
},
// Logs Tab