[Fleet] Implement single actions in new installed integrations UI (#217584)

This commit is contained in:
Nicolas Chaulet 2025-04-10 11:33:22 -04:00 committed by GitHub
parent 5080c5facb
commit 0cf0e75c9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 778 additions and 266 deletions

View file

@ -34,12 +34,7 @@ export const LicenseModal: React.FunctionComponent<Props> = ({
}) => {
const { notifications } = useStartServices();
const {
data: licenseResponse,
error: licenseError,
isLoading,
} = useGetFileByPathQuery(licensePath);
const licenseText = licenseResponse?.data;
const { data: licenseText, error: licenseError, isLoading } = useGetFileByPathQuery(licensePath);
if (licenseError) {
notifications.toasts.addError(licenseError, {

View file

@ -48,11 +48,10 @@ export const ChangelogModal: React.FunctionComponent<Props> = ({
const { notifications } = useStartServices();
const {
data: changelogResponse,
data: changelogText,
error: changelogError,
isLoading,
} = useGetFileByPathQuery(`/package/${packageName}/${latestVersion}/changelog.yml`);
const changelogText = changelogResponse?.data;
// currentVersion is used to display the changelog up to the current installed version, when there is a newer one available
const finalChangelog = currentVersion

View file

@ -8,6 +8,7 @@
import React, { useState } from 'react';
import { EuiCallOut, EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { InstalledPackageUIPackageListItem } from '../types';
@ -18,23 +19,43 @@ export const ConfirmBulkUninstallModal: React.FunctionComponent<{
}> = ({ onClose, onConfirm, selectedItems }) => {
const [isLoading, setIsLoading] = useState(false);
const isSingleItem = selectedItems.length === 1;
return (
<EuiConfirmModal
title={i18n.translate('xpack.fleet.installedIntegrations.bulkUninstallModal.title', {
defaultMessage: 'Uninstall {countIntegrations} integrations ',
values: {
countIntegrations: selectedItems.length,
},
})}
title={
isSingleItem
? i18n.translate('xpack.fleet.installedIntegrations.bulkUninstallModal.singleTitle', {
defaultMessage: 'Uninstall {integrationName} ',
values: {
integrationName: selectedItems[0].title,
},
})
: i18n.translate('xpack.fleet.installedIntegrations.bulkUninstallModal.title', {
defaultMessage: 'Uninstall {countIntegrations} integrations ',
values: {
countIntegrations: selectedItems.length,
},
})
}
confirmButtonText={i18n.translate(
'xpack.fleet.installedIntegrations.bulkUninstallModal.confirmButton',
{ defaultMessage: 'Uninstall integrations' }
{
defaultMessage: 'Uninstall {itemsCount, plural, one {integration} other {integrations}} ',
values: { itemsCount: selectedItems.length },
}
)}
buttonColor="danger"
cancelButtonText={i18n.translate(
'xpack.fleet.installedIntegrations.bulkUninstallModal.cancelButton',
{ defaultMessage: 'Review and edit selection' }
)}
cancelButtonText={
isSingleItem
? i18n.translate(
'xpack.fleet.installedIntegrations.bulkUninstallModal.cancelSingleButton',
{ defaultMessage: 'Cancel' }
)
: i18n.translate('xpack.fleet.installedIntegrations.bulkUninstallModal.cancelButton', {
defaultMessage: 'Review and edit selection',
})
}
onCancel={onClose}
isLoading={isLoading}
onConfirm={async () => {
@ -53,14 +74,19 @@ export const ConfirmBulkUninstallModal: React.FunctionComponent<{
title={i18n.translate('xpack.fleet.installedIntegrations.bulkUninstallModal.calloutTitle', {
defaultMessage: 'This action cannot be undone.',
})}
content={i18n.translate(
'xpack.fleet.installedIntegrations.bulkUninstallModal.calloutContent',
{
defaultMessage:
'All Kibana and Elasticsearch assets created by these integrations will be also removed. Review and edit your selection if needed.',
}
>
{isSingleItem ? (
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUninstallModal.calloutContentSingleItem"
defaultMessage="All Kibana and Elasticsearch assets created by this integration will be also removed."
/>
) : (
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUninstallModal.calloutContent"
defaultMessage="All Kibana and Elasticsearch assets created by these integrations will be also removed. Review and edit your selection if needed."
/>
)}
/>
</EuiCallOut>
</EuiConfirmModal>
);
};

View file

@ -7,46 +7,89 @@
import React, { useState } from 'react';
import {
EuiCallOut,
EuiAccordion,
EuiCodeBlock,
EuiConfirmModal,
EuiFormRow,
EuiIcon,
EuiPanel,
EuiSkeletonText,
EuiSpacer,
EuiSwitch,
EuiText,
EuiTextColor,
useEuiTheme,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useGetFileByPathQuery } from '../../../../../../../hooks';
import type { InstalledPackageUIPackageListItem } from '../types';
export const ViewChangelog: React.FunctionComponent<{
pkgName: string;
pkgVersion: string;
}> = ({ pkgName, pkgVersion }) => {
const { data: changelogText, isLoading } = useGetFileByPathQuery(
`/package/${pkgName}/${pkgVersion}/changelog.yml`
);
return (
<EuiSkeletonText lines={5} size="s" isLoading={isLoading} contentAriaLabel="changelog text">
<EuiCodeBlock overflowHeight={150}>{changelogText}</EuiCodeBlock>
</EuiSkeletonText>
);
};
export const ConfirmBulkUpgradeModal: React.FunctionComponent<{
selectedItems: InstalledPackageUIPackageListItem[];
onClose: () => void;
onConfirm: (params: { updatePolicies: boolean }) => void;
}> = ({ onClose, onConfirm, selectedItems }) => {
const { euiTheme } = useEuiTheme();
const [updatePolicies, setUpdatePolicies] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const isSingleItem = selectedItems.length === 1;
return (
<EuiConfirmModal
title={i18n.translate('xpack.fleet.installedIntegrations.bulkUpgradeModal.title', {
defaultMessage: 'Upgrade {countIntegrations} integrations and {countPolicies} policies',
values: {
countIntegrations: selectedItems.length,
countPolicies: selectedItems.reduce(
(acc, item) => acc + (item.packagePoliciesInfo?.count ?? 0),
0
),
},
})}
title={
isSingleItem
? i18n.translate('xpack.fleet.installedIntegrations.bulkUpgradeModal.title', {
defaultMessage: 'Upgrade {pkgName} and policies',
values: {
pkgName: selectedItems[0].title,
},
})
: i18n.translate('xpack.fleet.installedIntegrations.bulkUpgradeModal.title', {
defaultMessage:
'Upgrade {countIntegrations} integrations and {countPolicies} policies',
values: {
countIntegrations: selectedItems.length,
countPolicies: selectedItems.reduce(
(acc, item) => acc + (item.packagePoliciesInfo?.count ?? 0),
0
),
},
})
}
confirmButtonText={i18n.translate(
'xpack.fleet.installedIntegrations.bulkUpgradeModal.confirmButton',
{ defaultMessage: 'Upgrade to latest version' }
)}
cancelButtonText={i18n.translate(
'xpack.fleet.installedIntegrations.bulkUpgradeModal.cancelButton',
{ defaultMessage: 'Review integration selection' }
)}
cancelButtonText={
isSingleItem
? i18n.translate(
'xpack.fleet.installedIntegrations.bulkUpgradeModal.cancelSingleItemButton',
{
defaultMessage: 'Cancel',
}
)
: i18n.translate('xpack.fleet.installedIntegrations.bulkUpgradeModal.cancelButton', {
defaultMessage: 'Review integration selection',
})
}
onCancel={onClose}
isLoading={isLoading}
onConfirm={async () => {
@ -60,39 +103,87 @@ export const ConfirmBulkUpgradeModal: React.FunctionComponent<{
}}
>
<EuiText>
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.description"
defaultMessage={
'We will upgrade your integrations and policies to the latest available version.'
}
/>
{isSingleItem ? (
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.singleItemDescription"
defaultMessage={
'We will upgrade this integration and policies to the latest available version.'
}
/>
) : (
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.description"
defaultMessage={
'We will upgrade your integrations and policies to the latest available version.'
}
/>
)}
</EuiText>
<EuiSpacer size="l" />
<EuiFormRow fullWidth>
<EuiSwitch
checked={updatePolicies}
onChange={(e) => {
setUpdatePolicies(e.target.checked);
}}
label={
{isSingleItem ? (
<>
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.policiesSwitchLabel"
defaultMessage="Upgrade integration policies"
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.installedVersionText"
defaultMessage={'Installed version: {installedVersion}'}
values={{
installedVersion: <b>{selectedItems[0].installationInfo!.version}</b>,
}}
/>
}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiCallOut
iconType="iInCircle"
title={i18n.translate(
'xpack.fleet.installedIntegrations.bulkUpgradeModal.policiesCallout',
{
defaultMessage:
'When enabled, Fleet will attempt to upgrade and deploy integration policies automatically.',
}
)}
/>
</EuiText>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.latestVersionText"
defaultMessage={'Latest version available: {latestVersion}'}
values={{
latestVersion: <b>{selectedItems[0].version}</b>,
}}
/>
</EuiText>
<EuiSpacer size="m" />
<EuiAccordion
id="viewChangelog"
buttonContent={
<EuiTextColor color={euiTheme.colors.link}>
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.viewChangelogButton"
defaultMessage="View Changelog"
/>
</EuiTextColor>
}
>
<ViewChangelog pkgName={selectedItems[0].name} pkgVersion={selectedItems[0].version} />
</EuiAccordion>
<EuiSpacer size="l" />
</>
) : null}
<EuiPanel hasShadow={false} hasBorder={false} color="subdued">
<EuiFormRow fullWidth>
<EuiSwitch
data-test-subj="upgradeIntegrationsPoliciesSwitch"
checked={updatePolicies}
onChange={(e) => {
setUpdatePolicies(e.target.checked);
}}
label={
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.policiesSwitchLabel"
defaultMessage="Upgrade integration policies"
/>
}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiText size="xs" color="subdued">
<EuiIcon type="iInCircle" size="m" />
&nbsp;
<FormattedMessage
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.policiesCallout"
defaultMessage="When enabled, Fleet will attempt to upgrade and deploy integration policies automatically."
/>
</EuiText>
</EuiPanel>
</EuiConfirmModal>
);
};

View file

@ -22,6 +22,7 @@ import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'
import { useAuthz } from '../../../../../../../hooks';
import type { InstallFailedAttempt } from '../../../../../../../../common/types';
import type { InstalledPackageUIPackageListItem } from '../types';
import { useInstalledIntegrationsActions } from '../hooks/use_installed_integrations_actions';
import { DisabledWrapperTooltip } from './disabled_wrapper_tooltip';
@ -43,7 +44,9 @@ const UpgradeAvailableVersionStatus: React.FunctionComponent<{
}> = React.memo(({ item }) => {
const authz = useAuthz();
const isDisabled = !authz.integrations.upgradePackages;
const {
actions: { bulkUpgradeIntegrationsWithConfirmModal },
} = useInstalledIntegrationsActions();
return (
<DisabledWrapperTooltip
tooltipContent={
@ -58,8 +61,9 @@ const UpgradeAvailableVersionStatus: React.FunctionComponent<{
size="s"
iconType="gear"
flush="left"
// TODO Implement on click https://github.com/elastic/kibana/issues/209867
onClick={() => {}}
onClick={() => {
bulkUpgradeIntegrationsWithConfirmModal([item]);
}}
disabled={isDisabled}
>
<FormattedMessage

View file

@ -8,20 +8,14 @@
import React, { useState, useMemo, useCallback } from 'react';
import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { useStartServices } from '../../../../../../../hooks';
import type { InstalledPackageUIPackageListItem } from '../types';
import { useBulkActions } from '../hooks/use_bulk_actions';
import { ConfirmBulkUpgradeModal } from './confirm_bulk_upgrade_modal';
import { ConfirmBulkUninstallModal } from './confirm_bulk_uninstall_modal';
import { useInstalledIntegrationsActions } from '../hooks/use_installed_integrations_actions';
export const InstalledIntegrationsActionMenu: React.FunctionComponent<{
selectedItems: InstalledPackageUIPackageListItem[];
}> = ({ selectedItems }) => {
const [isOpen, setIsOpen] = useState(false);
const startServices = useStartServices();
const button = (
<EuiButton
@ -38,40 +32,18 @@ export const InstalledIntegrationsActionMenu: React.FunctionComponent<{
);
const {
actions: { bulkUpgradeIntegrations, bulkUninstallIntegrations },
} = useBulkActions();
actions: { bulkUpgradeIntegrationsWithConfirmModal, bulkUninstallIntegrationsWithConfirmModal },
} = useInstalledIntegrationsActions();
const openUpgradeModal = useCallback(() => {
setIsOpen(false);
const ref = startServices.overlays.openModal(
toMountPoint(
<ConfirmBulkUpgradeModal
onClose={() => {
ref.close();
}}
onConfirm={({ updatePolicies }) => bulkUpgradeIntegrations(selectedItems, updatePolicies)}
selectedItems={selectedItems}
/>,
startServices
)
);
}, [selectedItems, startServices, bulkUpgradeIntegrations]);
return bulkUpgradeIntegrationsWithConfirmModal(selectedItems);
}, [selectedItems, bulkUpgradeIntegrationsWithConfirmModal]);
const openUninstallModal = useCallback(() => {
const openUninstallModal = useCallback(async () => {
setIsOpen(false);
const ref = startServices.overlays.openModal(
toMountPoint(
<ConfirmBulkUninstallModal
onClose={() => {
ref.close();
}}
onConfirm={() => bulkUninstallIntegrations(selectedItems)}
selectedItems={selectedItems}
/>,
startServices
)
);
}, [selectedItems, startServices, bulkUninstallIntegrations]);
return bulkUninstallIntegrationsWithConfirmModal(selectedItems);
}, [selectedItems, bulkUninstallIntegrationsWithConfirmModal]);
const items = useMemo(() => {
const hasUpgreadableIntegrations = selectedItems.some(

View file

@ -17,16 +17,29 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Action } from '@elastic/eui/src/components/basic_table/action_types';
import { TableIcon } from '../../../../../../../components/package_icon';
import type { PackageListItem } from '../../../../../../../../common';
import { type UrlPagination, useLink, useAuthz } from '../../../../../../../hooks';
import type { InstalledPackageUIPackageListItem } from '../types';
import { useViewPolicies } from '../hooks/use_url_filters';
import { useInstalledIntegrationsActions } from '../hooks/use_installed_integrations_actions';
import { InstallationVersionStatus } from './installation_version_status';
import { DisabledWrapperTooltip } from './disabled_wrapper_tooltip';
function wrapActionWithDisabledTooltip(
action: Action<InstalledPackageUIPackageListItem>,
disabled: boolean,
tooltip: string
): Action<InstalledPackageUIPackageListItem> {
return {
...action,
...(disabled ? { enabled: () => false, description: tooltip } : {}),
};
}
export const InstalledIntegrationsTable: React.FunctionComponent<{
installedPackages: InstalledPackageUIPackageListItem[];
total: number;
@ -41,6 +54,9 @@ export const InstalledIntegrationsTable: React.FunctionComponent<{
const { getHref } = useLink();
const { selectedItems, setSelectedItems } = selection;
const { addViewPolicies } = useViewPolicies();
const {
actions: { bulkUninstallIntegrationsWithConfirmModal, bulkUpgradeIntegrationsWithConfirmModal },
} = useInstalledIntegrationsActions();
const { setPagination } = pagination;
const handleTablePagination = React.useCallback(
@ -94,7 +110,7 @@ export const InstalledIntegrationsTable: React.FunctionComponent<{
),
render: (item: PackageListItem) => {
const url = getHref('integration_details_overview', {
pkgkey: `${item.name}-${item.version}`,
pkgkey: `${item.name}-${item.installationInfo!.version}`,
});
return (
@ -168,33 +184,61 @@ export const InstalledIntegrationsTable: React.FunctionComponent<{
// TODO Actions are not yet implemented to be done in https://github.com/elastic/kibana/issues/209867
{
actions: [
{
name: i18n.translate('xpack.fleet.epmInstalledIntegrations.upgradeActionLabel', {
defaultMessage: 'Upgrade',
}),
description: (item) =>
i18n.translate('xpack.fleet.epmInstalledIntegrations.upgradeActionDescription', {
defaultMessage: 'Upgrade to {version}.',
values: { version: item.version },
wrapActionWithDisabledTooltip(
{
name: i18n.translate('xpack.fleet.epmInstalledIntegrations.upgradeActionLabel', {
defaultMessage: 'Upgrade',
}),
icon: 'refresh',
type: 'icon',
enabled: () => false,
},
{
name: i18n.translate('xpack.fleet.epmInstalledIntegrations.viewPoliciesLabel', {
defaultMessage: 'View policies',
}),
icon: 'search',
type: 'icon',
description: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.viewPoliciesLabel',
description: (item) =>
i18n.translate(
'xpack.fleet.epmInstalledIntegrations.upgradeActionDescription',
{
defaultMessage: 'Upgrade to {version}.',
values: { version: item.version },
}
),
icon: 'refresh',
type: 'icon',
onClick: (item) => bulkUpgradeIntegrationsWithConfirmModal([item]),
enabled: (item) =>
item.ui.installation_status === 'upgrade_available' ||
item.ui.installation_status === 'upgrade_failed' ||
item.ui.installation_status === 'install_failed',
},
!authz.integrations.upgradePackages,
i18n.translate(
'xpack.fleet.epmInstalledIntegrations.upgradeIntegrationsRequiredPermissionTooltip',
{
defaultMessage: 'View policies',
defaultMessage:
"You don't have permissions to upgrade integrations. Contact your administrator.",
}
),
enabled: (item) => (item?.packagePoliciesInfo?.count ?? 0) > 0,
},
)
),
wrapActionWithDisabledTooltip(
{
name: i18n.translate('xpack.fleet.epmInstalledIntegrations.viewPoliciesLabel', {
defaultMessage: 'View policies',
}),
icon: 'search',
type: 'icon',
description: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.viewPoliciesLabel',
{
defaultMessage: 'View policies',
}
),
onClick: (item) => addViewPolicies(item.name),
enabled: (item) => (item?.packagePoliciesInfo?.count ?? 0) > 0,
},
!authz.fleet.readAgentPolicies,
i18n.translate(
'xpack.fleet.epmInstalledIntegrations.agentPoliciesRequiredPermissionTooltip',
{
defaultMessage:
"You don't have permissions to view these policies. Contact your administrator.",
}
)
),
{
name: i18n.translate('xpack.fleet.epmInstalledIntegrations.editIntegrationLabel', {
defaultMessage: 'Edit integration',
@ -207,25 +251,38 @@ export const InstalledIntegrationsTable: React.FunctionComponent<{
defaultMessage: 'Edit integration',
}
),
enabled: () => false,
href: (item) =>
getHref('integration_details_overview', {
pkgkey: `${item.name}-${item.installationInfo!.version}`,
}),
},
{
name: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.uninstallIntegrationLabel',
wrapActionWithDisabledTooltip(
{
name: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.uninstallIntegrationLabel',
{
defaultMessage: 'Uninstall integration',
}
),
icon: 'trash',
type: 'icon',
description: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.uninstallIntegrationLabel',
{
defaultMessage: 'Uninstall integration',
}
),
onClick: (item) => bulkUninstallIntegrationsWithConfirmModal([item]),
},
!authz.integrations.removePackages,
i18n.translate(
'xpack.fleet.epmInstalledIntegrations.removeIntegrationsRequiredPermissionTooltip',
{
defaultMessage: 'Uninstall integration',
defaultMessage:
"You don't have permissions to remove integrations. Contact your administrator.",
}
),
icon: 'trash',
type: 'icon',
description: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.uninstallIntegrationLabel',
{
defaultMessage: 'Uninstall integration',
}
),
enabled: (item) => (item?.packagePoliciesInfo?.count ?? 0) === 0,
},
)
),
],
},
]}

View file

@ -5,13 +5,11 @@
* 2.0.
*/
import React, { useCallback, useContext, useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { useQueries, useQueryClient } from '@tanstack/react-query';
import {
sendBulkUninstallPackagesForRq,
sendBulkUpgradePackagesForRq,
sendGetOneBulkUninstallPackagesForRq,
sendGetOneBulkUpgradePackagesForRq,
useStartServices,
@ -145,110 +143,3 @@ export const BulkActionContextProvider: React.FunctionComponent<{ children: Reac
</bulkActionsContext.Provider>
);
};
export function useBulkActions() {
const {
upgradingIntegrations,
uninstallingIntegrations,
bulkActions: { setPollingBulkActions },
} = useContext(bulkActionsContext);
const {
notifications: { toasts },
} = useStartServices();
const bulkUpgradeIntegrations = useCallback(
async (items: InstalledPackageUIPackageListItem[], updatePolicies?: boolean) => {
try {
const res = await sendBulkUpgradePackagesForRq({
packages: items.map((item) => ({ name: item.name })),
upgrade_package_policies: updatePolicies,
});
setPollingBulkActions((actions) => [
...actions,
{
taskId: res.taskId,
type: 'bulk_upgrade',
integrations: items,
},
]);
toasts.addInfo({
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeInProgressTitle',
{
defaultMessage: 'Upgrade in progress',
}
),
content: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeInProgressDescription',
{
defaultMessage:
'The integrations and the policies are upgrading to the latest version.',
}
),
});
} catch (error) {
toasts.addError(error, {
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeErrorTitle',
{
defaultMessage: 'Error upgrading integrations',
}
),
});
}
},
[setPollingBulkActions, toasts]
);
const bulkUninstallIntegrations = useCallback(
async (items: InstalledPackageUIPackageListItem[], updatePolicies?: boolean) => {
try {
const res = await sendBulkUninstallPackagesForRq({
packages: items.map((item) => ({
name: item.name,
version: item.installationInfo!.version,
})),
});
setPollingBulkActions((actions) => [
...actions,
{
taskId: res.taskId,
type: 'bulk_uninstall',
integrations: items,
},
]);
toasts.addInfo({
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUninstallInProgressTitle',
{
defaultMessage: 'Uninstall in progress',
}
),
});
} catch (error) {
toasts.addError(error, {
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUninstallErrorTitle',
{
defaultMessage: 'Error uninstalling integrations',
}
),
});
}
},
[toasts, setPollingBulkActions]
);
const actions = useMemo(
() => ({ bulkUpgradeIntegrations, bulkUninstallIntegrations }),
[bulkUpgradeIntegrations, bulkUninstallIntegrations]
);
return {
actions,
upgradingIntegrations,
uninstallingIntegrations,
};
}

View file

@ -0,0 +1,247 @@
/*
* 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 { toMountPoint } from '@kbn/react-kibana-mount';
import { act } from 'react-dom/test-utils';
import {
sendRemovePackageForRq,
sendBulkUninstallPackagesForRq,
sendBulkUpgradePackagesForRq,
} from '../../../../../../../hooks/use_request/epm';
import { createFleetTestRendererMock } from '../../../../../../../mock';
import { useInstalledIntegrationsActions } from './use_installed_integrations_actions';
jest.mock('@kbn/react-kibana-mount');
jest.mock('../../../../../../../hooks/use_request/epm', () => ({
...jest.requireActual('../../../../../../../hooks/use_request/epm'),
sendRemovePackageForRq: jest.fn(),
sendBulkUninstallPackagesForRq: jest.fn(),
sendBulkUpgradePackagesForRq: jest.fn(),
}));
describe('useInstalledIntegrationsActions', () => {
beforeEach(() => {
jest.mocked(sendRemovePackageForRq).mockReset();
jest.mocked(sendBulkUninstallPackagesForRq).mockReset();
jest.mocked(sendBulkUpgradePackagesForRq).mockReset();
jest.mocked(toMountPoint).mockReset();
});
describe('bulkUninstallIntegrationsWithConfirmModal', () => {
it('should work with single integration', async () => {
const renderer = createFleetTestRendererMock();
const res = renderer.renderHook(() => useInstalledIntegrationsActions());
const bulkUninstallIntegrationsWithConfirmModalResult =
res.result.current.actions.bulkUninstallIntegrationsWithConfirmModal([
{
name: 'test',
version: '1.2.0',
installationInfo: {
version: '1.0.0',
},
},
] as any);
// Mount the modal
const modal = jest.mocked(toMountPoint).mock.lastCall![0];
const modalResult = renderer.render(modal as any);
modalResult.getByTestId('confirmModalConfirmButton').click();
await expect(bulkUninstallIntegrationsWithConfirmModalResult).resolves;
expect(sendRemovePackageForRq).toBeCalledTimes(1);
expect(sendRemovePackageForRq).toBeCalledWith({ pkgName: 'test', pkgVersion: '1.0.0' });
});
it('should work with multiple integrations', async () => {
const renderer = createFleetTestRendererMock();
const res = renderer.renderHook(() => useInstalledIntegrationsActions());
const bulkUninstallIntegrationsWithConfirmModalResult =
res.result.current.actions.bulkUninstallIntegrationsWithConfirmModal([
{
name: 'test',
version: '1.2.0',
installationInfo: {
version: '1.0.0',
},
},
{
name: 'test2',
version: '1.2.0',
installationInfo: {
version: '1.1.0',
},
},
] as any);
// Mount the modal
const modal = jest.mocked(toMountPoint).mock.lastCall![0];
const modalResult = renderer.render(modal as any);
modalResult.getByTestId('confirmModalConfirmButton').click();
await expect(bulkUninstallIntegrationsWithConfirmModalResult).resolves;
expect(sendBulkUninstallPackagesForRq).toBeCalledTimes(1);
expect(sendBulkUninstallPackagesForRq).toBeCalledWith({
packages: [
{ name: 'test', version: '1.0.0' },
{ name: 'test2', version: '1.1.0' },
],
});
});
it('should support canceling action', async () => {
const renderer = createFleetTestRendererMock();
const res = renderer.renderHook(() => useInstalledIntegrationsActions());
const bulkUninstallIntegrationsWithConfirmModalResult =
res.result.current.actions.bulkUninstallIntegrationsWithConfirmModal([
{
name: 'test',
version: '1.2.0',
installationInfo: {
version: '1.0.0',
},
},
] as any);
// Mount the modal
const modal = jest.mocked(toMountPoint).mock.lastCall![0];
const modalResult = renderer.render(modal as any);
modalResult.getByTestId('confirmModalCancelButton').click();
await expect(bulkUninstallIntegrationsWithConfirmModalResult).resolves;
expect(sendRemovePackageForRq).not.toHaveBeenCalled();
});
});
describe('bulkUpgradeIntegrationsWithConfirmModal', () => {
it('should work with single integration', async () => {
const renderer = createFleetTestRendererMock();
const res = renderer.renderHook(() => useInstalledIntegrationsActions());
const bulkUpgradeIntegrationsWithConfirmModalResult =
res.result.current.actions.bulkUpgradeIntegrationsWithConfirmModal([
{
name: 'test',
version: '1.2.0',
installationInfo: {
version: '1.0.0',
},
},
] as any);
// Mount the modal
const modal = jest.mocked(toMountPoint).mock.lastCall![0];
const modalResult = renderer.render(modal as any);
modalResult.getByTestId('confirmModalConfirmButton').click();
await expect(bulkUpgradeIntegrationsWithConfirmModalResult).resolves;
expect(sendBulkUpgradePackagesForRq).toBeCalledTimes(1);
expect(sendBulkUpgradePackagesForRq).toBeCalledWith({
packages: [{ name: 'test' }],
upgrade_package_policies: false,
});
});
it('should allow to upgrade related package policies', async () => {
const renderer = createFleetTestRendererMock();
const res = renderer.renderHook(() => useInstalledIntegrationsActions());
const bulkUpgradeIntegrationsWithConfirmModalResult =
res.result.current.actions.bulkUpgradeIntegrationsWithConfirmModal([
{
name: 'test',
version: '1.2.0',
installationInfo: {
version: '1.0.0',
},
},
] as any);
// Mount the modal
const modal = jest.mocked(toMountPoint).mock.lastCall![0];
const modalResult = renderer.render(modal as any);
act(() => modalResult.getByTestId('upgradeIntegrationsPoliciesSwitch').click());
act(() => modalResult.getByTestId('confirmModalConfirmButton').click());
await expect(bulkUpgradeIntegrationsWithConfirmModalResult).resolves;
expect(sendBulkUpgradePackagesForRq).toBeCalledTimes(1);
expect(sendBulkUpgradePackagesForRq).toBeCalledWith({
packages: [{ name: 'test' }],
upgrade_package_policies: true,
});
});
it('should work with multiple integrations', async () => {
const renderer = createFleetTestRendererMock();
const res = renderer.renderHook(() => useInstalledIntegrationsActions());
const bulkUpgradeIntegrationsWithConfirmModalResult =
res.result.current.actions.bulkUpgradeIntegrationsWithConfirmModal([
{
name: 'test',
version: '1.2.0',
installationInfo: {
version: '1.0.0',
},
},
{
name: 'test2',
version: '1.2.0',
installationInfo: {
version: '1.1.0',
},
},
] as any);
// Mount the modal
const modal = jest.mocked(toMountPoint).mock.lastCall![0];
const modalResult = renderer.render(modal as any);
act(() => modalResult.getByTestId('confirmModalConfirmButton').click());
await expect(bulkUpgradeIntegrationsWithConfirmModalResult).resolves;
expect(sendBulkUpgradePackagesForRq).toBeCalledTimes(1);
expect(sendBulkUpgradePackagesForRq).toBeCalledWith({
packages: [{ name: 'test' }, { name: 'test2' }],
upgrade_package_policies: false,
});
});
it('should support cancelling', async () => {
const renderer = createFleetTestRendererMock();
const res = renderer.renderHook(() => useInstalledIntegrationsActions());
const bulkUpgradeIntegrationsWithConfirmModalResult =
res.result.current.actions.bulkUpgradeIntegrationsWithConfirmModal([
{
name: 'test',
version: '1.2.0',
installationInfo: {
version: '1.0.0',
},
},
] as any);
// Mount the modal
const modal = jest.mocked(toMountPoint).mock.lastCall![0];
const modalResult = renderer.render(modal as any);
modalResult.getByTestId('confirmModalCancelButton').click();
await expect(bulkUpgradeIntegrationsWithConfirmModalResult).resolves;
expect(sendBulkUpgradePackagesForRq).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,216 @@
/*
* 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, useContext, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { QueryClientProvider, useQueryClient } from '@tanstack/react-query';
import { toMountPoint } from '@kbn/react-kibana-mount';
import {
sendBulkUninstallPackagesForRq,
sendBulkUpgradePackagesForRq,
sendRemovePackageForRq,
useStartServices,
} from '../../../../../../../hooks';
import type { InstalledPackageUIPackageListItem } from '../types';
import { ConfirmBulkUninstallModal } from '../components/confirm_bulk_uninstall_modal';
import { ConfirmBulkUpgradeModal } from '../components/confirm_bulk_upgrade_modal';
import { bulkActionsContext } from './use_bulk_actions_context';
export function useInstalledIntegrationsActions() {
const {
upgradingIntegrations,
uninstallingIntegrations,
bulkActions: { setPollingBulkActions },
} = useContext(bulkActionsContext);
const queryClient = useQueryClient();
const startServices = useStartServices();
const {
notifications: { toasts },
} = startServices;
const bulkUpgradeIntegrations = useCallback(
async (items: InstalledPackageUIPackageListItem[], updatePolicies?: boolean) => {
try {
const res = await sendBulkUpgradePackagesForRq({
packages: items.map((item) => ({ name: item.name })),
upgrade_package_policies: updatePolicies,
});
setPollingBulkActions((actions) => [
...actions,
{
taskId: res.taskId,
type: 'bulk_upgrade',
integrations: items,
},
]);
toasts.addInfo({
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeInProgressTitle',
{
defaultMessage: 'Upgrade in progress',
}
),
content: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeInProgressDescription',
{
defaultMessage:
'The integrations and the policies are upgrading to the latest version.',
}
),
});
return true;
} catch (error) {
toasts.addError(error, {
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeErrorTitle',
{
defaultMessage: 'Error upgrading integrations',
}
),
});
return false;
}
},
[setPollingBulkActions, toasts]
);
const bulkUninstallIntegrations = useCallback(
async (items: InstalledPackageUIPackageListItem[]) => {
try {
if (items.length === 1) {
await sendRemovePackageForRq({
pkgName: items[0].name,
pkgVersion: items[0].installationInfo!.version,
});
await queryClient.invalidateQueries(['get-packages']);
toasts.addSuccess({
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUninstallSuccessTitleSingle',
{
defaultMessage: 'Uninstalled {pkgName}',
values: { pkgName: items[0].name },
}
),
});
} else {
const res = await sendBulkUninstallPackagesForRq({
packages: items.map((item) => ({
name: item.name,
version: item.installationInfo!.version,
})),
});
setPollingBulkActions((actions) => [
...actions,
{
taskId: res.taskId,
type: 'bulk_uninstall',
integrations: items,
},
]);
toasts.addInfo({
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUninstallInProgressTitle',
{
defaultMessage: 'Uninstall in progress',
}
),
});
}
return true;
} catch (error) {
toasts.addError(error, {
title: i18n.translate(
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUninstallErrorTitle',
{
defaultMessage: 'Error uninstalling integrations',
}
),
});
return false;
}
},
[toasts, queryClient, setPollingBulkActions]
);
const bulkUninstallIntegrationsWithConfirmModal = useCallback(
(selectedItems: InstalledPackageUIPackageListItem[]) => {
return new Promise<void>((resolve, reject) => {
const ref = startServices.overlays.openModal(
toMountPoint(
<ConfirmBulkUninstallModal
onClose={() => {
ref.close();
resolve();
}}
onConfirm={async () => {
// Error handled in bulkUninstallIntegrations
const success = await bulkUninstallIntegrations(selectedItems);
if (success) {
resolve();
} else {
throw new Error('uninstall integrations failed');
}
}}
selectedItems={selectedItems}
/>,
startServices
)
);
});
},
[startServices, bulkUninstallIntegrations]
);
const bulkUpgradeIntegrationsWithConfirmModal = useCallback(
(selectedItems: InstalledPackageUIPackageListItem[]) => {
return new Promise<void>((resolve) => {
const ref = startServices.overlays.openModal(
toMountPoint(
<QueryClientProvider client={queryClient}>
<ConfirmBulkUpgradeModal
onClose={() => {
ref.close();
resolve();
}}
onConfirm={async ({ updatePolicies }) => {
const success = await bulkUpgradeIntegrations(selectedItems, updatePolicies);
if (success) {
resolve();
} else {
throw new Error('upgrade integrations failed');
}
}}
selectedItems={selectedItems}
/>
</QueryClientProvider>,
startServices
)
);
});
},
[startServices, queryClient, bulkUpgradeIntegrations]
);
const actions = useMemo(
() => ({
bulkUpgradeIntegrationsWithConfirmModal,
bulkUninstallIntegrationsWithConfirmModal,
}),
[bulkUpgradeIntegrationsWithConfirmModal, bulkUninstallIntegrationsWithConfirmModal]
);
return {
actions,
upgradingIntegrations,
uninstallingIntegrations,
};
}

View file

@ -17,7 +17,8 @@ import { useInstalledIntegrations } from './hooks/use_installed_integrations';
import { useUrlFilters, useViewPolicies } from './hooks/use_url_filters';
import { InstalledIntegrationsSearchBar } from './components/installed_integrations_search_bar';
import type { InstalledPackageUIPackageListItem } from './types';
import { BulkActionContextProvider, useBulkActions } from './hooks/use_bulk_actions';
import { useInstalledIntegrationsActions } from './hooks/use_installed_integrations_actions';
import { BulkActionContextProvider } from './hooks/use_bulk_actions_context';
import { PackagePoliciesPanel } from './components/package_policies_panel';
const ContentWrapper = styled.div`
@ -31,7 +32,7 @@ const InstalledIntegrationsPageContent: React.FunctionComponent = () => {
const filters = useUrlFilters();
const { selectedPackageViewPolicies } = useViewPolicies();
const pagination = useUrlPagination();
const { upgradingIntegrations, uninstallingIntegrations } = useBulkActions();
const { upgradingIntegrations, uninstallingIntegrations } = useInstalledIntegrationsActions();
const {
installedPackages,
countPerStatus,

View file

@ -9,8 +9,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import type { SendRequestResponse } from '@kbn/es-ui-shared-plugin/public';
import { epmRouteService, isVerificationError } from '../../services';
import type {
GetCategoriesRequest,
@ -259,8 +257,8 @@ export const useGetFileByPath = (filePath: string) => {
};
export const useGetFileByPathQuery = (filePath: string) => {
return useQuery<SendRequestResponse<string>, RequestError>(['get-file', filePath], () =>
sendRequest<string>({
return useQuery<string, RequestError>(['get-file', filePath], () =>
sendRequestForRq<string>({
path: epmRouteService.getFilePath(filePath),
method: 'get',
version: API_VERSIONS.public.v1,
@ -343,6 +341,9 @@ export const sendGetOneBulkUninstallPackagesForRq = (taskId: string) => {
});
};
/**
* @deprecated use sendRemovePackageForRq instead
*/
export function sendRemovePackage(
{ pkgName, pkgVersion }: DeletePackageRequest['params'],
query?: DeletePackageRequest['query']
@ -355,6 +356,18 @@ export function sendRemovePackage(
});
}
export function sendRemovePackageForRq(
{ pkgName, pkgVersion }: DeletePackageRequest['params'],
query?: DeletePackageRequest['query']
) {
return sendRequestForRq<DeletePackageResponse>({
path: epmRouteService.getRemovePath(pkgName, pkgVersion),
method: 'delete',
version: API_VERSIONS.public.v1,
query,
});
}
export const sendRequestReauthorizeTransforms = (
pkgName: string,
pkgVersion: string,