Skip to content

Commit

Permalink
[8.x] [Entity Analytics][Entity Store] Refactor enablement UI (elasti…
Browse files Browse the repository at this point in the history
…c#199762) (elastic#202404)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Entity Analytics][Entity Store] Refactor enablement UI
(elastic#199762)](elastic#199762)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Tiago Vila
Verde","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-11-28T12:01:56Z","message":"[Entity
Analytics][Entity Store] Refactor enablement UI (elastic#199762)\n\n##
Summary\r\n\r\nThis PR reworks the client side enablement flow for the
Entity Store.\r\nIt's the final piece for the work tracked
in\r\nhttps://github.com/elastic/security-team/issues/10846,\r\nhttps://github.com/elastic/security-team/issues/10847
and\r\nhttps://github.com/elastic/security-team/issues/10947\r\n\r\n\r\nhttps://github.com/user-attachments/assets/bb919c3c-b8dc-4e6b-a14b-4d413f8da13f\r\n\r\n##
How to test\r\n\r\nOptionally \r\n\r\nOn a fresh kibana and es cluster
instance:\r\n1. Load up some entity analytics data via
the\r\nhttps://github.com/elastic/security-documents-generator\r\n *
Running `yarn start entity-resolution-demo --mini` is enough \r\n1.
Navigate to `Security > Dashboards > Entity Analytics`\r\n3. Click the
`Enable` entity store button\r\n4. Both Risk Score and Entity Store
toggles should be checked.\r\n* This state represents the engines we
will install, _NOT_ the current\r\nstate\r\n5. Click `Enable`\r\n6. The
modal should close and Risk Scoring should initialize first\r\n7. As
soon as risk score initialization finished, the entity
store\r\ninitialization should start\r\n* Risk score related panels
should show up while the store is\r\ninitializing\r\n8. Finally, the
Entities List panel should appear \r\n\r\n\r\n##\r\n \r\n- [x] added
cypress tests to verify the correct enablement
flow","sha":"722900e6def904ac8dbdd20f0a04b5f418393617","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["technical
debt","release_note:skip","v9.0.0","ci:cloud-deploy","Theme:
entity_analytics","Team:Entity
Analytics","backport:version","v8.18.0"],"number":199762,"url":"https://github.com/elastic/kibana/pull/199762","mergeCommit":{"message":"[Entity
Analytics][Entity Store] Refactor enablement UI (elastic#199762)\n\n##
Summary\r\n\r\nThis PR reworks the client side enablement flow for the
Entity Store.\r\nIt's the final piece for the work tracked
in\r\nhttps://github.com/elastic/security-team/issues/10846,\r\nhttps://github.com/elastic/security-team/issues/10847
and\r\nhttps://github.com/elastic/security-team/issues/10947\r\n\r\n\r\nhttps://github.com/user-attachments/assets/bb919c3c-b8dc-4e6b-a14b-4d413f8da13f\r\n\r\n##
How to test\r\n\r\nOptionally \r\n\r\nOn a fresh kibana and es cluster
instance:\r\n1. Load up some entity analytics data via
the\r\nhttps://github.com/elastic/security-documents-generator\r\n *
Running `yarn start entity-resolution-demo --mini` is enough \r\n1.
Navigate to `Security > Dashboards > Entity Analytics`\r\n3. Click the
`Enable` entity store button\r\n4. Both Risk Score and Entity Store
toggles should be checked.\r\n* This state represents the engines we
will install, _NOT_ the current\r\nstate\r\n5. Click `Enable`\r\n6. The
modal should close and Risk Scoring should initialize first\r\n7. As
soon as risk score initialization finished, the entity
store\r\ninitialization should start\r\n* Risk score related panels
should show up while the store is\r\ninitializing\r\n8. Finally, the
Entities List panel should appear \r\n\r\n\r\n##\r\n \r\n- [x] added
cypress tests to verify the correct enablement
flow","sha":"722900e6def904ac8dbdd20f0a04b5f418393617"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/199762","number":199762,"mergeCommit":{"message":"[Entity
Analytics][Entity Store] Refactor enablement UI (elastic#199762)\n\n##
Summary\r\n\r\nThis PR reworks the client side enablement flow for the
Entity Store.\r\nIt's the final piece for the work tracked
in\r\nhttps://github.com/elastic/security-team/issues/10846,\r\nhttps://github.com/elastic/security-team/issues/10847
and\r\nhttps://github.com/elastic/security-team/issues/10947\r\n\r\n\r\nhttps://github.com/user-attachments/assets/bb919c3c-b8dc-4e6b-a14b-4d413f8da13f\r\n\r\n##
How to test\r\n\r\nOptionally \r\n\r\nOn a fresh kibana and es cluster
instance:\r\n1. Load up some entity analytics data via
the\r\nhttps://github.com/elastic/security-documents-generator\r\n *
Running `yarn start entity-resolution-demo --mini` is enough \r\n1.
Navigate to `Security > Dashboards > Entity Analytics`\r\n3. Click the
`Enable` entity store button\r\n4. Both Risk Score and Entity Store
toggles should be checked.\r\n* This state represents the engines we
will install, _NOT_ the current\r\nstate\r\n5. Click `Enable`\r\n6. The
modal should close and Risk Scoring should initialize first\r\n7. As
soon as risk score initialization finished, the entity
store\r\ninitialization should start\r\n* Risk score related panels
should show up while the store is\r\ninitializing\r\n8. Finally, the
Entities List panel should appear \r\n\r\n\r\n##\r\n \r\n- [x] added
cypress tests to verify the correct enablement
flow","sha":"722900e6def904ac8dbdd20f0a04b5f418393617"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
  • Loading branch information
tiansivive authored Dec 2, 2024
1 parent 1cdc6fb commit c3ad53e
Show file tree
Hide file tree
Showing 14 changed files with 703 additions and 681 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* 2.0.
*/
import { useMemo } from 'react';
import type {
GetEntityStoreStatusResponse,
InitEntityStoreRequestBody,
InitEntityStoreResponse,
} from '../../../common/api/entity_analytics/entity_store/enablement.gen';
import type {
DeleteEntityEngineResponse,
EntityType,
Expand All @@ -20,15 +25,32 @@ export const useEntityStoreRoutes = () => {
const http = useKibana().services.http;

return useMemo(() => {
const initEntityStore = async (entityType: EntityType) => {
const enableEntityStore = async (
options: InitEntityStoreRequestBody = { fieldHistoryLength: 10 }
) => {
return http.fetch<InitEntityStoreResponse>('/api/entity_store/enable', {
method: 'POST',
version: API_VERSIONS.public.v1,
body: JSON.stringify(options),
});
};

const getEntityStoreStatus = async () => {
return http.fetch<GetEntityStoreStatusResponse>('/api/entity_store/status', {
method: 'GET',
version: API_VERSIONS.public.v1,
});
};

const initEntityEngine = async (entityType: EntityType) => {
return http.fetch<InitEntityEngineResponse>(`/api/entity_store/engines/${entityType}/init`, {
method: 'POST',
version: API_VERSIONS.public.v1,
body: JSON.stringify({}),
});
};

const stopEntityStore = async (entityType: EntityType) => {
const stopEntityEngine = async (entityType: EntityType) => {
return http.fetch<StopEntityEngineResponse>(`/api/entity_store/engines/${entityType}/stop`, {
method: 'POST',
version: API_VERSIONS.public.v1,
Expand Down Expand Up @@ -59,8 +81,10 @@ export const useEntityStoreRoutes = () => {
};

return {
initEntityStore,
stopEntityStore,
enableEntityStore,
getEntityStoreStatus,
initEntityEngine,
stopEntityEngine,
getEntityEngine,
deleteEntityEngine,
listEntityEngines,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const useIsNewRiskScoreModuleInstalled = (): RiskScoreModuleStatus => {
return { isLoading: false, installed: !!riskEngineStatus?.isNewRiskScoreModuleInstalled };
};

interface RiskEngineStatus extends RiskEngineStatusResponse {
export interface RiskEngineStatus extends RiskEngineStatusResponse {
isUpdateAvailable: boolean;
isNewRiskScoreModuleInstalled: boolean;
isNewRiskScoreModuleAvailable: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* 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, { useCallback, useState } from 'react';
import {
EuiCallOut,
EuiPanel,
EuiEmptyPrompt,
EuiLoadingLogo,
EuiToolTip,
EuiButton,
EuiImage,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { UseQueryResult } from '@tanstack/react-query';
import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
import type { StoreStatus } from '../../../../../common/api/entity_analytics';
import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics';
import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation';
import { useEnableEntityStoreMutation } from '../hooks/use_entity_store';
import {
ENABLEMENT_INITIALIZING_RISK_ENGINE,
ENABLEMENT_INITIALIZING_ENTITY_STORE,
ENABLE_ALL_TITLE,
ENABLEMENT_DESCRIPTION_BOTH,
ENABLE_RISK_SCORE_TITLE,
ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY,
ENABLE_ENTITY_STORE_TITLE,
ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY,
} from '../translations';
import type { Enablements } from './enablement_modal';
import { EntityStoreEnablementModal } from './enablement_modal';
import dashboardEnableImg from '../../../images/entity_store_dashboard.png';
import type { RiskEngineStatus } from '../../../api/hooks/use_risk_engine_status';

interface EnableEntityStorePanelProps {
state: {
riskEngine: UseQueryResult<RiskEngineStatus>;
entityStore: UseQueryResult<GetEntityStoreStatusResponse>;
};
}

export const EnablementPanel: React.FC<EnableEntityStorePanelProps> = ({ state }) => {
const riskEngineStatus = state.riskEngine.data?.risk_engine_status;
const entityStoreStatus = state.entityStore.data?.status;

const [modal, setModalState] = useState({ visible: false });
const [riskEngineInitializing, setRiskEngineInitializing] = useState(false);

const initRiskEngine = useInitRiskEngineMutation();
const storeEnablement = useEnableEntityStoreMutation();

const enableEntityStore = useCallback(
(enable: Enablements) => () => {
if (enable.riskScore) {
const options = {
onSuccess: () => {
setRiskEngineInitializing(false);
if (enable.entityStore) {
storeEnablement.mutate();
}
},
};
setRiskEngineInitializing(true);
initRiskEngine.mutate(undefined, options);
setModalState({ visible: false });
return;
}

if (enable.entityStore) {
storeEnablement.mutate();
setModalState({ visible: false });
}
},
[storeEnablement, initRiskEngine]
);

if (storeEnablement.error) {
return (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.mutation.errorTitle"
defaultMessage={'There was a problem initializing the entity store'}
/>
}
color="danger"
iconType="error"
>
<p>{storeEnablement.error.body.message}</p>
</EuiCallOut>
</>
);
}

if (riskEngineInitializing) {
return (
<EuiPanel hasBorder data-test-subj="riskEngineInitializingPanel">
<EuiEmptyPrompt
icon={<EuiLoadingLogo logo="logoElastic" size="xl" />}
title={<h2>{ENABLEMENT_INITIALIZING_RISK_ENGINE}</h2>}
/>
</EuiPanel>
);
}

if (entityStoreStatus === 'installing' || storeEnablement.isLoading) {
return (
<EuiPanel hasBorder data-test-subj="entityStoreInitializingPanel">
<EuiEmptyPrompt
icon={<EuiLoadingLogo logo="logoElastic" size="xl" />}
title={<h2>{ENABLEMENT_INITIALIZING_ENTITY_STORE}</h2>}
body={
<p>
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.initializing.description"
defaultMessage="This can take up to 5 minutes."
/>
</p>
}
/>
</EuiPanel>
);
}

if (
riskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED &&
(entityStoreStatus === 'running' || entityStoreStatus === 'stopped')
) {
return null;
}

const [title, body] = getEnablementTexts(entityStoreStatus, riskEngineStatus);
return (
<>
<EuiEmptyPrompt
css={{ minWidth: '100%' }}
hasBorder
layout="horizontal"
title={<h2>{title}</h2>}
body={<p>{body}</p>}
actions={
<EuiToolTip content={title}>
<EuiButton
color="primary"
fill
onClick={() => setModalState({ visible: true })}
data-test-subj={`entityStoreEnablementButton`}
>
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.enableButton"
defaultMessage="Enable"
/>
</EuiButton>
</EuiToolTip>
}
icon={<EuiImage size="l" hasShadow src={dashboardEnableImg} alt={title} />}
data-test-subj="entityStoreEnablementPanel"
/>

<EntityStoreEnablementModal
visible={modal.visible}
toggle={(visible) => setModalState({ visible })}
enableStore={enableEntityStore}
riskScore={{
disabled: riskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED,
checked: riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED,
}}
entityStore={{
disabled: entityStoreStatus === 'running',
checked: entityStoreStatus === 'not_installed',
}}
/>
</>
);
};

const getEnablementTexts = (
entityStoreStatus?: StoreStatus,
riskEngineStatus?: RiskEngineStatus['risk_engine_status']
): [string, string] => {
if (
(entityStoreStatus === 'not_installed' || entityStoreStatus === 'stopped') &&
riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED
) {
return [ENABLE_ALL_TITLE, ENABLEMENT_DESCRIPTION_BOTH];
}

if (riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED) {
return [ENABLE_RISK_SCORE_TITLE, ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY];
}

return [ENABLE_ENTITY_STORE_TITLE, ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 from 'react';
import {
EuiEmptyPrompt,
EuiLoadingSpinner,
EuiFlexItem,
EuiFlexGroup,
EuiPanel,
EuiCallOut,
} from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';
import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { EntitiesList } from '../entities_list';
import { useEntityStoreStatus } from '../hooks/use_entity_store';
import { EntityAnalyticsRiskScores } from '../../entity_analytics_risk_score';
import { useRiskEngineStatus } from '../../../api/hooks/use_risk_engine_status';

import { EnablementPanel } from './dashboard_enablement_panel';

const EntityStoreDashboardPanelsComponent = () => {
const riskEngineStatus = useRiskEngineStatus();
const storeStatusQuery = useEntityStoreStatus({});

const callouts = (storeStatusQuery.data?.engines ?? [])
.filter((engine) => engine.status === 'error')
.map((engine) => {
const err = engine.error as {
message: string;
};
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.errors.title"
defaultMessage={'An error occurred during entity store resource initialization'}
/>
}
color="danger"
iconType="error"
>
<p>{err?.message}</p>
</EuiCallOut>
);
});

if (storeStatusQuery.status === 'loading') {
return (
<EuiPanel hasBorder>
<EuiEmptyPrompt icon={<EuiLoadingSpinner size="xl" />} />
</EuiPanel>
);
}

return (
<EuiFlexGroup direction="column" data-test-subj="entityStorePanelsGroup">
{storeStatusQuery.status === 'error' ? (
callouts
) : (
<EnablementPanel
state={{
riskEngine: riskEngineStatus,
entityStore: storeStatusQuery,
}}
/>
)}

{riskEngineStatus.data?.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED && (
<>
<EuiFlexItem>
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.user} />
</EuiFlexItem>
<EuiFlexItem>
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.host} />
</EuiFlexItem>
</>
)}
{storeStatusQuery.data?.status !== 'not_installed' &&
storeStatusQuery.data?.status !== 'installing' && (
<EuiFlexItem data-test-subj="entitiesListPanel">
<EntitiesList />
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

export const EntityStoreDashboardPanels = React.memo(EntityStoreDashboardPanelsComponent);
EntityStoreDashboardPanels.displayName = 'EntityStoreDashboardPanels';
Loading

0 comments on commit c3ad53e

Please sign in to comment.