Skip to content

Commit

Permalink
[Fleet] Add assets tab (#102517) (#102936)
Browse files Browse the repository at this point in the history
* very wip

* added new assets screen
* added routes to new assets view on the package details view

* Finished styling the assets page layout, need to work on adding
links

* rather use EuiHorizontalRule

* only show the assets tab if installed

* Added hacky version of linking to assets.

* added comment about deprecation of current linking functionality

* added an initial version of the success toast with a link to the agent flyout

* First iteration of end-to-end UX working. Need to add a lot of tests!

* fixed navigation bug and added a comment

* added a lot more padding to bottom of form

* restructured code for clarity, updated deprecation comments and moved relevant code closer together

* added a longer form comment about the origin policyId

* added logic for handling load error

* refactor assets accordions out of assets page component

* slightly larger text in badge

* added some basic jest test for view data step in enrollment flyout

* adjusted sizing of numbers in badges again, EuiText does not know about size="l"

* updated size limits for fleet

* updated styling and layout of assets accordion based on original
designs

* remove unused EuiTitle

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Jean-Louis Leysens <jloleysens@gmail.com>
  • Loading branch information
kibanamachine and jloleysens committed Jun 22, 2021
1 parent b911f5d commit ba95a28
Show file tree
Hide file tree
Showing 21 changed files with 676 additions and 125 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pageLoadAssetSize:
indexManagement: 140608
indexPatternManagement: 28222
infra: 184320
fleet: 450005
fleet: 465774
ingestPipelines: 58003
inputControlVis: 172819
inspector: 148999
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type InstallSource = 'registry' | 'upload';

export type EpmPackageInstallStatus = 'installed' | 'installing';

export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom';
export type DetailViewPanelName = 'overview' | 'policies' | 'assets' | 'settings' | 'custom';
export type ServiceName = 'kibana' | 'elasticsearch';
export type AgentAssetType = typeof agentAssetTypes;
export type DocAssetType = 'doc' | 'notice';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiLink,
} from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import type { ApplicationStart } from 'kibana/public';

import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
import type {
AgentPolicy,
PackageInfo,
Expand Down Expand Up @@ -60,7 +62,7 @@ const StepsWithLessPadding = styled(EuiSteps)`
`;

const CustomEuiBottomBar = styled(EuiBottomBar)`
// Set a relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar
/* A relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar */
z-index: 50;
`;

Expand All @@ -84,11 +86,26 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
const history = useHistory();
const handleNavigateTo = useNavigateToCallback();
const routeState = useIntraAppState<CreatePackagePolicyRouteState>();
const from: CreatePackagePolicyFrom = 'policyId' in params ? 'policy' : 'package';

const { search } = useLocation();
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
const policyId = useMemo(() => queryParams.get('policyId') ?? undefined, [queryParams]);
const queryParamsPolicyId = useMemo(() => queryParams.get('policyId') ?? undefined, [
queryParams,
]);

/**
* Please note: policyId can come from one of two sources. The URL param (in the URL path) or
* in the query params (?policyId=foo).
*
* Either way, we take this as an indication that a user is "coming from" the fleet policy UI
* since we link them out to packages (a.k.a. integrations) UI when choosing a new package. It is
* no longer possible to choose a package directly in the create package form.
*
* We may want to deprecate the ability to pass in policyId from URL params since there is no package
* creation possible if a user has not chosen one from the packages UI.
*/
const from: CreatePackagePolicyFrom =
'policyId' in params || queryParamsPolicyId ? 'policy' : 'package';

// Agent policy and package info states
const [agentPolicy, setAgentPolicy] = useState<AgentPolicy | undefined>();
Expand Down Expand Up @@ -280,29 +297,61 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
);
}

const fromPolicyWithoutAgentsAssigned = from === 'policy' && agentPolicy && agentCount === 0;

const fromPackageWithoutAgentsAssigned =
from === 'package' && packageInfo && agentPolicy && agentCount === 0;

const hasAgentsAssigned = agentCount && agentPolicy;

