From 81b4f8204d3ddb08300e4c1ee00df4acfbd16291 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 13 Jul 2020 10:12:20 -0700 Subject: [PATCH] [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 --- .../components/layout.tsx | 252 +++++++------- .../package_config_input_config.tsx | 281 ++++++++-------- .../components/package_config_input_panel.tsx | 317 +++++++++--------- .../package_config_input_stream.tsx | 293 ++++++++-------- .../package_config_input_var_field.tsx | 12 +- .../create_package_config_page/index.tsx | 213 +++++++----- .../has_invalid_but_required_var.test.ts | 94 ++++++ .../services/has_invalid_but_required_var.ts | 26 ++ .../services/index.ts | 3 + .../services/is_advanced_var.ts | 2 +- .../services/validate_package_config.ts | 17 +- .../step_configure_package.tsx | 120 +++---- .../step_define_package_config.tsx | 228 +++++++------ .../step_select_config.tsx | 231 ++++++++----- .../step_select_package.tsx | 25 +- .../edit_package_config_page/index.tsx | 191 ++++++----- .../list_page/components/create_config.tsx | 8 +- .../ingest_manager/types/index.ts | 1 + .../server/services/package_config.ts | 29 ++ .../translations/translations/ja-JP.json | 44 +++ .../translations/translations/zh-CN.json | 44 +++ .../apis/index.js | 1 + .../apis/package_config/create.ts | 43 +++ .../apis/package_config/update.ts | 127 +++++++ 24 files changed, 1594 insertions(+), 1008 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.test.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/package_config/update.ts diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx index e0f40f1b15375..7ccb59f0e741e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx @@ -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 = ( - - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - - - - - +}> = memo( + ({ + from, + cancelUrl, + onCancel, + agentConfig, + packageInfo, + children, + 'data-test-subj': dataTestSubj, + }) => { + const pageTitle = useMemo(() => { + if ((from === 'package' || from === 'edit') && packageInfo) { + return ( + + + + + + +

+ {from === 'edit' ? ( + + ) : ( + + )} +

+
+
+
+ ); + } + + return from === 'edit' ? (

- {from === 'edit' ? ( - - ) : ( - - )} +

-
- - - - {from === 'edit' ? ( + ) : ( + +

- ) : from === 'config' ? ( +

+
+ ); + }, [from, packageInfo]); + + const pageDescription = useMemo(() => { + return from === 'edit' ? ( + + ) : from === 'config' ? ( + + ) : ( + + ); + }, [from]); + + const leftColumn = ( + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + - ) : ( + + + {pageTitle} + + + + {pageDescription} + + + + ); + + const rightColumn = + agentConfig && (from === 'config' || from === 'edit') ? ( + + - )} -
-
-
- ); - const rightColumn = ( - - - - {agentConfig && (from === 'config' || from === 'edit') ? ( - - - - - - {agentConfig?.name || '-'} - - - ) : null} - {packageInfo && from === 'package' ? ( - - - - - - - - - - - {packageInfo?.title || packageInfo?.name || '-'} - - - - - ) : null} - - - ); + + {agentConfig?.name || '-'} + + ) : undefined; - const maxWidth = 770; - return ( - - {children} - - ); -}; + const maxWidth = 770; + return ( + + {children} + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx index 85c0f2134d8dc..98f04dbd92659 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx @@ -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) => void; inputVarsValidationResults: PackageConfigConfigValidationResults; forceShowErrors?: boolean; -}> = ({ - packageInputVars, - packageConfigInput, - updatePackageConfigInput, - inputVarsValidationResults, - forceShowErrors, -}) => { - // Showing advanced options toggle state - const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); +}> = memo( + ({ + packageInputVars, + packageConfigInput, + updatePackageConfigInput, + inputVarsValidationResults, + forceShowErrors, + }) => { + // Showing advanced options toggle state + const [isShowingAdvanced, setIsShowingAdvanced] = useState(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); + } + }); + } + + const advancedVarsWithErrorsCount: number = useMemo( + () => + advancedVars.filter( + ({ name: varName }) => inputVarsValidationResults.vars?.[varName]?.length + ).length, + [advancedVars, inputVarsValidationResults.vars] + ); - return ( - - - - - -

- + return ( + + + + + + +

- -

+

+ + + +

+ +

+
- {hasErrors ? ( - - - } - position="right" - type="alert" - iconProps={{ color: 'danger' }} - /> - - ) : null}
-
- - -

- -

-
-
- - - {requiredVars.map((varDef) => { - const { name: varName, type: varType } = varDef; - const value = packageConfigInput.vars![varName].value; - return ( - - { - updatePackageConfigInput({ - vars: { - ...packageConfigInput.vars, - [varName]: { - type: varType, - value: newValue, + + + + {requiredVars.map((varDef) => { + const { name: varName, type: varType } = varDef; + const value = packageConfigInput.vars![varName].value; + return ( + + { + updatePackageConfigInput({ + vars: { + ...packageConfigInput.vars, + [varName]: { + type: varType, + value: newValue, + }, }, - }, - }); - }} - errors={inputVarsValidationResults.vars![varName]} - forceShowErrors={forceShowErrors} - /> - - ); - })} - {advancedVars.length ? ( - - - {/* Wrapper div to prevent button from going full width */} -
- setIsShowingAdvanced(!isShowingAdvanced)} - flush="left" - > - - -
-
- {isShowingAdvanced - ? advancedVars.map((varDef) => { - const { name: varName, type: varType } = varDef; - const value = packageConfigInput.vars![varName].value; - return ( - - { - updatePackageConfigInput({ - vars: { - ...packageConfigInput.vars, - [varName]: { - type: varType, - value: newValue, - }, - }, - }); - }} - errors={inputVarsValidationResults.vars![varName]} - forceShowErrors={forceShowErrors} + }); + }} + errors={inputVarsValidationResults.vars![varName]} + forceShowErrors={forceShowErrors} + /> + + ); + })} + {advancedVars.length ? ( + + + {/* Wrapper div to prevent button from going full width */} + + + setIsShowingAdvanced(!isShowingAdvanced)} + flush="left" + > + + + + {!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? ( + + + + - ); - }) - : null} - - ) : null} -
-
-
- ); -}; + ) : null} +
+ + {isShowingAdvanced + ? advancedVars.map((varDef) => { + const { name: varName, type: varType } = varDef; + const value = packageConfigInput.vars![varName].value; + return ( + + { + updatePackageConfigInput({ + vars: { + ...packageConfigInput.vars, + [varName]: { + type: varType, + value: newValue, + }, + }, + }); + }} + errors={inputVarsValidationResults.vars![varName]} + forceShowErrors={forceShowErrors} + /> + + ); + }) + : null} + + ) : null} + + + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx index f9c9dcd469b25..af26afdbf74d7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx @@ -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, + 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; @@ -42,148 +67,136 @@ export const PackageConfigInputPanel: React.FunctionComponent<{ updatePackageConfigInput: (updatedInput: Partial) => void; inputValidationResults: PackageConfigInputValidationResults; forceShowErrors?: boolean; -}> = ({ - packageInput, - packageInputStreams, - packageConfigInput, - updatePackageConfigInput, - inputValidationResults, - forceShowErrors, -}) => { - // Showing streams toggle state - const [isShowingStreams, setIsShowingStreams] = useState(false); +}> = memo( + ({ + packageInput, + packageInputStreams, + packageConfigInput, + updatePackageConfigInput, + inputValidationResults, + forceShowErrors, + }) => { + // Showing streams toggle state + const [isShowingStreams, setIsShowingStreams] = useState( + shouldShowStreamsByDefault(packageInput, packageInputStreams, packageConfigInput) + ); - // Errors state - const hasErrors = forceShowErrors && validationHasErrors(inputValidationResults); + // Errors state + const errorCount = countValidationErrors(inputValidationResults); + const hasErrors = forceShowErrors && errorCount; - return ( - - {/* Header / input-level toggle */} - - - - - -

- - {packageInput.title || packageInput.type} - -

-
-
- {hasErrors ? ( + 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 */} + + + - - } - position="right" - type="alert" - iconProps={{ color: 'danger' }} - /> + +

{packageInput.title || packageInput.type}

+
- ) : null} -
- } - checked={packageConfigInput.enabled} - onChange={(e) => { - const enabled = e.target.checked; - updatePackageConfigInput({ - enabled, - streams: packageConfigInput.streams.map((stream) => ({ - ...stream, +
+ } + checked={packageConfigInput.enabled} + onChange={(e) => { + const enabled = e.target.checked; + updatePackageConfigInput({ enabled, - })), - }); - }} - /> - - - - - - - - {packageConfigInput.streams.filter((stream) => stream.enabled).length} - - - ), - total: packageInputStreams.length, - }} - /> - - - - 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, - }, - } - ) + streams: packageConfigInput.streams.map((stream) => ({ + ...stream, + enabled, + })), + }); + if (!enabled && isShowingStreams) { + setIsShowingStreams(false); } - /> - - - - + }} + /> + + + + {hasErrors ? ( + + + + + + ) : null} + + 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, + }, + } + ) + } + /> + + + + - {/* Header rule break */} - {isShowingStreams ? : null} + {/* Header rule break */} + {isShowingStreams ? : null} - {/* Input level configuration */} - {isShowingStreams && packageInput.vars && packageInput.vars.length ? ( - - - - - ) : null} + {/* Input level configuration */} + {isShowingStreams && packageInput.vars && packageInput.vars.length ? ( + + + + + ) : null} - {/* Per-stream configuration */} - {isShowingStreams ? ( - - {packageInputStreams.map((packageInputStream) => { - const packageConfigInputStream = packageConfigInput.streams.find( - (stream) => stream.dataset.name === packageInputStream.dataset.name - ); - return packageConfigInputStream ? ( - + {/* Per-stream configuration */} + {isShowingStreams ? ( + + {inputStreams.map(({ packageInputStream, packageConfigInputStream }, index) => ( + ) => { @@ -213,17 +226,21 @@ export const PackageConfigInputPanel: React.FunctionComponent<{ updatePackageConfigInput(updatedInput); }} inputStreamValidationResults={ - inputValidationResults.streams![packageConfigInputStream.id] + inputValidationResults.streams![packageConfigInputStream!.id] } forceShowErrors={forceShowErrors} /> - - + {index !== inputStreams.length - 1 ? ( + <> + + + + ) : null} - ) : null; - })} - - ) : null} -
- ); -}; + ))} + + ) : null} + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx index 52a4748fe14c7..11a9df276485b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx @@ -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) => void; inputStreamValidationResults: PackageConfigConfigValidationResults; forceShowErrors?: boolean; -}> = ({ - packageInputStream, - packageConfigInputStream, - updatePackageConfigInputStream, - inputStreamValidationResults, - forceShowErrors, -}) => { - // Showing advanced options toggle state - const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); +}> = memo( + ({ + packageInputStream, + packageConfigInputStream, + updatePackageConfigInputStream, + inputStreamValidationResults, + forceShowErrors, + }) => { + // Showing advanced options toggle state + const [isShowingAdvanced, setIsShowingAdvanced] = useState(); - // 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 ( - - - - - - {packageInputStream.title} - - - {hasErrors ? ( - - - } - position="right" - type="alert" - iconProps={{ color: 'danger' }} - /> - + const advancedVarsWithErrorsCount: number = useMemo( + () => + advancedVars.filter( + ({ name: varName }) => inputStreamValidationResults.vars?.[varName]?.length + ).length, + [advancedVars, inputStreamValidationResults.vars] + ); + + return ( + + + + + + { + const enabled = e.target.checked; + updatePackageConfigInputStream({ + enabled, + }); + }} + /> + {packageInputStream.description ? ( + + + + + + ) : null} - - } - checked={packageConfigInputStream.enabled} - onChange={(e) => { - const enabled = e.target.checked; - updatePackageConfigInputStream({ - enabled, - }); - }} - /> - {packageInputStream.description ? ( - - - - - - - ) : null} - - - - {requiredVars.map((varDef) => { - const { name: varName, type: varType } = varDef; - const value = packageConfigInputStream.vars![varName].value; - return ( - - { - updatePackageConfigInputStream({ - vars: { - ...packageConfigInputStream.vars, - [varName]: { - type: varType, - value: newValue, + + + + + + {requiredVars.map((varDef) => { + const { name: varName, type: varType } = varDef; + const value = packageConfigInputStream.vars![varName].value; + return ( + + { + updatePackageConfigInputStream({ + vars: { + ...packageConfigInputStream.vars, + [varName]: { + type: varType, + value: newValue, + }, }, - }, - }); - }} - errors={inputStreamValidationResults.vars![varName]} - forceShowErrors={forceShowErrors} - /> - - ); - })} - {advancedVars.length ? ( - - - {/* Wrapper div to prevent button from going full width */} -
- setIsShowingAdvanced(!isShowingAdvanced)} - flush="left" - > - - -
-
- {isShowingAdvanced - ? advancedVars.map((varDef) => { - const { name: varName, type: varType } = varDef; - const value = packageConfigInputStream.vars![varName].value; - return ( - - { - updatePackageConfigInputStream({ - vars: { - ...packageConfigInputStream.vars, - [varName]: { - type: varType, - value: newValue, - }, - }, - }); - }} - errors={inputStreamValidationResults.vars![varName]} - forceShowErrors={forceShowErrors} + }); + }} + errors={inputStreamValidationResults.vars![varName]} + forceShowErrors={forceShowErrors} + /> + + ); + })} + {advancedVars.length ? ( + + + + + setIsShowingAdvanced(!isShowingAdvanced)} + flush="left" + > + + + + {!isShowingAdvanced && hasErrors && advancedVarsWithErrorsCount ? ( + + + + - ); - }) - : null} - - ) : null} -
-
-
- ); -}; + ) : null} + + + {isShowingAdvanced + ? advancedVars.map((varDef) => { + const { name: varName, type: varType } = varDef; + const value = packageConfigInputStream.vars![varName].value; + return ( + + { + updatePackageConfigInputStream({ + vars: { + ...packageConfigInputStream.vars, + [varName]: { + type: varType, + value: newValue, + }, + }, + }); + }} + errors={inputStreamValidationResults.vars![varName]} + forceShowErrors={forceShowErrors} + /> + + ); + }) + : null} + + ) : null} + + + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx index 8868e00ecc1f1..eb681096a080e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx @@ -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(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 ( setIsDirty(true)} /> ); - }; + }, [isInvalid, multi, onChange, type, value]); return ( } > - {renderField()} + {field} ); -}; +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx index b446e6bf97e7b..74cbcdca512db 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx @@ -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(); const [packageInfo, setPackageInfo] = useState(); + const [isLoadingSecondStep, setIsLoadingSecondStep] = useState(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) => { - 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 updatePackageConfig = useCallback( + (updatedFields: Partial) => { + const newPackageConfig = { + ...packageConfig, + ...updatedFields, + }; + setPackageConfig(newPackageConfig); - 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 ? ( + + ) : agentConfig && packageInfo ? ( + <> + + + + ) : ( +
+ ), + [ + agentConfig, + formState, + isLoadingSecondStep, + packageConfig, + packageInfo, + updatePackageConfig, + validationResults, + ] + ); + const steps: EuiStepProps[] = [ from === 'package' ? { @@ -310,44 +363,16 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => { }), children: stepSelectPackage, }, - { - title: i18n.translate( - 'xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle', - { - defaultMessage: 'Configure integration', - } - ), - status: !packageInfo || !agentConfig ? 'disabled' : undefined, - children: - agentConfig && packageInfo ? ( - - ) : null, - }, { title: i18n.translate( 'xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle', { - defaultMessage: 'Select the data you want to collect', + defaultMessage: 'Configure integration', } ), - status: !packageInfo || !agentConfig ? 'disabled' : undefined, + status: !packageInfo || !agentConfig || isLoadingSecondStep ? 'disabled' : undefined, 'data-test-subj': 'dataCollectionSetupStep', - children: - agentConfig && packageInfo ? ( - - ) : null, + children: stepConfigurePackage, }, ]; @@ -371,7 +396,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => { : agentConfig && ( )} - + {/* TODO #64541 - Remove classes */} { : undefined } > - + - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - + {!isLoadingSecondStep && agentConfig && packageInfo && formState === 'INVALID' ? ( - + ) : null} - - - + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + + + + + + + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.test.ts new file mode 100644 index 0000000000000..679ae4b1456d6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.test.ts @@ -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); + }); +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.ts new file mode 100644 index 0000000000000..f632d40a05621 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/has_invalid_but_required_var.ts @@ -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) + ) + ) + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts index 6cfb1c74bd661..0d33a4e113f03 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts @@ -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'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts index 398f1d675c5df..a2f4a6675ac80 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts @@ -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; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts index cd301747c3f53..bd9d216ca969a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts @@ -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 validationHasErrors = ( +export const countValidationErrors = ( validationResults: | PackageConfigValidationResults | PackageConfigInputValidationResults | PackageConfigConfigValidationResults -) => { +): number => { const flattenedValidation = getFlattenedObject(validationResults); + const errors = Object.values(flattenedValidation).filter((value) => Boolean(value)) || []; + return errors.length; +}; - return !!Object.entries(flattenedValidation).find(([, value]) => !!value); +export const validationHasErrors = ( + validationResults: + | PackageConfigValidationResults + | PackageConfigInputValidationResults + | PackageConfigConfigValidationResults +): boolean => { + return countValidationErrors(validationResults) > 0; }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx index eecd204a5e307..380a03e15695b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx @@ -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 ? ( - - {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 ? ( - - ) => { - 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} - /> - - ) : null; - })} - + <> + + + {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 ? ( + + ) => { + 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} + /> + + + ) : null; + })} + + ) : ( - - - + ); - return validationResults ? ( - - {renderConfigureInputs()} - {hasErrors && submitAttempted ? ( - - - -

