[7.x] task/management-details (#58308) (#60014)

* task/management-details (#58308)

Adds basic details flyout for host management page
This commit is contained in:
Candace Park 2020-03-12 17:22:55 -04:00 committed by GitHub
parent dd64488d0d
commit 0ed3da3a90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 630 additions and 131 deletions

View file

@ -252,6 +252,8 @@ interface AlertMetadata {
export type AlertData = AlertEvent & AlertMetadata;
export interface EndpointMetadata {
'@timestamp': string;
host: HostFields;
event: {
created: Date;
};
@ -264,17 +266,6 @@ export interface EndpointMetadata {
version: string;
id: string;
};
host: {
id: string;
hostname: string;
ip: string[];
mac: string[];
os: {
name: string;
full: string;
version: string;
};
};
}
/**

View file

@ -38,10 +38,10 @@ interface RouterProps {
}
const AppRoot: React.FunctionComponent<RouterProps> = React.memo(
({ basename, store, coreStart: { http } }) => (
({ basename, store, coreStart: { http, notifications } }) => (
<Provider store={store}>
<KibanaContextProvider services={{ http }}>
<I18nProvider>
<I18nProvider>
<KibanaContextProvider services={{ http, notifications }}>
<BrowserRouter basename={basename}>
<RouteCapture>
<HeaderNavigation basename={basename} />
@ -72,8 +72,8 @@ const AppRoot: React.FunctionComponent<RouterProps> = React.memo(
</Switch>
</RouteCapture>
</BrowserRouter>
</I18nProvider>
</KibanaContextProvider>
</KibanaContextProvider>
</I18nProvider>
</Provider>
)
);

View file

@ -4,14 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ManagementListPagination } from '../../types';
import { EndpointResultList } from '../../../../../common/types';
import { ManagementListPagination, ServerApiError } from '../../types';
import { EndpointResultList, EndpointMetadata } from '../../../../../common/types';
interface ServerReturnedManagementList {
type: 'serverReturnedManagementList';
payload: EndpointResultList;
}
interface ServerReturnedManagementDetails {
type: 'serverReturnedManagementDetails';
payload: EndpointMetadata;
}
interface ServerFailedToReturnManagementDetails {
type: 'serverFailedToReturnManagementDetails';
payload: ServerApiError;
}
interface UserExitedManagementList {
type: 'userExitedManagementList';
}
@ -23,5 +33,7 @@ interface UserPaginatedManagementList {
export type ManagementAction =
| ServerReturnedManagementList
| ServerReturnedManagementDetails
| ServerFailedToReturnManagementDetails
| UserExitedManagementList
| UserPaginatedManagementList;

View file

@ -19,6 +19,7 @@ describe('endpoint_list store concerns', () => {
};
const generateEndpoint = (): EndpointMetadata => {
return {
'@timestamp': new Date(1582231151055).toString(),
event: {
created: new Date(0),
},
@ -40,6 +41,7 @@ describe('endpoint_list store concerns', () => {
name: '',
full: '',
version: '',
variant: '',
},
},
};

View file

@ -6,6 +6,7 @@
import { CoreStart, HttpSetup } from 'kibana/public';
import { applyMiddleware, createStore, Dispatch, Store } from 'redux';
import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { History, createBrowserHistory } from 'history';
import { managementListReducer, managementMiddlewareFactory } from './index';
import { EndpointMetadata, EndpointResultList } from '../../../../../common/types';
import { ManagementListState } from '../../types';
@ -18,9 +19,12 @@ describe('endpoint list saga', () => {
let store: Store<ManagementListState>;
let getState: typeof store['getState'];
let dispatch: Dispatch<AppAction>;
let history: History<never>;
// https://github.com/elastic/endpoint-app-team/issues/131
const generateEndpoint = (): EndpointMetadata => {
return {
'@timestamp': new Date(1582231151055).toString(),
event: {
created: new Date(0),
},
@ -42,6 +46,7 @@ describe('endpoint list saga', () => {
name: '',
full: '',
version: '',
variant: '',
},
},
};
@ -63,12 +68,20 @@ describe('endpoint list saga', () => {
);
getState = store.getState;
dispatch = store.dispatch;
history = createBrowserHistory();
});
test('it handles `userNavigatedToPage`', async () => {
test('it handles `userChangedUrl`', async () => {
const apiResponse = getEndpointListApiResponse();
fakeHttpServices.post.mockResolvedValue(apiResponse);
expect(fakeHttpServices.post).not.toHaveBeenCalled();
dispatch({ type: 'userNavigatedToPage', payload: 'managementPage' });
dispatch({
type: 'userChangedUrl',
payload: {
...history.location,
pathname: '/management',
},
});
await sleep();
expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', {
body: JSON.stringify({

View file

@ -5,19 +5,28 @@
*/
import { MiddlewareFactory } from '../../types';
import { pageIndex, pageSize } from './selectors';
import {
pageIndex,
pageSize,
isOnManagementPage,
hasSelectedHost,
uiQueryParams,
} from './selectors';
import { ManagementListState } from '../../types';
import { AppAction } from '../action';
export const managementMiddlewareFactory: MiddlewareFactory<ManagementListState> = coreStart => {
return ({ getState, dispatch }) => next => async (action: AppAction) => {
next(action);
const state = getState();
if (
(action.type === 'userNavigatedToPage' && action.payload === 'managementPage') ||
(action.type === 'userChangedUrl' &&
isOnManagementPage(state) &&
hasSelectedHost(state) !== true) ||
action.type === 'userPaginatedManagementList'
) {
const managementPageIndex = pageIndex(getState());
const managementPageSize = pageSize(getState());
const managementPageIndex = pageIndex(state);
const managementPageSize = pageSize(state);
const response = await coreStart.http.post('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [
@ -32,5 +41,20 @@ export const managementMiddlewareFactory: MiddlewareFactory<ManagementListState>
payload: response,
});
}
if (action.type === 'userChangedUrl' && hasSelectedHost(state) !== false) {
const { selected_host: selectedHost } = uiQueryParams(state);
try {
const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`);
dispatch({
type: 'serverReturnedManagementDetails',
payload: response,
});
} catch (error) {
dispatch({
type: 'serverFailedToReturnManagementDetails',
payload: error,
});
}
}
};
};

View file

@ -0,0 +1,63 @@
/*
* 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 { EndpointResultList } from '../../../../../common/types';
export const mockHostResultList: (options?: {
total?: number;
request_page_size?: number;
request_page_index?: number;
}) => EndpointResultList = (options = {}) => {
const {
total = 1,
request_page_size: requestPageSize = 10,
request_page_index: requestPageIndex = 0,
} = options;
// Skip any that are before the page we're on
const numberToSkip = requestPageSize * requestPageIndex;
// total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0
const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0);
const endpoints = [];
for (let index = 0; index < actualCountToReturn; index++) {
endpoints.push({
'@timestamp': new Date(1582231151055).toString(),
event: {
created: new Date('2020-02-20T20:39:11.055Z'),
},
endpoint: {
policy: {
id: '00000000-0000-0000-0000-000000000000',
},
},
agent: {
version: '6.9.2',
id: '9a87fdac-e6c0-4f27-a25c-e349e7093cb1',
},
host: {
id: '3ca26fe5-1c7d-42b8-8763-98256d161c9f',
hostname: 'bea-0.example.com',
ip: ['10.154.150.114', '10.43.37.62', '10.217.73.149'],
mac: ['ea-5a-a8-c0-5-95', '7e-d8-fe-7f-b6-4e', '23-31-5d-af-e6-2b'],
os: {
name: 'windows 6.2',
full: 'Windows Server 2012',
version: '6.2',
variant: 'Windows Server Release 2',
},
},
});
}
const mock: EndpointResultList = {
endpoints,
total,
request_page_size: requestPageSize,
request_page_index: requestPageIndex,
};
return mock;
};

View file

@ -15,6 +15,9 @@ const initialState = (): ManagementListState => {
pageIndex: 0,
total: 0,
loading: false,
detailsError: undefined,
details: undefined,
location: undefined,
};
};
@ -37,18 +40,30 @@ export const managementListReducer: Reducer<ManagementListState, AppAction> = (
pageIndex,
loading: false,
};
}
if (action.type === 'userExitedManagementList') {
} else if (action.type === 'serverReturnedManagementDetails') {
return {
...state,
details: action.payload,
};
} else if (action.type === 'serverFailedToReturnManagementDetails') {
return {
...state,
detailsError: action.payload,
};
} else if (action.type === 'userExitedManagementList') {
return initialState();
}
if (action.type === 'userPaginatedManagementList') {
} else if (action.type === 'userPaginatedManagementList') {
return {
...state,
...action.payload,
loading: true,
};
} else if (action.type === 'userChangedUrl') {
return {
...state,
location: action.payload,
detailsError: undefined,
};
}
return state;

View file

@ -3,8 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ManagementListState } from '../../types';
import querystring from 'querystring';
import { createSelector } from 'reselect';
import { Immutable } from '../../../../../common/types';
import { ManagementListState, ManagingIndexUIQueryParams } from '../../types';
export const listData = (state: ManagementListState) => state.endpoints;
@ -15,3 +17,44 @@ export const pageSize = (state: ManagementListState) => state.pageSize;
export const totalHits = (state: ManagementListState) => state.total;
export const isLoading = (state: ManagementListState) => state.loading;
export const detailsError = (state: ManagementListState) => state.detailsError;
export const detailsData = (state: ManagementListState) => {
return state.details;
};
export const isOnManagementPage = (state: ManagementListState) =>
state.location ? state.location.pathname === '/management' : false;
export const uiQueryParams: (
state: ManagementListState
) => Immutable<ManagingIndexUIQueryParams> = createSelector(
(state: ManagementListState) => state.location,
(location: ManagementListState['location']) => {
const data: ManagingIndexUIQueryParams = {};
if (location) {
// Removes the `?` from the beginning of query string if it exists
const query = querystring.parse(location.search.slice(1));
const keys: Array<keyof ManagingIndexUIQueryParams> = ['selected_host'];
for (const key of keys) {
const value = query[key];
if (typeof value === 'string') {
data[key] = value;
} else if (Array.isArray(value)) {
data[key] = value[value.length - 1];
}
}
}
return data;
}
);
export const hasSelectedHost: (state: ManagementListState) => boolean = createSelector(
uiQueryParams,
({ selected_host: selectedHost }) => {
return selectedHost !== undefined;
}
);

View file

@ -28,12 +28,24 @@ export interface ManagementListState {
pageSize: number;
pageIndex: number;
loading: boolean;
detailsError?: ServerApiError;
details?: Immutable<EndpointMetadata>;
location?: Immutable<EndpointAppLocation>;
}
export interface ManagementListPagination {
pageIndex: number;
pageSize: number;
}
export interface ManagingIndexUIQueryParams {
selected_host?: string;
}
export interface ServerApiError {
statusCode: number;
error: string;
message: string;
}
// REFACTOR to use Types from Ingest Manager - see: https://github.com/elastic/endpoint-app-team/issues/150
export interface PolicyData {

View file

@ -0,0 +1,159 @@
/*
* 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 React, { useCallback, useMemo, memo, useEffect } from 'react';
import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiDescriptionList,
EuiLoadingContent,
EuiHorizontalRule,
EuiSpacer,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { useManagementListSelector } from './hooks';
import { urlFromQueryParams } from './url_from_query_params';
import { uiQueryParams, detailsData, detailsError } from './../../store/managing/selectors';
const HostDetails = memo(() => {
const details = useManagementListSelector(detailsData);
if (details === undefined) {
return null;
}
const detailsResultsUpper = useMemo(() => {
return [
{
title: i18n.translate('xpack.endpoint.management.details.os', {
defaultMessage: 'OS',
}),
description: details.host.os.full,
},
{
title: i18n.translate('xpack.endpoint.management.details.lastSeen', {
defaultMessage: 'Last Seen',
}),
description: details['@timestamp'],
},
{
title: i18n.translate('xpack.endpoint.management.details.alerts', {
defaultMessage: 'Alerts',
}),
description: '0',
},
];
}, [details]);
const detailsResultsLower = useMemo(() => {
return [
{
title: i18n.translate('xpack.endpoint.management.details.policy', {
defaultMessage: 'Policy',
}),
description: details.endpoint.policy.id,
},
{
title: i18n.translate('xpack.endpoint.management.details.policyStatus', {
defaultMessage: 'Policy Status',
}),
description: 'active',
},
{
title: i18n.translate('xpack.endpoint.management.details.ipAddress', {
defaultMessage: 'IP Address',
}),
description: details.host.ip,
},
{
title: i18n.translate('xpack.endpoint.management.details.hostname', {
defaultMessage: 'Hostname',
}),
description: details.host.hostname,
},
{
title: i18n.translate('xpack.endpoint.management.details.sensorVersion', {
defaultMessage: 'Sensor Version',
}),
description: details.agent.version,
},
];
}, [details.agent.version, details.endpoint.policy.id, details.host.hostname, details.host.ip]);
return (
<>
<EuiDescriptionList
type="column"
listItems={detailsResultsUpper}
data-test-subj="managementDetailsUpperList"
/>
<EuiHorizontalRule margin="s" />
<EuiDescriptionList
type="column"
listItems={detailsResultsLower}
data-test-subj="managementDetailsLowerList"
/>
</>
);
});
export const ManagementDetails = () => {
const history = useHistory();
const { notifications } = useKibana();
const queryParams = useManagementListSelector(uiQueryParams);
const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams;
const details = useManagementListSelector(detailsData);
const error = useManagementListSelector(detailsError);
const handleFlyoutClose = useCallback(() => {
history.push(urlFromQueryParams(queryParamsWithoutSelectedHost));
}, [history, queryParamsWithoutSelectedHost]);
useEffect(() => {
if (error !== undefined) {
notifications.toasts.danger({
title: (
<FormattedMessage
id="xpack.endpoint.managementDetails.errorTitle"
defaultMessage="Could not find host"
/>
),
body: (
<FormattedMessage
id="xpack.endpoint.managementDetails.errorBody"
defaultMessage="Please exit the flyout and select an available host."
/>
),
toastLifeTimeMs: 10000,
});
}
}, [error, notifications.toasts]);
return (
<EuiFlyout onClose={handleFlyoutClose} data-test-subj="managementDetailsFlyout">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2 data-test-subj="managementDetailsTitle">
{details === undefined ? <EuiLoadingContent lines={1} /> : details.host.hostname}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{details === undefined ? (
<>
<EuiLoadingContent lines={3} /> <EuiSpacer size="l" /> <EuiLoadingContent lines={3} />
</>
) : (
<HostDetails />
)}
</EuiFlyoutBody>
</EuiFlyout>
);
};

View file

@ -0,0 +1,123 @@
/*
* 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 React from 'react';
import * as reactTestingLibrary from '@testing-library/react';
import { Provider } from 'react-redux';
import { I18nProvider } from '@kbn/i18n/react';
import { appStoreFactory } from '../../store';
import { coreMock } from 'src/core/public/mocks';
import { RouteCapture } from '../route_capture';
import { createMemoryHistory, MemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import { AppAction } from '../../types';
import { ManagementList } from './index';
import { mockHostResultList } from '../../store/managing/mock_host_result_list';
describe('when on the managing page', () => {
let render: () => reactTestingLibrary.RenderResult;
let history: MemoryHistory<never>;
let store: ReturnType<typeof appStoreFactory>;
let queryByTestSubjId: (
renderResult: reactTestingLibrary.RenderResult,
testSubjId: string
) => Promise<Element | null>;
beforeEach(async () => {
history = createMemoryHistory<never>();
store = appStoreFactory(coreMock.createStart(), true);
render = () => {
return reactTestingLibrary.render(
<Provider store={store}>
<I18nProvider>
<Router history={history}>
<RouteCapture>
<ManagementList />
</RouteCapture>
</Router>
</I18nProvider>
</Provider>
);
};
queryByTestSubjId = async (renderResult, testSubjId) => {
return await reactTestingLibrary.waitForElement(
() => document.body.querySelector(`[data-test-subj="${testSubjId}"]`),
{
container: renderResult.container,
}
);
};
});
it('should show a table', async () => {
const renderResult = render();
const table = await queryByTestSubjId(renderResult, 'managementListTable');
expect(table).not.toBeNull();
});
describe('when there is no selected host in the url', () => {
it('should not show the flyout', () => {
const renderResult = render();
expect.assertions(1);
return queryByTestSubjId(renderResult, 'managementDetailsFlyout').catch(e => {
expect(e).not.toBeNull();
});
});
describe('when data loads', () => {
beforeEach(() => {
reactTestingLibrary.act(() => {
const action: AppAction = {
type: 'serverReturnedManagementList',
payload: mockHostResultList(),
};
store.dispatch(action);
});
});
it('should render the management summary row in the table', async () => {
const renderResult = render();
const rows = await renderResult.findAllByRole('row');
expect(rows).toHaveLength(2);
});
describe('when the user clicks the hostname in the table', () => {
let renderResult: reactTestingLibrary.RenderResult;
beforeEach(async () => {
renderResult = render();
const detailsLink = await queryByTestSubjId(renderResult, 'hostnameCellLink');
if (detailsLink) {
reactTestingLibrary.fireEvent.click(detailsLink);
}
});
it('should show the flyout', () => {
return queryByTestSubjId(renderResult, 'managementDetailsFlyout').then(flyout => {
expect(flyout).not.toBeNull();
});
});
});
});
});
describe('when there is a selected host in the url', () => {
beforeEach(() => {
reactTestingLibrary.act(() => {
history.push({
...history.location,
search: '?selected_host=1',
});
});
});
it('should show the flyout', () => {
const renderResult = render();
return queryByTestSubjId(renderResult, 'managementDetailsFlyout').then(flyout => {
expect(flyout).not.toBeNull();
});
});
});
});

View file

@ -6,6 +6,7 @@
import React, { useMemo, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import {
EuiPage,
EuiPageBody,
@ -16,26 +17,30 @@ import {
EuiTitle,
EuiBasicTable,
EuiTextColor,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { createStructuredSelector } from 'reselect';
import { ManagementDetails } from './details';
import * as selectors from '../../store/managing/selectors';
import { ManagementAction } from '../../store/managing/action';
import { useManagementListSelector } from './hooks';
import { usePageId } from '../use_page_id';
import { CreateStructuredSelector } from '../../types';
import { urlFromQueryParams } from './url_from_query_params';
const selector = (createStructuredSelector as CreateStructuredSelector)(selectors);
export const ManagementList = () => {
usePageId('managementPage');
const dispatch = useDispatch<(a: ManagementAction) => void>();
const history = useHistory();
const {
listData,
pageIndex,
pageSize,
totalHits: totalItemCount,
isLoading,
uiQueryParams: queryParams,
hasSelectedHost,
} = useManagementListSelector(selector);
const paginationSetup = useMemo(() => {
@ -59,109 +64,129 @@ export const ManagementList = () => {
[dispatch]
);
const columns = [
{
field: 'host.hostname',
name: i18n.translate('xpack.endpoint.management.list.host', {
defaultMessage: 'Hostname',
}),
},
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.policy', {
defaultMessage: 'Policy',
}),
render: () => {
return 'Policy Name';
const columns = useMemo(() => {
return [
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.host', {
defaultMessage: 'Hostname',
}),
render: ({ host: { hostname, id } }: { host: { hostname: string; id: string } }) => {
return (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink
data-test-subj="hostnameCellLink"
href={'?' + urlFromQueryParams({ ...queryParams, selected_host: id }).search}
onClick={(ev: React.MouseEvent) => {
ev.preventDefault();
history.push(urlFromQueryParams({ ...queryParams, selected_host: id }));
}}
>
{hostname}
</EuiLink>
);
},
},
},
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.policyStatus', {
defaultMessage: 'Policy Status',
}),
render: () => {
return 'Policy Status';
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.policy', {
defaultMessage: 'Policy',
}),
render: () => {
return 'Policy Name';
},
},
},
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.alerts', {
defaultMessage: 'Alerts',
}),
render: () => {
return '0';
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.policyStatus', {
defaultMessage: 'Policy Status',
}),
render: () => {
return 'Policy Status';
},
},
},
{
field: 'host.os.name',
name: i18n.translate('xpack.endpoint.management.list.os', {
defaultMessage: 'Operating System',
}),
},
{
field: 'host.ip',
name: i18n.translate('xpack.endpoint.management.list.ip', {
defaultMessage: 'IP Address',
}),
},
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.sensorVersion', {
defaultMessage: 'Sensor Version',
}),
render: () => {
return 'version';
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.alerts', {
defaultMessage: 'Alerts',
}),
render: () => {
return '0';
},
},
},
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.lastActive', {
defaultMessage: 'Last Active',
}),
render: () => {
return 'xxxx';
{
field: 'host.os.name',
name: i18n.translate('xpack.endpoint.management.list.os', {
defaultMessage: 'Operating System',
}),
},
},
];
{
field: 'host.ip',
name: i18n.translate('xpack.endpoint.management.list.ip', {
defaultMessage: 'IP Address',
}),
},
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.sensorVersion', {
defaultMessage: 'Sensor Version',
}),
render: () => {
return 'version';
},
},
{
field: '',
name: i18n.translate('xpack.endpoint.management.list.lastActive', {
defaultMessage: 'Last Active',
}),
render: () => {
return 'xxxx';
},
},
];
}, [queryParams, history]);
return (
<EuiPage>
<EuiPageBody>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2 data-test-subj="managementViewTitle">
<FormattedMessage
id="xpack.endpoint.managementList.hosts"
defaultMessage="Hosts"
/>
</h2>
</EuiTitle>
<h4>
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.endpoint.managementList.totalCount"
defaultMessage="{totalItemCount} Hosts"
values={{ totalItemCount }}
/>
</EuiTextColor>
</h4>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiBasicTable
data-test-subj="managementListTable"
items={listData}
columns={columns}
loading={isLoading}
pagination={paginationSetup}
onChange={onTableChange}
/>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
<>
{hasSelectedHost && <ManagementDetails />}
<EuiPage>
<EuiPageBody>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2 data-test-subj="managementViewTitle">
<FormattedMessage
id="xpack.endpoint.managementList.hosts"
defaultMessage="Hosts"
/>
</h2>
</EuiTitle>
<h4>
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.endpoint.managementList.totalCount"
defaultMessage="{totalItemCount} Hosts"
values={{ totalItemCount }}
/>
</EuiTextColor>
</h4>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiBasicTable
data-test-subj="managementListTable"
items={listData}
columns={columns}
loading={isLoading}
pagination={paginationSetup}
onChange={onTableChange}
/>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</>
);
};

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;
* you may not use this file except in compliance with the Elastic License.
*/
import querystring from 'querystring';
import { EndpointAppLocation, ManagingIndexUIQueryParams } from '../../types';
export function urlFromQueryParams(
queryParams: ManagingIndexUIQueryParams
): Partial<EndpointAppLocation> {
const search = querystring.stringify(queryParams);
return {
search,
};
}