From 632a5aee56d1e123f4b098a8d00ab421129cd8c9 Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Tue, 24 Mar 2020 12:55:13 -0700
Subject: [PATCH 1/4] Initial pass at datasource configuration validation
---
.../components/datasource_input_config.tsx | 16 +-
.../components/datasource_input_panel.tsx | 17 +-
.../datasource_input_stream_config.tsx | 16 +-
.../components/datasource_input_var_field.tsx | 28 ++-
.../components/index.ts | 1 +
.../create_datasource_page/index.tsx | 16 +-
.../create_datasource_page/services/index.ts | 7 +
.../services/validate_datasource.ts | 211 ++++++++++++++++++
.../step_configure_datasource.tsx | 153 ++++++++-----
.../ingest_manager/services/index.ts | 2 +
.../ingest_manager/types/index.ts | 1 +
11 files changed, 404 insertions(+), 64 deletions(-)
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx
index 356739af1ff9a..7694c35c35ad8 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx
@@ -15,14 +15,22 @@ import {
EuiTitle,
} from '@elastic/eui';
import { DatasourceInput, RegistryVarsEntry } from '../../../../types';
-import { isAdvancedVar } from '../services';
+import { isAdvancedVar, DatasourceInputValidationResults } from '../services';
import { DatasourceInputVarField } from './datasource_input_var_field';
export const DatasourceInputConfig: React.FunctionComponent<{
packageInputVars?: RegistryVarsEntry[];
datasourceInput: DatasourceInput;
updateDatasourceInput: (updatedInput: Partial) => void;
-}> = ({ packageInputVars, datasourceInput, updateDatasourceInput }) => {
+ inputVarsValidationResults: DatasourceInputValidationResults;
+ forceShowErrors?: boolean;
+}> = ({
+ packageInputVars,
+ datasourceInput,
+ updateDatasourceInput,
+ inputVarsValidationResults,
+ forceShowErrors,
+}) => {
// Showing advanced options toggle state
const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
@@ -81,6 +89,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{
},
});
}}
+ errors={inputVarsValidationResults.config![varName]}
+ forceShowErrors={forceShowErrors}
/>
);
@@ -123,6 +133,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{
},
});
}}
+ errors={inputVarsValidationResults.config![varName]}
+ forceShowErrors={forceShowErrors}
/>
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx
index 74b08f48df12d..a60ee4b9f6253 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx
@@ -19,6 +19,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { DatasourceInput, DatasourceInputStream, RegistryInput } from '../../../../types';
+import { DatasourceInputValidationResults } from '../services';
import { DatasourceInputConfig } from './datasource_input_config';
import { DatasourceInputStreamConfig } from './datasource_input_stream_config';
@@ -32,7 +33,15 @@ export const DatasourceInputPanel: React.FunctionComponent<{
packageInput: RegistryInput;
datasourceInput: DatasourceInput;
updateDatasourceInput: (updatedInput: Partial) => void;
-}> = ({ packageInput, datasourceInput, updateDatasourceInput }) => {
+ inputValidationResults: DatasourceInputValidationResults;
+ forceShowErrors?: boolean;
+}> = ({
+ packageInput,
+ datasourceInput,
+ updateDatasourceInput,
+ inputValidationResults,
+ forceShowErrors,
+}) => {
// Showing streams toggle state
const [isShowingStreams, setIsShowingStreams] = useState(false);
@@ -122,6 +131,8 @@ export const DatasourceInputPanel: React.FunctionComponent<{
packageInputVars={packageInput.vars}
datasourceInput={datasourceInput}
updateDatasourceInput={updateDatasourceInput}
+ inputVarsValidationResults={inputValidationResults}
+ forceShowErrors={forceShowErrors}
/>
@@ -165,6 +176,10 @@ export const DatasourceInputPanel: React.FunctionComponent<{
updateDatasourceInput(updatedInput);
}}
+ inputStreamValidationResults={
+ inputValidationResults.streams![datasourceInputStream.id]
+ }
+ forceShowErrors={forceShowErrors}
/>
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx
index 3bf5b2bb4c0f0..64829c6d08cd4 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx
@@ -16,14 +16,22 @@ import {
EuiButtonEmpty,
} from '@elastic/eui';
import { DatasourceInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types';
-import { isAdvancedVar } from '../services';
+import { isAdvancedVar, DatasourceConfigValidationResults } from '../services';
import { DatasourceInputVarField } from './datasource_input_var_field';
export const DatasourceInputStreamConfig: React.FunctionComponent<{
packageInputStream: RegistryStream;
datasourceInputStream: DatasourceInputStream;
updateDatasourceInputStream: (updatedStream: Partial) => void;
-}> = ({ packageInputStream, datasourceInputStream, updateDatasourceInputStream }) => {
+ inputStreamValidationResults: DatasourceConfigValidationResults;
+ forceShowErrors?: boolean;
+}> = ({
+ packageInputStream,
+ datasourceInputStream,
+ updateDatasourceInputStream,
+ inputStreamValidationResults,
+ forceShowErrors,
+}) => {
// Showing advanced options toggle state
const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
@@ -83,6 +91,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{
},
});
}}
+ errors={inputStreamValidationResults.config![varName]}
+ forceShowErrors={forceShowErrors}
/>
);
@@ -125,6 +135,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{
},
});
}}
+ errors={inputStreamValidationResults.config![varName]}
+ forceShowErrors={forceShowErrors}
/>
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx
index bcb99eed88ac0..846a807f9240d 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_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 from 'react';
+import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFormRow, EuiFieldText, EuiComboBox, EuiText, EuiCodeEditor } from '@elastic/eui';
@@ -16,12 +16,20 @@ export const DatasourceInputVarField: React.FunctionComponent<{
varDef: RegistryVarsEntry;
value: any;
onChange: (newValue: any) => void;
-}> = ({ varDef, value, onChange }) => {
+ errors?: string[] | null;
+ forceShowErrors?: boolean;
+}> = ({ 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 = () => {
- if (varDef.multi) {
+ if (multi) {
return (
({ label: val }))}
onCreateOption={(newVal: any) => {
onChange([...value, newVal]);
@@ -29,10 +37,11 @@ export const DatasourceInputVarField: React.FunctionComponent<{
onChange={(newVals: any[]) => {
onChange(newVals.map(val => val.label));
}}
+ onBlur={() => setIsDirty(true)}
/>
);
}
- if (varDef.type === 'yaml') {
+ if (type === 'yaml') {
return (
onChange(newVal)}
+ onBlur={() => setIsDirty(true)}
/>
);
}
return (
onChange(e.target.value)}
+ onBlur={() => setIsDirty(true)}
/>
);
};
return (
) : null
}
- helpText={}
+ helpText={}
>
{renderField()}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts
index e5f18e1449d1b..3bfca75668911 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts
@@ -5,3 +5,4 @@
*/
export { CreateDatasourcePageLayout } from './layout';
export { DatasourceInputPanel } from './datasource_input_panel';
+export { DatasourceInputVarField } from './datasource_input_var_field';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx
index 23d0f3317a667..7815ab9cd1d6e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx
@@ -21,6 +21,7 @@ import { useLinks as useEPMLinks } from '../../epm/hooks';
import { CreateDatasourcePageLayout } from './components';
import { CreateDatasourceFrom, CreateDatasourceStep } from './types';
import { CREATE_DATASOURCE_STEP_PATHS } from './constants';
+import { DatasourceValidationResults, validateDatasource } from './services';
import { StepSelectPackage } from './step_select_package';
import { StepSelectConfig } from './step_select_config';
import { StepConfigureDatasource } from './step_configure_datasource';
@@ -51,6 +52,9 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
inputs: [],
});
+ // Datasource validation state
+ const [validationResults, setValidationResults] = useState();
+
// Update package info method
const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => {
if (updatedPackageInfo) {
@@ -84,9 +88,18 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
...updatedFields,
};
setDatasource(newDatasource);
-
// eslint-disable-next-line no-console
console.debug('Datasource updated', newDatasource);
+ updateDatasourceValidation(newDatasource);
+ };
+
+ const updateDatasourceValidation = (newDatasource?: NewDatasource) => {
+ if (packageInfo) {
+ const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo);
+ setValidationResults(newValidationResult);
+ // eslint-disable-next-line no-console
+ console.debug('Datasource validation results', newValidationResult);
+ }
};
// Cancel url
@@ -202,6 +215,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
packageInfo={packageInfo}
datasource={datasource}
updateDatasource={updateDatasource}
+ validationResults={validationResults!}
backLink={
{from === 'config' ? (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts
index 44e5bfa41cb9b..d99f0712db3c3 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts
@@ -4,3 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { isAdvancedVar } from './is_advanced_var';
+export {
+ DatasourceValidationResults,
+ DatasourceConfigValidationResults,
+ DatasourceInputValidationResults,
+ validateDatasource,
+ validationHasErrors,
+} from './validate_datasource';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
new file mode 100644
index 0000000000000..3867ace9a0620
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
@@ -0,0 +1,211 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { safeLoad } from 'js-yaml';
+import { getFlattenedObject } from '../../../../services';
+import {
+ NewDatasource,
+ DatasourceInput,
+ DatasourceInputStream,
+ DatasourceConfigRecordEntry,
+ PackageInfo,
+ RegistryInput,
+ RegistryVarsEntry,
+} from '../../../../types';
+
+type Errors = string[] | null;
+
+type ValidationEntry = Record;
+
+export interface DatasourceConfigValidationResults {
+ config?: ValidationEntry;
+}
+
+export type DatasourceInputValidationResults = DatasourceConfigValidationResults & {
+ streams?: Record;
+};
+
+export interface DatasourceValidationResults {
+ name: Errors;
+ description: Errors;
+ inputs: Record;
+}
+
+// Returns validation information for a given datasource configuration and package info
+export const validateDatasource = (
+ datasource: NewDatasource,
+ packageInfo: PackageInfo
+): DatasourceValidationResults => {
+ const validationResults: DatasourceValidationResults = {
+ name: null,
+ description: null,
+ inputs: {},
+ };
+
+ if (!datasource.name.trim()) {
+ validationResults.name = [
+ i18n.translate('xpack.ingestManager.datasourceValidation.nameRequiredErrorMessage', {
+ defaultMessage: 'Name is required',
+ }),
+ ];
+ }
+
+ if (
+ !packageInfo.datasources ||
+ packageInfo.datasources.length === 0 ||
+ !packageInfo.datasources[0] ||
+ !packageInfo.datasources[0].inputs ||
+ packageInfo.datasources[0].inputs.length === 0
+ ) {
+ return validationResults;
+ }
+
+ const registryInputsByType: Record<
+ string,
+ RegistryInput
+ > = packageInfo.datasources[0].inputs.reduce((inputs, registryInput) => {
+ inputs[registryInput.type] = registryInput;
+ return inputs;
+ }, {} as Record);
+
+ // Validate each datasource input with either its own config fields or streams
+ datasource.inputs.forEach(input => {
+ if (!input.config && !input.streams) {
+ return;
+ }
+
+ const inputValidationResults: DatasourceInputValidationResults = {
+ config: undefined,
+ streams: {},
+ };
+
+ const inputVarsByName = (registryInputsByType[input.type].vars || []).reduce(
+ (vars, registryVar) => {
+ vars[registryVar.name] = registryVar;
+ return vars;
+ },
+ {} as Record
+ );
+
+ // Validate input-level config fields
+ inputValidationResults.config = Object.entries(input.config || {}).reduce(
+ (results, [name, configEntry]) => {
+ results[name] = input.enabled
+ ? validateDatasourceConfig(configEntry, inputVarsByName[name])
+ : null;
+ return results;
+ },
+ {} as ValidationEntry
+ );
+
+ // Validate each input stream with config fields
+ input.streams.forEach(stream => {
+ if (!stream.config) {
+ return;
+ }
+
+ const streamValidationResults: DatasourceConfigValidationResults = {
+ config: undefined,
+ };
+
+ const streamVarsByName = (
+ (
+ registryInputsByType[input.type].streams.find(
+ registryStream => registryStream.dataset === stream.dataset
+ ) || {}
+ ).vars || []
+ ).reduce((vars, registryVar) => {
+ vars[registryVar.name] = registryVar;
+ return vars;
+ }, {} as Record);
+
+ // Validate stream-level config fields
+ streamValidationResults.config = Object.entries(stream.config).reduce(
+ (results, [name, configEntry]) => {
+ results[name] = stream.enabled
+ ? validateDatasourceConfig(configEntry, streamVarsByName[name])
+ : null;
+ return results;
+ },
+ {} as ValidationEntry
+ );
+
+ inputValidationResults.streams![stream.id] = streamValidationResults;
+ });
+
+ validationResults.inputs[input.type] = inputValidationResults;
+ });
+
+ return validationResults;
+};
+
+const validateDatasourceConfig = (
+ configEntry: DatasourceConfigRecordEntry,
+ varDef: RegistryVarsEntry
+): string[] | null => {
+ const errors = [];
+ const { value } = configEntry;
+ let parsedValue: any = value;
+
+ if (typeof value === 'string') {
+ parsedValue = value.trim();
+ }
+
+ if (varDef.type === 'yaml') {
+ try {
+ parsedValue = safeLoad(value);
+ } catch (e) {
+ errors.push(
+ i18n.translate('xpack.ingestManager.datasourceValidation.invalidYamlFormatErrorMessage', {
+ defaultMessage: 'Invalid YAML format',
+ })
+ );
+ }
+ }
+
+ if (varDef.required) {
+ if (varDef.multi) {
+ if (parsedValue && !Array.isArray(parsedValue)) {
+ errors.push(
+ i18n.translate('xpack.ingestManager.datasourceValidation.invalidArrayErrorMessage', {
+ defaultMessage: 'Invalid format',
+ })
+ );
+ }
+ if (!parsedValue || (Array.isArray(parsedValue) && parsedValue.length === 0)) {
+ errors.push(
+ i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', {
+ defaultMessage: '{fieldName} is required',
+ values: {
+ fieldName: varDef.title || varDef.name,
+ },
+ })
+ );
+ }
+ } else {
+ if (parsedValue === undefined || (typeof parsedValue === 'string' && !parsedValue)) {
+ i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', {
+ defaultMessage: '{fieldName} is required',
+ values: {
+ fieldName: varDef.title || varDef.name,
+ },
+ });
+ }
+ }
+ }
+
+ return errors.length ? errors : null;
+};
+
+export const validationHasErrors = (
+ validationResults:
+ | DatasourceValidationResults
+ | DatasourceInputValidationResults
+ | ValidationEntry
+) => {
+ const flattenedValidation = getFlattenedObject(validationResults);
+ return !!Object.entries(flattenedValidation).find(([, value]) => !!value);
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
index b45beef4a8b5e..95c78e2ee2609 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
@@ -13,13 +13,13 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
- EuiFieldText,
EuiButtonEmpty,
EuiSpacer,
EuiEmptyPrompt,
EuiText,
EuiButton,
EuiComboBox,
+ EuiCallOut,
} from '@elastic/eui';
import {
AgentConfig,
@@ -28,21 +28,37 @@ import {
NewDatasource,
DatasourceInput,
} from '../../../types';
+import { Loading } from '../../../components';
import { packageToConfigDatasourceInputs } from '../../../services';
-import { DatasourceInputPanel } from './components';
+import { DatasourceValidationResults, validationHasErrors } from './services';
+import { DatasourceInputPanel, DatasourceInputVarField } from './components';
export const StepConfigureDatasource: React.FunctionComponent<{
agentConfig: AgentConfig;
packageInfo: PackageInfo;
datasource: NewDatasource;
updateDatasource: (fields: Partial) => void;
+ validationResults: DatasourceValidationResults;
backLink: JSX.Element;
cancelUrl: string;
onNext: () => void;
-}> = ({ agentConfig, packageInfo, datasource, updateDatasource, backLink, cancelUrl, onNext }) => {
+}> = ({
+ agentConfig,
+ packageInfo,
+ datasource,
+ updateDatasource,
+ validationResults,
+ backLink,
+ cancelUrl,
+ onNext,
+}) => {
// Form show/hide states
const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false);
+ // Form submit state
+ const [submitAttempted, setSubmitAttempted] = useState(false);
+ const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
+
// Update datasource's package and config info
useEffect(() => {
const dsPackage = datasource.package;
@@ -81,54 +97,54 @@ export const StepConfigureDatasource: React.FunctionComponent<{
}, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]);
// Step A, define datasource
- const DefineDatasource = (
+ const renderDefineDatasource = () => (
-
- }
- >
-
- updateDatasource({
- name: e.target.value,
- })
- }
- />
-
+ {
+ updateDatasource({
+ name: newValue,
+ });
+ }}
+ errors={validationResults.name}
+ forceShowErrors={submitAttempted}
+ />
-
- }
- labelAppend={
-
-
-
- }
- >
-
- updateDatasource({
- description: e.target.value,
- })
- }
- />
-
+ {
+ updateDatasource({
+ description: newValue,
+ });
+ }}
+ errors={validationResults.description}
+ forceShowErrors={submitAttempted}
+ />
@@ -182,7 +198,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{
// Step B, configure inputs (and their streams)
// Assume packages only export one datasource for now
- const ConfigureInputs =
+ const renderConfigureInputs = () =>
packageInfo.datasources &&
packageInfo.datasources[0] &&
packageInfo.datasources[0].inputs &&
@@ -208,6 +224,8 @@ export const StepConfigureDatasource: React.FunctionComponent<{
inputs: newInputs,
});
}}
+ inputValidationResults={validationResults.inputs[datasourceInput.type]}
+ forceShowErrors={submitAttempted}
/>
) : null;
@@ -232,7 +250,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{
);
- return (
+ return validationResults ? (
@@ -251,7 +269,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{
defaultMessage: 'Define your datasource',
}
),
- children: DefineDatasource,
+ children: renderDefineDatasource(),
},
{
title: i18n.translate(
@@ -260,13 +278,34 @@ export const StepConfigureDatasource: React.FunctionComponent<{
defaultMessage: 'Choose the data you want to collect',
}
),
- children: ConfigureInputs,
+ children: renderConfigureInputs(),
},
]}
/>
+ {hasErrors && submitAttempted ? (
+
+
+
+
+
+
+
+
+ ) : null}
@@ -278,7 +317,17 @@ export const StepConfigureDatasource: React.FunctionComponent<{
- onNext()}>
+ {
+ setSubmitAttempted(true);
+ if (!hasErrors) {
+ onNext();
+ }
+ }}
+ >
+ ) : (
+
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts
index 0aa08602e4d4d..5ebd1300baf65 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts
@@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export { getFlattenedObject } from '../../../../../../../src/core/utils';
+
export {
agentConfigRouteService,
datasourceRouteService,
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 a59fb06145a3a..de1bf42ac6490 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
@@ -16,6 +16,7 @@ export {
NewDatasource,
DatasourceInput,
DatasourceInputStream,
+ DatasourceConfigRecordEntry,
// API schemas - Agent Config
GetAgentConfigsResponse,
GetAgentConfigsResponseItem,
From d64749978d82314c9ca1c9e3acea0f866ab48961 Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Tue, 24 Mar 2020 14:30:43 -0700
Subject: [PATCH 2/4] Show error icon and red text at input and stream levels
---
.../components/datasource_input_config.tsx | 51 ++++++++++++++-----
.../components/datasource_input_panel.tsx | 37 ++++++++++++--
.../datasource_input_stream_config.tsx | 41 ++++++++++++---
.../services/validate_datasource.ts | 2 +-
.../step_configure_datasource.tsx | 16 +++---
5 files changed, 113 insertions(+), 34 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx
index 7694c35c35ad8..0e8763cb2d4c0 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx
@@ -6,23 +6,24 @@
import React, { useState, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiText,
+ EuiTextColor,
EuiSpacer,
EuiButtonEmpty,
EuiTitle,
+ EuiIconTip,
} from '@elastic/eui';
import { DatasourceInput, RegistryVarsEntry } from '../../../../types';
-import { isAdvancedVar, DatasourceInputValidationResults } from '../services';
+import { isAdvancedVar, DatasourceConfigValidationResults, validationHasErrors } from '../services';
import { DatasourceInputVarField } from './datasource_input_var_field';
export const DatasourceInputConfig: React.FunctionComponent<{
packageInputVars?: RegistryVarsEntry[];
datasourceInput: DatasourceInput;
updateDatasourceInput: (updatedInput: Partial) => void;
- inputVarsValidationResults: DatasourceInputValidationResults;
+ inputVarsValidationResults: DatasourceConfigValidationResults;
forceShowErrors?: boolean;
}> = ({
packageInputVars,
@@ -34,6 +35,9 @@ export const DatasourceInputConfig: React.FunctionComponent<{
// Showing advanced options toggle state
const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
+ // Errors state
+ const hasErrors = forceShowErrors && validationHasErrors(inputVarsValidationResults);
+
const requiredVars: RegistryVarsEntry[] = [];
const advancedVars: RegistryVarsEntry[] = [];
@@ -48,15 +52,36 @@ export const DatasourceInputConfig: React.FunctionComponent<{
}
return (
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+ {hasErrors ? (
+
+
+ }
+ position="right"
+ type="alert"
+ iconProps={{ color: 'danger' }}
+ />
+
+ ) : null}
+
@@ -68,7 +93,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{
-
+
{requiredVars.map(varDef => {
const { name: varName, type: varType } = varDef;
@@ -144,6 +169,6 @@ export const DatasourceInputConfig: React.FunctionComponent<{
) : null}
-
+
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx
index a60ee4b9f6253..6b0c68ccb7d3f 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx
@@ -17,9 +17,10 @@ import {
EuiButtonIcon,
EuiHorizontalRule,
EuiSpacer,
+ EuiIconTip,
} from '@elastic/eui';
import { DatasourceInput, DatasourceInputStream, RegistryInput } from '../../../../types';
-import { DatasourceInputValidationResults } from '../services';
+import { DatasourceInputValidationResults, validationHasErrors } from '../services';
import { DatasourceInputConfig } from './datasource_input_config';
import { DatasourceInputStreamConfig } from './datasource_input_stream_config';
@@ -45,6 +46,9 @@ export const DatasourceInputPanel: React.FunctionComponent<{
// Showing streams toggle state
const [isShowingStreams, setIsShowingStreams] = useState(false);
+ // Errors state
+ const hasErrors = forceShowErrors && validationHasErrors(inputValidationResults);
+
return (
{/* Header / input-level toggle */}
@@ -52,9 +56,32 @@ export const DatasourceInputPanel: React.FunctionComponent<{
- {packageInput.title || packageInput.type}
-
+
+
+
+
+
+ {packageInput.title || packageInput.type}
+
+
+
+
+ {hasErrors ? (
+
+
+ }
+ position="right"
+ type="alert"
+ iconProps={{ color: 'danger' }}
+ />
+
+ ) : null}
+
}
checked={datasourceInput.enabled}
onChange={e => {
@@ -131,7 +158,7 @@ export const DatasourceInputPanel: React.FunctionComponent<{
packageInputVars={packageInput.vars}
datasourceInput={datasourceInput}
updateDatasourceInput={updateDatasourceInput}
- inputVarsValidationResults={inputValidationResults}
+ inputVarsValidationResults={{ config: inputValidationResults.config }}
forceShowErrors={forceShowErrors}
/>
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx
index 64829c6d08cd4..43e8f5a2c060d 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx
@@ -7,16 +7,17 @@ import React, { useState, Fragment } 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 { DatasourceInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types';
-import { isAdvancedVar, DatasourceConfigValidationResults } from '../services';
+import { isAdvancedVar, DatasourceConfigValidationResults, validationHasErrors } from '../services';
import { DatasourceInputVarField } from './datasource_input_var_field';
export const DatasourceInputStreamConfig: React.FunctionComponent<{
@@ -35,6 +36,9 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{
// Showing advanced options toggle state
const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
+ // Errors state
+ const hasErrors = forceShowErrors && validationHasErrors(inputStreamValidationResults);
+
const requiredVars: RegistryVarsEntry[] = [];
const advancedVars: RegistryVarsEntry[] = [];
@@ -49,10 +53,33 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{
}
return (
-
-
+
+
+
+
+ {packageInputStream.title || packageInputStream.dataset}
+
+
+ {hasErrors ? (
+
+
+ }
+ position="right"
+ type="alert"
+ iconProps={{ color: 'danger' }}
+ />
+
+ ) : null}
+
+ }
checked={datasourceInputStream.enabled}
onChange={e => {
const enabled = e.target.checked;
@@ -70,7 +97,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{
) : null}
-
+
{requiredVars.map(varDef => {
const { name: varName, type: varType } = varDef;
@@ -146,6 +173,6 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{
) : null}
-
+
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
index 3867ace9a0620..30df27f794db6 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
@@ -204,7 +204,7 @@ export const validationHasErrors = (
validationResults:
| DatasourceValidationResults
| DatasourceInputValidationResults
- | ValidationEntry
+ | DatasourceConfigValidationResults
) => {
const flattenedValidation = getFlattenedObject(validationResults);
return !!Object.entries(flattenedValidation).find(([, value]) => !!value);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
index 95c78e2ee2609..aa3bfde9aefd7 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
@@ -9,7 +9,6 @@ import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiSteps,
EuiPanel,
- EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
@@ -99,8 +98,8 @@ export const StepConfigureDatasource: React.FunctionComponent<{
// Step A, define datasource
const renderDefineDatasource = () => (
-
-
+
+
-
+
-
+
-
-
+
+
-
+
+
) : null}
From 01ebec42fa8533d1dfba05cad6dab7e7d89475bd Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Wed, 25 Mar 2020 15:33:32 -0700
Subject: [PATCH 3/4] Add tests, fix bugs in validation method
---
.../services/validate_datasource.test.ts | 504 ++++++++++++++++++
.../services/validate_datasource.ts | 147 ++---
.../ingest_manager/types/index.ts | 2 +
3 files changed, 590 insertions(+), 63 deletions(-)
create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts
new file mode 100644
index 0000000000000..82c807f31c4bc
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts
@@ -0,0 +1,504 @@
+/*
+ * 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 {
+ PackageInfo,
+ InstallationStatus,
+ NewDatasource,
+ RegistryDatasource,
+} from '../../../../types';
+import { validateDatasource, validationHasErrors } from './validate_datasource';
+
+describe('Ingest Manager - validateDatasource()', () => {
+ const mockPackage: PackageInfo = {
+ name: 'mock-package',
+ title: 'Mock package',
+ version: '0.0.0',
+ description: 'description',
+ type: 'mock',
+ categories: [],
+ requirement: { kibana: { versions: '' }, elasticsearch: { versions: '' } },
+ format_version: '',
+ download: '',
+ path: '',
+ assets: {
+ kibana: {
+ dashboard: [],
+ visualization: [],
+ search: [],
+ 'index-pattern': [],
+ },
+ },
+ status: InstallationStatus.notInstalled,
+ datasources: [
+ {
+ name: 'datasource1',
+ title: 'Datasource 1',
+ description: 'test datasource',
+ inputs: [
+ {
+ type: 'foo',
+ title: 'Foo',
+ vars: [
+ { default: 'foo-input-var-value', name: 'foo-input-var-name', type: 'text' },
+ {
+ default: 'foo-input2-var-value',
+ name: 'foo-input2-var-name',
+ required: true,
+ type: 'text',
+ },
+ { name: 'foo-input3-var-name', type: 'text', required: true, multi: true },
+ ],
+ streams: [
+ {
+ dataset: 'foo',
+ input: 'foo',
+ title: 'Foo',
+ vars: [{ name: 'var-name', type: 'yaml' }],
+ },
+ ],
+ },
+ {
+ type: 'bar',
+ title: 'Bar',
+ vars: [
+ {
+ default: ['value1', 'value2'],
+ name: 'bar-input-var-name',
+ type: 'text',
+ multi: true,
+ },
+ { name: 'bar-input2-var-name', required: true, type: 'text' },
+ ],
+ streams: [
+ {
+ dataset: 'bar',
+ input: 'bar',
+ title: 'Bar',
+ vars: [{ name: 'var-name', type: 'yaml', required: true }],
+ },
+ {
+ dataset: 'bar2',
+ input: 'bar2',
+ title: 'Bar 2',
+ vars: [{ default: 'bar2-var-value', name: 'var-name', type: 'text' }],
+ },
+ ],
+ },
+ {
+ type: 'with-no-config-or-streams',
+ title: 'With no config or streams',
+ streams: [],
+ },
+ {
+ type: 'with-disabled-streams',
+ title: 'With disabled streams',
+ streams: [
+ {
+ dataset: 'disabled',
+ input: 'disabled',
+ title: 'Disabled',
+ enabled: false,
+ vars: [{ multi: true, required: true, name: 'var-name', type: 'text' }],
+ },
+ { dataset: 'disabled2', input: 'disabled2', title: 'Disabled 2', enabled: false },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ const validDatasource: NewDatasource = {
+ name: 'datasource1-1',
+ config_id: 'test-config',
+ enabled: true,
+ output_id: 'test-output',
+ inputs: [
+ {
+ type: 'foo',
+ enabled: true,
+ config: {
+ 'foo-input-var-name': { value: 'foo-input-var-value', type: 'text' },
+ 'foo-input2-var-name': { value: 'foo-input2-var-value', type: 'text' },
+ 'foo-input3-var-name': { value: ['test'], type: 'text' },
+ },
+ streams: [
+ {
+ id: 'foo-foo',
+ dataset: 'foo',
+ enabled: true,
+ config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } },
+ },
+ ],
+ },
+ {
+ type: 'bar',
+ enabled: true,
+ config: {
+ 'bar-input-var-name': { value: ['value1', 'value2'], type: 'text' },
+ 'bar-input2-var-name': { value: 'test', type: 'text' },
+ },
+ streams: [
+ {
+ id: 'bar-bar',
+ dataset: 'bar',
+ enabled: true,
+ config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } },
+ },
+ {
+ id: 'bar-bar2',
+ dataset: 'bar2',
+ enabled: true,
+ config: { 'var-name': { value: undefined, type: 'text' } },
+ },
+ ],
+ },
+ {
+ type: 'with-no-config-or-streams',
+ enabled: true,
+ streams: [],
+ },
+ {
+ type: 'with-disabled-streams',
+ enabled: true,
+ streams: [
+ {
+ id: 'with-disabled-streams-disabled',
+ dataset: 'disabled',
+ enabled: false,
+ config: { 'var-name': { value: undefined, type: 'text' } },
+ },
+ {
+ id: 'with-disabled-streams-disabled2',
+ dataset: 'disabled2',
+ enabled: false,
+ },
+ ],
+ },
+ ],
+ };
+
+ const invalidDatasource: NewDatasource = {
+ ...validDatasource,
+ name: '',
+ inputs: [
+ {
+ type: 'foo',
+ enabled: true,
+ config: {
+ 'foo-input-var-name': { value: undefined, type: 'text' },
+ 'foo-input2-var-name': { value: '', type: 'text' },
+ 'foo-input3-var-name': { value: [], type: 'text' },
+ },
+ streams: [
+ {
+ id: 'foo-foo',
+ dataset: 'foo',
+ enabled: true,
+ config: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } },
+ },
+ ],
+ },
+ {
+ type: 'bar',
+ enabled: true,
+ config: {
+ 'bar-input-var-name': { value: 'invalid value for multi', type: 'text' },
+ 'bar-input2-var-name': { value: undefined, type: 'text' },
+ },
+ streams: [
+ {
+ id: 'bar-bar',
+ dataset: 'bar',
+ enabled: true,
+ config: { 'var-name': { value: ' \n\n', type: 'yaml' } },
+ },
+ {
+ id: 'bar-bar2',
+ dataset: 'bar2',
+ enabled: true,
+ config: { 'var-name': { value: undefined, type: 'text' } },
+ },
+ ],
+ },
+ {
+ type: 'with-no-config-or-streams',
+ enabled: true,
+ streams: [],
+ },
+ {
+ type: 'with-disabled-streams',
+ enabled: true,
+ streams: [
+ {
+ id: 'with-disabled-streams-disabled',
+ dataset: 'disabled',
+ enabled: false,
+ config: {
+ 'var-name': {
+ value: 'invalid value but not checked due to not enabled',
+ type: 'text',
+ },
+ },
+ },
+ {
+ id: 'with-disabled-streams-disabled2',
+ dataset: 'disabled2',
+ enabled: false,
+ },
+ ],
+ },
+ ],
+ };
+
+ const noErrorsValidationResults = {
+ name: null,
+ description: null,
+ inputs: {
+ foo: {
+ config: {
+ 'foo-input-var-name': null,
+ 'foo-input2-var-name': null,
+ 'foo-input3-var-name': null,
+ },
+ streams: { 'foo-foo': { config: { 'var-name': null } } },
+ },
+ bar: {
+ config: { 'bar-input-var-name': null, 'bar-input2-var-name': null },
+ streams: {
+ 'bar-bar': { config: { 'var-name': null } },
+ 'bar-bar2': { config: { 'var-name': null } },
+ },
+ },
+ 'with-disabled-streams': {
+ streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } },
+ },
+ },
+ };
+
+ it('returns no errors for valid datasource configuration', () => {
+ expect(validateDatasource(validDatasource, mockPackage)).toEqual(noErrorsValidationResults);
+ });
+
+ it('returns errors for invalid datasource configuration', () => {
+ expect(validateDatasource(invalidDatasource, mockPackage)).toEqual({
+ name: ['Name is required'],
+ description: null,
+ inputs: {
+ foo: {
+ config: {
+ 'foo-input-var-name': null,
+ 'foo-input2-var-name': ['foo-input2-var-name is required'],
+ 'foo-input3-var-name': ['foo-input3-var-name is required'],
+ },
+ streams: { 'foo-foo': { config: { 'var-name': ['Invalid YAML format'] } } },
+ },
+ bar: {
+ config: {
+ 'bar-input-var-name': ['Invalid format'],
+ 'bar-input2-var-name': ['bar-input2-var-name is required'],
+ },
+ streams: {
+ 'bar-bar': { config: { 'var-name': ['var-name is required'] } },
+ 'bar-bar2': { config: { 'var-name': null } },
+ },
+ },
+ 'with-disabled-streams': {
+ streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } },
+ },
+ },
+ });
+ });
+
+ it('returns no errors for disabled inputs', () => {
+ const disabledInputs = invalidDatasource.inputs.map(input => ({ ...input, enabled: false }));
+ expect(validateDatasource({ ...validDatasource, inputs: disabledInputs }, mockPackage)).toEqual(
+ noErrorsValidationResults
+ );
+ });
+
+ it('returns only datasource and input-level errors for disabled streams', () => {
+ const inputsWithDisabledStreams = invalidDatasource.inputs.map(input =>
+ input.streams
+ ? {
+ ...input,
+ streams: input.streams.map(stream => ({ ...stream, enabled: false })),
+ }
+ : input
+ );
+ expect(
+ validateDatasource({ ...invalidDatasource, inputs: inputsWithDisabledStreams }, mockPackage)
+ ).toEqual({
+ name: ['Name is required'],
+ description: null,
+ inputs: {
+ foo: {
+ config: {
+ 'foo-input-var-name': null,
+ 'foo-input2-var-name': ['foo-input2-var-name is required'],
+ 'foo-input3-var-name': ['foo-input3-var-name is required'],
+ },
+ streams: { 'foo-foo': { config: { 'var-name': null } } },
+ },
+ bar: {
+ config: {
+ 'bar-input-var-name': ['Invalid format'],
+ 'bar-input2-var-name': ['bar-input2-var-name is required'],
+ },
+ streams: {
+ 'bar-bar': { config: { 'var-name': null } },
+ 'bar-bar2': { config: { 'var-name': null } },
+ },
+ },
+ 'with-disabled-streams': {
+ streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } },
+ },
+ },
+ });
+ });
+
+ it('returns no errors for packages with no datasources', () => {
+ expect(
+ validateDatasource(validDatasource, {
+ ...mockPackage,
+ datasources: undefined,
+ })
+ ).toEqual({
+ name: null,
+ description: null,
+ inputs: null,
+ });
+ expect(
+ validateDatasource(validDatasource, {
+ ...mockPackage,
+ datasources: [],
+ })
+ ).toEqual({
+ name: null,
+ description: null,
+ inputs: null,
+ });
+ });
+
+ it('returns no errors for packages with no inputs', () => {
+ expect(
+ validateDatasource(validDatasource, {
+ ...mockPackage,
+ datasources: [{} as RegistryDatasource],
+ })
+ ).toEqual({
+ name: null,
+ description: null,
+ inputs: null,
+ });
+ expect(
+ validateDatasource(validDatasource, {
+ ...mockPackage,
+ datasources: [({ inputs: [] } as unknown) as RegistryDatasource],
+ })
+ ).toEqual({
+ name: null,
+ description: null,
+ inputs: null,
+ });
+ });
+});
+
+describe('Ingest Manager - validationHasErrors()', () => {
+ it('returns true for stream validation results with errors', () => {
+ expect(
+ validationHasErrors({
+ config: { foo: ['foo error'], bar: null },
+ })
+ ).toBe(true);
+ });
+
+ it('returns false for stream validation results with no errors', () => {
+ expect(
+ validationHasErrors({
+ config: { foo: null, bar: null },
+ })
+ ).toBe(false);
+ });
+
+ it('returns true for input validation results with errors', () => {
+ expect(
+ validationHasErrors({
+ config: { foo: ['foo error'], bar: null },
+ streams: { stream1: { config: { foo: null, bar: null } } },
+ })
+ ).toBe(true);
+ expect(
+ validationHasErrors({
+ config: { foo: null, bar: null },
+ streams: { stream1: { config: { foo: ['foo error'], bar: null } } },
+ })
+ ).toBe(true);
+ });
+
+ it('returns false for input validation results with no errors', () => {
+ expect(
+ validationHasErrors({
+ config: { foo: null, bar: null },
+ streams: { stream1: { config: { foo: null, bar: null } } },
+ })
+ ).toBe(false);
+ });
+
+ it('returns true for datasource validation results with errors', () => {
+ expect(
+ validationHasErrors({
+ name: ['name error'],
+ description: null,
+ inputs: {
+ input1: {
+ config: { foo: null, bar: null },
+ streams: { stream1: { config: { foo: null, bar: null } } },
+ },
+ },
+ })
+ ).toBe(true);
+ expect(
+ validationHasErrors({
+ name: null,
+ description: null,
+ inputs: {
+ input1: {
+ config: { foo: ['foo error'], bar: null },
+ streams: { stream1: { config: { foo: null, bar: null } } },
+ },
+ },
+ })
+ ).toBe(true);
+ expect(
+ validationHasErrors({
+ name: null,
+ description: null,
+ inputs: {
+ input1: {
+ config: { foo: null, bar: null },
+ streams: { stream1: { config: { foo: ['foo error'], bar: null } } },
+ },
+ },
+ })
+ ).toBe(true);
+ });
+
+ it('returns false for datasource validation results with no errors', () => {
+ expect(
+ validationHasErrors({
+ name: null,
+ description: null,
+ inputs: {
+ input1: {
+ config: { foo: null, bar: null },
+ streams: { stream1: { config: { foo: null, bar: null } } },
+ },
+ },
+ })
+ ).toBe(false);
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
index 30df27f794db6..518e2bfc1af07 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts
@@ -31,10 +31,13 @@ export type DatasourceInputValidationResults = DatasourceConfigValidationResults
export interface DatasourceValidationResults {
name: Errors;
description: Errors;
- inputs: Record;
+ inputs: Record | null;
}
-// Returns validation information for a given datasource configuration and package info
+/*
+ * Returns validation information for a given datasource configuration and package info
+ * Note: this method assumes that `datasource` is correctly structured for the given package
+ */
export const validateDatasource = (
datasource: NewDatasource,
packageInfo: PackageInfo
@@ -60,6 +63,7 @@ export const validateDatasource = (
!packageInfo.datasources[0].inputs ||
packageInfo.datasources[0].inputs.length === 0
) {
+ validationResults.inputs = null;
return validationResults;
}
@@ -91,54 +95,66 @@ export const validateDatasource = (
);
// Validate input-level config fields
- inputValidationResults.config = Object.entries(input.config || {}).reduce(
- (results, [name, configEntry]) => {
+ const inputConfigs = Object.entries(input.config || {});
+ if (inputConfigs.length) {
+ inputValidationResults.config = inputConfigs.reduce((results, [name, configEntry]) => {
results[name] = input.enabled
? validateDatasourceConfig(configEntry, inputVarsByName[name])
: null;
return results;
- },
- {} as ValidationEntry
- );
+ }, {} as ValidationEntry);
+ } else {
+ delete inputValidationResults.config;
+ }
// Validate each input stream with config fields
- input.streams.forEach(stream => {
- if (!stream.config) {
- return;
- }
-
- const streamValidationResults: DatasourceConfigValidationResults = {
- config: undefined,
- };
-
- const streamVarsByName = (
- (
- registryInputsByType[input.type].streams.find(
- registryStream => registryStream.dataset === stream.dataset
- ) || {}
- ).vars || []
- ).reduce((vars, registryVar) => {
- vars[registryVar.name] = registryVar;
- return vars;
- }, {} as Record);
-
- // Validate stream-level config fields
- streamValidationResults.config = Object.entries(stream.config).reduce(
- (results, [name, configEntry]) => {
- results[name] = stream.enabled
- ? validateDatasourceConfig(configEntry, streamVarsByName[name])
- : null;
- return results;
- },
- {} as ValidationEntry
- );
+ if (input.streams.length) {
+ input.streams.forEach(stream => {
+ if (!stream.config) {
+ return;
+ }
+
+ const streamValidationResults: DatasourceConfigValidationResults = {
+ config: undefined,
+ };
+
+ const streamVarsByName = (
+ (
+ registryInputsByType[input.type].streams.find(
+ registryStream => registryStream.dataset === stream.dataset
+ ) || {}
+ ).vars || []
+ ).reduce((vars, registryVar) => {
+ vars[registryVar.name] = registryVar;
+ return vars;
+ }, {} as Record);
+
+ // Validate stream-level config fields
+ streamValidationResults.config = Object.entries(stream.config).reduce(
+ (results, [name, configEntry]) => {
+ results[name] =
+ input.enabled && stream.enabled
+ ? validateDatasourceConfig(configEntry, streamVarsByName[name])
+ : null;
+ return results;
+ },
+ {} as ValidationEntry
+ );
- inputValidationResults.streams![stream.id] = streamValidationResults;
- });
+ inputValidationResults.streams![stream.id] = streamValidationResults;
+ });
+ } else {
+ delete inputValidationResults.streams;
+ }
- validationResults.inputs[input.type] = inputValidationResults;
+ if (inputValidationResults.config || inputValidationResults.streams) {
+ validationResults.inputs![input.type] = inputValidationResults;
+ }
});
+ if (Object.entries(validationResults.inputs!).length === 0) {
+ validationResults.inputs = null;
+ }
return validationResults;
};
@@ -154,6 +170,19 @@ const validateDatasourceConfig = (
parsedValue = value.trim();
}
+ if (varDef.required) {
+ if (parsedValue === undefined || (typeof parsedValue === 'string' && !parsedValue)) {
+ errors.push(
+ i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', {
+ defaultMessage: '{fieldName} is required',
+ values: {
+ fieldName: varDef.title || varDef.name,
+ },
+ })
+ );
+ }
+ }
+
if (varDef.type === 'yaml') {
try {
parsedValue = safeLoad(value);
@@ -166,34 +195,26 @@ const validateDatasourceConfig = (
}
}
- if (varDef.required) {
- if (varDef.multi) {
- if (parsedValue && !Array.isArray(parsedValue)) {
- errors.push(
- i18n.translate('xpack.ingestManager.datasourceValidation.invalidArrayErrorMessage', {
- defaultMessage: 'Invalid format',
- })
- );
- }
- if (!parsedValue || (Array.isArray(parsedValue) && parsedValue.length === 0)) {
- errors.push(
- i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', {
- defaultMessage: '{fieldName} is required',
- values: {
- fieldName: varDef.title || varDef.name,
- },
- })
- );
- }
- } else {
- if (parsedValue === undefined || (typeof parsedValue === 'string' && !parsedValue)) {
+ if (varDef.multi) {
+ if (parsedValue && !Array.isArray(parsedValue)) {
+ errors.push(
+ i18n.translate('xpack.ingestManager.datasourceValidation.invalidArrayErrorMessage', {
+ defaultMessage: 'Invalid format',
+ })
+ );
+ }
+ if (
+ varDef.required &&
+ (!parsedValue || (Array.isArray(parsedValue) && parsedValue.length === 0))
+ ) {
+ errors.push(
i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', {
defaultMessage: '{fieldName} is required',
values: {
fieldName: varDef.title || varDef.name,
},
- });
- }
+ })
+ );
}
}
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 de1bf42ac6490..eba5c00d03790 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
@@ -56,6 +56,7 @@ export {
RegistryVarsEntry,
RegistryInput,
RegistryStream,
+ RegistryDatasource,
PackageList,
PackageListItem,
PackagesGroupedByStatus,
@@ -70,4 +71,5 @@ export {
DeletePackageResponse,
DetailViewPanelName,
InstallStatus,
+ InstallationStatus,
} from '../../../../common';
From dd7b9fd4fe33b461bfd4876509d6181ec07706d5 Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Wed, 8 Apr 2020 11:44:33 -0700
Subject: [PATCH 4/4] Fix typings
---
.../services/validate_datasource.test.ts | 4 ++--
.../create_datasource_page/step_configure_datasource.tsx | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts
index 82c807f31c4bc..a45fabeb5ed6a 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts
@@ -12,7 +12,7 @@ import {
import { validateDatasource, validationHasErrors } from './validate_datasource';
describe('Ingest Manager - validateDatasource()', () => {
- const mockPackage: PackageInfo = {
+ const mockPackage = ({
name: 'mock-package',
title: 'Mock package',
version: '0.0.0',
@@ -109,7 +109,7 @@ describe('Ingest Manager - validateDatasource()', () => {
],
},
],
- };
+ } as unknown) as PackageInfo;
const validDatasource: NewDatasource = {
name: 'datasource1-1',
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
index aa3bfde9aefd7..105d6c66a5704 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx
@@ -118,7 +118,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{
name: newValue,
});
}}
- errors={validationResults.name}
+ errors={validationResults!.name}
forceShowErrors={submitAttempted}
/>
@@ -141,7 +141,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{
description: newValue,
});
}}
- errors={validationResults.description}
+ errors={validationResults!.description}
forceShowErrors={submitAttempted}
/>
@@ -224,7 +224,7 @@ export const StepConfigureDatasource: React.FunctionComponent<{
inputs: newInputs,
});
}}
- inputValidationResults={validationResults.inputs[datasourceInput.type]}
+ inputValidationResults={validationResults!.inputs![datasourceInput.type]}
forceShowErrors={submitAttempted}
/>