[Fleet] Adjust add integration flow (#101714)

* Initial commit, very WIP

- added link to the integrations UI instead of add integration
  in fleet
- added a new piece of route state that gets passed to integration
  from fleet when browsing integrations for a policy
- when the integration is being added the page will send the user
  back to the policy overview instead of back to integrations UI

* remove unnecessary agent policy clear function

* added # to path so that navigation is correctly handled

* added logic to read the forward agent policy id

* remove inline select integration package from fleet add integration

* updated toast notification

* using query parameter to pass policy id back to create policy package page

* removed policyId from route path

* fix type issue

* updated the select agent field layout per the designs

* simpified item rendering in combobox and fixed combobox z-index issue

* added comment

* fix types and i18n

* updated icon and removed unused i18n

* refactor to using styled components for cusomt z-index styling

* attempt to fix integration test

* added scroll functionality for dealing with fixed footers that might be obstructing content

* fix scroll direction!

* attempting another scroll algorithm

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2021-06-15 09:26:37 +02:00 committed by GitHub
parent d3144e1439
commit f70542abe2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 265 additions and 372 deletions

View file

@ -27,7 +27,7 @@
* SOFTWARE.
*/
export function scrollIntoViewIfNecessary(target, fixedHeaderHeight) {
export function scrollIntoViewIfNecessary(target, fixedHeaderHeight, fixedFooterHeight) {
var rootScroller = document.scrollingElement || document.documentElement;
if (!rootScroller) {
throw new Error('Unable to find document.scrollingElement or document.documentElement');
@ -63,4 +63,11 @@ export function scrollIntoViewIfNecessary(target, fixedHeaderHeight) {
if (additionalScrollNecessary > 0) {
rootScroller.scrollTop = rootScroller.scrollTop - additionalScrollNecessary;
}
if (fixedFooterHeight) {
var bottomOfVisibility = viewportHeight - fixedFooterHeight;
if (bottomOfVisibility < boundingRect.bottom) {
rootScroller.scrollTop = rootScroller.scrollTop + fixedFooterHeight;
}
}
}

View file

@ -692,11 +692,22 @@ export class WebElementWrapper {
* @nonstandard
* @return {Promise<void>}
*/
public async scrollIntoViewIfNecessary(topOffset?: number): Promise<void> {
public async scrollIntoViewIfNecessary(
topOffsetOrOptions?: number | { topOffset?: number; bottomOffset?: number }
): Promise<void> {
let topOffset: undefined | number;
let bottomOffset: undefined | number;
if (typeof topOffsetOrOptions === 'number') {
topOffset = topOffsetOrOptions;
} else {
topOffset = topOffsetOrOptions?.topOffset;
bottomOffset = topOffsetOrOptions?.bottomOffset;
}
await this.driver.executeScript(
scrollIntoViewIfNecessary,
this._webElement,
topOffset || this.fixedHeaderHeight
topOffset || this.fixedHeaderHeight,
bottomOffset
);
}

View file

@ -7,7 +7,7 @@
import type { ReactEventHandler } from 'react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useRouteMatch, useHistory } from 'react-router-dom';
import { useRouteMatch, useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -49,7 +49,6 @@ import { CreatePackagePolicyPageLayout } from './components';
import type { CreatePackagePolicyFrom, PackagePolicyFormState } from './types';
import type { PackagePolicyValidationResults } from './services';
import { validatePackagePolicy, validationHasErrors } from './services';
import { StepSelectPackage } from './step_select_package';
import { StepSelectAgentPolicy } from './step_select_agent_policy';
import { StepConfigurePackagePolicy } from './step_configure_package';
import { StepDefinePackagePolicy } from './step_define_package_policy';
@ -60,9 +59,15 @@ 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
z-index: 50;
`;
interface AddToPolicyParams {
pkgkey: string;
integration?: string;
policyId?: string;
}
interface AddFromPolicyParams {
@ -81,10 +86,14 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
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]);
// Agent policy and package info states
const [agentPolicy, setAgentPolicy] = useState<AgentPolicy>();
const [agentPolicy, setAgentPolicy] = useState<AgentPolicy | undefined>();
const [packageInfo, setPackageInfo] = useState<PackageInfo>();
const [isLoadingSecondStep, setIsLoadingSecondStep] = useState<boolean>(false);
const [isLoadingAgentPolicyStep, setIsLoadingAgentPolicyStep] = useState<boolean>(false);
// Retrieve agent count
const agentPolicyId = agentPolicy?.id;
@ -286,6 +295,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
agentPolicyName: agentPolicy.name,
},
})
: (params as AddToPolicyParams)?.policyId && agentPolicy && agentCount === 0
? i18n.translate('xpack.fleet.createPackagePolicy.addAgentNextNotification', {
defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`,
values: {
agentPolicyName: agentPolicy.name,
},
})
: undefined,
'data-test-subj': 'packagePolicyCreateSuccessToast',
});
@ -337,32 +353,20 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
<StepSelectAgentPolicy
pkgkey={(params as AddToPolicyParams).pkgkey}
updatePackageInfo={updatePackageInfo}
defaultAgentPolicyId={policyId}
agentPolicy={agentPolicy}
updateAgentPolicy={updateAgentPolicy}
setIsLoadingSecondStep={setIsLoadingSecondStep}
setIsLoadingSecondStep={setIsLoadingAgentPolicyStep}
/>
),
[params, updatePackageInfo, agentPolicy, updateAgentPolicy]
[params, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId]
);
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
const stepSelectPackage = useMemo(
() => (
<StepSelectPackage
agentPolicyId={(params as AddFromPolicyParams).policyId}
updateAgentPolicy={updateAgentPolicy}
packageInfo={packageInfo}
updatePackageInfo={updatePackageInfo}
setIsLoadingSecondStep={setIsLoadingSecondStep}
/>
),
[params, updateAgentPolicy, packageInfo, updatePackageInfo]
);
const stepConfigurePackagePolicy = useMemo(
() =>
isLoadingSecondStep ? (
isLoadingAgentPolicyStep ? (
<Loading />
) : agentPolicy && packageInfo ? (
<>
@ -399,7 +403,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
<div />
),
[
isLoadingSecondStep,
isLoadingAgentPolicyStep,
agentPolicy,
packageInfo,
packagePolicy,
@ -413,27 +417,20 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
);
const steps: EuiStepProps[] = [
from === 'package'
? {
title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
defaultMessage: 'Select an agent policy',
}),
children: stepSelectAgentPolicy,
}
: {
title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectPackageTitle', {
defaultMessage: 'Select an integration',
}),
children: stepSelectPackage,
},
{
title: i18n.translate('xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle', {
defaultMessage: 'Configure integration',
}),
status: !packageInfo || !agentPolicy || isLoadingSecondStep ? 'disabled' : undefined,
status: !packageInfo || !agentPolicy || isLoadingAgentPolicyStep ? 'disabled' : undefined,
'data-test-subj': 'dataCollectionSetupStep',
children: stepConfigurePackagePolicy,
},
{
title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
defaultMessage: 'Apply to agent policy',
}),
children: stepSelectAgentPolicy,
},
];
return (
@ -459,10 +456,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
)}
<StepsWithLessPadding steps={steps} />
<EuiSpacer size="l" />
<EuiBottomBar>
<CustomEuiBottomBar data-test-subj="integrationsBottomBar">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
{!isLoadingSecondStep && agentPolicy && packageInfo && formState === 'INVALID' ? (
{!isLoadingAgentPolicyStep && agentPolicy && packageInfo && formState === 'INVALID' ? (
<FormattedMessage
id="xpack.fleet.createPackagePolicy.errorOnSaveText"
defaultMessage="Your integration policy has errors. Please fix them before saving."
@ -504,7 +501,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
</CustomEuiBottomBar>
</CreatePackagePolicyPageLayout>
);
};

View file

@ -14,9 +14,11 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiComboBox,
EuiTextColor,
EuiPortal,
EuiFormRow,
EuiDescribedFormGroup,
EuiTitle,
EuiText,
EuiLink,
} from '@elastic/eui';
@ -32,38 +34,32 @@ import {
} from '../../../hooks';
import { CreateAgentPolicyFlyout } from '../list_page/components';
const AgentPolicyWrapper = styled(EuiFormRow)`
const AgentPolicyFormRow = styled(EuiFormRow)`
.euiFormRow__label {
width: 100%;
}
`;
// Custom styling for drop down list items due to:
// 1) the max-width and overflow properties is added to prevent long agent policy
// names/descriptions from overflowing the flex items
// 2) max-width is built from the grow property on the flex items because the value
// changes based on if Fleet is enabled/setup or not
const AgentPolicyNameColumn = styled(EuiFlexItem)`
max-width: ${(props) => `${((props.grow as number) / 9) * 100}%`};
overflow: hidden;
`;
const AgentPolicyDescriptionColumn = styled(EuiFlexItem)`
max-width: ${(props) => `${((props.grow as number) / 9) * 100}%`};
overflow: hidden;
`;
export const StepSelectAgentPolicy: React.FunctionComponent<{
pkgkey: string;
updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
defaultAgentPolicyId?: string;
agentPolicy: AgentPolicy | undefined;
updateAgentPolicy: (agentPolicy: AgentPolicy | undefined) => void;
setIsLoadingSecondStep: (isLoading: boolean) => void;
}> = ({ pkgkey, updatePackageInfo, agentPolicy, updateAgentPolicy, setIsLoadingSecondStep }) => {
}> = ({
pkgkey,
updatePackageInfo,
agentPolicy,
updateAgentPolicy,
setIsLoadingSecondStep,
defaultAgentPolicyId,
}) => {
const { isReady: isFleetReady } = useFleetStatus();
// Selected agent policy state
const [selectedPolicyId, setSelectedPolicyId] = useState<string | undefined>(
agentPolicy ? agentPolicy.id : undefined
agentPolicy?.id ?? defaultAgentPolicyId
);
const [selectedAgentPolicyError, setSelectedAgentPolicyError] = useState<Error>();
@ -223,95 +219,91 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
) : null}
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<AgentPolicyWrapper
fullWidth={true}
label={
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiDescribedFormGroup
title={
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyLabel"
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyFormGroupTitle"
defaultMessage="Agent policy"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
<EuiLink
disabled={!hasWriteCapabilites}
onClick={() => setIsCreateAgentPolicyFlyoutOpen(true)}
>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.addButton"
defaultMessage="Create agent policy"
/>
</EuiLink>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</h3>
</EuiTitle>
}
helpText={
isFleetReady && selectedPolicyId ? (
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText"
defaultMessage="{count, plural, one {# agent} other {# agents}} are enrolled with the selected agent policy."
values={{
count: agentPoliciesById[selectedPolicyId]?.agents ?? 0,
}}
/>
) : null
description={
<EuiText color="subdued" size="s">
<p>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyFormGroupDescription"
defaultMessage="Agent policies are used to manage a group of integrations across a set of agents"
/>
</p>
</EuiText>
}
>
<EuiComboBox
placeholder={i18n.translate(
'xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyPlaceholderText',
{
defaultMessage: 'Select an agent policy to add this integration to',
}
)}
singleSelection={{ asPlainText: true }}
isClearable={false}
<AgentPolicyFormRow
fullWidth={true}
isLoading={isAgentPoliciesLoading || isPackageInfoLoading}
options={agentPolicyOptions}
renderOption={(option: EuiComboBoxOptionOption<string>) => {
return (
<EuiFlexGroup>
<AgentPolicyNameColumn grow={2}>
<span className="eui-textTruncate">{option.label}</span>
</AgentPolicyNameColumn>
<AgentPolicyDescriptionColumn grow={isFleetReady ? 5 : 7}>
<EuiTextColor className="eui-textTruncate" color="subdued">
{agentPoliciesById[option.value!].description}
</EuiTextColor>
</AgentPolicyDescriptionColumn>
{isFleetReady ? (
<EuiFlexItem grow={2} className="eui-textRight">
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsCountText"
defaultMessage="{count, plural, one {# agent} other {# agents}} enrolled"
values={{
count: agentPoliciesById[option.value!]?.agents ?? 0,
}}
/>
</EuiTextColor>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
}}
selectedOptions={selectedAgentPolicyOption ? [selectedAgentPolicyOption] : []}
onChange={(options) => {
const selectedOption = options[0] || undefined;
if (selectedOption) {
if (selectedOption.value !== selectedPolicyId) {
setSelectedPolicyId(selectedOption.value);
label={
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyLabel"
defaultMessage="Agent policy"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
<EuiLink
disabled={!hasWriteCapabilites}
onClick={() => setIsCreateAgentPolicyFlyoutOpen(true)}
>
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.addButton"
defaultMessage="Create agent policy"
/>
</EuiLink>
</div>
</EuiFlexItem>
</EuiFlexGroup>
}
helpText={
isFleetReady && selectedPolicyId ? (
<FormattedMessage
id="xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText"
defaultMessage="{count, plural, one {# agent is} other {# agents are}} enrolled with the selected agent policy."
values={{
count: agentPoliciesById[selectedPolicyId]?.agents ?? 0,
}}
/>
) : null
}
>
<EuiComboBox
placeholder={i18n.translate(
'xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyPlaceholderText',
{
defaultMessage: 'Select an agent policy to add this integration to',
}
} else {
setSelectedPolicyId(undefined);
}
}}
/>
</AgentPolicyWrapper>
)}
singleSelection={{ asPlainText: true }}
isClearable={false}
fullWidth={true}
isLoading={isAgentPoliciesLoading || isPackageInfoLoading}
options={agentPolicyOptions}
selectedOptions={selectedAgentPolicyOption ? [selectedAgentPolicyOption] : []}
onChange={(options) => {
const selectedOption = options[0] || undefined;
if (selectedOption) {
if (selectedOption.value !== selectedPolicyId) {
setSelectedPolicyId(selectedOption.value);
}
} else {
setSelectedPolicyId(undefined);
}
}}
/>
</AgentPolicyFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
{/* Display selected agent policy error if there is one */}
{selectedAgentPolicyError ? (

View file

@ -1,198 +0,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, { useEffect, useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer } from '@elastic/eui';
import { Error, PackageIcon } from '../../../components';
import type { AgentPolicy, PackageInfo, PackagePolicy, GetPackagesResponse } from '../../../types';
import {
useGetOneAgentPolicy,
useGetPackages,
useGetLimitedPackages,
sendGetPackageInfoByKey,
} from '../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../services';
export const StepSelectPackage: React.FunctionComponent<{
agentPolicyId: string;
updateAgentPolicy: (agentPolicy: AgentPolicy | undefined) => void;
packageInfo?: PackageInfo;
updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
setIsLoadingSecondStep: (isLoading: boolean) => void;
}> = ({
agentPolicyId,
updateAgentPolicy,
packageInfo,
updatePackageInfo,
setIsLoadingSecondStep,
}) => {
// Selected package state
const [selectedPkgKey, setSelectedPkgKey] = useState<string | undefined>(
packageInfo ? pkgKeyFromPackageInfo(packageInfo) : undefined
);
const [selectedPkgError, setSelectedPkgError] = useState<Error>();
// Fetch agent policy info
const {
data: agentPolicyData,
error: agentPolicyError,
isLoading: isAgentPoliciesLoading,
} = useGetOneAgentPolicy(agentPolicyId);
// Fetch packages info
// Filter out limited packages already part of selected agent policy
const [packages, setPackages] = useState<GetPackagesResponse['response']>([]);
const {
data: packagesData,
error: packagesError,
isLoading: isPackagesLoading,
} = useGetPackages();
const {
data: limitedPackagesData,
isLoading: isLimitedPackagesLoading,
} = useGetLimitedPackages();
useEffect(() => {
if (packagesData?.response && limitedPackagesData?.response && agentPolicyData?.item) {
const allPackages = packagesData.response;
const limitedPackages = limitedPackagesData.response;
const usedLimitedPackages = (agentPolicyData.item.package_policies as PackagePolicy[])
.map((packagePolicy) => packagePolicy.package?.name || '')
.filter((pkgName) => limitedPackages.includes(pkgName));
setPackages(allPackages.filter((pkg) => !usedLimitedPackages.includes(pkg.name)));
}
}, [packagesData, limitedPackagesData, agentPolicyData]);
// Update parent agent policy state
useEffect(() => {
if (agentPolicyData && agentPolicyData.item) {
updateAgentPolicy(agentPolicyData.item);
}
}, [agentPolicyData, updateAgentPolicy]);
// Update parent selected package state
useEffect(() => {
const fetchPackageInfo = async () => {
if (selectedPkgKey) {
setIsLoadingSecondStep(true);
const { data, error } = await sendGetPackageInfoByKey(selectedPkgKey);
if (error) {
setSelectedPkgError(error);
updatePackageInfo(undefined);
} else if (data && data.response) {
setSelectedPkgError(undefined);
updatePackageInfo(data.response);
}
setIsLoadingSecondStep(false);
} else {
setSelectedPkgError(undefined);
updatePackageInfo(undefined);
}
};
if (!packageInfo || selectedPkgKey !== pkgKeyFromPackageInfo(packageInfo)) {
fetchPackageInfo();
}
}, [selectedPkgKey, packageInfo, updatePackageInfo, setIsLoadingSecondStep]);
// Display agent policy error if there is one
if (agentPolicyError) {
return (
<Error
title={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle"
defaultMessage="Error loading agent policy information"
/>
}
error={agentPolicyError}
/>
);
}
// Display packages list error if there is one
if (packagesError) {
return (
<Error
title={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle"
defaultMessage="Error loading integrations"
/>
}
error={packagesError}
/>
);
}
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiSelectable
searchable
allowExclusions={false}
singleSelection={true}
isLoading={isPackagesLoading || isLimitedPackagesLoading || isAgentPoliciesLoading}
options={packages.map(({ title, name, version, icons }) => {
const pkgkey = `${name}-${version}`;
return {
label: title || name,
key: pkgkey,
prepend: <PackageIcon packageName={name} version={version} icons={icons} size="m" />,
checked: selectedPkgKey === pkgkey ? 'on' : undefined,
};
})}
listProps={{
bordered: true,
}}
searchProps={{
placeholder: i18n.translate(
'xpack.fleet.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder',
{
defaultMessage: 'Search for integrations',
}
),
}}
height={240}
onChange={(options) => {
const selectedOption = options.find((option) => option.checked === 'on');
if (selectedOption) {
if (selectedOption.key !== selectedPkgKey) {
setSelectedPkgKey(selectedOption.key);
}
} else {
setSelectedPkgKey(undefined);
}
}}
>
{(list, search) => (
<Fragment>
{search}
<EuiSpacer size="m" />
{list}
</Fragment>
)}
</EuiSelectable>
</EuiFlexItem>
{/* Display selected package error if there is one */}
{selectedPkgError ? (
<EuiFlexItem>
<Error
title={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle"
defaultMessage="Error loading selected integration"
/>
}
error={selectedPkgError}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
};

View file

@ -18,9 +18,11 @@ import {
EuiText,
} from '@elastic/eui';
import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../common';
import { pagePathGetters } from '../../../../../../../constants';
import type { AgentPolicy, PackagePolicy } from '../../../../../types';
import { PackageIcon, PackagePolicyActionsMenu } from '../../../../../components';
import { useCapabilities, useLink } from '../../../../../hooks';
import { useCapabilities, useStartServices } from '../../../../../hooks';
interface InMemoryPackagePolicy extends PackagePolicy {
packageName?: string;
@ -49,7 +51,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
agentPolicy,
...rest
}) => {
const { getHref } = useLink();
const { application } = useStartServices();
const hasWriteCapabilities = useCapabilities().write;
// With the package policies provided on input, generate the list of package policies
@ -194,8 +196,13 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
<EuiButton
key="addPackagePolicyButton"
isDisabled={!hasWriteCapabilities}
iconType="plusInCircle"
href={getHref('add_integration_from_policy', { policyId: agentPolicy.id })}
iconType="refresh"
onClick={() => {
application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
path: `#${pagePathGetters.integrations_all()[1]}`,
state: { forAgentPolicyId: agentPolicy.id },
});
}}
>
<FormattedMessage
id="xpack.fleet.policyDetails.addPackagePolicyButtonText"

