mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* task/management-details (#58308) Adds basic details flyout for host management page
This commit is contained in:
parent
dd64488d0d
commit
0ed3da3a90
14 changed files with 630 additions and 131 deletions
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue