diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 8ad63fb3a94b..f393f1eff263 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -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; - }; - }; } /** diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 296587706e6a..a126cb36a86f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -38,10 +38,10 @@ interface RouterProps { } const AppRoot: React.FunctionComponent = React.memo( - ({ basename, store, coreStart: { http } }) => ( + ({ basename, store, coreStart: { http, notifications } }) => ( - - + + @@ -72,8 +72,8 @@ const AppRoot: React.FunctionComponent = React.memo( - - + + ) ); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts index e916dc66c59f..a42e23e57d10 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts @@ -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; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts index dde0ba1e96a8..6903c37d4684 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts @@ -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: '', }, }, }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts index 8d1ab391ff47..f29e90509785 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts @@ -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; let getState: typeof store['getState']; let dispatch: Dispatch; + let history: History; + // 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({ diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts index 754a855c171a..1131e8d769fc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts @@ -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 = 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 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, + }); + } + } }; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/mock_host_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/mock_host_result_list.ts new file mode 100644 index 000000000000..866e5c59329e --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/mock_host_result_list.ts @@ -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; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts index bbbbdc4d17ce..582aa6b7138c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts @@ -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 = ( 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; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts index 3dcb144c2bad..a7776f09fe2b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts @@ -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 = 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 = ['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; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index bff419b2db23..4ceb4cec23d0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -28,12 +28,24 @@ export interface ManagementListState { pageSize: number; pageIndex: number; loading: boolean; + detailsError?: ServerApiError; + details?: Immutable; + location?: Immutable; } 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 { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/details.tsx new file mode 100644 index 000000000000..9f2a73204271 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/details.tsx @@ -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 ( + <> + + + + + ); +}); + +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: ( + + ), + body: ( + + ), + toastLifeTimeMs: 10000, + }); + } + }, [error, notifications.toasts]); + + return ( + + + +

+ {details === undefined ? : details.host.hostname} +

+
+
+ + {details === undefined ? ( + <> + + + ) : ( + + )} + +
+ ); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.test.tsx new file mode 100644 index 000000000000..216e4df61b0d --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.test.tsx @@ -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; + let store: ReturnType; + + let queryByTestSubjId: ( + renderResult: reactTestingLibrary.RenderResult, + testSubjId: string + ) => Promise; + + beforeEach(async () => { + history = createMemoryHistory(); + store = appStoreFactory(coreMock.createStart(), true); + render = () => { + return reactTestingLibrary.render( + + + + + + + + + + ); + }; + + 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(); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx index 44b08f25c765..ba9a931a233b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx @@ -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 + { + ev.preventDefault(); + history.push(urlFromQueryParams({ ...queryParams, selected_host: id })); + }} + > + {hostname} + + ); + }, }, - }, - { - 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 ( - - - - - - -

- -

-
-

- - - -

-
-
- - - -
-
-
+ <> + {hasSelectedHost && } + + + + + + +

+ +

+
+

+ + + +

+
+
+ + + +
+
+
+ ); }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/url_from_query_params.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/url_from_query_params.ts new file mode 100644 index 000000000000..ea6a4c6f684a --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/url_from_query_params.ts @@ -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 { + const search = querystring.stringify(queryParams); + return { + search, + }; +}