View file

@ -29,6 +29,7 @@ import type { FleetConfigType, FleetStartServices } from '../../plugin';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common';
import { AgentPolicyContextProvider } from './hooks';
import { INTEGRATIONS_ROUTING_PATHS } from './constants';
import { Error, Loading } from './components';
@ -201,9 +202,11 @@ export const IntegrationsAppContext: React.FC<{
<FleetStatusProvider>
<IntraAppStateProvider kibanaScopedHistory={history}>
<Router history={routerHistoryInstance}>
<PackageInstallProvider notifications={startServices.notifications}>
{children}
</PackageInstallProvider>
<AgentPolicyContextProvider>
<PackageInstallProvider notifications={startServices.notifications}>
{children}
</PackageInstallProvider>
</AgentPolicyContextProvider>
</Router>
</IntraAppStateProvider>
</FleetStatusProvider>

View file

@ -10,3 +10,4 @@ export { useBreadcrumbs } from './use_breadcrumbs';
export { useLinks } from './use_links';
export * from './use_local_search';
export * from './use_package_install';
export * from './use_agent_policy_context';

View file

@ -0,0 +1,36 @@
/*
* 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 type { FunctionComponent } from 'react';
import React, { createContext, useContext, useRef, useCallback } from 'react';
import type { IntegrationsAppBrowseRouteState } from '../../../types';
import { useIntraAppState } from '../../../hooks';
interface AgentPolicyContextValue {
getId(): string | undefined;
}
const AgentPolicyContext = createContext<AgentPolicyContextValue>({ getId: () => undefined });
export const AgentPolicyContextProvider: FunctionComponent = ({ children }) => {
const maybeState = useIntraAppState<undefined | IntegrationsAppBrowseRouteState>();
const ref = useRef<undefined | string>(maybeState?.forAgentPolicyId);
const getId = useCallback(() => {
return ref.current;
}, []);
return <AgentPolicyContext.Provider value={{ getId }}>{children}</AgentPolicyContext.Provider>;
};
export const useAgentPolicyContext = () => {
const ctx = useContext(AgentPolicyContext);
if (!ctx) {
throw new Error('useAgentPolicyContext can only be used inside of AgentPolicyContextProvider');
}
return ctx;
};

View file

@ -37,7 +37,12 @@ import {
INTEGRATIONS_ROUTING_PATHS,
pagePathGetters,
} from '../../../../constants';
import { useCapabilities, useGetPackageInfoByKey, useLink } from '../../../../hooks';
import {
useCapabilities,
useGetPackageInfoByKey,
useLink,
useAgentPolicyContext,
} from '../../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../../services';
import type {
CreatePackagePolicyRouteState,
@ -79,6 +84,7 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) {
}
export function Detail() {
const { getId: getAgentPolicyId } = useAgentPolicyContext();
const { pkgkey, panel } = useParams<DetailParams>();
const { getHref } = useLink();
const hasWriteCapabilites = useCapabilities().write;
@ -87,6 +93,7 @@ export function Detail() {
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
const integration = useMemo(() => queryParams.get('integration'), [queryParams]);
const services = useStartServices();
const agentPolicyIdFromContext = getAgentPolicyId();
// Package info state
const [packageInfo, setPackageInfo] = useState<PackageInfo | null>(null);
@ -211,24 +218,42 @@ export function Detail() {
search,
hash,
});
const redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] &
CreatePackagePolicyRouteState['onCancelNavigateTo'] = [
INTEGRATIONS_PLUGIN_ID,
{
path: currentPath,
},
];
const path = pagePathGetters.add_integration_to_policy({
pkgkey,
...(integration ? { integration } : {}),
...(agentPolicyIdFromContext ? { agentPolicyId: agentPolicyIdFromContext } : {}),
})[1];
let redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] &
CreatePackagePolicyRouteState['onCancelNavigateTo'];
if (agentPolicyIdFromContext) {
redirectToPath = [
PLUGIN_ID,
{
path: `#${
pagePathGetters.policy_details({
policyId: agentPolicyIdFromContext,
})[1]
}`,
},
];
} else {
redirectToPath = [
INTEGRATIONS_PLUGIN_ID,
{
path: currentPath,
},
];
}
const redirectBackRouteState: CreatePackagePolicyRouteState = {
onSaveNavigateTo: redirectToPath,
onCancelNavigateTo: redirectToPath,
onCancelUrl: currentPath,
};
const path = pagePathGetters.add_integration_to_policy({
pkgkey,
...(integration ? { integration } : {}),
})[1];
services.application.navigateToApp(PLUGIN_ID, {
// Necessary because of Fleet's HashRouter. Can be changed when
// https://github.com/elastic/kibana/issues/96134 is resolved
@ -236,7 +261,16 @@ export function Detail() {
state: redirectBackRouteState,
});
},
[history, hash, pathname, search, pkgkey, integration, services.application]
[
history,
hash,
pathname,
search,
pkgkey,
integration,
services.application,
agentPolicyIdFromContext,
]
);
const headerRightContent = useMemo(
@ -284,6 +318,9 @@ export function Detail() {
href={getHref('add_integration_to_policy', {
pkgkey,
...(integration ? { integration } : {}),
...(agentPolicyIdFromContext
? { agentPolicyId: agentPolicyIdFromContext }
: {}),
})}
onClick={handleAddIntegrationPolicyClick}
data-test-subj="addIntegrationPolicyButton"
@ -325,6 +362,7 @@ export function Detail() {
packageInstallStatus,
pkgkey,
updateAvailable,
agentPolicyIdFromContext,
]
);

View file

@ -111,9 +111,10 @@ export const pagePathGetters: {
FLEET_BASE_PATH,
`/policies/${policyId}/add-integration`,
],
add_integration_to_policy: ({ pkgkey, integration }) => [
add_integration_to_policy: ({ pkgkey, integration, agentPolicyId }) => [
FLEET_BASE_PATH,
`/integrations/${pkgkey}/add-integration${integration ? `/${integration}` : ''}`,
// prettier-ignore
`/integrations/${pkgkey}/add-integration${integration ? `/${integration}` : ''}${agentPolicyId ? `?policyId=${agentPolicyId}` : ''}`,
],
edit_integration: ({ policyId, packagePolicyId }) => [
FLEET_BASE_PATH,

View file

@ -39,6 +39,11 @@ export interface AgentDetailsReassignPolicyAction {
onDoneNavigateTo?: Parameters<ApplicationStart['navigateToApp']>;
}
export interface IntegrationsAppBrowseRouteState {
/** The agent policy that we are browsing integrations for */
forAgentPolicyId: string;
}
/**
* All possible Route states.
*/

