Skip to content

Commit

Permalink
[Security Solution][Endpoint] Display empty state UI on the artifacts…
Browse files Browse the repository at this point in the history
… subtab of policy details when no trusted application exist (#113802)

* Adds new empty prom when there is no TA or non already assigned one

* Adds policy name to text message

* Fix error in tabs component

* Fix mulilangs in empty state components

* API call that checks if any TA exists with actions and reducers

* Adds current policy id and name to the empty state component instead of a fake ones

* Adds unit test for layout

* Switch empty state depending on results and added unit test

* Fix multilang keys and join code into a hook to avoid duplications

* Fix TS error

* Canges icon

* Fixes pr comments

* Fix ts error in test
  • Loading branch information
dasansol92 authored Oct 7, 2021
1 parent 280d1d8 commit 9a31e86
Show file tree
Hide file tree
Showing 15 changed files with 463 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export interface PolicyArtifactsAssignableListPageDataFilter {
payload: { filter: string };
}

export interface PolicyArtifactsDeosAnyTrustedAppExists {
type: 'policyArtifactsDeosAnyTrustedAppExists';
payload: AsyncResourceState<boolean>;
}

export interface AssignedTrustedAppsListStateChanged
extends Action<'assignedTrustedAppsListStateChanged'> {
payload: PolicyArtifactsState['assignedList'];
Expand All @@ -62,6 +67,7 @@ export type PolicyTrustedAppsAction =
| PolicyArtifactsUpdateTrustedAppsChanged
| PolicyArtifactsAssignableListExistDataChanged
| PolicyArtifactsAssignableListPageDataFilter
| PolicyArtifactsDeosAnyTrustedAppExists
| AssignedTrustedAppsListStateChanged
| PolicyDetailsListOfAllPoliciesStateChanged
| PolicyDetailsTrustedAppsForceListDataRefresh;
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
getCurrentArtifactsLocation,
isOnPolicyTrustedAppsView,
getCurrentUrlLocationPaginationParams,
getDoesAnyTrustedAppExistsIsLoading,
} from '../selectors';
import {
ImmutableArray,
Expand Down Expand Up @@ -131,6 +132,38 @@ const checkIfThereAreAssignableTrustedApps = async (
}
};

const checkIfAnyTrustedApp = async (
store: ImmutableMiddlewareAPI<PolicyDetailsState, PolicyDetailsAction>,
trustedAppsService: TrustedAppsService
) => {
const state = store.getState();
if (getDoesAnyTrustedAppExistsIsLoading(state)) {
return;
}
store.dispatch({
type: 'policyArtifactsDeosAnyTrustedAppExists',
// Ignore will be fixed with when AsyncResourceState is refactored (#830)
// @ts-ignore
payload: createLoadingResourceState({ previousState: createUninitialisedResourceState() }),
});
try {
const trustedApps = await trustedAppsService.getTrustedAppsList({
page: 1,
per_page: 100,
});

store.dispatch({
type: 'policyArtifactsDeosAnyTrustedAppExists',
payload: createLoadedResourceState(!isEmpty(trustedApps.data)),
});
} catch (err) {
store.dispatch({
type: 'policyArtifactsDeosAnyTrustedAppExists',
payload: createFailedResourceState<boolean>(err.body ?? err),
});
}
};

const searchTrustedApps = async (
store: ImmutableMiddlewareAPI<PolicyDetailsState, PolicyDetailsAction>,
trustedAppsService: TrustedAppsService,
Expand Down Expand Up @@ -285,6 +318,9 @@ const fetchPolicyTrustedAppsIfNeeded = async (
artifacts: fetchResponse,
}),
});
if (!fetchResponse.total) {
await checkIfAnyTrustedApp({ getState, dispatch }, trustedAppsService);
}
} catch (error) {
dispatch({
type: 'assignedTrustedAppsListStateChanged',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const initialPolicyDetailsState: () => Immutable<PolicyDetailsState> = ()
assignableList: createUninitialisedResourceState(),
trustedAppsToUpdate: createUninitialisedResourceState(),
assignableListEntriesExist: createUninitialisedResourceState(),
doesAnyTrustedAppExists: createUninitialisedResourceState(),
assignedList: createUninitialisedResourceState(),
policies: createUninitialisedResourceState(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ export const policyTrustedAppsReducer: ImmutableReducer<PolicyDetailsState, AppA
};
}

if (action.type === 'policyArtifactsDeosAnyTrustedAppExists') {
return {
...state,
artifacts: {
...state?.artifacts,
doesAnyTrustedAppExists: action.payload,
},
};
}
if (action.type === 'assignedTrustedAppsListStateChanged') {
return {
...state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { createSelector } from 'reselect';
import { Pagination } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import {
PolicyArtifactsState,
PolicyAssignedTrustedApps,
Expand Down Expand Up @@ -34,10 +35,11 @@ import { getCurrentArtifactsLocation } from './policy_common_selectors';
export const doesPolicyHaveTrustedApps = (
state: PolicyDetailsState
): { loading: boolean; hasTrustedApps: boolean } => {
// TODO: implement empty state (task #1645)
return {
loading: false,
hasTrustedApps: true,
loading: isLoadingResourceState(state.artifacts.assignedList),
hasTrustedApps: isLoadedResourceState(state.artifacts.assignedList)
? !isEmpty(state.artifacts.assignedList.data.artifacts.data)
: false,
};
};

Expand Down Expand Up @@ -105,6 +107,24 @@ export const getUpdateArtifacts = (
: undefined;
};

/**
* Returns does any TA exists
*/
export const getDoesTrustedAppExists = (state: Immutable<PolicyDetailsState>): boolean => {
return (
isLoadedResourceState(state.artifacts.doesAnyTrustedAppExists) &&
state.artifacts.doesAnyTrustedAppExists.data
);
};

/**
* Returns does any TA exists loading
*/
export const doesTrustedAppExistsLoading = (state: Immutable<PolicyDetailsState>): boolean => {
return isLoadingResourceState(state.artifacts.doesAnyTrustedAppExists);
};

/** Returns a boolean of whether the user is on the policy details page or not */
export const getCurrentPolicyAssignedTrustedAppsState: PolicyDetailsSelector<
PolicyArtifactsState['assignedList']
> = (state) => {
Expand Down Expand Up @@ -181,3 +201,14 @@ export const getTrustedAppsAllPoliciesById: PolicyDetailsSelector<
return mapById;
}, {}) as Immutable<Record<string, Immutable<PolicyData>>>;
});

export const getDoesAnyTrustedAppExists: PolicyDetailsSelector<
PolicyDetailsState['artifacts']['doesAnyTrustedAppExists']
> = (state) => state.artifacts.doesAnyTrustedAppExists;

export const getDoesAnyTrustedAppExistsIsLoading: PolicyDetailsSelector<boolean> = createSelector(
getDoesAnyTrustedAppExists,
(doesAnyTrustedAppExists) => {
return isLoadingResourceState(doesAnyTrustedAppExists);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export interface PolicyArtifactsState {
assignableListEntriesExist: AsyncResourceState<boolean>;
/** A list of trusted apps going to be updated */
trustedAppsToUpdate: AsyncResourceState<PostTrustedAppCreateResponse[]>;
/** Represents if there is any trusted app existing */
doesAnyTrustedAppExists: AsyncResourceState<boolean>;
/** List of artifacts currently assigned to the policy (body specific and global) */
assignedList: AsyncResourceState<PolicyAssignedTrustedApps>;
/** A list of all available polices */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ export const usePolicyTrustedAppsNotification = () => {
),
});
} else if (updateFailed) {
toasts.addSuccess(
toasts.addDanger(
i18n.translate(
'xpack.securitySolution.endpoint.policy.trustedApps.layout.flyout.toastError.text',
{
defaultMessage: 'An error occurred updating artifacts',
defaultMessage: `An error occurred updating artifacts`,
}
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,17 @@ export const PolicyTabs = React.memo(() => {
[]
);

const getInitialSelectedTab = () => {
const currentSelectedTab = useMemo(() => {
let initialTab = tabs[0];

if (isInSettingsTab) initialTab = tabs[0];
else if (isInTrustedAppsTab) initialTab = tabs[1];
else initialTab = tabs[0];
if (isInSettingsTab) {
initialTab = tabs[0];
} else if (isInTrustedAppsTab) {
initialTab = tabs[1];
}

return initialTab;
};
}, [isInSettingsTab, isInTrustedAppsTab, tabs]);

const onTabClickHandler = useCallback(
(selectedTab: EuiTabbedContentTab) => {
Expand All @@ -81,8 +83,7 @@ export const PolicyTabs = React.memo(() => {
return (
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={getInitialSelectedTab()}
autoFocus="selected"
selectedTab={currentSelectedTab}
size="l"
onTabClick={onTabClickHandler}
/>
Expand Down
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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export { PolicyTrustedAppsEmptyUnassigned } from './policy_trusted_apps_empty_unassigned';
export { PolicyTrustedAppsEmptyUnexisting } from './policy_trusted_apps_empty_unexisting';
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { memo, useCallback } from 'react';
import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { usePolicyDetailsNavigateCallback } from '../../policy_hooks';
import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks';

interface CommonProps {
policyId: string;
policyName: string;
}

export const PolicyTrustedAppsEmptyUnassigned = memo<CommonProps>(({ policyId, policyName }) => {
const navigateCallback = usePolicyDetailsNavigateCallback();
const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName);
const onClickPrimaryButtonHandler = useCallback(
() =>
navigateCallback({
show: 'list',
}),
[navigateCallback]
);
return (
<EuiPageTemplate template="centeredContent">
<EuiEmptyPrompt
iconType="plusInCircle"
data-test-subj="policy-trusted-apps-empty-unassigned"
title={
<h2>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.title"
defaultMessage="No assigned trusted applications"
/>
</h2>
}
body={
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.content"
defaultMessage="There are currently no trusted applications assigned to {policyName}. Assign trusted applications now or add and manage them on the trusted applications page."
values={{ policyName }}
/>
}
actions={[
<EuiButton color="primary" fill onClick={onClickPrimaryButtonHandler}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.primaryAction"
defaultMessage="Assign trusted applications"
/>
</EuiButton>,
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink onClick={onClickHandler} href={toRouteUrl}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.trustedApps.empty.unassigned.secondaryAction"
defaultMessage="Manage trusted applications"
/>
</EuiLink>,
]}
/>
</EuiPageTemplate>
);
});

PolicyTrustedAppsEmptyUnassigned.displayName = 'PolicyTrustedAppsEmptyUnassigned';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { memo } from 'react';
import { EuiEmptyPrompt, EuiButton, EuiPageTemplate } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks';

interface CommonProps {
policyId: string;
policyName: string;
}

export const PolicyTrustedAppsEmptyUnexisting = memo<CommonProps>(({ policyId, policyName }) => {
const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName);
return (
<EuiPageTemplate template="centeredContent">
<EuiEmptyPrompt
iconType="plusInCircle"
data-test-subj="policy-trusted-apps-empty-unexisting"
title={
<h2>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.title"
defaultMessage="No trusted applications exist"
/>
</h2>
}
body={
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.content"
defaultMessage="There are currently no trusted applications applied to your endpoints."
/>
}
actions={
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiButton color="primary" fill onClick={onClickHandler} href={toRouteUrl}>
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.trustedApps.empty.unexisting.action"
defaultMessage="Add trusted application"
/>
</EuiButton>
}
/>
</EuiPageTemplate>
);
});

PolicyTrustedAppsEmptyUnexisting.displayName = 'PolicyTrustedAppsEmptyUnexisting';
Loading

0 comments on commit 9a31e86

Please sign in to comment.