Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Endpoint] add policy details route #59951

Merged
merged 12 commits into from
Mar 13, 2020
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -70,6 +71,7 @@ const AppRoot: React.FunctionComponent<RouterProps> = React.memo(
<Route path="/management" component={ManagementList} />
<Route path="/alerts" component={AlertIndex} />
<Route path="/policy" exact component={PolicyList} />
<Route path="/policy/:id" exact component={PolicyDetails} />
<Route
render={() => (
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,11 @@ import { ManagementAction } from './managing';
import { AlertAction } from './alerts';
import { RoutingAction } from './routing';
import { PolicyListAction } from './policy_list';
import { PolicyDetailsAction } from './policy_details';

export type AppAction = ManagementAction | AlertAction | RoutingAction | PolicyListAction;
export type AppAction =
| ManagementAction
| AlertAction
| RoutingAction
| PolicyListAction
| PolicyDetailsAction;
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { appReducer } from './reducer';
import { alertMiddlewareFactory } from './alerts/middleware';
import { managementMiddlewareFactory } from './managing';
import { policyListMiddlewareFactory } from './policy_list';
import { policyDetailsMiddlewareFactory } from './policy_details';
import { GlobalState } from '../types';
import { AppAction } from './action';
import { EndpointPluginStartDependencies } from '../../../plugin';
Expand Down Expand Up @@ -75,6 +76,10 @@ export const appStoreFactory: (middlewareDeps?: {
globalState => globalState.policyList,
policyListMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
globalState => globalState.policyDetails,
policyDetailsMiddlewareFactory(coreStart, depsStart)
),
substateMiddlewareFactory(
globalState => globalState.alertList,
alertMiddlewareFactory(coreStart, depsStart)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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<PolicyDetailsState> = 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,
},
});
}
};
};
Original file line number Diff line number Diff line change
@@ -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<PolicyDetailsState, AppAction> = (
state = initialPolicyDetailsState(),
action
) => {
if (action.type === 'serverReturnedPolicyDetailsData') {
return {
...state,
...action.payload,
isLoading: false,
};
}

if (action.type === 'userChangedUrl') {
return {
...state,
location: action.payload,
};
}

return state;
};
Original file line number Diff line number Diff line change
@@ -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(
paul-tavares marked this conversation as resolved.
Show resolved Hide resolved
(state: PolicyDetailsState) => state.location,
(location: PolicyDetailsState['location']) => {
if (location) {
return location.pathname.split('/')[2];
}
return '';
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<GlobalState, AppAction> = combineReducers({
managementList: managementListReducer,
alertList: alertListReducer,
policyList: policyListReducer,
policyDetails: policyDetailsReducer,
});
13 changes: 13 additions & 0 deletions x-pack/plugins/endpoint/public/applications/endpoint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface PolicyData {
total: number;
pending: number;
failed: number;
id: string;
created_by: string;
created: string;
updated_by: string;
Expand All @@ -78,10 +79,22 @@ export interface PolicyListState {
isLoading: boolean;
}

/**
* Policy list store state
*/
export interface PolicyDetailsState {
/** Array of policy items */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is is supposed to be an array of PolicyData or a single one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a single one, you caught my c/p. Corrected.

policyItem: PolicyData | undefined;
/** data is being retrieved from server */
isLoading: boolean;
location?: Immutable<EndpointAppLocation>;
}

export interface GlobalState {
readonly managementList: ManagementListState;
readonly alertList: AlertListState;
readonly policyList: PolicyListState;
readonly policyDetails: PolicyDetailsState;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
*/

export * from './policy_list';
export * from './policy_details';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { 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 <span data-test-subj="policyDetailsName">{policyItem.name}</span>;
} else {
return <span data-test-subj="policyDetailsNotFound">Policy Not Found</span>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should i18n this message

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, done

}
}

return (
<EuiTitle size="l">
<h1 data-test-subj="policyDetailsViewTitle">{policyName()}</h1>
</EuiTitle>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
*/

import { useSelector } from 'react-redux';
import { GlobalState, PolicyListState } from '../../types';
import { GlobalState, PolicyListState, PolicyDetailsState } from '../../types';

export function usePolicyListSelector<TSelected>(selector: (state: PolicyListState) => TSelected) {
return useSelector((state: GlobalState) => selector(state.policyList));
}

export function usePolicyDetailsSelector<TSelected>(
selector: (state: PolicyDetailsState) => TSelected
) {
return useSelector((state: GlobalState) => selector(state.policyDetails));
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
EuiText,
EuiTableFieldDataColumnType,
EuiToolTip,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
Expand All @@ -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,
Expand Down Expand Up @@ -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 (
<EuiLink
onClick={(event: React.MouseEvent) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also add href so that user can open in new window/tab or copy url. maybe on next PR :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paul-tavares I implemented this locally, looks like EuiLink doesn't accept both href and onClick

image

We'll have to revisit that, I'll talk with EUI team

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah - I remember hitting that recently in ingest. Fortunately, I ended up not needing it there since that project is using hash router.
/cc me on the discussion with EUI if you remember

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i had to do something like this for the endpoint list, which i put right above the eui component
// eslint-disable-next-line @elastic/eui/href-or-on-click

event.preventDefault();
history.push(route);
}}
>
{name}
</EuiLink>
);
};

const renderPolicyNameLink = (value: string, _item: PolicyData) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i noticed this in Paul's code too, what does the _item do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it gives you access to the all of the data that the table columns are based on, in this case the PolicyData

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to expand on @parkiino 's comment. The confusion might be in that in my commit I was actually NOT using the item data (thus why I named it with a _), but needed to define the parameter in order to get around a TS error - thread here (slack EUI channel) for reference

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinlog do you remember when i had that type error on the endpoint details pr? Is this another way to solve that?

return <PolicyLink name={value} route={`/policy/${_item.id}`} />;
};

const renderDate = (date: string, _item: PolicyData) => (
<TruncateTooltipText>
<EuiToolTip content={date}>
Expand Down Expand Up @@ -124,6 +145,7 @@ export const PolicyList = React.memo(() => {
name: i18n.translate('xpack.endpoint.policyList.nameField', {
defaultMessage: 'Policy Name',
}),
render: renderPolicyNameLink,
truncateText: true,
},
{
Expand Down
1 change: 1 addition & 0 deletions x-pack/test/functional/apps/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
}
Loading