View file

@ -8996,11 +8996,6 @@
"xpack.fleet.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション",
"xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle": "統合の構成",
"xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle": "エージェントポリシーを選択",
"xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle": "統合の読み込みエラー",
"xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle": "エージェントポリシー情報の読み込みエラー",
"xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択した統合の読み込みエラー",
"xpack.fleet.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder": "統合を検索",
"xpack.fleet.createPackagePolicy.stepSelectPackageTitle": "統合を選択",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.addButton": "エージェントポリシーを作成",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyLabel": "エージェントポリシー",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyPlaceholderText": "この統合を追加するエージェントポリシーを選択",

View file

@ -9077,13 +9077,7 @@
"xpack.fleet.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项",
"xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle": "配置集成",
"xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle": "选择代理策略",
"xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle": "加载集成时出错",
"xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle": "加载代理策略信息时出错",
"xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定集成时出错",
"xpack.fleet.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder": "搜索集成",
"xpack.fleet.createPackagePolicy.stepSelectPackageTitle": "选择集成",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.addButton": "创建代理策略",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsCountText": "{count, plural, other {# 个代理}}已注册",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText": "{count, plural, other {# 个代理}}已注册到选定代理策略。",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyLabel": "代理策略",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyPlaceholderText": "选择要将此集成添加到的代理策略",

View file

@ -16,6 +16,8 @@ export function SyntheticsIntegrationPageProvider({
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
const fixedFooterHeight = 72; // Size of EuiBottomBar more or less
return {
/**
* Navigates to the Synthetics Integration page
@ -85,6 +87,7 @@ export function SyntheticsIntegrationPageProvider({
*/
async fillTextInputByTestSubj(testSubj: string, value: string) {
const field = await testSubjects.find(testSubj, 5000);
await field.scrollIntoViewIfNecessary({ bottomOffset: fixedFooterHeight });
await field.click();
await field.clearValue();
await field.type(value);
@ -96,6 +99,7 @@ export function SyntheticsIntegrationPageProvider({
* @params {value} the value of the input
*/
async fillTextInput(field: WebElementWrapper, value: string) {
await field.scrollIntoViewIfNecessary({ bottomOffset: fixedFooterHeight });
await field.click();
await field.clearValue();
await field.type(value);