diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 308487c601a69..cec51f570f95d 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -18,6 +18,7 @@ import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; import { ManagementList } from './view/managing'; import { PolicyList } from './view/policy'; +import { PolicyDetails } from './view/policy'; import { HeaderNavigation } from './components/header_nav'; /** @@ -70,6 +71,7 @@ const AppRoot: React.FunctionComponent = React.memo( + ( globalState.policyList, policyListMiddlewareFactory(coreStart, depsStart) ), + substateMiddlewareFactory( + globalState => globalState.policyDetails, + policyDetailsMiddlewareFactory(coreStart, depsStart) + ), substateMiddlewareFactory( globalState => globalState.alertList, alertMiddlewareFactory(coreStart, depsStart) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts new file mode 100644 index 0000000000000..cf875e01a6fde --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/action.ts @@ -0,0 +1,16 @@ +/* + * 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 { PolicyData } from '../../types'; + +interface ServerReturnedPolicyDetailsData { + type: 'serverReturnedPolicyDetailsData'; + payload: { + policyItem: PolicyData | undefined; + }; +} + +export type PolicyDetailsAction = ServerReturnedPolicyDetailsData; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.ts new file mode 100644 index 0000000000000..39f0f13d2daa2 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { policyDetailsMiddlewareFactory } from './middleware'; +export { PolicyDetailsAction } from './action'; +export { policyDetailsReducer } from './reducer'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts new file mode 100644 index 0000000000000..92a1c036c0211 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/middleware.ts @@ -0,0 +1,29 @@ +/* + * 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 { MiddlewareFactory, PolicyDetailsState } from '../../types'; +import { selectPolicyIdFromParams, isOnPolicyDetailsPage } from './selectors'; + +export const policyDetailsMiddlewareFactory: MiddlewareFactory = coreStart => { + return ({ getState, dispatch }) => next => async action => { + next(action); + const state = getState(); + + if (action.type === 'userChangedUrl' && isOnPolicyDetailsPage(state)) { + const id = selectPolicyIdFromParams(state); + + const { getFakeDatasourceDetailsApiResponse } = await import('../policy_list/fake_data'); + const policyItem = await getFakeDatasourceDetailsApiResponse(id); + + dispatch({ + type: 'serverReturnedPolicyDetailsData', + payload: { + policyItem, + }, + }); + } + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts new file mode 100644 index 0000000000000..1d37e4aa24b65 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/reducer.ts @@ -0,0 +1,48 @@ +/* + * 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 { Reducer } from 'redux'; +import { PolicyDetailsState } from '../../types'; +import { AppAction } from '../action'; + +const initialPolicyDetailsState = (): PolicyDetailsState => { + return { + policyItem: { + name: '', + total: 0, + pending: 0, + failed: 0, + id: '', + created_by: '', + created: '', + updated_by: '', + updated: '', + }, + isLoading: false, + }; +}; + +export const policyDetailsReducer: Reducer = ( + state = initialPolicyDetailsState(), + action +) => { + if (action.type === 'serverReturnedPolicyDetailsData') { + return { + ...state, + ...action.payload, + isLoading: false, + }; + } + + if (action.type === 'userChangedUrl') { + return { + ...state, + location: action.payload, + }; + } + + return state; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts new file mode 100644 index 0000000000000..a08130d0f4b30 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_details/selectors.ts @@ -0,0 +1,29 @@ +/* + * 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 { createSelector } from 'reselect'; +import { PolicyDetailsState } from '../../types'; + +export const selectPolicyDetails = (state: PolicyDetailsState) => state.policyItem; + +export const isOnPolicyDetailsPage = (state: PolicyDetailsState) => { + if (state.location) { + const pathnameParts = state.location.pathname.split('/'); + return pathnameParts[1] === 'policy' && pathnameParts[2]; + } else { + return false; + } +}; + +export const selectPolicyIdFromParams: (state: PolicyDetailsState) => string = createSelector( + (state: PolicyDetailsState) => state.location, + (location: PolicyDetailsState['location']) => { + if (location) { + return location.pathname.split('/')[2]; + } + return ''; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/fake_data.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/fake_data.ts index 62bdd28f30be1..2312d3397f7be 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/fake_data.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/fake_data.ts @@ -29,25 +29,35 @@ const getRandomNumber = () => { return randomNumbers[randomIndex]; }; +const policyItem = (id: string) => { + return { + name: `policy with some protections ${id}`, + total: getRandomNumber(), + pending: getRandomNumber(), + failed: getRandomNumber(), + id: `${id}`, + created_by: `admin ABC`, + created: getRandomDateIsoString(), + updated_by: 'admin 123', + updated: getRandomDateIsoString(), + }; +}; + export const getFakeDatasourceApiResponse = async (page: number, pageSize: number) => { await new Promise(resolve => setTimeout(resolve, 500)); // Emulates the API response - see PR: // https://github.com/elastic/kibana/pull/56567/files#diff-431549a8739efe0c56763f164c32caeeR25 return { - items: Array.from({ length: pageSize }, (x, i) => ({ - name: `policy with some protections ${i + 1}`, - total: getRandomNumber(), - pending: getRandomNumber(), - failed: getRandomNumber(), - created_by: `admin ABC`, - created: getRandomDateIsoString(), - updated_by: 'admin 123', - updated: getRandomDateIsoString(), - })), + items: Array.from({ length: pageSize }, (x, i) => policyItem(`${i + 1}`)), success: true, total: pageSize * 10, page, perPage: pageSize, }; }; + +export const getFakeDatasourceDetailsApiResponse = async (id: string) => { + await new Promise(resolve => setTimeout(resolve, 500)); + return policyItem(id); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts index 3d9d21c0da9c3..e655a8d5e46db 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts @@ -9,9 +9,11 @@ import { AppAction } from './action'; import { alertListReducer } from './alerts'; import { GlobalState } from '../types'; import { policyListReducer } from './policy_list'; +import { policyDetailsReducer } from './policy_details'; export const appReducer: Reducer = combineReducers({ managementList: managementListReducer, alertList: alertListReducer, policyList: policyListReducer, + policyDetails: policyDetailsReducer, }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 3b70a580436fe..91be6e4936dbe 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -56,6 +56,7 @@ export interface PolicyData { total: number; pending: number; failed: number; + id: string; created_by: string; created: string; updated_by: string; @@ -78,10 +79,23 @@ export interface PolicyListState { isLoading: boolean; } +/** + * Policy list store state + */ +export interface PolicyDetailsState { + /** A single policy item */ + policyItem: PolicyData | undefined; + /** data is being retrieved from server */ + isLoading: boolean; + /** current location of the application */ + location?: Immutable; +} + export interface GlobalState { readonly managementList: ManagementListState; readonly alertList: AlertListState; readonly policyList: PolicyListState; + readonly policyDetails: PolicyDetailsState; } /** diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/index.ts index d561da7574de0..9c227ca81a426 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/index.ts @@ -5,3 +5,4 @@ */ export * from './policy_list'; +export * from './policy_details'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx new file mode 100644 index 0000000000000..bdbd323eaab72 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -0,0 +1,36 @@ +/* + * 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 { EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { usePolicyDetailsSelector } from './policy_hooks'; +import { selectPolicyDetails } from '../../store/policy_details/selectors'; + +export const PolicyDetails = React.memo(() => { + const policyItem = usePolicyDetailsSelector(selectPolicyDetails); + + function policyName() { + if (policyItem) { + return {policyItem.name}; + } else { + return ( + + + + ); + } + } + + return ( + +

{policyName()}

+
+ ); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_hooks.ts index 14558fb6504bb..5bfce15d680bf 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_hooks.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_hooks.ts @@ -5,8 +5,14 @@ */ import { useSelector } from 'react-redux'; -import { GlobalState, PolicyListState } from '../../types'; +import { GlobalState, PolicyListState, PolicyDetailsState } from '../../types'; export function usePolicyListSelector(selector: (state: PolicyListState) => TSelected) { return useSelector((state: GlobalState) => selector(state.policyList)); } + +export function usePolicyDetailsSelector( + selector: (state: PolicyDetailsState) => TSelected +) { + return useSelector((state: GlobalState) => selector(state.policyDetails)); +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx index 75ffa5e8806e9..cf573da3703cc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx @@ -17,6 +17,7 @@ import { EuiText, EuiTableFieldDataColumnType, EuiToolTip, + EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { @@ -28,6 +29,7 @@ import { } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { useHistory } from 'react-router-dom'; import { usePageId } from '../use_page_id'; import { selectIsLoading, @@ -70,6 +72,25 @@ const FormattedDateAndTime: React.FC<{ date: Date }> = ({ date }) => { ); }; +const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) => { + const history = useHistory(); + + return ( + { + event.preventDefault(); + history.push(route); + }} + > + {name} + + ); +}; + +const renderPolicyNameLink = (value: string, _item: PolicyData) => { + return ; +}; + const renderDate = (date: string, _item: PolicyData) => ( @@ -124,6 +145,7 @@ export const PolicyList = React.memo(() => { name: i18n.translate('xpack.endpoint.policyList.nameField', { defaultMessage: 'Policy Name', }), + render: renderPolicyNameLink, truncateText: true, }, { diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts index 0c9179c23ea6c..c6a7f723bfa2d 100644 --- a/x-pack/test/functional/apps/endpoint/index.ts +++ b/x-pack/test/functional/apps/endpoint/index.ts @@ -14,6 +14,7 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./header_nav')); loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./policy_list')); + loadTestFile(require.resolve('./policy_details')); loadTestFile(require.resolve('./alert_list')); }); } diff --git a/x-pack/test/functional/apps/endpoint/policy_details.ts b/x-pack/test/functional/apps/endpoint/policy_details.ts new file mode 100644 index 0000000000000..39b6e7a9f4fb7 --- /dev/null +++ b/x-pack/test/functional/apps/endpoint/policy_details.ts @@ -0,0 +1,24 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'endpoint']); + const testSubjects = getService('testSubjects'); + + describe('Endpoint Policy Details', function() { + this.tags(['ciGroup7']); + + it('loads the Policy Details Page', async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/policy/123'); + await testSubjects.existOrFail('policyDetailsViewTitle'); + + const policyDetailsNotFoundTitle = await testSubjects.getVisibleText('policyDetailsName'); + expect(policyDetailsNotFoundTitle).to.equal('policy with some protections 123'); + }); + }); +}