[Security Solution] [Endpoint] Show es_connection error in fleet agent details (#140167)

* Shows es_connection error in fleet agent details. Include new extension point for generic package errors list

* Use filter instead of for loop

* Updates wrong description comment

* Rename variable and use a new one to improve readability

* Updates data-test-subj names

* Update x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts

Co-authored-by: Joe Peeples <joe.peeples@elastic.co>

* Remove useMemo and fix typo in test-subj name

* Remove temporary url in order to remove UI link until we get the final troubleshooting url

* Update x-pack/plugins/security_solution/public/management/components/package_action_item/package_action_formatter.ts

Co-authored-by: Joe Peeples <joe.peeples@elastic.co>

* Uses filter instead of for loop to get the agent components

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Joe Peeples <joe.peeples@elastic.co>
This commit is contained in:
David Sánchez 2022-09-13 10:50:53 +02:00 committed by GitHub
parent e490ca9e61
commit f821e6e2de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 301 additions and 3 deletions

View file

@ -360,6 +360,10 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
macos_system_ext: `${SECURITY_SOLUTION_DOCS}deploy-elastic-endpoint.html#system-extension-endpoint`,
linux_deadlock: `${SECURITY_SOLUTION_DOCS}ts-management.html#linux-deadlock`,
},
packageActionTroubleshooting: {
// TODO: Pending to be updated when docs are ready
es_connection: '',
},
responseActions: `${SECURITY_SOLUTION_DOCS}response-actions.html`,
},
query: {

View file

@ -264,6 +264,9 @@ export interface DocLinks {
macos_system_ext: string;
linux_deadlock: string;
};
readonly packageActionTroubleshooting: {
es_connection: string;
};
readonly threatIntelInt: string;
readonly responseActions: string;
};

View file

@ -81,6 +81,7 @@ interface AgentBase {
user_provided_metadata: AgentMetadata;
local_metadata: AgentMetadata;
tags?: string[];
components?: FleetServerAgentComponent[];
}
export interface Agent extends AgentBase {
@ -121,7 +122,7 @@ export interface ActionStatus {
}
// Generated from FleetServer schema.json
interface FleetServerAgentComponentUnit {
export interface FleetServerAgentComponentUnit {
id: string;
type: 'input' | 'output';
status: FleetServerAgentComponentStatus;

View file

@ -44,6 +44,12 @@ export class Plugin {
view: 'package-policy-response',
Component: getLazyEndpointPolicyResponseExtension(core, plugins),
});
registerExtension({
package: 'endpoint',
view: 'package-generic-errors-list',
Component: getLazyEndpointGenericErrorsListExtension(core, plugins),
});
}
//...
}

View file

