Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Ingest Pipelines] Error messages #70167

Merged
merged 12 commits into from
Jul 2, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({
error={form.getErrors()}
>
{/* Request error */}
{saveError && <PipelineFormError errorMessage={saveError.message} />}
{saveError && <PipelineFormError error={saveError} />}

{/* All form fields */}
<PipelineFormFields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,161 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiSpacer, EuiCallOut } from '@elastic/eui';
import React, { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
import { useKibana } from '../../../shared_imports';

interface Props {
errorMessage: string;
error: unknown;
}
interface PipelineError {
reason: string;
processorType?: string;
}
interface PipelineErrors {
errors: PipelineError[];
}

interface ErrorNode {
reason: string;
processor_type?: string;
suppressed?: ErrorNode[];
}

interface ErrorAttributesObject {
attributes: {
error: {
root_cause: [ErrorNode];
};
};
}

const flattenErrorsTree = (node: ErrorNode): PipelineError[] => {
const result: PipelineError[] = [];
const recurse = (_node: ErrorNode) => {
result.push({ reason: _node.reason, processorType: _node.processor_type });
if (_node.suppressed && Array.isArray(_node.suppressed)) {
_node.suppressed.forEach(recurse);
}
};
recurse(node);
return result;
};

const toKnownError = (error: unknown): PipelineErrors => {
if (
typeof error === 'object' &&
error != null &&
(error as any).attributes?.error?.root_cause?.[0]
) {
const errorAttributes = error as ErrorAttributesObject;
const rootCause = errorAttributes.attributes.error.root_cause[0];
return { errors: flattenErrorsTree(rootCause) };
}

if (typeof error === 'string') {
return { errors: [{ reason: error }] };
}

if (
error instanceof Error ||
(typeof error === 'object' && error != null && (error as any).message)
) {
return { errors: [{ reason: (error as any).message }] };
}

return { errors: [{ reason: 'An unknown error occurred.' }] };
jloleysens marked this conversation as resolved.
Show resolved Hide resolved
};

const title = i18n.translate('xpack.ingestPipelines.form.savePipelineError', {
defaultMessage: 'Unable to create pipeline',
});

export const PipelineFormError: React.FunctionComponent<Props> = ({ error }) => {
const { services } = useKibana();
const [isShowingAllErrors, setIsShowingAllErrors] = useState<boolean>(false);
const safeErrorResult = toKnownError(error);
const hasMoreErrors = safeErrorResult.errors.length > 5;
jloleysens marked this conversation as resolved.
Show resolved Hide resolved
const hiddenErrorsCount = safeErrorResult.errors.length - 5;
const results = isShowingAllErrors ? safeErrorResult.errors : safeErrorResult.errors.slice(0, 5);

const renderErrorListItem = ({ processorType, reason }: PipelineError) => {
return (
<>
{processorType
? i18n.translate('xpack.ingestPipelines.form.savePipelineError.processorLabel', {
defaultMessage: '{type} processor',
values: { type: processorType },
})
: undefined}
&nbsp;
{reason}
</>
);
};

export const PipelineFormError: React.FunctionComponent<Props> = ({ errorMessage }) => {
useEffect(() => {
services.notifications.toasts.addDanger({ title });
}, [services, error]);
return (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.ingestPipelines.form.savePipelineError"
defaultMessage="Unable to create pipeline"
/>
}
color="danger"
iconType="alert"
data-test-subj="savePipelineError"
>
<p>{errorMessage}</p>
<EuiCallOut title={title} color="danger" iconType="alert" data-test-subj="savePipelineError">
{results.length > 1 ? (
<ul>
{results.map((e, idx) => (
<li key={idx}>{renderErrorListItem(e)}</li>
))}
</ul>
) : (
renderErrorListItem(results[0])
)}
{hasMoreErrors ? (
<EuiFlexGroup
direction="column"
responsive={false}
gutterSize="xs"
justifyContent="center"
alignItems="flexStart"
>
<EuiFlexItem grow={false}>
{isShowingAllErrors ? (
<EuiButtonEmpty
size="s"
onClick={() => setIsShowingAllErrors(false)}
color="danger"
iconSide="right"
iconType="arrowUp"
>
{i18n.translate(
'xpack.ingestPipelines.form.savePip10mbelineError.showFewerButton',
{
defaultMessage: 'Hide {count, plural, one {# error} other {# errors}}',
values: {
count: hiddenErrorsCount,
},
}
)}
</EuiButtonEmpty>
) : (
<EuiButtonEmpty
size="s"
onClick={() => setIsShowingAllErrors(true)}
color="danger"
iconSide="right"
iconType="arrowDown"
>
{i18n.translate('xpack.ingestPipelines.form.savePipelineError.showAllButton', {
defaultMessage: 'Show {count, plural, one {# error} other {# errors}}',
values: {
count: hiddenErrorsCount,
},
})}
</EuiButtonEmpty>
)}
</EuiFlexItem>
</EuiFlexGroup>
) : undefined}
</EuiCallOut>
<EuiSpacer size="m" />
</>
Expand Down
8 changes: 7 additions & 1 deletion x-pack/plugins/ingest_pipelines/server/routes/api/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Pipeline } from '../../../common/types';
import { API_BASE_PATH } from '../../../common/constants';
import { RouteDependencies } from '../../types';
import { pipelineSchema } from './pipeline_schema';
import { isObjectWithKeys } from './shared';

const bodySchema = schema.object({
name: schema.string(),
Expand Down Expand Up @@ -70,7 +71,12 @@ export const registerCreateRoute = ({
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
body: error,
body: isObjectWithKeys(error.body)
? {
message: error.message,
attributes: error.body,
}
: error,
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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.
*/

export { isObjectWithKeys } from './is_object_with_keys';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export const isObjectWithKeys = (value: unknown) => {
return typeof value === 'object' && !!value && Object.keys(value).length > 0;
};
8 changes: 7 additions & 1 deletion x-pack/plugins/ingest_pipelines/server/routes/api/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema';
import { API_BASE_PATH } from '../../../common/constants';
import { RouteDependencies } from '../../types';
import { pipelineSchema } from './pipeline_schema';
import { isObjectWithKeys } from './shared';

const bodySchema = schema.object(pipelineSchema);

Expand Down Expand Up @@ -52,7 +53,12 @@ export const registerUpdateRoute = ({
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
body: error,
body: isObjectWithKeys(error.body)
? {
message: error.message,
attributes: error.body,
}
: error,
});
}

Expand Down