[Ingest Manager] Simplify add/edit package config (integration) form (#71187) (#71460)

* Match add integration page with designs

* Clean up package config layout code

* Match edit integration config page with designs

* Fix typing and i18n issues

* Add back data test subj

* Add loading UI for second step; code clean up

* Fix limited packages incorrect response

* Add ability to create agent config when selecting config to add integration to

* Add error count to input-level panel; memoize children components

* Add error count next to all advanced options toggles

* Move general form error to bottom bar

* #69750 Auto-expand inputs with required & empty (invalid) vars

* #68019 Enforce unique package config names, per agent config

* Fix typing

* Fix i18n

* Fix reloading when new agent config _wasn't_ created

* Memoize edit integration and fix fields not collapsing on edit

* Really fix types
# Conflicts:
#	x-pack/plugins/translations/translations/ja-JP.json
#	x-pack/plugins/translations/translations/zh-CN.json
This commit is contained in:
Jen Huang 2020-07-13 10:12:20 -07:00 committed by GitHub
parent e2d7e2cbdd
commit 81b4f8204d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1606 additions and 1020 deletions

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
@ -27,130 +27,148 @@ export const CreatePackageConfigPageLayout: React.FunctionComponent<{
agentConfig?: AgentConfig;
packageInfo?: PackageInfo;
'data-test-subj'?: string;
}> = ({
from,
cancelUrl,
onCancel,
agentConfig,
packageInfo,
children,
'data-test-subj': dataTestSubj,
}) => {
const leftColumn = (
<EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart">
<EuiFlexItem>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiButtonEmpty
size="xs"
iconType="arrowLeft"
flush="left"
href={cancelUrl}
onClick={onCancel}
data-test-subj={`${dataTestSubj}_cancelBackLink`}
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.cancelLinkText"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
}> = memo(
({
from,
cancelUrl,
onCancel,
agentConfig,
packageInfo,
children,
'data-test-subj': dataTestSubj,
}) => {
const pageTitle = useMemo(() => {
if ((from === 'package' || from === 'edit') && packageInfo) {
return (
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<PackageIcon
packageName={packageInfo?.name || ''}
version={packageInfo?.version || ''}
icons={packageInfo?.icons}
size="xl"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<h1>
{from === 'edit' ? (
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.pageTitleWithPackageName"
defaultMessage="Edit {packageName} integration"
values={{
packageName: packageInfo.title,
}}
/>
) : (
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.pageTitleWithPackageName"
defaultMessage="Add {packageName} integration"
values={{
packageName: packageInfo.title,
}}
/>
)}
</h1>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return from === 'edit' ? (
<EuiText>
<h1>
{from === 'edit' ? (
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.pageTitle"
defaultMessage="Edit integration"
/>
) : (
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.pageTitle"
defaultMessage="Add integration"
/>
)}
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.pageTitle"
defaultMessage="Edit integration"
/>
</h1>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{from === 'edit' ? (
) : (
<EuiText>
<h1>
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.pageDescription"
defaultMessage="Follow the instructions below to edit this integration."
id="xpack.ingestManager.createPackageConfig.pageTitle"
defaultMessage="Add integration"
/>
) : from === 'config' ? (
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.pageDescriptionfromConfig"
defaultMessage="Follow the instructions below to add an integration to this agent configuration."
/>
) : (
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.pageDescriptionfromPackage"
defaultMessage="Follow the instructions below to add this integration to an agent configuration."
/>
)}
</h1>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
const rightColumn = (
<EuiFlexGroup justifyContent="flexEnd" direction={'row'} gutterSize="xl">
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
{agentConfig && (from === 'config' || from === 'edit') ? (
<EuiDescriptionList style={{ textAlign: 'right' }} textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.agentConfigurationNameLabel"
defaultMessage="Configuration"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{agentConfig?.name || '-'}
</EuiDescriptionListDescription>
</EuiDescriptionList>
) : null}
{packageInfo && from === 'package' ? (
<EuiDescriptionList style={{ textAlign: 'right' }} textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.packageNameLabel"
defaultMessage="Integration"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<PackageIcon
packageName={packageInfo?.name || ''}
version={packageInfo?.version || ''}
icons={packageInfo?.icons}
size="m"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{packageInfo?.title || packageInfo?.name || '-'}
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescriptionListDescription>
</EuiDescriptionList>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
);
);
}, [from, packageInfo]);
const maxWidth = 770;
return (
<WithHeaderLayout
restrictHeaderWidth={maxWidth}
restrictWidth={maxWidth}
leftColumn={leftColumn}
rightColumn={rightColumn}
rightColumnGrow={false}
data-test-subj={dataTestSubj}
>
{children}
</WithHeaderLayout>
);
};
const pageDescription = useMemo(() => {
return from === 'edit' ? (
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.pageDescription"
defaultMessage="Modify integration settings and deploy changes to the selected agent configuration."
/>
) : from === 'config' ? (
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.pageDescriptionfromConfig"
defaultMessage="Configure an integration for the selected agent configuration."
/>
) : (
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.pageDescriptionfromPackage"
defaultMessage="Follow the instructions below to add this integration to an agent configuraiton."
/>
);
}, [from]);
const leftColumn = (
<EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart">
<EuiFlexItem>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiButtonEmpty
size="xs"
iconType="arrowLeft"
flush="left"
href={cancelUrl}
onClick={onCancel}
data-test-subj={`${dataTestSubj}_cancelBackLink`}
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.cancelLinkText"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>{pageTitle}</EuiFlexItem>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{pageDescription}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
const rightColumn =
agentConfig && (from === 'config' || from === 'edit') ? (
<EuiDescriptionList className="eui-textRight" textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.agentConfigurationNameLabel"
defaultMessage="Agent configuration"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{agentConfig?.name || '-'}</EuiDescriptionListDescription>
</EuiDescriptionList>
) : undefined;
const maxWidth = 770;
return (
<WithHeaderLayout
restrictHeaderWidth={maxWidth}
restrictWidth={maxWidth}
leftColumn={leftColumn}
rightColumn={rightColumn}
rightColumnGrow={false}
data-test-subj={dataTestSubj}
>
{children}
</WithHeaderLayout>
);
}
);

View file

@ -3,17 +3,15 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, Fragment } from 'react';
import React, { useState, Fragment, memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTextColor,
EuiSpacer,
EuiButtonEmpty,
EuiTitle,
EuiIconTip,
} from '@elastic/eui';
import { PackageConfigInput, RegistryVarsEntry } from '../../../../types';
import {
@ -29,150 +27,157 @@ export const PackageConfigInputConfig: React.FunctionComponent<{
updatePackageConfigInput: (updatedInput: Partial<PackageConfigInput>) => void;
inputVarsValidationResults: PackageConfigConfigValidationResults;
forceShowErrors?: boolean;
}> = ({
packageInputVars,
packageConfigInput,
updatePackageConfigInput,
inputVarsValidationResults,
forceShowErrors,
}) => {
// Showing advanced options toggle state
const [isShowingAdvanced, setIsShowingAdvanced] = useState<boolean>(false);
}> = memo(
({
packageInputVars,
packageConfigInput,
updatePackageConfigInput,
inputVarsValidationResults,
forceShowErrors,
}) => {
// Showing advanced options toggle state
const [isShowingAdvanced, setIsShowingAdvanced] = useState<boolean>(false);
// Errors state
const hasErrors = forceShowErrors && validationHasErrors(inputVarsValidationResults);
// Errors state
const hasErrors = forceShowErrors && validationHasErrors(inputVarsValidationResults);
const requiredVars: RegistryVarsEntry[] = [];
const advancedVars: RegistryVarsEntry[] = [];
const requiredVars: RegistryVarsEntry[] = [];
const advancedVars: RegistryVarsEntry[] = [];
if (packageInputVars) {
packageInputVars.forEach((varDef) => {
if (isAdvancedVar(varDef)) {
advancedVars.push(varDef);
} else {
requiredVars.push(varDef);
}
});
}
if (packageInputVars) {
packageInputVars.forEach((varDef) => {
if (isAdvancedVar(varDef)) {
advancedVars.push(varDef);
} else {
requiredVars.push(varDef);
}
});
}
return (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1}>
<EuiTitle size="s">
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<h4>
<EuiTextColor color={hasErrors ? 'danger' : 'default'}>
const advancedVarsWithErrorsCount: number = useMemo(
() =>
advancedVars.filter(
({ name: varName }) => inputVarsValidationResults.vars?.[varName]?.length
).length,
[advancedVars, inputVarsValidationResults.vars]
);
return (
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiFlexGroup gutterSize="none" alignItems="flexStart">
<EuiFlexItem grow={1} />
<EuiFlexItem grow={5}>
<EuiText>
<h4>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsTitle"
defaultMessage="Settings"
/>
</EuiTextColor>
</h4>
</h4>
</EuiText>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
<p>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsDescription"
defaultMessage="The following settings are applicable to all inputs below."
/>
</p>
</EuiText>
</EuiFlexItem>
{hasErrors ? (
<EuiFlexItem grow={false}>
<EuiIconTip
content={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.inputConfigErrorsTooltip"
defaultMessage="Fix configuration errors"
/>
}
position="right"
type="alert"
iconProps={{ color: 'danger' }}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="m">
{requiredVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packageConfigInput.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackageConfigInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackageConfigInput({
vars: {
...packageConfigInput.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={inputVarsValidationResults.vars![varName]}
forceShowErrors={forceShowErrors}
/>
</EuiFlexItem>
);
})}
{advancedVars.length ? (
<Fragment>
<EuiFlexItem>
{/* Wrapper div to prevent button from going full width */}
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'}
onClick={() => setIsShowingAdvanced(!isShowingAdvanced)}
flush="left"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText"
defaultMessage="Advanced options"
/>
</EuiButtonEmpty>
</EuiFlexItem>
{!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? (
<EuiFlexItem grow={false}>
<EuiText color="danger" size="s">
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.errorCountText"
defaultMessage="{count, plural, one {# error} other {# errors}}"
values={{ count: advancedVarsWithErrorsCount }}
/>
</EuiText>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
{isShowingAdvanced
? advancedVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packageConfigInput.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackageConfigInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackageConfigInput({
vars: {
...packageConfigInput.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={inputVarsValidationResults.vars![varName]}
forceShowErrors={forceShowErrors}
/>
</EuiFlexItem>
);
})
: null}
</Fragment>
) : null}
</EuiFlexGroup>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText color="subdued" size="s">
<p>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsDescription"
defaultMessage="The following settings are applicable to all streams."
/>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiFlexGroup direction="column" gutterSize="m">
{requiredVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packageConfigInput.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackageConfigInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackageConfigInput({
vars: {
...packageConfigInput.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={inputVarsValidationResults.vars![varName]}
forceShowErrors={forceShowErrors}
/>
</EuiFlexItem>
);
})}
{advancedVars.length ? (
<Fragment>
<EuiFlexItem>
{/* Wrapper div to prevent button from going full width */}
<div>
<EuiButtonEmpty
size="xs"
iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'}
onClick={() => setIsShowingAdvanced(!isShowingAdvanced)}
flush="left"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText"
defaultMessage="Advanced options"
/>
</EuiButtonEmpty>
</div>
</EuiFlexItem>
{isShowingAdvanced
? advancedVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packageConfigInput.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackageConfigInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackageConfigInput({
vars: {
...packageConfigInput.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={inputVarsValidationResults.vars![varName]}
forceShowErrors={forceShowErrors}
/>
</EuiFlexItem>
);
})
: null}
</Fragment>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};
</EuiFlexItem>
</EuiFlexGrid>
);
}
);

View file

@ -3,21 +3,18 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, Fragment } from 'react';
import React, { useState, Fragment, memo } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiSwitch,
EuiText,
EuiTextColor,
EuiButtonIcon,
EuiHorizontalRule,
EuiSpacer,
EuiIconTip,
} from '@elastic/eui';
import {
PackageConfigInput,
@ -25,16 +22,44 @@ import {
RegistryInput,
RegistryStream,
} from '../../../../types';
import { PackageConfigInputValidationResults, validationHasErrors } from '../services';
import {
PackageConfigInputValidationResults,
hasInvalidButRequiredVar,
countValidationErrors,
} from '../services';
import { PackageConfigInputConfig } from './package_config_input_config';
import { PackageConfigInputStreamConfig } from './package_config_input_stream';
const FlushHorizontalRule = styled(EuiHorizontalRule)`
margin-left: -${(props) => props.theme.eui.paddingSizes.m};
margin-right: -${(props) => props.theme.eui.paddingSizes.m};
width: auto;
const ShortenedHorizontalRule = styled(EuiHorizontalRule)`
&&& {
width: ${(11 / 12) * 100}%;
margin-left: auto;
}
`;
const shouldShowStreamsByDefault = (
packageInput: RegistryInput,
packageInputStreams: Array<RegistryStream & { dataset: { name: string } }>,
packageConfigInput: PackageConfigInput
): boolean => {
return (
packageConfigInput.enabled &&
(hasInvalidButRequiredVar(packageInput.vars, packageConfigInput.vars) ||
Boolean(
packageInputStreams.find(
(stream) =>
stream.enabled &&
hasInvalidButRequiredVar(
stream.vars,
packageConfigInput.streams.find(
(pkgStream) => stream.dataset.name === pkgStream.dataset.name
)?.vars
)
)
))
);
};
export const PackageConfigInputPanel: React.FunctionComponent<{
packageInput: RegistryInput;
packageInputStreams: Array<RegistryStream & { dataset: { name: string } }>;
@ -42,148 +67,136 @@ export const PackageConfigInputPanel: React.FunctionComponent<{
updatePackageConfigInput: (updatedInput: Partial<PackageConfigInput>) => void;
inputValidationResults: PackageConfigInputValidationResults;
forceShowErrors?: boolean;
}> = ({
packageInput,
packageInputStreams,
packageConfigInput,
updatePackageConfigInput,
inputValidationResults,
forceShowErrors,
}) => {
// Showing streams toggle state
const [isShowingStreams, setIsShowingStreams] = useState<boolean>(false);
}> = memo(
({
packageInput,
packageInputStreams,
packageConfigInput,
updatePackageConfigInput,
inputValidationResults,
forceShowErrors,
}) => {
// Showing streams toggle state
const [isShowingStreams, setIsShowingStreams] = useState<boolean>(
shouldShowStreamsByDefault(packageInput, packageInputStreams, packageConfigInput)
);
// Errors state
const hasErrors = forceShowErrors && validationHasErrors(inputValidationResults);
// Errors state
const errorCount = countValidationErrors(inputValidationResults);
const hasErrors = forceShowErrors && errorCount;
return (
<EuiPanel>
{/* Header / input-level toggle */}
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiSwitch
label={
<EuiFlexGroup alignItems="center" gutterSize="s">
const inputStreams = packageInputStreams
.map((packageInputStream) => {
return {
packageInputStream,
packageConfigInputStream: packageConfigInput.streams.find(
(stream) => stream.dataset.name === packageInputStream.dataset.name
),
};
})
.filter((stream) => Boolean(stream.packageConfigInputStream));
return (
<>
{/* Header / input-level toggle */}
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiSwitch
label={
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText>
<h4>{packageInput.title || packageInput.type}</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
checked={packageConfigInput.enabled}
onChange={(e) => {
const enabled = e.target.checked;
updatePackageConfigInput({
enabled,
streams: packageConfigInput.streams.map((stream) => ({
...stream,
enabled,
})),
});
if (!enabled && isShowingStreams) {
setIsShowingStreams(false);
}
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" alignItems="center">
{hasErrors ? (
<EuiFlexItem grow={false}>
<EuiText>
<h4>
<EuiTextColor color={hasErrors ? 'danger' : 'default'}>
{packageInput.title || packageInput.type}
</EuiTextColor>
</h4>
<EuiText color="danger" size="s">
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.errorCountText"
defaultMessage="{count, plural, one {# error} other {# errors}}"
values={{ count: errorCount }}
/>
</EuiText>
</EuiFlexItem>
{hasErrors ? (
<EuiFlexItem grow={false}>
<EuiIconTip
content={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.inputLevelErrorsTooltip"
defaultMessage="Fix configuration errors"
/>
}
position="right"
type="alert"
iconProps={{ color: 'danger' }}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
}
checked={packageConfigInput.enabled}
onChange={(e) => {
const enabled = e.target.checked;
updatePackageConfigInput({
enabled,
streams: packageConfigInput.streams.map((stream) => ({
...stream,
enabled,
})),
});
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>
<EuiText color="subdued">
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.streamsEnabledCountText"
defaultMessage="{count} / {total, plural, one {# stream} other {# streams}} enabled"
values={{
count: (
<EuiTextColor color="default">
<strong>
{packageConfigInput.streams.filter((stream) => stream.enabled).length}
</strong>
</EuiTextColor>
),
total: packageInputStreams.length,
}}
) : null}
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType={isShowingStreams ? 'arrowUp' : 'arrowDown'}
onClick={() => setIsShowingStreams(!isShowingStreams)}
color={hasErrors ? 'danger' : 'text'}
aria-label={
isShowingStreams
? i18n.translate(
'xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel',
{
defaultMessage: 'Hide {type} inputs',
values: {
type: packageInput.type,
},
}
)
: i18n.translate(
'xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel',
{
defaultMessage: 'Show {type} inputs',
values: {
type: packageInput.type,
},
}
)
}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType={isShowingStreams ? 'arrowUp' : 'arrowDown'}
onClick={() => setIsShowingStreams(!isShowingStreams)}
color="text"
aria-label={
isShowingStreams
? i18n.translate(
'xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel',
{
defaultMessage: 'Hide {type} streams',
values: {
type: packageInput.type,
},
}
)
: i18n.translate(
'xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel',
{
defaultMessage: 'Show {type} streams',
values: {
type: packageInput.type,
},
}
)
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
{/* Header rule break */}
{isShowingStreams ? <FlushHorizontalRule margin="m" /> : null}
{/* Header rule break */}
{isShowingStreams ? <EuiSpacer size="l" /> : null}
{/* Input level configuration */}
{isShowingStreams && packageInput.vars && packageInput.vars.length ? (
<Fragment>
<PackageConfigInputConfig
packageInputVars={packageInput.vars}
packageConfigInput={packageConfigInput}
updatePackageConfigInput={updatePackageConfigInput}
inputVarsValidationResults={{ vars: inputValidationResults.vars }}
forceShowErrors={forceShowErrors}
/>
<EuiHorizontalRule margin="m" />
</Fragment>
) : null}
{/* Input level configuration */}
{isShowingStreams && packageInput.vars && packageInput.vars.length ? (
<Fragment>
<PackageConfigInputConfig
packageInputVars={packageInput.vars}
packageConfigInput={packageConfigInput}
updatePackageConfigInput={updatePackageConfigInput}
inputVarsValidationResults={{ vars: inputValidationResults.vars }}
forceShowErrors={forceShowErrors}
/>
<ShortenedHorizontalRule margin="m" />
</Fragment>
) : null}
{/* Per-stream configuration */}
{isShowingStreams ? (
<EuiFlexGroup direction="column">
{packageInputStreams.map((packageInputStream) => {
const packageConfigInputStream = packageConfigInput.streams.find(
(stream) => stream.dataset.name === packageInputStream.dataset.name
);
return packageConfigInputStream ? (
<EuiFlexItem key={packageInputStream.dataset.name}>
{/* Per-stream configuration */}
{isShowingStreams ? (
<EuiFlexGroup direction="column">
{inputStreams.map(({ packageInputStream, packageConfigInputStream }, index) => (
<EuiFlexItem key={index}>
<PackageConfigInputStreamConfig
packageInputStream={packageInputStream}
packageConfigInputStream={packageConfigInputStream}
packageConfigInputStream={packageConfigInputStream!}
updatePackageConfigInputStream={(
updatedStream: Partial<PackageConfigInputStream>
) => {
@ -213,17 +226,21 @@ export const PackageConfigInputPanel: React.FunctionComponent<{
updatePackageConfigInput(updatedInput);
}}
inputStreamValidationResults={
inputValidationResults.streams![packageConfigInputStream.id]
inputValidationResults.streams![packageConfigInputStream!.id]
}
forceShowErrors={forceShowErrors}
/>
<EuiSpacer size="m" />
<EuiHorizontalRule margin="none" />
{index !== inputStreams.length - 1 ? (
<>
<EuiSpacer size="m" />
<ShortenedHorizontalRule margin="none" />
</>
) : null}
</EuiFlexItem>
) : null;
})}
</EuiFlexGroup>
) : null}
</EuiPanel>
);
};
))}
</EuiFlexGroup>
) : null}
</>
);
}
);

View file

@ -3,18 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, Fragment } from 'react';
import React, { useState, Fragment, memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiSwitch,
EuiText,
EuiSpacer,
EuiButtonEmpty,
EuiTextColor,
EuiIconTip,
} from '@elastic/eui';
import { PackageConfigInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types';
import {
@ -30,153 +29,157 @@ export const PackageConfigInputStreamConfig: React.FunctionComponent<{
updatePackageConfigInputStream: (updatedStream: Partial<PackageConfigInputStream>) => void;
inputStreamValidationResults: PackageConfigConfigValidationResults;
forceShowErrors?: boolean;
}> = ({
packageInputStream,
packageConfigInputStream,
updatePackageConfigInputStream,
inputStreamValidationResults,
forceShowErrors,
}) => {
// Showing advanced options toggle state
const [isShowingAdvanced, setIsShowingAdvanced] = useState<boolean>(false);
}> = memo(
({
packageInputStream,
packageConfigInputStream,
updatePackageConfigInputStream,
inputStreamValidationResults,
forceShowErrors,
}) => {
// Showing advanced options toggle state
const [isShowingAdvanced, setIsShowingAdvanced] = useState<boolean>();
// Errors state
const hasErrors = forceShowErrors && validationHasErrors(inputStreamValidationResults);
// Errors state
const hasErrors = forceShowErrors && validationHasErrors(inputStreamValidationResults);
const requiredVars: RegistryVarsEntry[] = [];
const advancedVars: RegistryVarsEntry[] = [];
const requiredVars: RegistryVarsEntry[] = [];
const advancedVars: RegistryVarsEntry[] = [];
if (packageInputStream.vars && packageInputStream.vars.length) {
packageInputStream.vars.forEach((varDef) => {
if (isAdvancedVar(varDef)) {
advancedVars.push(varDef);
} else {
requiredVars.push(varDef);
}
});
}
if (packageInputStream.vars && packageInputStream.vars.length) {
packageInputStream.vars.forEach((varDef) => {
if (isAdvancedVar(varDef)) {
advancedVars.push(varDef);
} else {
requiredVars.push(varDef);
}
});
}
return (
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiSwitch
label={
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTextColor color={hasErrors ? 'danger' : 'default'}>
{packageInputStream.title}
</EuiTextColor>
</EuiFlexItem>
{hasErrors ? (
<EuiFlexItem grow={false}>
<EuiIconTip
content={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.streamLevelErrorsTooltip"
defaultMessage="Fix configuration errors"
/>
}
position="right"
type="alert"
iconProps={{ color: 'danger' }}
const advancedVarsWithErrorsCount: number = useMemo(
() =>
advancedVars.filter(
({ name: varName }) => inputStreamValidationResults.vars?.[varName]?.length
).length,
[advancedVars, inputStreamValidationResults.vars]
);
return (
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiFlexGroup gutterSize="none" alignItems="flexStart">
<EuiFlexItem grow={1} />
<EuiFlexItem grow={5}>
<EuiSwitch
label={packageInputStream.title}
checked={packageConfigInputStream.enabled}
onChange={(e) => {
const enabled = e.target.checked;
updatePackageConfigInputStream({
enabled,
});
}}
/>
{packageInputStream.description ? (
<Fragment>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<ReactMarkdown source={packageInputStream.description} />
</EuiText>
</Fragment>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="m">
{requiredVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packageConfigInputStream.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackageConfigInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackageConfigInputStream({
vars: {
...packageConfigInputStream.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={inputStreamValidationResults.vars![varName]}
forceShowErrors={forceShowErrors}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
}
checked={packageConfigInputStream.enabled}
onChange={(e) => {
const enabled = e.target.checked;
updatePackageConfigInputStream({
enabled,
});
}}
/>
{packageInputStream.description ? (
<Fragment>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<ReactMarkdown source={packageInputStream.description} />
</EuiText>
</Fragment>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiFlexGroup direction="column" gutterSize="m">
{requiredVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packageConfigInputStream.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackageConfigInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackageConfigInputStream({
vars: {
...packageConfigInputStream.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={inputStreamValidationResults.vars![varName]}
forceShowErrors={forceShowErrors}
/>
</EuiFlexItem>
);
})}
{advancedVars.length ? (
<Fragment>
<EuiFlexItem>
{/* Wrapper div to prevent button from going full width */}
<div>
<EuiButtonEmpty
size="xs"
iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'}
onClick={() => setIsShowingAdvanced(!isShowingAdvanced)}
flush="left"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText"
defaultMessage="Advanced options"
/>
</EuiButtonEmpty>
</div>
</EuiFlexItem>
{isShowingAdvanced
? advancedVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packageConfigInputStream.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackageConfigInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackageConfigInputStream({
vars: {
...packageConfigInputStream.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={inputStreamValidationResults.vars![varName]}
forceShowErrors={forceShowErrors}
);
})}
{advancedVars.length ? (
<Fragment>
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'}
onClick={() => setIsShowingAdvanced(!isShowingAdvanced)}
flush="left"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText"
defaultMessage="Advanced options"
/>
</EuiButtonEmpty>
</EuiFlexItem>
{!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? (
<EuiFlexItem grow={false}>
<EuiText color="danger" size="s">
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.errorCountText"
defaultMessage="{count, plural, one {# error} other {# errors}}"
values={{ count: advancedVarsWithErrorsCount }}
/>
</EuiText>
</EuiFlexItem>
);
})
: null}
</Fragment>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};
) : null}
</EuiFlexGroup>
</EuiFlexItem>
{isShowingAdvanced
? advancedVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packageConfigInputStream.vars![varName].value;
return (
<EuiFlexItem key={varName}>
<PackageConfigInputVarField
varDef={varDef}
value={value}
onChange={(newValue: any) => {
updatePackageConfigInputStream({
vars: {
...packageConfigInputStream.vars,
[varName]: {
type: varType,
value: newValue,
},
},
});
}}
errors={inputStreamValidationResults.vars![varName]}
forceShowErrors={forceShowErrors}
/>
</EuiFlexItem>
);
})
: null}
</Fragment>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGrid>
);
}
);

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import React, { useState, memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFormRow, EuiFieldText, EuiComboBox, EuiText, EuiCodeEditor } from '@elastic/eui';
@ -18,13 +18,13 @@ export const PackageConfigInputVarField: React.FunctionComponent<{
onChange: (newValue: any) => void;
errors?: string[] | null;
forceShowErrors?: boolean;
}> = ({ varDef, value, onChange, errors: varErrors, forceShowErrors }) => {
}> = memo(({ varDef, value, onChange, errors: varErrors, forceShowErrors }) => {
const [isDirty, setIsDirty] = useState<boolean>(false);
const { multi, required, type, title, name, description } = varDef;
const isInvalid = (isDirty || forceShowErrors) && !!varErrors;
const errors = isInvalid ? varErrors : null;
const renderField = () => {
const field = useMemo(() => {
if (multi) {
return (
<EuiComboBox
@ -67,7 +67,7 @@ export const PackageConfigInputVarField: React.FunctionComponent<{
onBlur={() => setIsDirty(true)}
/>
);
};
}, [isInvalid, multi, onChange, type, value]);
return (
<EuiFormRow
@ -86,7 +86,7 @@ export const PackageConfigInputVarField: React.FunctionComponent<{
}
helpText={<ReactMarkdown source={description} />}
>
{renderField()}
{field}
</EuiFormRow>
);
};
});

View file

@ -5,6 +5,7 @@
*/
import React, { useState, useEffect, useMemo, useCallback, ReactEventHandler } from 'react';
import { useRouteMatch, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@ -31,6 +32,7 @@ import {
useConfig,
sendGetAgentStatus,
} from '../../../hooks';
import { Loading } from '../../../components';
import { ConfirmDeployConfigModal } from '../components';
import { CreatePackageConfigPageLayout } from './components';
import { CreatePackageConfigFrom, PackageConfigFormState } from './types';
@ -45,6 +47,12 @@ import { StepConfigurePackage } from './step_configure_package';
import { StepDefinePackageConfig } from './step_define_package_config';
import { useIntraAppState } from '../../../hooks/use_intra_app_state';
const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
padding-bottom: ${(props) => props.theme.eui.paddingSizes.m};
}
`;
export const CreatePackageConfigPage: React.FunctionComponent = () => {
const {
notifications,
@ -75,6 +83,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
// Agent config and package info states
const [agentConfig, setAgentConfig] = useState<AgentConfig>();
const [packageInfo, setPackageInfo] = useState<PackageInfo>();
const [isLoadingSecondStep, setIsLoadingSecondStep] = useState<boolean>(false);
const agentConfigId = agentConfig?.id;
// Retrieve agent count
@ -151,40 +160,47 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
// Update package config validation
const updatePackageConfigValidation = useCallback(
(newPackageConfig?: NewPackageConfig) => {
if (packageInfo) {
const newValidationResult = validatePackageConfig(
newPackageConfig || packageConfig,
packageInfo
);
setValidationResults(newValidationResult);
// eslint-disable-next-line no-console
console.debug('Package config validation results', newValidationResult);
return newValidationResult;
}
},
[packageConfig, packageInfo]
);
// Update package config method
const updatePackageConfig = (updatedFields: Partial<NewPackageConfig>) => {
const newPackageConfig = {
...packageConfig,
...updatedFields,
};
setPackageConfig(newPackageConfig);
const updatePackageConfig = useCallback(
(updatedFields: Partial<NewPackageConfig>) => {
const newPackageConfig = {
...packageConfig,
...updatedFields,
};
setPackageConfig(newPackageConfig);
// eslint-disable-next-line no-console
console.debug('Package config updated', newPackageConfig);
const newValidationResults = updatePackageConfigValidation(newPackageConfig);
const hasPackage = newPackageConfig.package;
const hasValidationErrors = newValidationResults
? validationHasErrors(newValidationResults)
: false;
const hasAgentConfig = newPackageConfig.config_id && newPackageConfig.config_id !== '';
if (hasPackage && hasAgentConfig && !hasValidationErrors) {
setFormState('VALID');
}
};
const updatePackageConfigValidation = (newPackageConfig?: NewPackageConfig) => {
if (packageInfo) {
const newValidationResult = validatePackageConfig(
newPackageConfig || packageConfig,
packageInfo
);
setValidationResults(newValidationResult);
// eslint-disable-next-line no-console
console.debug('Package config validation results', newValidationResult);
return newValidationResult;
}
};
console.debug('Package config updated', newPackageConfig);
const newValidationResults = updatePackageConfigValidation(newPackageConfig);
const hasPackage = newPackageConfig.package;
const hasValidationErrors = newValidationResults
? validationHasErrors(newValidationResults)
: false;
const hasAgentConfig = newPackageConfig.config_id && newPackageConfig.config_id !== '';
if (hasPackage && hasAgentConfig && !hasValidationErrors) {
setFormState('VALID');
}
},
[packageConfig, updatePackageConfigValidation]
);
// Cancel path
const cancelUrl = useMemo(() => {
@ -276,6 +292,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
updatePackageInfo={updatePackageInfo}
agentConfig={agentConfig}
updateAgentConfig={updateAgentConfig}
setIsLoadingSecondStep={setIsLoadingSecondStep}
/>
),
[pkgkey, updatePackageInfo, agentConfig, updateAgentConfig]
@ -288,11 +305,47 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
updateAgentConfig={updateAgentConfig}
packageInfo={packageInfo}
updatePackageInfo={updatePackageInfo}
setIsLoadingSecondStep={setIsLoadingSecondStep}
/>
),
[configId, updateAgentConfig, packageInfo, updatePackageInfo]
);
const stepConfigurePackage = useMemo(
() =>
isLoadingSecondStep ? (
<Loading />
) : agentConfig && packageInfo ? (
<>
<StepDefinePackageConfig
agentConfig={agentConfig}
packageInfo={packageInfo}
packageConfig={packageConfig}
updatePackageConfig={updatePackageConfig}
validationResults={validationResults!}
/>
<StepConfigurePackage
packageInfo={packageInfo}
packageConfig={packageConfig}
updatePackageConfig={updatePackageConfig}
validationResults={validationResults!}
submitAttempted={formState === 'INVALID'}
/>
</>
) : (
<div />
),
[
agentConfig,
formState,
isLoadingSecondStep,
packageConfig,
packageInfo,
updatePackageConfig,
validationResults,
]
);
const steps: EuiStepProps[] = [
from === 'package'
? {
@ -312,42 +365,14 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
},
{
title: i18n.translate(
'xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle',
'xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle',
{
defaultMessage: 'Configure integration',
}
),
status: !packageInfo || !agentConfig ? 'disabled' : undefined,
children:
agentConfig && packageInfo ? (
<StepDefinePackageConfig
agentConfig={agentConfig}
packageInfo={packageInfo}
packageConfig={packageConfig}
updatePackageConfig={updatePackageConfig}
validationResults={validationResults!}
/>
) : null,
},
{
title: i18n.translate(
'xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle',
{
defaultMessage: 'Select the data you want to collect',
}
),
status: !packageInfo || !agentConfig ? 'disabled' : undefined,
status: !packageInfo || !agentConfig || isLoadingSecondStep ? 'disabled' : undefined,
'data-test-subj': 'dataCollectionSetupStep',
children:
agentConfig && packageInfo ? (
<StepConfigurePackage
packageInfo={packageInfo}
packageConfig={packageConfig}
updatePackageConfig={updatePackageConfig}
validationResults={validationResults!}
submitAttempted={formState === 'INVALID'}
/>
) : null,
children: stepConfigurePackage,
},
];
@ -371,7 +396,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
: agentConfig && (
<ConfigurationBreadcrumb configName={agentConfig.name} configId={agentConfig.id} />
)}
<EuiSteps steps={steps} />
<StepsWithLessPadding steps={steps} />
<EuiSpacer size="l" />
{/* TODO #64541 - Remove classes */}
<EuiBottomBar
@ -383,36 +408,48 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => {
: undefined
}
>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiButtonEmpty
color="ghost"
href={cancelUrl}
onClick={cancelClickHandler}
data-test-subj="createPackageConfigCancelButton"
>
{!isLoadingSecondStep && agentConfig && packageInfo && formState === 'INVALID' ? (
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.cancelButton"
defaultMessage="Cancel"
id="xpack.ingestManager.createPackageConfig.errorOnSaveText"
defaultMessage="Your integration configuration has errors. Please fix them before saving."
/>
</EuiButtonEmpty>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSubmit}
isLoading={formState === 'LOADING'}
disabled={formState !== 'VALID'}
iconType="save"
color="primary"
fill
data-test-subj="createPackageConfigSaveButton"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.saveButton"
defaultMessage="Save integration"
/>
</EuiButton>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiButtonEmpty
color="ghost"
href={cancelUrl}
onClick={cancelClickHandler}
data-test-subj="createPackageConfigCancelButton"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSubmit}
isLoading={formState === 'LOADING'}
disabled={formState !== 'VALID'}
iconType="save"
color="primary"
fill
data-test-subj="createPackageConfigSaveButton"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.saveButton"
defaultMessage="Save integration"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { hasInvalidButRequiredVar } from './has_invalid_but_required_var';
describe('Ingest Manager - hasInvalidButRequiredVar', () => {
it('returns true for invalid & required vars', () => {
expect(
hasInvalidButRequiredVar(
[
{
name: 'mock_var',
type: 'text',
required: true,
},
],
{}
)
).toBe(true);
expect(
hasInvalidButRequiredVar(
[
{
name: 'mock_var',
type: 'text',
required: true,
},
],
{
mock_var: {
value: undefined,
},
}
)
).toBe(true);
});
it('returns false for valid & required vars', () => {
expect(
hasInvalidButRequiredVar(
[
{
name: 'mock_var',
type: 'text',
required: true,
},
],
{
mock_var: {
value: 'foo',
},
}
)
).toBe(false);
});
it('returns false for optional vars', () => {
expect(
hasInvalidButRequiredVar(
[
{
name: 'mock_var',
type: 'text',
},
],
{
mock_var: {
value: 'foo',
},
}
)
).toBe(false);
expect(
hasInvalidButRequiredVar(
[
{
name: 'mock_var',
type: 'text',
required: false,
},
],
{
mock_var: {
value: undefined,
},
}
)
).toBe(false);
});
});

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PackageConfigConfigRecord, RegistryVarsEntry } from '../../../../types';
import { validatePackageConfigConfig } from './';
export const hasInvalidButRequiredVar = (
registryVars?: RegistryVarsEntry[],
packageConfigVars?: PackageConfigConfigRecord
): boolean => {
return (
(registryVars && !packageConfigVars) ||
Boolean(
registryVars &&
registryVars.find(
(registryVar) =>
registryVar.required &&
(!packageConfigVars ||
!packageConfigVars[registryVar.name] ||
validatePackageConfigConfig(packageConfigVars[registryVar.name], registryVar)?.length)
)
)
);
};

View file

@ -4,10 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { isAdvancedVar } from './is_advanced_var';
export { hasInvalidButRequiredVar } from './has_invalid_but_required_var';
export {
PackageConfigValidationResults,
PackageConfigConfigValidationResults,
PackageConfigInputValidationResults,
validatePackageConfig,
validatePackageConfigConfig,
validationHasErrors,
countValidationErrors,
} from './validate_package_config';

View file

@ -6,7 +6,7 @@
import { RegistryVarsEntry } from '../../../../types';
export const isAdvancedVar = (varDef: RegistryVarsEntry): boolean => {
if (varDef.show_user || (varDef.required && !varDef.default)) {
if (varDef.show_user || (varDef.required && varDef.default === undefined)) {
return false;
}
return true;

View file

@ -171,7 +171,7 @@ export const validatePackageConfig = (
return validationResults;
};
const validatePackageConfigConfig = (
export const validatePackageConfigConfig = (
configEntry: PackageConfigConfigRecordEntry,
varDef: RegistryVarsEntry
): string[] | null => {
@ -237,13 +237,22 @@ const validatePackageConfigConfig = (
return errors.length ? errors : null;
};
export const countValidationErrors = (
validationResults:
| PackageConfigValidationResults
| PackageConfigInputValidationResults
| PackageConfigConfigValidationResults
): number => {
const flattenedValidation = getFlattenedObject(validationResults);
const errors = Object.values(flattenedValidation).filter((value) => Boolean(value)) || [];
return errors.length;
};
export const validationHasErrors = (
validationResults:
| PackageConfigValidationResults
| PackageConfigInputValidationResults
| PackageConfigConfigValidationResults
) => {
const flattenedValidation = getFlattenedObject(validationResults);
return !!Object.entries(flattenedValidation).find(([, value]) => !!value);
): boolean => {
return countValidationErrors(validationResults) > 0;
};

View file

@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { PackageInfo, RegistryStream, NewPackageConfig, PackageConfigInput } from '../../../types';
import { Loading } from '../../../components';
import { PackageConfigValidationResults, validationHasErrors } from './services';
import { PackageConfigValidationResults } from './services';
import { PackageConfigInputPanel, CustomPackageConfig } from './components';
import { CreatePackageConfigFrom } from './types';
@ -52,8 +50,6 @@ export const StepConfigurePackage: React.FunctionComponent<{
validationResults,
submitAttempted,
}) => {
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
// Configure inputs (and their streams)
// Assume packages only export one config template for now
const renderConfigureInputs = () =>
@ -61,76 +57,50 @@ export const StepConfigurePackage: React.FunctionComponent<{
packageInfo.config_templates[0] &&
packageInfo.config_templates[0].inputs &&
packageInfo.config_templates[0].inputs.length ? (
<EuiFlexGroup direction="column">
{packageInfo.config_templates[0].inputs.map((packageInput) => {
const packageConfigInput = packageConfig.inputs.find(
(input) => input.type === packageInput.type
);
const packageInputStreams = findStreamsForInputType(packageInput.type, packageInfo);
return packageConfigInput ? (
<EuiFlexItem key={packageInput.type}>
<PackageConfigInputPanel
packageInput={packageInput}
packageInputStreams={packageInputStreams}
packageConfigInput={packageConfigInput}
updatePackageConfigInput={(updatedInput: Partial<PackageConfigInput>) => {
const indexOfUpdatedInput = packageConfig.inputs.findIndex(
(input) => input.type === packageInput.type
);
const newInputs = [...packageConfig.inputs];
newInputs[indexOfUpdatedInput] = {
...newInputs[indexOfUpdatedInput],
...updatedInput,
};
updatePackageConfig({
inputs: newInputs,
});
}}
inputValidationResults={validationResults!.inputs![packageConfigInput.type]}
forceShowErrors={submitAttempted}
/>
</EuiFlexItem>
) : null;
})}
</EuiFlexGroup>
<>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup direction="column" gutterSize="none">
{packageInfo.config_templates[0].inputs.map((packageInput) => {
const packageConfigInput = packageConfig.inputs.find(
(input) => input.type === packageInput.type
);
const packageInputStreams = findStreamsForInputType(packageInput.type, packageInfo);
return packageConfigInput ? (
<EuiFlexItem key={packageInput.type}>
<PackageConfigInputPanel
packageInput={packageInput}
packageInputStreams={packageInputStreams}
packageConfigInput={packageConfigInput}
updatePackageConfigInput={(updatedInput: Partial<PackageConfigInput>) => {
const indexOfUpdatedInput = packageConfig.inputs.findIndex(
(input) => input.type === packageInput.type
);
const newInputs = [...packageConfig.inputs];
newInputs[indexOfUpdatedInput] = {
...newInputs[indexOfUpdatedInput],
...updatedInput,
};
updatePackageConfig({
inputs: newInputs,
});
}}
inputValidationResults={validationResults!.inputs![packageConfigInput.type]}
forceShowErrors={submitAttempted}
/>
<EuiHorizontalRule margin="m" />
</EuiFlexItem>
) : null;
})}
</EuiFlexGroup>
</>
) : (
<EuiPanel>
<CustomPackageConfig
from={from}
packageName={packageInfo.name}
packageConfig={packageConfig}
packageConfigId={packageConfigId}
/>
</EuiPanel>
<CustomPackageConfig
from={from}
packageName={packageInfo.name}
packageConfig={packageConfig}
packageConfigId={packageConfigId}
/>
);
return validationResults ? (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>{renderConfigureInputs()}</EuiFlexItem>
{hasErrors && submitAttempted ? (
<EuiFlexItem>
<EuiSpacer size="m" />
<EuiCallOut
title={i18n.translate(
'xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorTitle',
{
defaultMessage: 'Your integration configuration has errors',
}
)}
color="danger"
>
<p>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorText"
defaultMessage="Please fix the above errors before continuing"
/>
</p>
</EuiCallOut>
<EuiSpacer size="m" />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
) : (
<Loading />
);
return validationResults ? renderConfigureInputs() : <Loading />;
};

View file

@ -3,17 +3,18 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useState, Fragment } from 'react';
import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGrid,
EuiFlexItem,
EuiFormRow,
EuiFieldText,
EuiButtonEmpty,
EuiSpacer,
EuiText,
EuiComboBox,
EuiDescribedFormGroup,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { AgentConfig, PackageInfo, PackageConfig, NewPackageConfig } from '../../../types';
import { packageToPackageConfigInputs } from '../../../services';
@ -28,7 +29,7 @@ export const StepDefinePackageConfig: React.FunctionComponent<{
validationResults: PackageConfigValidationResults;
}> = ({ agentConfig, packageInfo, packageConfig, updatePackageConfig, validationResults }) => {
// Form show/hide states
const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState<boolean>(false);
const [isShowingAdvanced, setIsShowingAdvanced] = useState<boolean>(false);
// Update package config's package and config info
useEffect(() => {
@ -74,111 +75,140 @@ export const StepDefinePackageConfig: React.FunctionComponent<{
]);
return validationResults ? (
<>
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiFormRow
isInvalid={!!validationResults.name}
error={validationResults.name}
label={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNameInputLabel"
defaultMessage="Integration name"
/>
}
>
<EuiFieldText
value={packageConfig.name}
onChange={(e) =>
updatePackageConfig({
name: e.target.value,
})
}
data-test-subj="packageConfigNameInput"
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.integrationSettingsSectionTitle"
defaultMessage="Integration settings"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.integrationSettingsSectionDescription"
defaultMessage="Choose a name and description to help identify how this integration will be used."
/>
}
>
<>
{/* Name */}
<EuiFormRow
isInvalid={!!validationResults.name}
error={validationResults.name}
label={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNameInputLabel"
defaultMessage="Integration name"
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigDescriptionInputLabel"
defaultMessage="Description"
/>
}
>
<EuiFieldText
value={packageConfig.name}
onChange={(e) =>
updatePackageConfig({
name: e.target.value,
})
}
labelAppend={
<EuiText size="xs" color="subdued">
data-test-subj="packageConfigNameInput"
/>
</EuiFormRow>
{/* Description */}
<EuiFormRow
label={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigDescriptionInputLabel"
defaultMessage="Description"
/>
}
labelAppend={
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.inputVarFieldOptionalLabel"
defaultMessage="Optional"
/>
</EuiText>
}
isInvalid={!!validationResults.description}
error={validationResults.description}
>
<EuiFieldText
value={packageConfig.description}
onChange={(e) =>
updatePackageConfig({
description: e.target.value,
})
}
/>
</EuiFormRow>
<EuiSpacer size="m" />
{/* Advanced options toggle */}
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
iconType={isShowingAdvanced ? 'arrowDown' : 'arrowRight'}
onClick={() => setIsShowingAdvanced(!isShowingAdvanced)}
flush="left"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.advancedOptionsToggleLinkText"
defaultMessage="Advanced options"
/>
</EuiButtonEmpty>
</EuiFlexItem>
{!isShowingAdvanced && !!validationResults.namespace ? (
<EuiFlexItem grow={false}>
<EuiText color="danger" size="s">
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.inputVarFieldOptionalLabel"
defaultMessage="Optional"
id="xpack.ingestManager.createPackageConfig.stepConfigure.errorCountText"
defaultMessage="{count, plural, one {# error} other {# errors}}"
values={{ count: 1 }}
/>
</EuiText>
}
isInvalid={!!validationResults.description}
error={validationResults.description}
>
<EuiFieldText
value={packageConfig.description}
onChange={(e) =>
updatePackageConfig({
description: e.target.value,
})
}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGrid>
<EuiSpacer size="m" />
<EuiButtonEmpty
flush="left"
size="xs"
iconType={isShowingAdvancedDefine ? 'arrowUp' : 'arrowDown'}
onClick={() => setIsShowingAdvancedDefine(!isShowingAdvancedDefine)}
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.advancedOptionsToggleLinkText"
defaultMessage="Advanced options"
/>
</EuiButtonEmpty>
{/* Todo: Populate list of existing namespaces */}
{isShowingAdvancedDefine || !!validationResults.namespace ? (
<Fragment>
<EuiSpacer size="m" />
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiFormRow
isInvalid={!!validationResults.namespace}
error={validationResults.namespace}
label={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNamespaceInputLabel"
defaultMessage="Namespace"
/>
}
>
<EuiComboBox
noSuggestions
singleSelection={true}
selectedOptions={
packageConfig.namespace ? [{ label: packageConfig.namespace }] : []
}
onCreateOption={(newNamespace: string) => {
updatePackageConfig({
namespace: newNamespace,
});
}}
onChange={(newNamespaces: Array<{ label: string }>) => {
updatePackageConfig({
namespace: newNamespaces.length ? newNamespaces[0].label : '',
});
}}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGrid>
</Fragment>
) : null}
</>
) : null}
</EuiFlexGroup>
{/* Advanced options content */}
{/* Todo: Populate list of existing namespaces */}
{isShowingAdvanced ? (
<>
<EuiSpacer size="m" />
<EuiFormRow
isInvalid={!!validationResults.namespace}
error={validationResults.namespace}
label={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNamespaceInputLabel"
defaultMessage="Namespace"
/>
}
>
<EuiComboBox
noSuggestions
singleSelection={true}
selectedOptions={
packageConfig.namespace ? [{ label: packageConfig.namespace }] : []
}
onCreateOption={(newNamespace: string) => {
updatePackageConfig({
namespace: newNamespace,
});
}}
onChange={(newNamespaces: Array<{ label: string }>) => {
updatePackageConfig({
namespace: newNamespaces.length ? newNamespaces[0].label : '',
});
}}
/>
</EuiFormRow>
</>
) : null}
</>
</EuiDescribedFormGroup>
) : (
<Loading />
);

View file

@ -6,29 +6,50 @@
import React, { useEffect, useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer, EuiTextColor } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSelectable,
EuiSpacer,
EuiTextColor,
EuiPortal,
EuiButtonEmpty,
} from '@elastic/eui';
import { Error } from '../../../components';
import { AgentConfig, PackageInfo, GetAgentConfigsResponseItem } from '../../../types';
import { isPackageLimited, doesAgentConfigAlreadyIncludePackage } from '../../../services';
import { useGetPackageInfoByKey, useGetAgentConfigs, sendGetOneAgentConfig } from '../../../hooks';
import {
useGetPackageInfoByKey,
useGetAgentConfigs,
sendGetOneAgentConfig,
useCapabilities,
} from '../../../hooks';
import { CreateAgentConfigFlyout } from '../list_page/components';
export const StepSelectConfig: React.FunctionComponent<{
pkgkey: string;
updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
agentConfig: AgentConfig | undefined;
updateAgentConfig: (config: AgentConfig | undefined) => void;
}> = ({ pkgkey, updatePackageInfo, agentConfig, updateAgentConfig }) => {
setIsLoadingSecondStep: (isLoading: boolean) => void;
}> = ({ pkgkey, updatePackageInfo, agentConfig, updateAgentConfig, setIsLoadingSecondStep }) => {
// Selected config state
const [selectedConfigId, setSelectedConfigId] = useState<string | undefined>(
agentConfig ? agentConfig.id : undefined
);
const [selectedConfigError, setSelectedConfigError] = useState<Error>();
// Create new config flyout state
const hasWriteCapabilites = useCapabilities().write;
const [isCreateAgentConfigFlyoutOpen, setIsCreateAgentConfigFlyoutOpen] = useState<boolean>(
false
);
// Fetch package info
const {
data: packageInfoData,
error: packageInfoError,
isLoading: packageInfoLoading,
isLoading: isPackageInfoLoading,
} = useGetPackageInfoByKey(pkgkey);
const isLimitedPackage = (packageInfoData && isPackageLimited(packageInfoData.response)) || false;
@ -37,6 +58,7 @@ export const StepSelectConfig: React.FunctionComponent<{
data: agentConfigsData,
error: agentConfigsError,
isLoading: isAgentConfigsLoading,
sendRequest: refreshAgentConfigs,
} = useGetAgentConfigs({
page: 1,
perPage: 1000,
@ -64,6 +86,7 @@ export const StepSelectConfig: React.FunctionComponent<{
useEffect(() => {
const fetchAgentConfigInfo = async () => {
if (selectedConfigId) {
setIsLoadingSecondStep(true);
const { data, error } = await sendGetOneAgentConfig(selectedConfigId);
if (error) {
setSelectedConfigError(error);
@ -76,11 +99,12 @@ export const StepSelectConfig: React.FunctionComponent<{
setSelectedConfigError(undefined);
updateAgentConfig(undefined);
}
setIsLoadingSecondStep(false);
};
if (!agentConfig || selectedConfigId !== agentConfig.id) {
fetchAgentConfigInfo();
}
}, [selectedConfigId, agentConfig, updateAgentConfig]);
}, [selectedConfigId, agentConfig, updateAgentConfig, setIsLoadingSecondStep]);
// Display package error if there is one
if (packageInfoError) {
@ -113,92 +137,125 @@ export const StepSelectConfig: React.FunctionComponent<{
}
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiSelectable
searchable
allowExclusions={false}
singleSelection={true}
isLoading={isAgentConfigsLoading || packageInfoLoading}
options={agentConfigs.map((agentConf) => {
const alreadyHasLimitedPackage =
(isLimitedPackage &&
packageInfoData &&
doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) ||
false;
return {
label: agentConf.name,
key: agentConf.id,
checked: selectedConfigId === agentConf.id ? 'on' : undefined,
disabled: alreadyHasLimitedPackage,
'data-test-subj': 'agentConfigItem',
};
})}
renderOption={(option) => (
<EuiFlexGroup>
<EuiFlexItem grow={false}>{option.label}</EuiFlexItem>
<EuiFlexItem>
<EuiTextColor color="subdued">
{agentConfigsById[option.key!].description}
</EuiTextColor>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.StepSelectConfig.agentConfigAgentsCountText"
defaultMessage="{count, plural, one {# agent} other {# agents}}"
values={{
count: agentConfigsById[option.key!].agents || 0,
}}
/>
</EuiTextColor>
</EuiFlexItem>
</EuiFlexGroup>
)}
listProps={{
bordered: true,
}}
searchProps={{
placeholder: i18n.translate(
'xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder',
{
defaultMessage: 'Search for agent configurations',
<>
{isCreateAgentConfigFlyoutOpen ? (
<EuiPortal>
<CreateAgentConfigFlyout
onClose={(newAgentConfig?: AgentConfig) => {
setIsCreateAgentConfigFlyoutOpen(false);
if (newAgentConfig) {
refreshAgentConfigs();
setSelectedConfigId(newAgentConfig.id);
}
),
}}
height={240}
onChange={(options) => {
const selectedOption = options.find((option) => option.checked === 'on');
if (selectedOption) {
setSelectedConfigId(selectedOption.key);
} else {
setSelectedConfigId(undefined);
}
}}
>
{(list, search) => (
<Fragment>
{search}
<EuiSpacer size="m" />
{list}
</Fragment>
)}
</EuiSelectable>
</EuiFlexItem>
{/* Display selected agent config error if there is one */}
{selectedConfigError ? (
<EuiFlexItem>
<Error
title={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingSelectedAgentConfigTitle"
defaultMessage="Error loading selected agent config"
/>
}
error={selectedConfigError}
}}
/>
</EuiFlexItem>
</EuiPortal>
) : null}
</EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiSelectable
searchable
allowExclusions={false}
singleSelection={true}
isLoading={isAgentConfigsLoading || isPackageInfoLoading}
options={agentConfigs.map((agentConf) => {
const alreadyHasLimitedPackage =
(isLimitedPackage &&
packageInfoData &&
doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) ||
false;
return {
label: agentConf.name,
key: agentConf.id,
checked: selectedConfigId === agentConf.id ? 'on' : undefined,
disabled: alreadyHasLimitedPackage,
'data-test-subj': 'agentConfigItem',
};
})}
renderOption={(option) => (
<EuiFlexGroup>
<EuiFlexItem grow={false}>{option.label}</EuiFlexItem>
<EuiFlexItem>
<EuiTextColor color="subdued">
{agentConfigsById[option.key!].description}
</EuiTextColor>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.StepSelectConfig.agentConfigAgentsCountText"
defaultMessage="{count, plural, one {# agent} other {# agents}}"
values={{
count: agentConfigsById[option.key!].agents || 0,
}}
/>
</EuiTextColor>
</EuiFlexItem>
</EuiFlexGroup>
)}
listProps={{
bordered: true,
}}
searchProps={{
placeholder: i18n.translate(
'xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder',
{
defaultMessage: 'Search for agent configurations',
}
),
}}
height={180}
onChange={(options) => {
const selectedOption = options.find((option) => option.checked === 'on');
if (selectedOption) {
if (selectedOption.key !== selectedConfigId) {
setSelectedConfigId(selectedOption.key);
}
} else {
setSelectedConfigId(undefined);
}
}}
>
{(list, search) => (
<Fragment>
{search}
<EuiSpacer size="m" />
{list}
</Fragment>
)}
</EuiSelectable>
</EuiFlexItem>
{/* Display selected agent config error if there is one */}
{selectedConfigError ? (
<EuiFlexItem>
<Error
title={
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingSelectedAgentConfigTitle"
defaultMessage="Error loading selected agent config"
/>
}
error={selectedConfigError}
/>
</EuiFlexItem>
) : null}
<EuiFlexItem>
<div>
<EuiButtonEmpty
iconType="plusInCircle"
isDisabled={!hasWriteCapabilites}
onClick={() => setIsCreateAgentConfigFlyoutOpen(true)}
flush="left"
size="s"
>
<FormattedMessage
id="xpack.ingestManager.createPackageConfig.StepSelectConfig.addButton"
defaultMessage="New agent configuration"
/>
</EuiButtonEmpty>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -22,7 +22,14 @@ export const StepSelectPackage: React.FunctionComponent<{
updateAgentConfig: (config: AgentConfig | undefined) => void;
packageInfo?: PackageInfo;
updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
}> = ({ agentConfigId, updateAgentConfig, packageInfo, updatePackageInfo }) => {
setIsLoadingSecondStep: (isLoading: boolean) => void;
}> = ({
agentConfigId,
updateAgentConfig,
packageInfo,
updatePackageInfo,
setIsLoadingSecondStep,
}) => {
// Selected package state
const [selectedPkgKey, setSelectedPkgKey] = useState<string | undefined>(
packageInfo ? `${packageInfo.name}-${packageInfo.version}` : undefined
@ -30,7 +37,11 @@ export const StepSelectPackage: React.FunctionComponent<{
const [selectedPkgError, setSelectedPkgError] = useState<Error>();
// Fetch agent config info
const { data: agentConfigData, error: agentConfigError } = useGetOneAgentConfig(agentConfigId);
const {
data: agentConfigData,
error: agentConfigError,
isLoading: isAgentConfigsLoading,
} = useGetOneAgentConfig(agentConfigId);
// Fetch packages info
// Filter out limited packages already part of selected agent config
@ -66,6 +77,7 @@ export const StepSelectPackage: React.FunctionComponent<{
useEffect(() => {
const fetchPackageInfo = async () => {
if (selectedPkgKey) {
setIsLoadingSecondStep(true);
const { data, error } = await sendGetPackageInfoByKey(selectedPkgKey);
if (error) {
setSelectedPkgError(error);
@ -74,6 +86,7 @@ export const StepSelectPackage: React.FunctionComponent<{
setSelectedPkgError(undefined);
updatePackageInfo(data.response);
}
setIsLoadingSecondStep(false);
} else {
setSelectedPkgError(undefined);
updatePackageInfo(undefined);
@ -82,7 +95,7 @@ export const StepSelectPackage: React.FunctionComponent<{
if (!packageInfo || selectedPkgKey !== `${packageInfo.name}-${packageInfo.version}`) {
fetchPackageInfo();
}
}, [selectedPkgKey, packageInfo, updatePackageInfo]);
}, [selectedPkgKey, packageInfo, updatePackageInfo, setIsLoadingSecondStep]);
// Display agent config error if there is one
if (agentConfigError) {
@ -121,7 +134,7 @@ export const StepSelectPackage: React.FunctionComponent<{
searchable
allowExclusions={false}
singleSelection={true}
isLoading={isPackagesLoading || isLimitedPackagesLoading}
isLoading={isPackagesLoading || isLimitedPackagesLoading || isAgentConfigsLoading}
options={packages.map(({ title, name, version, icons }) => {
const pkgkey = `${name}-${version}`;
return {
@ -154,7 +167,9 @@ export const StepSelectPackage: React.FunctionComponent<{
onChange={(options) => {
const selectedOption = options.find((option) => option.checked === 'on');
if (selectedOption) {
setSelectedPkgKey(selectedOption.key);
if (selectedOption.key !== selectedPkgKey) {
setSelectedPkgKey(selectedOption.key);
}
} else {
setSelectedPkgKey(undefined);
}

View file

@ -3,14 +3,13 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useRouteMatch, useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButtonEmpty,
EuiButton,
EuiSteps,
EuiBottomBar,
EuiFlexGroup,
EuiFlexItem,
@ -160,38 +159,45 @@ export const EditPackageConfigPage: React.FunctionComponent = () => {
const [validationResults, setValidationResults] = useState<PackageConfigValidationResults>();
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
// Update package config validation
const updatePackageConfigValidation = useCallback(
(newPackageConfig?: UpdatePackageConfig) => {
if (packageInfo) {
const newValidationResult = validatePackageConfig(
newPackageConfig || packageConfig,
packageInfo
);
setValidationResults(newValidationResult);
// eslint-disable-next-line no-console
console.debug('Package config validation results', newValidationResult);
return newValidationResult;
}
},
[packageConfig, packageInfo]
);
// Update package config method
const updatePackageConfig = (updatedFields: Partial<UpdatePackageConfig>) => {
const newPackageConfig = {
...packageConfig,
...updatedFields,
};
setPackageConfig(newPackageConfig);
const updatePackageConfig = useCallback(
(updatedFields: Partial<UpdatePackageConfig>) => {
const newPackageConfig = {
...packageConfig,
...updatedFields,
};
setPackageConfig(newPackageConfig);
// eslint-disable-next-line no-console
console.debug('Package config updated', newPackageConfig);
const newValidationResults = updatePackageConfigValidation(newPackageConfig);
const hasValidationErrors = newValidationResults
? validationHasErrors(newValidationResults)
: false;
if (!hasValidationErrors) {
setFormState('VALID');
}
};
const updatePackageConfigValidation = (newPackageConfig?: UpdatePackageConfig) => {
if (packageInfo) {
const newValidationResult = validatePackageConfig(
newPackageConfig || packageConfig,
packageInfo
);
setValidationResults(newValidationResult);
// eslint-disable-next-line no-console
console.debug('Package config validation results', newValidationResult);
return newValidationResult;
}
};
console.debug('Package config updated', newPackageConfig);
const newValidationResults = updatePackageConfigValidation(newPackageConfig);
const hasValidationErrors = newValidationResults
? validationHasErrors(newValidationResults)
: false;
if (!hasValidationErrors) {
setFormState('VALID');
}
},
[packageConfig, updatePackageConfigValidation]
);
// Cancel url
const cancelUrl = getHref('configuration_details', { configId });
@ -271,6 +277,40 @@ export const EditPackageConfigPage: React.FunctionComponent = () => {
packageInfo,
};
const configurePackage = useMemo(
() =>
agentConfig && packageInfo ? (
<>
<StepDefinePackageConfig
agentConfig={agentConfig}
packageInfo={packageInfo}
packageConfig={packageConfig}
updatePackageConfig={updatePackageConfig}
validationResults={validationResults!}
/>
<StepConfigurePackage
from={'edit'}
packageInfo={packageInfo}
packageConfig={packageConfig}
packageConfigId={packageConfigId}
updatePackageConfig={updatePackageConfig}
validationResults={validationResults!}
submitAttempted={formState === 'INVALID'}
/>
</>
) : null,
[
agentConfig,
formState,
packageConfig,
packageConfigId,
packageInfo,
updatePackageConfig,
validationResults,
]
);
return (
<CreatePackageConfigPageLayout {...layoutProps} data-test-subj="editPackageConfig">
{isLoadingData ? (
@ -301,46 +341,7 @@ export const EditPackageConfigPage: React.FunctionComponent = () => {
onCancel={() => setFormState('VALID')}
/>
)}
<EuiSteps
steps={[
{
title: i18n.translate(
'xpack.ingestManager.editPackageConfig.stepDefinePackageConfigTitle',
{
defaultMessage: 'Define your integration',
}
),
children: (
<StepDefinePackageConfig
agentConfig={agentConfig}
packageInfo={packageInfo}
packageConfig={packageConfig}
updatePackageConfig={updatePackageConfig}
validationResults={validationResults!}
/>
),
},
{
title: i18n.translate(
'xpack.ingestManager.editPackageConfig.stepConfigurePackageConfigTitle',
{
defaultMessage: 'Select the data you want to collect',
}
),
children: (
<StepConfigurePackage
from={'edit'}
packageInfo={packageInfo}
packageConfig={packageConfig}
packageConfigId={packageConfigId}
updatePackageConfig={updatePackageConfig}
validationResults={validationResults!}
submitAttempted={formState === 'INVALID'}
/>
),
},
]}
/>
{configurePackage}
<EuiSpacer size="l" />
{/* TODO #64541 - Remove classes */}
<EuiBottomBar
@ -352,29 +353,41 @@ export const EditPackageConfigPage: React.FunctionComponent = () => {
: undefined
}
>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="ghost" href={cancelUrl}>
{agentConfig && packageInfo && formState === 'INVALID' ? (
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.cancelButton"
defaultMessage="Cancel"
id="xpack.ingestManager.createPackageConfig.errorOnSaveText"
defaultMessage="Your integration configuration has errors. Please fix them before saving."
/>
</EuiButtonEmpty>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSubmit}
isLoading={formState === 'LOADING'}
disabled={formState !== 'VALID'}
iconType="save"
color="primary"
fill
>
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.saveButton"
defaultMessage="Save integration"
/>
</EuiButton>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="ghost" href={cancelUrl}>
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.cancelButton"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSubmit}
isLoading={formState === 'LOADING'}
disabled={formState !== 'VALID'}
iconType="save"
color="primary"
fill
>
<FormattedMessage
id="xpack.ingestManager.editPackageConfig.saveButton"
defaultMessage="Save integration"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>

View file

@ -18,12 +18,12 @@ import {
EuiButton,
EuiText,
} from '@elastic/eui';
import { NewAgentConfig } from '../../../../types';
import { NewAgentConfig, AgentConfig } from '../../../../types';
import { useCapabilities, useCore, sendCreateAgentConfig } from '../../../../hooks';
import { AgentConfigForm, agentConfigFormValidation } from '../../components';
interface Props {
onClose: () => void;
onClose: (createdAgentConfig?: AgentConfig) => void;
}
export const CreateAgentConfigFlyout: React.FunctionComponent<Props> = ({ onClose }) => {
@ -86,7 +86,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent<Props> = ({ onClos
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
<EuiButtonEmpty iconType="cross" onClick={() => onClose()} flush="left">
<FormattedMessage
id="xpack.ingestManager.createAgentConfig.cancelButtonLabel"
defaultMessage="Cancel"
@ -113,7 +113,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent<Props> = ({ onClos
}
)
);
onClose();
onClose(data.item);
} else {
notifications.toasts.addDanger(
error

View file

@ -18,6 +18,7 @@ export {
UpdatePackageConfig,
PackageConfigInput,
PackageConfigInputStream,
PackageConfigConfigRecord,
PackageConfigConfigRecordEntry,
Output,
DataStream,

View file

@ -44,6 +44,20 @@ class PackageConfigService {
packageConfig: NewPackageConfig,
options?: { id?: string; user?: AuthenticatedUser }
): Promise<PackageConfig> {
// Check that its agent config does not have a package config with the same name
const parentAgentConfig = await agentConfigService.get(soClient, packageConfig.config_id);
if (!parentAgentConfig) {
throw new Error('Agent config not found');
} else {
if (
(parentAgentConfig.package_configs as PackageConfig[]).find(
(siblingPackageConfig) => siblingPackageConfig.name === packageConfig.name
)
) {
throw new Error('There is already a package with the same name on this agent config');
}
}
// Make sure the associated package is installed
if (packageConfig.package?.name) {
const [, pkgInfo] = await Promise.all([
@ -225,6 +239,21 @@ class PackageConfigService {
throw new Error('Package config not found');
}
// Check that its agent config does not have a package config with the same name
const parentAgentConfig = await agentConfigService.get(soClient, packageConfig.config_id);
if (!parentAgentConfig) {
throw new Error('Agent config not found');
} else {
if (
(parentAgentConfig.package_configs as PackageConfig[]).find(
(siblingPackageConfig) =>
siblingPackageConfig.id !== id && siblingPackageConfig.name === packageConfig.name
)
) {
throw new Error('There is already a package with the same name on this agent config');
}
}
await soClient.update<PackageConfigSOAttributes>(
SAVED_OBJECT_TYPE,
id,

View file

@ -8187,6 +8187,42 @@
"xpack.ingestManager.createAgentConfig.flyoutTitleDescription": "エージェント構成は、エージェントのグループ全体にわたる設定を管理する目的で使用されます。エージェント構成にデータソースを追加すると、エージェントで収集するデータを指定できます。エージェント構成の編集時には、フリートを使用して、指定したエージェントのグループに更新をデプロイできます。",
"xpack.ingestManager.createAgentConfig.submitButtonLabel": "エージェント構成を作成",
"xpack.ingestManager.createAgentConfig.successNotificationTitle": "エージェント構成「{name}」を作成しました",
"xpack.ingestManager.createPackageConfig.addedNotificationMessage": "フリートは'{agentConfigName}'構成で使用されているすべてのエージェントに更新をデプロイします。",
"xpack.ingestManager.createPackageConfig.addedNotificationTitle": "正常に'{packageConfigName}'を追加しました",
"xpack.ingestManager.createPackageConfig.agentConfigurationNameLabel": "構成",
"xpack.ingestManager.createPackageConfig.cancelButton": "キャンセル",
"xpack.ingestManager.createPackageConfig.cancelLinkText": "キャンセル",
"xpack.ingestManager.createPackageConfig.pageDescriptionfromConfig": "次の手順に従い、統合をこのエージェント構成に追加します。",
"xpack.ingestManager.createPackageConfig.pageDescriptionfromPackage": "次の手順に従い、この統合をエージェント構成に追加します。",
"xpack.ingestManager.createPackageConfig.pageTitle": "データソースを追加",
"xpack.ingestManager.createPackageConfig.saveButton": "データソースを保存",
"xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle": "収集するデータを選択",
"xpack.ingestManager.createPackageConfig.stepConfigure.advancedOptionsToggleLinkText": "高度なオプション",
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigDescriptionInputLabel": "説明",
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNameInputLabel": "データソース名",
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNamespaceInputLabel": "名前空間",
"xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel": "{type} ストリームを隠す",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsDescription": "次の設定はすべてのストリームに適用されます。",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsTitle": "設定",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputVarFieldOptionalLabel": "オプション",
"xpack.ingestManager.createPackageConfig.stepConfigure.noConfigOptionsMessage": "構成するものがありません",
"xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel": "{type} ストリームを表示",
"xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション",
"xpack.ingestManager.createPackageConfig.stepSelectAgentConfigTitle": "エージェント構成を選択する",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# エージェント} other {# エージェント}}",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingAgentConfigsTitle": "エージェント構成の読み込みエラー",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingPackageTitle": "パッケージ情報の読み込みエラー",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "選択したエージェント構成の読み込みエラー",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder": "エージェント構成の検索",
"xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingConfigTitle": "エージェント構成情報の読み込みエラー",
"xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingPackagesTitle": "統合の読み込みエラー",
"xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択した統合の読み込みエラー",
"xpack.ingestManager.createPackageConfig.stepSelectPackage.filterPackagesInputPlaceholder": "統合を検索",
"xpack.ingestManager.createPackageConfig.stepSelectPackageTitle": "統合を選択",
"xpack.ingestManager.packageConfigValidation.invalidArrayErrorMessage": "無効なフォーマット",
"xpack.ingestManager.packageConfigValidation.invalidYamlFormatErrorMessage": "YAML形式が無効です",
"xpack.ingestManager.packageConfigValidation.nameRequiredErrorMessage": "名前が必要です",
"xpack.ingestManager.packageConfigValidation.requiredErrorMessage": "{fieldName}が必要です",
"xpack.ingestManager.dataStreamList.actionsColumnTitle": "アクション",
"xpack.ingestManager.dataStreamList.datasetColumnTitle": "データセット",
"xpack.ingestManager.dataStreamList.integrationColumnTitle": "統合",
@ -8224,6 +8260,14 @@
"xpack.ingestManager.editAgentConfig.savingButtonText": "保存中…",
"xpack.ingestManager.editAgentConfig.successNotificationTitle": "正常に'{name}'設定を更新しました",
"xpack.ingestManager.editAgentConfig.unsavedChangesText": "保存されていない変更があります",
"xpack.ingestManager.editPackageConfig.cancelButton": "キャンセル",
"xpack.ingestManager.editPackageConfig.errorLoadingDataMessage": "このデータソース情報の読み込みエラーが発生しました",
"xpack.ingestManager.editPackageConfig.errorLoadingDataTitle": "データの読み込み中にエラーが発生",
"xpack.ingestManager.editPackageConfig.pageDescription": "次の手順に従い、このデータソースを編集します。",
"xpack.ingestManager.editPackageConfig.pageTitle": "データソースを編集",
"xpack.ingestManager.editPackageConfig.saveButton": "データソースを保存",
"xpack.ingestManager.editPackageConfig.updatedNotificationMessage": "フリートは'{agentConfigName}'構成で使用されているすべてのエージェントに更新をデプロイします。",
"xpack.ingestManager.editPackageConfig.updatedNotificationTitle": "正常に'{packageConfigName}'を更新しました",
"xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "登録トークンが見つかりません。",
"xpack.ingestManager.enrollemntAPIKeyList.loadingTokensMessage": "登録トークンを読み込んでいます...",
"xpack.ingestManager.enrollmentInstructions.copyButton": "コマンドをコピー",

View file

@ -8191,6 +8191,42 @@
"xpack.ingestManager.createAgentConfig.flyoutTitleDescription": "代理配置用于管理整个代理组的设置。您可以将数据源添加到代理配置以指定代理收集的数据。编辑代理配置时,可以使用 Fleet 将更新部署到指定的代理组。",
"xpack.ingestManager.createAgentConfig.submitButtonLabel": "创建代理配置",
"xpack.ingestManager.createAgentConfig.successNotificationTitle": "代理配置“{name}”已创建",
"xpack.ingestManager.createPackageConfig.addedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentConfigName}”的代理",
"xpack.ingestManager.createPackageConfig.addedNotificationTitle": "已成功添加“{packageConfigName}”",
"xpack.ingestManager.createPackageConfig.agentConfigurationNameLabel": "配置",
"xpack.ingestManager.createPackageConfig.cancelButton": "取消",
"xpack.ingestManager.createPackageConfig.cancelLinkText": "取消",
"xpack.ingestManager.createPackageConfig.pageDescriptionfromConfig": "按照下面的说明将集成添加此代理配置。",
"xpack.ingestManager.createPackageConfig.pageDescriptionfromPackage": "按照下面的说明将此集成添加代理配置。",
"xpack.ingestManager.createPackageConfig.pageTitle": "添加数据源",
"xpack.ingestManager.createPackageConfig.saveButton": "保存数据源",
"xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle": "选择要收集的数据",
"xpack.ingestManager.createPackageConfig.stepConfigure.advancedOptionsToggleLinkText": "高级选项",
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigDescriptionInputLabel": "描述",
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNameInputLabel": "数据源名称",
"xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNamespaceInputLabel": "命名空间",
"xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel": "隐藏 {type} 流",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsDescription": "以下设置适用于所有流。",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsTitle": "设置",
"xpack.ingestManager.createPackageConfig.stepConfigure.inputVarFieldOptionalLabel": "可选",
"xpack.ingestManager.createPackageConfig.stepConfigure.noConfigOptionsMessage": "没有可配置的内容",
"xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel": "显示 {type} 流",
"xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项",
"xpack.ingestManager.createPackageConfig.stepSelectAgentConfigTitle": "选择代理配置",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# 个代理} other {# 个代理}}",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingAgentConfigsTitle": "加载代理配置时出错",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingPackageTitle": "加载软件包信息时出错",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "加载选定代理配置时出错",
"xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder": "搜索代理配置",
"xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingConfigTitle": "加载代理配置信息时出错",
"xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingPackagesTitle": "加载集成时出错",
"xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定集成时出错",
"xpack.ingestManager.createPackageConfig.stepSelectPackage.filterPackagesInputPlaceholder": "搜索集成",
"xpack.ingestManager.createPackageConfig.stepSelectPackageTitle": "选择集成",
"xpack.ingestManager.packageConfigValidation.invalidArrayErrorMessage": "格式无效",
"xpack.ingestManager.packageConfigValidation.invalidYamlFormatErrorMessage": "YAML 格式无效",
"xpack.ingestManager.packageConfigValidation.nameRequiredErrorMessage": "“名称”必填",
"xpack.ingestManager.packageConfigValidation.requiredErrorMessage": "“{fieldName}”必填",
"xpack.ingestManager.dataStreamList.actionsColumnTitle": "操作",
"xpack.ingestManager.dataStreamList.datasetColumnTitle": "数据集",
"xpack.ingestManager.dataStreamList.integrationColumnTitle": "集成",
@ -8228,6 +8264,14 @@
"xpack.ingestManager.editAgentConfig.savingButtonText": "正在保存……",
"xpack.ingestManager.editAgentConfig.successNotificationTitle": "已成功更新“{name}”设置",
"xpack.ingestManager.editAgentConfig.unsavedChangesText": "您有未保存更改",
"xpack.ingestManager.editPackageConfig.cancelButton": "取消",
"xpack.ingestManager.editPackageConfig.errorLoadingDataMessage": "加载此数据源信息时出错",
"xpack.ingestManager.editPackageConfig.errorLoadingDataTitle": "加载数据时出错",
"xpack.ingestManager.editPackageConfig.pageDescription": "按照下面的说明编辑此数据源。",
"xpack.ingestManager.editPackageConfig.pageTitle": "编辑数据源",
"xpack.ingestManager.editPackageConfig.saveButton": "保存数据源",
"xpack.ingestManager.editPackageConfig.updatedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentConfigName}”的代理",
"xpack.ingestManager.editPackageConfig.updatedNotificationTitle": "已成功更新“{packageConfigName}”",
"xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "未找到任何注册令牌。",
"xpack.ingestManager.enrollemntAPIKeyList.loadingTokensMessage": "正在加载注册令牌......",
"xpack.ingestManager.enrollmentInstructions.copyButton": "复制命令",

View file

@ -17,5 +17,6 @@ export default function ({ loadTestFile }) {
// Package configs
loadTestFile(require.resolve('./package_config/create'));
loadTestFile(require.resolve('./package_config/update'));
});
}

View file

@ -126,5 +126,48 @@ export default function ({ getService }: FtrProviderContext) {
warnAndSkipTest(this, log);
}
});
it('should return a 500 if there is another package config with the same name', async function () {
if (server.enabled) {
await supertest
.post(`/api/ingest_manager/package_configs`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'same-name-test-1',
description: '',
namespace: 'default',
config_id: agentConfigId,
enabled: true,
output_id: '',
inputs: [],
package: {
name: 'filetest',
title: 'For File Tests',
version: '0.1.0',
},
})
.expect(200);
await supertest
.post(`/api/ingest_manager/package_configs`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'same-name-test-1',
description: '',
namespace: 'default',
config_id: agentConfigId,
enabled: true,
output_id: '',
inputs: [],
package: {
name: 'filetest',
title: 'For File Tests',
version: '0.1.0',
},
})
.expect(500);
} else {
warnAndSkipTest(this, log);
}
});
});
}

View file

@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { warnAndSkipTest } from '../../helpers';
export default function ({ getService }: FtrProviderContext) {
const log = getService('log');
const supertest = getService('supertest');
const dockerServers = getService('dockerServers');
const server = dockerServers.get('registry');
// use function () {} and not () => {} here
// because `this` has to point to the Mocha context
// see https://mochajs.org/#arrow-functions
describe('Package Config - update', async function () {
let agentConfigId: string;
let packageConfigId: string;
let packageConfigId2: string;
before(async function () {
const { body: agentConfigResponse } = await supertest
.post(`/api/ingest_manager/agent_configs`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Test config',
namespace: 'default',
});
agentConfigId = agentConfigResponse.item.id;
const { body: packageConfigResponse } = await supertest
.post(`/api/ingest_manager/package_configs`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'filetest-1',
description: '',
namespace: 'default',
config_id: agentConfigId,
enabled: true,
output_id: '',
inputs: [],
package: {
name: 'filetest',
title: 'For File Tests',
version: '0.1.0',
},
});
packageConfigId = packageConfigResponse.item.id;
const { body: packageConfigResponse2 } = await supertest
.post(`/api/ingest_manager/package_configs`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'filetest-2',
description: '',
namespace: 'default',
config_id: agentConfigId,
enabled: true,
output_id: '',
inputs: [],
package: {
name: 'filetest',
title: 'For File Tests',
version: '0.1.0',
},
});
packageConfigId2 = packageConfigResponse2.item.id;
});
it('should work with valid values', async function () {
if (server.enabled) {
const { body: apiResponse } = await supertest
.put(`/api/ingest_manager/package_configs/${packageConfigId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'filetest-1',
description: '',
namespace: 'updated_namespace',
config_id: agentConfigId,
enabled: true,
output_id: '',
inputs: [],
package: {
name: 'filetest',
title: 'For File Tests',
version: '0.1.0',
},
})
.expect(200);
expect(apiResponse.success).to.be(true);
} else {
warnAndSkipTest(this, log);
}
});
it('should return a 500 if there is another package config with the same name', async function () {
if (server.enabled) {
await supertest
.put(`/api/ingest_manager/package_configs/${packageConfigId2}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'filetest-1',
description: '',
namespace: 'updated_namespace',
config_id: agentConfigId,
enabled: true,
output_id: '',
inputs: [],
package: {
name: 'filetest',
title: 'For File Tests',
version: '0.1.0',
},
})
.expect(500);
} else {
warnAndSkipTest(this, log);
}
});
});
}