Skip to content

Commit

Permalink
[UII] Support content packages in UI (elastic#195831)
Browse files Browse the repository at this point in the history
## Summary

Resolves elastic#192484. This PR adds support for content packages in UI. When
a package is of `type: content`:

- `Content only` badge is shown on its card in Integrations list, and on
header of its details page
- `Add integration` button is replaced by `Install assets` button in
header
- References to agent policies are hidden
- Package policy service throws error if attempting to create or bulk
create policies for content packages

<img width="1403" alt="image"
src="https://github.com/user-attachments/assets/a82c310a-f849-4b68-b56c-ff6bb31cd6bf">

<img width="1401" alt="image"
src="https://github.com/user-attachments/assets/63eb3982-9ec9-494f-a95a-2b8992a408ba">

## How to test
The only current content package is `kubernetes_otel`. You will need to
bump up the max allowed spec version and search with beta (prerelease)
packages enabled to find it:
```
xpack.fleet.internal.registry.spec.max: '3.4'
```

Test UI scenarios as above. The API can be tested by running:
```
POST kbn:/api/fleet/package_policies
{
  "policy_ids": [
    ""
  ],
  "package": {
    "name": "kubernetes_otel",
    "version": "0.0.2"
  },
  "name": "kubernetes_otel-1",
  "description": "",
  "namespace": "",
  "inputs": {}
}
```

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
jen-huang authored Oct 15, 2024
1 parent 0764261 commit 9512f6c
Show file tree
Hide file tree
Showing 20 changed files with 252 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ export function useOnSubmit({

// Check if agentless is configured in ESS and Serverless until Agentless API migrates to Serverless
const isAgentlessConfigured =
isAgentlessAgentPolicy(createdPolicy) || isAgentlessPackagePolicy(data!.item);
isAgentlessAgentPolicy(createdPolicy) || (data && isAgentlessPackagePolicy(data.item));

// Removing this code will disabled the Save and Continue button. We need code below update form state and trigger correct modal depending on agent count
if (hasFleetAddAgentsPrivileges && !isAgentlessConfigured) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function PackageCard({
name,
title,
version,
type,
icons,
integration,
url,
Expand All @@ -78,7 +79,6 @@ export function PackageCard({
maxCardHeight,
}: PackageCardProps) {
let releaseBadge: React.ReactNode | null = null;

if (release && release !== 'ga') {
releaseBadge = (
<EuiFlexItem grow={false}>
Expand Down Expand Up @@ -108,7 +108,6 @@ export function PackageCard({
}

let hasDeferredInstallationsBadge: React.ReactNode | null = null;

if (isReauthorizationRequired && showLabels) {
hasDeferredInstallationsBadge = (
<EuiFlexItem grow={false}>
Expand All @@ -127,7 +126,6 @@ export function PackageCard({
}

let updateAvailableBadge: React.ReactNode | null = null;

if (isUpdateAvailable && showLabels) {
updateAvailableBadge = (
<EuiFlexItem grow={false}>
Expand All @@ -145,7 +143,6 @@ export function PackageCard({
}

let collectionButton: React.ReactNode | null = null;

if (isCollectionCard) {
collectionButton = (
<EuiFlexItem>
Expand All @@ -163,6 +160,23 @@ export function PackageCard({
);
}

let contentBadge: React.ReactNode | null = null;
if (type === 'content') {
contentBadge = (
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<span>
<EuiBadge color="hollow">
<FormattedMessage
id="xpack.fleet.packageCard.contentPackageLabel"
defaultMessage="Content only"
/>
</EuiBadge>
</span>
</EuiFlexItem>
);
}

const { application } = useStartServices();
const isGuidedOnboardingActive = useIsGuidedOnboardingActive(name);

Expand Down Expand Up @@ -235,6 +249,7 @@ export function PackageCard({
{showLabels && extraLabelsBadges ? extraLabelsBadges : null}
{verifiedBadge}
{updateAvailableBadge}
{contentBadge}
{releaseBadge}
{hasDeferredInstallationsBadge}
{collectionButton}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import { Configs } from './configs';

import './index.scss';
import type { InstallPkgRouteOptions } from './utils/get_install_route_options';
import { InstallButton } from './settings/install_button';

export type DetailViewPanelName =
| 'overview'
Expand Down Expand Up @@ -362,13 +363,23 @@ export function Detail() {
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiBadge color="default">
{i18n.translate('xpack.fleet.epm.elasticAgentBadgeLabel', {
defaultMessage: 'Elastic Agent',
})}
</EuiBadge>
</EuiFlexItem>
{packageInfo?.type === 'content' ? (
<EuiFlexItem grow={false}>
<EuiBadge color="default">
{i18n.translate('xpack.fleet.epm.contentPackageBadgeLabel', {
defaultMessage: 'Content only',
})}
</EuiBadge>
</EuiFlexItem>
) : (
<EuiFlexItem grow={false}>
<EuiBadge color="default">
{i18n.translate('xpack.fleet.epm.elasticAgentBadgeLabel', {
defaultMessage: 'Elastic Agent',
})}
</EuiBadge>
</EuiFlexItem>
)}
{packageInfo?.release && packageInfo.release !== 'ga' ? (
<EuiFlexItem grow={false}>
<HeaderReleaseBadge release={getPackageReleaseLabel(packageInfo.version)} />
Expand Down Expand Up @@ -520,7 +531,7 @@ export function Detail() {
</EuiFlexGroup>
),
},
...(isInstalled
...(isInstalled && packageInfo.type !== 'content'
? [
{ isDivider: true },
{
Expand All @@ -532,31 +543,37 @@ export function Detail() {
},
]
: []),
{ isDivider: true },
{
content: (
<WithGuidedOnboardingTour
packageKey={pkgkey}
tourType={'addIntegrationButton'}
isTourVisible={isOverviewPage && isGuidedOnboardingActive}
tourOffset={10}
>
<AddIntegrationButton
userCanInstallPackages={userCanInstallPackages}
href={getHref('add_integration_to_policy', {
pkgkey,
...(integration ? { integration } : {}),
...(agentPolicyIdFromContext
? { agentPolicyId: agentPolicyIdFromContext }
: {}),
})}
missingSecurityConfiguration={missingSecurityConfiguration}
packageName={integrationInfo?.title || packageInfo.title}
onClick={handleAddIntegrationPolicyClick}
/>
</WithGuidedOnboardingTour>
),
},
...(packageInfo.type === 'content'
? !isInstalled
? [{ isDivider: true }, { content: <InstallButton {...packageInfo} /> }]
: [] // if content package is already installed, don't show install button in header
: [
{ isDivider: true },
{
content: (
<WithGuidedOnboardingTour
packageKey={pkgkey}
tourType={'addIntegrationButton'}
isTourVisible={isOverviewPage && isGuidedOnboardingActive}
tourOffset={10}
>
<AddIntegrationButton
userCanInstallPackages={userCanInstallPackages}
href={getHref('add_integration_to_policy', {
pkgkey,
...(integration ? { integration } : {}),
...(agentPolicyIdFromContext
? { agentPolicyId: agentPolicyIdFromContext }
: {}),
})}
missingSecurityConfiguration={missingSecurityConfiguration}
packageName={integrationInfo?.title || packageInfo.title}
onClick={handleAddIntegrationPolicyClick}
/>
</WithGuidedOnboardingTour>
),
},
]),
].map((item, index) => (
<EuiFlexItem grow={false} key={index} data-test-subj={item['data-test-subj']}>
{item.isDivider ?? false ? (
Expand Down Expand Up @@ -619,7 +636,7 @@ export function Detail() {
},
];

if (canReadIntegrationPolicies && isInstalled) {
if (canReadIntegrationPolicies && isInstalled && packageInfo.type !== 'content') {
tabs.push({
id: 'policies',
name: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ interface ConfirmPackageInstallProps {
onConfirm: () => void;
packageName: string;
numOfAssets: number;
numOfTransformAssets: number;
}

import { TransformInstallWithCurrentUserPermissionCallout } from '../../../../../../../components/transform_install_as_current_user_callout';

export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => {
const { onCancel, onConfirm, packageName, numOfAssets } = props;
const { onCancel, onConfirm, packageName, numOfAssets, numOfTransformAssets } = props;
return (
<EuiConfirmModal
title={
Expand Down Expand Up @@ -53,6 +57,12 @@ export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => {
/>
}
/>
{numOfTransformAssets > 0 ? (
<>
<EuiSpacer size="m" />
<TransformInstallWithCurrentUserPermissionCallout count={numOfTransformAssets} />
</>
) : null}
<EuiSpacer size="l" />
<p>
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,37 @@ import type { PackageInfo, UpgradePackagePolicyDryRunResponse } from '../../../.
import { InstallStatus } from '../../../../../types';
import { useAuthz, useGetPackageInstallStatus, useInstallPackage } from '../../../../../hooks';

import { getNumTransformAssets } from '../../../../../../../components/transform_install_as_current_user_callout';

import { ConfirmPackageInstall } from './confirm_package_install';
type InstallationButtonProps = Pick<PackageInfo, 'name' | 'title' | 'version'> & {

type InstallationButtonProps = Pick<PackageInfo, 'name' | 'title' | 'version' | 'assets'> & {
disabled?: boolean;
dryRunData?: UpgradePackagePolicyDryRunResponse | null;
isUpgradingPackagePolicies?: boolean;
latestVersion?: string;
numOfAssets: number;
packagePolicyIds?: string[];
setIsUpgradingPackagePolicies?: React.Dispatch<React.SetStateAction<boolean>>;
};
export function InstallButton(props: InstallationButtonProps) {
const { name, numOfAssets, title, version } = props;
const { name, title, version, assets } = props;

const canInstallPackages = useAuthz().integrations.installPackages;
const installPackage = useInstallPackage();
const getPackageInstallStatus = useGetPackageInstallStatus();
const { status: installationStatus } = getPackageInstallStatus(name);

const numOfAssets = Object.entries(assets).reduce(
(acc, [serviceName, serviceNameValue]) =>
acc +
Object.entries(serviceNameValue || {}).reduce(
(acc2, [assetName, assetNameValue]) => acc2 + assetNameValue.length,
0
),
0
);
const numOfTransformAssets = getNumTransformAssets(assets);

const isInstalling = installationStatus === InstallStatus.installing;
const [isInstallModalVisible, setIsInstallModalVisible] = useState<boolean>(false);
const toggleInstallModal = useCallback(() => {
Expand All @@ -44,6 +58,7 @@ export function InstallButton(props: InstallationButtonProps) {
const installModal = (
<ConfirmPackageInstall
numOfAssets={numOfAssets}
numOfTransformAssets={numOfTransformAssets}
packageName={title}
onCancel={toggleInstallModal}
onConfirm={handleClickInstall}
Expand All @@ -61,15 +76,15 @@ export function InstallButton(props: InstallationButtonProps) {
{isInstalling ? (
<FormattedMessage
id="xpack.fleet.integrations.installPackage.installingPackageButtonLabel"
defaultMessage="Installing {title} assets"
defaultMessage="Installing {title}"
values={{
title,
}}
/>
) : (
<FormattedMessage
id="xpack.fleet.integrations.installPackage.installPackageButtonLabel"
defaultMessage="Install {title} assets"
defaultMessage="Install {title}"
values={{
title,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ import {

import { i18n } from '@kbn/i18n';

import {
getNumTransformAssets,
TransformInstallWithCurrentUserPermissionCallout,
} from '../../../../../../../components/transform_install_as_current_user_callout';

import type { FleetStartServices } from '../../../../../../../plugin';
import type { PackageInfo, PackageMetadata } from '../../../../../types';
import { InstallStatus } from '../../../../../types';
Expand Down Expand Up @@ -238,22 +233,6 @@ export const SettingsPage: React.FC<Props> = memo(

const isUpdating = installationStatus === InstallStatus.installing && installedVersion;

const { numOfAssets, numTransformAssets } = useMemo(
() => ({
numTransformAssets: getNumTransformAssets(packageInfo.assets),
numOfAssets: Object.entries(packageInfo.assets).reduce(
(acc, [serviceName, serviceNameValue]) =>
acc +
Object.entries(serviceNameValue || {}).reduce(
(acc2, [assetName, assetNameValue]) => acc2 + assetNameValue.length,
0
),
0
),
}),
[packageInfo.assets]
);

return (
<>
<EuiFlexGroup alignItems="flexStart">
Expand Down Expand Up @@ -365,15 +344,6 @@ export const SettingsPage: React.FC<Props> = memo(
</h4>
</EuiTitle>
<EuiSpacer size="s" />

{numTransformAssets > 0 ? (
<>
<TransformInstallWithCurrentUserPermissionCallout
count={numTransformAssets}
/>
<EuiSpacer size="s" />
</>
) : null}
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageInstallDescription"
Expand All @@ -388,7 +358,6 @@ export const SettingsPage: React.FC<Props> = memo(
<p>
<InstallButton
{...packageInfo}
numOfAssets={numOfAssets}
disabled={packageMetadata?.has_policies}
/>
</p>
Expand Down Expand Up @@ -418,7 +387,6 @@ export const SettingsPage: React.FC<Props> = memo(
<div>
<UninstallButton
{...packageInfo}
numOfAssets={numOfAssets}
latestVersion={latestVersion}
disabled={packageMetadata?.has_policies}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@ import { useAuthz, useGetPackageInstallStatus, useUninstallPackage } from '../..

import { ConfirmPackageUninstall } from './confirm_package_uninstall';

interface UninstallButtonProps extends Pick<PackageInfo, 'name' | 'title' | 'version'> {
interface UninstallButtonProps extends Pick<PackageInfo, 'name' | 'title' | 'version' | 'assets'> {
disabled?: boolean;
latestVersion?: string;
numOfAssets: number;
}

export const UninstallButton: React.FunctionComponent<UninstallButtonProps> = ({
disabled = false,
latestVersion,
name,
numOfAssets,
assets,
title,
version,
}) => {
Expand All @@ -38,6 +37,16 @@ export const UninstallButton: React.FunctionComponent<UninstallButtonProps> = ({

const [isUninstallModalVisible, setIsUninstallModalVisible] = useState<boolean>(false);

const numOfAssets = Object.entries(assets).reduce(
(acc, [serviceName, serviceNameValue]) =>
acc +
Object.entries(serviceNameValue || {}).reduce(
(acc2, [assetName, assetNameValue]) => acc2 + assetNameValue.length,
0
),
0
);

const handleClickUninstall = useCallback(() => {
uninstallPackage({ name, version, title, redirectToVersion: latestVersion ?? version });
setIsUninstallModalVisible(false);
Expand Down
Loading

0 comments on commit 9512f6c

Please sign in to comment.