notifications.toasts.addSuccess({
title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', {
defaultMessage: `'{packagePolicyName}' integration added.`,
values: {
packagePolicyName: packagePolicy.name,
},
}),
text:
agentCount && agentPolicy
? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
values: {
agentPolicyName: agentPolicy.name,
},
})
: (params as AddToPolicyParams)?.policyId && agentPolicy && agentCount === 0
? i18n.translate('xpack.fleet.createPackagePolicy.addAgentNextNotification', {
text: fromPolicyWithoutAgentsAssigned
? i18n.translate(
'xpack.fleet.createPackagePolicy.policyContextAddAgentNextNotificationMessage',
{
defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`,
values: {
agentPolicyName: agentPolicy.name,
agentPolicyName: agentPolicy!.name,
},
})
: undefined,
}
)
: fromPackageWithoutAgentsAssigned
? toMountPoint(
// To render the link below we need to mount this JSX in the success toast
<FormattedMessage
id="xpack.fleet.createPackagePolicy.integrationsContextaddAgentNextNotificationMessage"
defaultMessage="Next, {link} to start ingesting data."
values={{
link: (
<EuiLink
href={getHref('integration_details_policies', {
pkgkey: `${packageInfo!.name}-${packageInfo!.version}`,
addAgentToPolicyId: agentPolicy!.id,
})}
>
{i18n.translate(
'xpack.fleet.createPackagePolicy.integrationsContextAddAgentLinkMessage',
{ defaultMessage: 'add an agent' }
)}
</EuiLink>
),
}}
/>
)
: hasAgentsAssigned
? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
values: {
agentPolicyName: agentPolicy!.name,
},
})
: undefined,
'data-test-subj': 'packagePolicyCreateSuccessToast',
});
} else {
Expand All @@ -312,6 +361,9 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
setFormState('VALID');
}
}, [
getHref,
from,
packageInfo,
agentCount,
agentPolicy,
formState,
Expand Down Expand Up @@ -353,13 +405,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
<StepSelectAgentPolicy
pkgkey={(params as AddToPolicyParams).pkgkey}
updatePackageInfo={updatePackageInfo}
defaultAgentPolicyId={policyId}
defaultAgentPolicyId={queryParamsPolicyId}
agentPolicy={agentPolicy}
updateAgentPolicy={updateAgentPolicy}
setIsLoadingSecondStep={setIsLoadingAgentPolicyStep}
/>
),
[params, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId]
[params, updatePackageInfo, agentPolicy, updateAgentPolicy, queryParamsPolicyId]
);

const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
Expand Down Expand Up @@ -455,7 +507,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
<PolicyBreadcrumb policyName={agentPolicy.name} policyId={agentPolicy.id} />
)}
<StepsWithLessPadding steps={steps} />
<EuiSpacer size="l" />
<EuiSpacer size="xl" />
<EuiSpacer size="xl" />
<CustomEuiBottomBar data-test-subj="integrationsBottomBar">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import React, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';

import { useCapabilities, useLink } from '../../../../../hooks';
import { useCapabilities, useStartServices } from '../../../../../hooks';
import { pagePathGetters, INTEGRATIONS_PLUGIN_ID } from '../../../../../constants';

export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
const { getHref } = useLink();
const { application } = useStartServices();
const hasWriteCapabilities = useCapabilities().write;

return (
Expand All @@ -36,7 +37,12 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
<EuiButton
isDisabled={!hasWriteCapabilities}
fill
href={getHref('add_integration_from_policy', { policyId })}
onClick={() =>
application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
path: `#${pagePathGetters.integrations_all()[1]}`,
state: { forAgentPolicyId: policyId },
})
}
>
<FormattedMessage
id="xpack.fleet.policyDetailsPackagePolicies.createFirstButtonText"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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, { useEffect, useState } from 'react';
import { Redirect } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';

import { Loading, Error } from '../../../../../components';

import type { PackageInfo } from '../../../../../types';
import { InstallStatus } from '../../../../../types';

import { useGetPackageInstallStatus, useLink, useStartServices } from '../../../../../hooks';

import type { AssetSavedObject } from './types';
import { allowedAssetTypes } from './constants';
import { AssetsAccordion } from './assets_accordion';

interface AssetsPanelProps {
packageInfo: PackageInfo;
}

export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
const { name, version } = packageInfo;
const {
savedObjects: { client: savedObjectsClient },
} = useStartServices();

const { getPath } = useLink();
const getPackageInstallStatus = useGetPackageInstallStatus();
const packageInstallStatus = getPackageInstallStatus(packageInfo.name);

const [assetSavedObjects, setAssetsSavedObjects] = useState<undefined | AssetSavedObject[]>();
const [fetchError, setFetchError] = useState<undefined | Error>();
const [isLoading, setIsLoading] = useState<boolean>(true);

useEffect(() => {
const fetchAssetSavedObjects = async () => {
if ('savedObject' in packageInfo) {
const {
savedObject: { attributes: packageAttributes },
} = packageInfo;

if (
!packageAttributes.installed_kibana ||
packageAttributes.installed_kibana.length === 0
) {
setIsLoading(false);
return;
}

try {
const objectsToGet = packageAttributes.installed_kibana.map(({ id, type }) => ({
id,
type,
}));
const { savedObjects } = await savedObjectsClient.bulkGet(objectsToGet);
setAssetsSavedObjects(savedObjects as AssetSavedObject[]);
} catch (e) {
setFetchError(e);
} finally {
setIsLoading(false);
}
} else {
setIsLoading(false);
}
};
fetchAssetSavedObjects();
}, [savedObjectsClient, packageInfo]);

// if they arrive at this page and the package is not installed, send them to overview
// this happens if they arrive with a direct url or they uninstall while on this tab
if (packageInstallStatus.status !== InstallStatus.installed) {
return (
<Redirect
to={getPath('integration_details_overview', {
pkgkey: `${name}-${version}`,
})}
/>
);
}

let content: JSX.Element | Array<JSX.Element | null>;

if (isLoading) {
content = <Loading />;
} else if (fetchError) {
content = (
<Error
title={
<FormattedMessage
id="xpack.fleet.epm.packageDetails.assets.fetchAssetsErrorTitle"
defaultMessage="Error loading assets"
/>
}
error={fetchError}
/>
);
} else if (assetSavedObjects === undefined) {
content = (
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.fleet.epm.packageDetails.assets.noAssetsFoundLabel"
defaultMessage="No assets found"
/>
</h2>
</EuiTitle>
);
} else {
content = allowedAssetTypes.map((assetType) => {
const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType);

if (!sectionAssetSavedObjects.length) {
return null;
}

return (
<>
<AssetsAccordion savedObjects={sectionAssetSavedObjects} type={assetType} />
<EuiSpacer size="l" />
</>
);
});
}

return (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1} />
<EuiFlexItem grow={6}>{content}</EuiFlexItem>
</EuiFlexGroup>
);
};
Loading

0 comments on commit ba95a28

Please sign in to comment.