Skip to content

Commit

Permalink
[Fleet] Display package update errors (#172065)
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet authored Nov 29, 2023
1 parent 6073eb6 commit 30f3c8e
Show file tree
Hide file tree
Showing 6 changed files with 416 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function PackageCard({
isUnverified,
isUpdateAvailable,
showLabels = true,
extraLabelsBadges,
}: PackageCardProps) {
let releaseBadge: React.ReactNode | null = null;

Expand All @@ -63,7 +64,6 @@ export function PackageCard({
}

let verifiedBadge: React.ReactNode | null = null;

if (isUnverified && showLabels) {
verifiedBadge = (
<EuiFlexItem grow={false}>
Expand Down Expand Up @@ -106,7 +106,7 @@ export function PackageCard({
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<span>
<EuiBadge color="warning">
<EuiBadge color="hollow" iconType="sortUp">
<FormattedMessage
id="xpack.fleet.packageCard.updateAvailableLabel"
defaultMessage="Update available"
Expand Down Expand Up @@ -161,6 +161,7 @@ export function PackageCard({
onClick={onCardClick}
>
<EuiFlexGroup gutterSize="xs" wrap={true}>
{showLabels && extraLabelsBadges ? extraLabelsBadges : null}
{verifiedBadge}
{updateAvailableBadge}
{releaseBadge}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 { createIntegrationsTestRendererMock } from '../../../../../../mock';
import type { PackageListItem } from '../../../../types';

import { getIntegrationLabels } from './card_utils';

function renderIntegrationLabels(item: Partial<PackageListItem>) {
const renderer = createIntegrationsTestRendererMock();

return renderer.render(<>{getIntegrationLabels(item as any)}</>);
}

describe('Card utils', () => {
describe('getIntegrationLabels', () => {
it('should return an empty list for an integration without errors', () => {
const res = renderIntegrationLabels({
installationInfo: {
install_status: 'installed',
} as any,
});
const badges = res.container.querySelectorAll('.euiBadge');
expect(badges).toHaveLength(0);
});

it('should return a badge for install_failed for an integration with status:install_failled', () => {
const res = renderIntegrationLabels({
installationInfo: {
install_status: 'install_failed',
} as any,
});
const badges = res.container.querySelectorAll('.euiBadge');
expect(badges).toHaveLength(1);
expect(res.queryByText('Install failed')).not.toBeNull();
});

it('should return a badge if there is an upgrade failed in the last_attempt_errors', () => {
const res = renderIntegrationLabels({
installationInfo: {
version: '1.0.0',
install_status: 'installed',
latest_install_failed_attempts: [
{
created_at: new Date().toISOString(),
error: {
name: 'Test',
message: 'test error 123',
},
target_version: '2.0.0',
},
],
} as any,
});
const badges = res.container.querySelectorAll('.euiBadge');
expect(badges).toHaveLength(1);
expect(res.queryByText('Update failed')).not.toBeNull();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*
* 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.
*/

/*
* 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 { FormattedMessage, FormattedDate, FormattedTime } from '@kbn/i18n-react';
import { EuiBadge, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui';
import semverLt from 'semver/functions/lt';

import type {
CustomIntegration,
CustomIntegrationIcon,
} from '@kbn/custom-integrations-plugin/common';

import { hasDeferredInstallations } from '../../../../../../services/has_deferred_installations';
import { getPackageReleaseLabel } from '../../../../../../../common/services';

import { installationStatuses } from '../../../../../../../common/constants';
import type {
InstallFailedAttempt,
IntegrationCardReleaseLabel,
PackageSpecIcon,
} from '../../../../../../../common/types';

import type { DynamicPage, DynamicPagePathValues, StaticPage } from '../../../../constants';
import { isPackageUnverified, isPackageUpdatable } from '../../../../services';

import type { PackageListItem } from '../../../../types';

export interface IntegrationCardItem {
url: string;
release?: IntegrationCardReleaseLabel;
description: string;
name: string;
title: string;
version: string;
icons: Array<PackageSpecIcon | CustomIntegrationIcon>;
integration: string;
id: string;
categories: string[];
fromIntegrations?: string;
isReauthorizationRequired?: boolean;
isUnverified?: boolean;
isUpdateAvailable?: boolean;
showLabels?: boolean;
extraLabelsBadges?: React.ReactNode[];
}

export const mapToCard = ({
getAbsolutePath,
getHref,
item,
addBasePath,
packageVerificationKeyId,
selectedCategory,
}: {
getAbsolutePath: (p: string) => string;
getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => string;
addBasePath: (url: string) => string;
item: CustomIntegration | PackageListItem;
packageVerificationKeyId?: string;
selectedCategory?: string;
}): IntegrationCardItem => {
let uiInternalPathUrl: string;

let isUnverified = false;

const version = 'version' in item ? item.version || '' : '';

let isUpdateAvailable = false;
let isReauthorizationRequired = false;
if (item.type === 'ui_link') {
uiInternalPathUrl = item.id.includes('language_client.')
? addBasePath(item.uiInternalPath)
: item.uiExternalLink || getAbsolutePath(item.uiInternalPath);
} else {
let urlVersion = item.version;
if (item?.installationInfo?.version) {
urlVersion = item.installationInfo.version || item.version;
isUnverified = isPackageUnverified(item, packageVerificationKeyId);
isUpdateAvailable = isPackageUpdatable(item);

isReauthorizationRequired = hasDeferredInstallations(item);
}

const url = getHref('integration_details_overview', {
pkgkey: `${item.name}-${urlVersion}`,
...(item.integration ? { integration: item.integration } : {}),
});

uiInternalPathUrl = url;
}

const release: IntegrationCardReleaseLabel = getPackageReleaseLabel(version);

let extraLabelsBadges: React.ReactNode[] | undefined;
if (item.type === 'integration') {
extraLabelsBadges = getIntegrationLabels(item);
}

return {
id: `${item.type === 'ui_link' ? 'ui_link' : 'epr'}:${item.id}`,
description: item.description,
icons: !item.icons || !item.icons.length ? [] : item.icons,
title: item.title,
url: uiInternalPathUrl,
fromIntegrations: selectedCategory,
integration: 'integration' in item ? item.integration || '' : '',
name: 'name' in item ? item.name : item.id,
version,
release,
categories: ((item.categories || []) as string[]).filter((c: string) => !!c),
isReauthorizationRequired,
isUnverified,
isUpdateAvailable,
extraLabelsBadges,
};
};

export function getIntegrationLabels(item: PackageListItem): React.ReactNode[] {
const extraLabelsBadges: React.ReactNode[] = [];

if (
item?.installationInfo?.latest_install_failed_attempts?.some(
(attempt) =>
item.installationInfo && semverLt(item.installationInfo.version, attempt.target_version)
)
) {
const updateFailedAttempt = item.installationInfo?.latest_install_failed_attempts?.find(
(attempt) =>
item.installationInfo && semverLt(item.installationInfo.version, attempt.target_version)
);
extraLabelsBadges.push(
<EuiFlexItem key="update_failed_badge" grow={false}>
<EuiSpacer size="xs" />
<span>
<EuiToolTip
title={
<FormattedMessage
id="xpack.fleet.packageCard.updateFailedTooltipTitle"
defaultMessage="Update failed"
/>
}
content={updateFailedAttempt ? formatAttempt(updateFailedAttempt) : undefined}
>
<EuiBadge color="danger" iconType="error">
<FormattedMessage
id="xpack.fleet.packageCard.updateFailed"
defaultMessage="Update failed"
/>
</EuiBadge>
</EuiToolTip>
</span>
</EuiFlexItem>
);
}

if (item.installationInfo?.install_status === installationStatuses.InstallFailed) {
const installFailedAttempt = item.installationInfo?.latest_install_failed_attempts?.find(
(attempt) => attempt.target_version === item.installationInfo?.version
);

extraLabelsBadges.push(
<EuiFlexItem key="install_failed_badge" grow={false}>
<EuiSpacer size="xs" />
<span>
<EuiToolTip
title={
<FormattedMessage
id="xpack.fleet.packageCard.installFailedTooltipTitle"
defaultMessage="Install failed"
/>
}
content={installFailedAttempt ? formatAttempt(installFailedAttempt) : undefined}
>
<EuiBadge color="danger" iconType="error">
<FormattedMessage
id="xpack.fleet.packageCard.installFailed"
defaultMessage="Install failed"
/>
</EuiBadge>
</EuiToolTip>
</span>
</EuiFlexItem>
);
}

return extraLabelsBadges;
}

function formatAttempt(attempt: InstallFailedAttempt): React.ReactNode {
return (
<>
<FormattedMessage
id="xpack.fleet.packageCard.faileAttemptDescription"
defaultMessage="Failed at {attemptDate}."
values={{
attemptDate: (
<>
<FormattedDate
value={attempt.created_at}
year="numeric"
month="short"
day="numeric"
/>
<> @ </>
<FormattedTime
value={attempt.created_at}
hour="numeric"
minute="numeric"
second="numeric"
/>
</>
),
}}
/>
<p>
{attempt.error?.name || ''} : {attempt.error?.message || ''}
</p>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export interface CategoryFacet {
}

export const UPDATES_AVAILABLE = 'updates_available';
export const INSTALL_FAILED = 'install_failed';
export const UPDATE_FAILED = 'update_failed';

export type ExtendedIntegrationCategory = IntegrationCategory | typeof UPDATES_AVAILABLE | '';

export const ALL_CATEGORY = {
Expand All @@ -45,6 +48,20 @@ export const UPDATES_AVAILABLE_CATEGORY = {
}),
};

export const INSTALL_FAILED_CATEGORY = {
id: INSTALL_FAILED,
title: i18n.translate('xpack.fleet.epmList.installFailedFilterLinkText', {
defaultMessage: 'Install failed',
}),
};

export const UPDATE_FAILED_CATEGORY = {
id: UPDATE_FAILED,
title: i18n.translate('xpack.fleet.epmList.updateFailedFilterLinkText', {
defaultMessage: 'Update failed',
}),
};

export interface Props {
isLoading?: boolean;
categories: CategoryFacet[];
Expand Down
Loading

0 comments on commit 30f3c8e

Please sign in to comment.