@ -20,11 +20,13 @@ import {
EuiBadge,
useEuiTheme,
} from '@elastic/eui';
import { filter } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import styled from 'styled-components';
import type { Agent, AgentPolicy, PackagePolicy } from '../../../../../types';
import type { FleetServerAgentComponentUnit } from '../../../../../../../../common/types/models/agent';
import { useLink, useUIExtension } from '../../../../../hooks';
import { ExtensionWrapper, PackageIcon } from '../../../../../components';
@ -94,11 +96,16 @@ export const AgentDetailsIntegration: React.FunctionComponent<{
const { getHref } = useLink();
const theme = useEuiTheme();
const [showNeedsAttentionBadge, setShowNeedsAttentionBadge] = useState(false);
const [isAttentionBadgeNeededForPolicyResponse, setIsAttentionBadgeNeededForPolicyResponse] =
useState(false);
const extensionView = useUIExtension(
packagePolicy.package?.name ?? '',
'package-policy-response'
);
const genericErrorsListExtensionView = useUIExtension(
packagePolicy.package?.name ?? '',
'package-generic-errors-list'
);
const policyResponseExtensionView = useMemo(() => {
return (
@ -106,13 +113,41 @@ export const AgentDetailsIntegration: React.FunctionComponent<{
<ExtensionWrapper>
<extensionView.Component
agent={agent}
onShowNeedsAttentionBadge={setShowNeedsAttentionBadge}
onShowNeedsAttentionBadge={setIsAttentionBadgeNeededForPolicyResponse}
/>
</ExtensionWrapper>
)
);
}, [agent, extensionView]);
const packageErrors = useMemo(() => {
const packageErrorUnits: FleetServerAgentComponentUnit[] = [];
if (!agent.components) {
return packageErrorUnits;
}
const filteredPackageComponents = filter(agent.components, {
type: packagePolicy.package?.name,
});
filteredPackageComponents.forEach((component) => {
packageErrorUnits.push(...filter(component.units, { status: 'failed' }));
});
return packageErrorUnits;
}, [agent.components, packagePolicy]);
const showNeedsAttentionBadge = isAttentionBadgeNeededForPolicyResponse || packageErrors.length;
const genericErrorsListExtensionViewWrapper = useMemo(() => {
return (
genericErrorsListExtensionView && (
<ExtensionWrapper>
<genericErrorsListExtensionView.Component packageErrors={packageErrors} />
</ExtensionWrapper>
)
);
}, [packageErrors, genericErrorsListExtensionView]);
const inputItems = [
{
label: (
@ -217,6 +252,7 @@ export const AgentDetailsIntegration: React.FunctionComponent<{
aria-labelledby="inputsTreeView"
/>
{policyResponseExtensionView}
{genericErrorsListExtensionViewWrapper}
<EuiSpacer />
</CollapsablePanel>
);

View file

@ -42,6 +42,8 @@ export type {
PackagePolicyResponseExtension,
PackagePolicyResponseExtensionComponent,
PackagePolicyResponseExtensionComponentProps,
PackageGenericErrorsListProps,
PackageGenericErrorsListComponent,
UIExtensionPoint,
UIExtensionRegistrationCallback,
UIExtensionsStorage,

View file

@ -8,6 +8,8 @@
import type { EuiStepProps } from '@elastic/eui';
import type { ComponentType, LazyExoticComponent } from 'react';
import type { FleetServerAgentComponentUnit } from '../../common/types/models/agent';
import type { Agent, NewPackagePolicy, PackageInfo, PackagePolicy } from '.';
/** Register a Fleet UI extension */
@ -60,6 +62,17 @@ export interface PackagePolicyResponseExtensionComponentProps {
onShowNeedsAttentionBadge?: (val: boolean) => void;
}
/**
* UI Component Extension is used on the pages displaying the ability to see
* a generic endpoint errors list
*/
export type PackageGenericErrorsListComponent = ComponentType<PackageGenericErrorsListProps>;
export interface PackageGenericErrorsListProps {
/** A list of errors from a package */
packageErrors: FleetServerAgentComponentUnit[];
}
/** Extension point registration contract for Integration Policy Edit views */
export interface PackagePolicyEditExtension {
package: string;
@ -74,6 +87,12 @@ export interface PackagePolicyResponseExtension {
Component: LazyExoticComponent<PackagePolicyResponseExtensionComponent>;
}
export interface PackageGenericErrorsListExtension {
package: string;
view: 'package-generic-errors-list';
Component: LazyExoticComponent<PackageGenericErrorsListComponent>;
}
/** Extension point registration contract for Integration Policy Edit tabs views */
export interface PackagePolicyEditTabsExtension {
package: string;
@ -158,4 +177,5 @@ export type UIExtensionPoint =
| PackageCustomExtension
| PackagePolicyCreateExtension
| PackageAssetsExtension
| PackageGenericErrorsListExtension
| AgentEnrollmentFlyoutFinalStepExtension;

View file

@ -0,0 +1,81 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { DocLinks } from '@kbn/doc-links';
type PackageActions = 'es_connection';
export const titles = Object.freeze(
new Map<PackageActions, string>([
[
'es_connection',
i18n.translate('xpack.securitySolution.endpoint.details.packageActions.es_connection.title', {
defaultMessage: 'Elasticsearch connection failure',
}),
],
])
);
export const descriptions = Object.freeze(
new Map<Partial<PackageActions> | string, string>([
[
'es_connection',
i18n.translate(
'xpack.securitySolution.endpoint.details.packageActions.es_connection.description',
{
defaultMessage:
"The endpoint's connection to Elasticsearch is either down or misconfigured. Make sure it is configured correctly.",
}
),
],
])
);
const linkTexts = Object.freeze(
new Map<Partial<PackageActions> | string, string>([
[
'es_connection',
i18n.translate(
'xpack.securitySolution.endpoint.details.packageActions.link.text.es_connection',
{
defaultMessage: ' Read more.',
}
),
],
])
);
export class PackageActionFormatter {
public key: PackageActions;
public title: string;
public description: string;
public linkText?: string;
constructor(
code: number,
message: string,
private docLinks: DocLinks['securitySolution']['packageActionTroubleshooting']
) {
this.key = this.getKeyFromErrorCode(code);
this.title = titles.get(this.key) ?? this.key;
this.description = descriptions.get(this.key) || message;
this.linkText = linkTexts.get(this.key);
}
public get linkUrl(): string {
return this.docLinks[this.key];
}
private getKeyFromErrorCode(code: number): PackageActions {
if (code === 123) {
return 'es_connection';
} else {
throw new Error(`Invalid error code ${code}`);
}
}
}

View file

@ -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 styled from 'styled-components';
import { EuiLink, EuiCallOut, EuiText } from '@elastic/eui';
import type { PackageActionFormatter } from './package_action_formatter';
const StyledEuiCallout = styled(EuiCallOut)`
padding: ${({ theme }) => theme.eui.euiSizeS};
`;
const StyledEuiText = styled(EuiText)`
white-space: break-spaces;
text-align: left;
line-height: inherit;
`;
interface PackageActionItemErrorProps {
actionFormatter: PackageActionFormatter;
}
/**
* A package action item error
*/
export const PackageActionItemError = memo(({ actionFormatter }: PackageActionItemErrorProps) => {
return (
<StyledEuiCallout
title={actionFormatter.title}
color="danger"
iconType="alert"
data-test-subj="packageItemErrorCallOut"
>
<StyledEuiText size="s" data-test-subj="packageItemErrorCallOutMessage">
{actionFormatter.description}
{actionFormatter.linkText && actionFormatter.linkUrl && (
<EuiLink
target="_blank"
href={actionFormatter.linkUrl}
data-test-subj="packageItemErrorCallOutLink"
>
{actionFormatter.linkText}
</EuiLink>
)}
</StyledEuiText>
</StyledEuiCallout>
);
});
PackageActionItemError.displayName = 'PackageActionItemError';

View file

@ -0,0 +1,52 @@
/*
* 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, useMemo } from 'react';
import type { PackageGenericErrorsListProps } from '@kbn/fleet-plugin/public';
import { EuiSpacer } from '@elastic/eui';
import { useKibana } from '../../../../../common/lib/kibana';
import { PackageActionFormatter } from '../../../../components/package_action_item/package_action_formatter';
import { PackageActionItemError } from '../../../../components/package_action_item/package_action_item_error';
/**
* Exports Endpoint-generic errors list
*/
export const EndpointGenericErrorsList = memo<PackageGenericErrorsListProps>(
({ packageErrors }) => {
const { docLinks } = useKibana().services;
const globalEndpointErrors = useMemo(() => {
const errors: PackageActionFormatter[] = [];
packageErrors.forEach((unit) => {
if (unit.payload && unit.payload.error) {
errors.push(
new PackageActionFormatter(
unit.payload.error.code,
unit.payload.error.message,
docLinks.links.securitySolution.packageActionTroubleshooting
)
);
}
});
return errors;
}, [packageErrors, docLinks.links.securitySolution.packageActionTroubleshooting]);
return (
<>
{globalEndpointErrors.map((error) => (
<React.Fragment key={error.key}>
<PackageActionItemError actionFormatter={error} />
<EuiSpacer size="m" />
</React.Fragment>
))}
</>
);
}
);
EndpointGenericErrorsList.displayName = 'EndpointGenericErrorsList';

View file

@ -0,0 +1,34 @@
/*
* 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 { lazy } from 'react';
import type { CoreStart } from '@kbn/core/public';
import type {
PackageGenericErrorsListComponent,
PackageGenericErrorsListProps,
} from '@kbn/fleet-plugin/public';
import type { StartPlugins } from '../../../../../types';
export const getLazyEndpointGenericErrorsListExtension = (
coreStart: CoreStart,
depsStart: Pick<StartPlugins, 'data' | 'fleet'>
) => {
return lazy<PackageGenericErrorsListComponent>(async () => {
const [{ withSecurityContext }, { EndpointGenericErrorsList }] = await Promise.all([
import('./with_security_context/with_security_context'),
import('./endpoint_generic_errors_list'),
]);
return {
default: withSecurityContext<PackageGenericErrorsListProps>({
coreStart,
depsStart,
WrappedComponent: EndpointGenericErrorsList,
}),
};
});
};

View file

@ -62,6 +62,7 @@ import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/vi
import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension';
import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension';
import { getLazyEndpointPolicyResponseExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_response_extension';
import { getLazyEndpointGenericErrorsListExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list';
import type { ExperimentalFeatures } from '../common/experimental_features';
import { parseExperimentalConfigValue } from '../common/experimental_features';
import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension';
@ -232,6 +233,11 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
view: 'package-policy-response',
Component: getLazyEndpointPolicyResponseExtension(core, plugins),
});
registerExtension({
package: 'endpoint',
view: 'package-generic-errors-list',
Component: getLazyEndpointGenericErrorsListExtension(core, plugins),
});
}
registerExtension({