- -

-
- -
- ) : null} -
- ) : ( - - ); + return validationResults ? renderConfigureInputs() : ; }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx index b2ffe62104eb1..a04d023ebcc48 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx @@ -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(false); + const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); // Update package config's package and config info useEffect(() => { @@ -74,111 +75,140 @@ export const StepDefinePackageConfig: React.FunctionComponent<{ ]); return validationResults ? ( - <> - - - + + + + } + description={ + + } + > + <> + {/* Name */} + + } + > + + updatePackageConfig({ + name: e.target.value, + }) } - > - - updatePackageConfig({ - name: e.target.value, - }) - } - data-test-subj="packageConfigNameInput" + data-test-subj="packageConfigNameInput" + /> + + + {/* Description */} + - - - - + + } + isInvalid={!!validationResults.description} + error={validationResults.description} + > + + updatePackageConfig({ + description: e.target.value, + }) } - labelAppend={ - + /> + + + + {/* Advanced options toggle */} + + + setIsShowingAdvanced(!isShowingAdvanced)} + flush="left" + > + + + + {!isShowingAdvanced && !!validationResults.namespace ? ( + + - } - isInvalid={!!validationResults.description} - error={validationResults.description} - > - - updatePackageConfig({ - description: e.target.value, - }) + + ) : null} + + + {/* Advanced options content */} + {/* Todo: Populate list of existing namespaces */} + {isShowingAdvanced ? ( + <> + + } - /> - - - - - setIsShowingAdvancedDefine(!isShowingAdvancedDefine)} - > - - - {/* Todo: Populate list of existing namespaces */} - {isShowingAdvancedDefine || !!validationResults.namespace ? ( - - - - - + > + - { - updatePackageConfig({ - namespace: newNamespace, - }); - }} - onChange={(newNamespaces: Array<{ label: string }>) => { - updatePackageConfig({ - namespace: newNamespaces.length ? newNamespaces[0].label : '', - }); - }} - /> - - - - - ) : null} - + onCreateOption={(newNamespace: string) => { + updatePackageConfig({ + namespace: newNamespace, + }); + }} + onChange={(newNamespaces: Array<{ label: string }>) => { + updatePackageConfig({ + namespace: newNamespaces.length ? newNamespaces[0].label : '', + }); + }} + /> + + + ) : null} + + ) : ( ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx index f6391cf1fa456..d3120f9051f45 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx @@ -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( agentConfig ? agentConfig.id : undefined ); const [selectedConfigError, setSelectedConfigError] = useState(); + // Create new config flyout state + const hasWriteCapabilites = useCapabilities().write; + const [isCreateAgentConfigFlyoutOpen, setIsCreateAgentConfigFlyoutOpen] = useState( + 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 ( - - - { - 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) => ( - - {option.label} - - - {agentConfigsById[option.key!].description} - - - - - - - - - )} - listProps={{ - bordered: true, - }} - searchProps={{ - placeholder: i18n.translate( - 'xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder', - { - defaultMessage: 'Search for agent configurations', + <> + {isCreateAgentConfigFlyoutOpen ? ( + + { + setIsCreateAgentConfigFlyoutOpen(false); + if (newAgentConfig) { + refreshAgentConfigs(); + setSelectedConfigId(newAgentConfig.id); + } + }} + /> + + ) : null} + + + { + 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) => ( + + {option.label} + + + {agentConfigsById[option.key!].description} + + + + + + + + + )} + 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) => ( + + {search} + + {list} + + )} + + + {/* Display selected agent config error if there is one */} + {selectedConfigError ? ( + + } - ), - }} - height={240} - onChange={(options) => { - const selectedOption = options.find((option) => option.checked === 'on'); - if (selectedOption) { - setSelectedConfigId(selectedOption.key); - } else { - setSelectedConfigId(undefined); - } - }} - > - {(list, search) => ( - - {search} - - {list} - - )} - - - {/* Display selected agent config error if there is one */} - {selectedConfigError ? ( + error={selectedConfigError} + /> + + ) : null} - + setIsCreateAgentConfigFlyoutOpen(true)} + flush="left" + size="s" + > - } - error={selectedConfigError} - /> + +
- ) : null} - + + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx index 204b862bd4dc4..048ae101fcd6f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx @@ -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( packageInfo ? `${packageInfo.name}-${packageInfo.version}` : undefined @@ -30,7 +37,11 @@ export const StepSelectPackage: React.FunctionComponent<{ const [selectedPkgError, setSelectedPkgError] = useState(); // 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); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx index 52fd95d663671..f4411a6057a15 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx @@ -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(); const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - // Update package config method - const updatePackageConfig = (updatedFields: Partial) => { - const newPackageConfig = { - ...packageConfig, - ...updatedFields, - }; - setPackageConfig(newPackageConfig); + // 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); - // 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'); - } - }; + return newValidationResult; + } + }, + [packageConfig, packageInfo] + ); - 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); + // Update package config method + const updatePackageConfig = useCallback( + (updatedFields: Partial) => { + const newPackageConfig = { + ...packageConfig, + ...updatedFields, + }; + setPackageConfig(newPackageConfig); - return newValidationResult; - } - }; + // 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'); + } + }, + [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 ? ( + <> + + + + + ) : null, + [ + agentConfig, + formState, + packageConfig, + packageConfigId, + packageInfo, + updatePackageConfig, + validationResults, + ] + ); + return ( {isLoadingData ? ( @@ -301,46 +341,7 @@ export const EditPackageConfigPage: React.FunctionComponent = () => { onCancel={() => setFormState('VALID')} /> )} - - ), - }, - { - title: i18n.translate( - 'xpack.ingestManager.editPackageConfig.stepConfigurePackageConfigTitle', - { - defaultMessage: 'Select the data you want to collect', - } - ), - children: ( - - ), - }, - ]} - /> + {configurePackage} {/* TODO #64541 - Remove classes */} { : undefined } > - + - + {agentConfig && packageInfo && formState === 'INVALID' ? ( - + ) : null} - - - + + + + + + + + + + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx index d1abd88adba86..795c46ec282c5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx @@ -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 = ({ onClose }) => { @@ -86,7 +86,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClos - + onClose()} flush="left"> = ({ onClos } ) ); - onClose(); + onClose(data.item); } else { notifications.toasts.addDanger( error diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 170a9cedc08d9..dc27da18bc008 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -18,6 +18,7 @@ export { UpdatePackageConfig, PackageConfigInput, PackageConfigInputStream, + PackageConfigConfigRecord, PackageConfigConfigRecordEntry, Output, DataStream, diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts index 9433a81e74b07..e8ca09a83c2b6 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -44,6 +44,20 @@ class PackageConfigService { packageConfig: NewPackageConfig, options?: { id?: string; user?: AuthenticatedUser } ): Promise { + // 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( SAVED_OBJECT_TYPE, id, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d5d7b1648e47f..b18f2999c5635 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "コマンドをコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b22b89a40a774..f671dd4553ac9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "复制命令", diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index 30c49140c6e2a..81848917f9b05 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -17,5 +17,6 @@ export default function ({ loadTestFile }) { // Package configs loadTestFile(require.resolve('./package_config/create')); + loadTestFile(require.resolve('./package_config/update')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts index c7748ab255f43..cae4ff79bdef6 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts @@ -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); + } + }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_config/update.ts b/x-pack/test/ingest_manager_api_integration/apis/package_config/update.ts new file mode 100644 index 0000000000000..0251fef5f767c --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/package_config/update.ts @@ -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); + } + }); + }); +}