Skip to content

Commit

Permalink
[8.6] [Infrastructure UI] Improve metrics settings error handling (#1…
Browse files Browse the repository at this point in the history
…46272) (#146675)

# Backport

This will backport the following commits from `main` to `8.6`:
- [[Infrastructure UI] Improve metrics settings error handling
(#146272)](#146272)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Marco Antonio
Ghiani","email":"[email protected]"},"sourceCommit":{"committedDate":"2022-11-30T10:54:37Z","message":"[Infrastructure
UI] Improve metrics settings error handling (#146272)\n\nCloses
[#145238](https://github.com/elastic/kibana/issues/145238)\r\n\r\n##
Summary\r\n\r\nThese changes add validation to the Metric Indices passed
into the\r\nMetrics settings page.\r\nNew validation is added both in
the UI and in the endpoint, performing\r\nthe following checks:\r\n-
Index pattern is not an empty string\r\n- Index pattern does not contain
empty spaces (start, middle, end) (the\r\npattern is not trimmed)\r\n-
Index pattern does not contain empty entries, comma-separated
values\r\nshould have an acceptable value.\r\n\r\nIn case the value is
not valid, the UI will render an appropriate error\r\nmessage.\r\nIf the
`PATCH /api/metrics/source/{sourceId}` request to update the\r\nvalue is
manually sent with an invalid value, the server will respond\r\nwith a
400 status code and an error message.\r\n\r\nAlso, for retro
compatibility and to not block the user when the\r\nconfiguration can't
be successfully retrieved, in case of internal error\r\nthe `GET
/api/metrics/source/{sourceId}` will return a 404 and on the\r\nUI,
instead of rendering a blank page, the user will see the empty
form\r\nand will be able to re-insert the right values.\r\n\r\n##
Testing\r\n\r\nNavigate to `Inventory`-> Click on `Settings` on the
topbar -> Start\r\nwriting different metric indices in the Metric
Indices field.\r\n\r\n### Editing Metric Indices
validation\r\n\r\n\r\nhttps://user-images.githubusercontent.com/34506779/203763021-0f4d8926-ffa4-448a-a038-696732158f4e.mov\r\n\r\n###
Missing/Broken configuration
response\r\n\r\n\r\nhttps://user-images.githubusercontent.com/34506779/203763120-ffc91cd3-9bf4-43da-a04f-5561ceabf591.mov\r\n\r\nCo-authored-by:
Marco Antonio Ghiani <[email protected]>\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"ddcbf73284d48b1f1f6eb9322f6f108f312f8e02","branchLabelMapping":{"^v8.7.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Metrics
UI","Team:Infra Monitoring
UI","release_note:skip","backport:all-open","v8.7.0"],"number":146272,"url":"https://github.com/elastic/kibana/pull/146272","mergeCommit":{"message":"[Infrastructure
UI] Improve metrics settings error handling (#146272)\n\nCloses
[#145238](https://github.com/elastic/kibana/issues/145238)\r\n\r\n##
Summary\r\n\r\nThese changes add validation to the Metric Indices passed
into the\r\nMetrics settings page.\r\nNew validation is added both in
the UI and in the endpoint, performing\r\nthe following checks:\r\n-
Index pattern is not an empty string\r\n- Index pattern does not contain
empty spaces (start, middle, end) (the\r\npattern is not trimmed)\r\n-
Index pattern does not contain empty entries, comma-separated
values\r\nshould have an acceptable value.\r\n\r\nIn case the value is
not valid, the UI will render an appropriate error\r\nmessage.\r\nIf the
`PATCH /api/metrics/source/{sourceId}` request to update the\r\nvalue is
manually sent with an invalid value, the server will respond\r\nwith a
400 status code and an error message.\r\n\r\nAlso, for retro
compatibility and to not block the user when the\r\nconfiguration can't
be successfully retrieved, in case of internal error\r\nthe `GET
/api/metrics/source/{sourceId}` will return a 404 and on the\r\nUI,
instead of rendering a blank page, the user will see the empty
form\r\nand will be able to re-insert the right values.\r\n\r\n##
Testing\r\n\r\nNavigate to `Inventory`-> Click on `Settings` on the
topbar -> Start\r\nwriting different metric indices in the Metric
Indices field.\r\n\r\n### Editing Metric Indices
validation\r\n\r\n\r\nhttps://user-images.githubusercontent.com/34506779/203763021-0f4d8926-ffa4-448a-a038-696732158f4e.mov\r\n\r\n###
Missing/Broken configuration
response\r\n\r\n\r\nhttps://user-images.githubusercontent.com/34506779/203763120-ffc91cd3-9bf4-43da-a04f-5561ceabf591.mov\r\n\r\nCo-authored-by:
Marco Antonio Ghiani <[email protected]>\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"ddcbf73284d48b1f1f6eb9322f6f108f312f8e02"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.7.0","labelRegex":"^v8.7.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/146272","number":146272,"mergeCommit":{"message":"[Infrastructure
UI] Improve metrics settings error handling (#146272)\n\nCloses
[#145238](https://github.com/elastic/kibana/issues/145238)\r\n\r\n##
Summary\r\n\r\nThese changes add validation to the Metric Indices passed
into the\r\nMetrics settings page.\r\nNew validation is added both in
the UI and in the endpoint, performing\r\nthe following checks:\r\n-
Index pattern is not an empty string\r\n- Index pattern does not contain
empty spaces (start, middle, end) (the\r\npattern is not trimmed)\r\n-
Index pattern does not contain empty entries, comma-separated
values\r\nshould have an acceptable value.\r\n\r\nIn case the value is
not valid, the UI will render an appropriate error\r\nmessage.\r\nIf the
`PATCH /api/metrics/source/{sourceId}` request to update the\r\nvalue is
manually sent with an invalid value, the server will respond\r\nwith a
400 status code and an error message.\r\n\r\nAlso, for retro
compatibility and to not block the user when the\r\nconfiguration can't
be successfully retrieved, in case of internal error\r\nthe `GET
/api/metrics/source/{sourceId}` will return a 404 and on the\r\nUI,
instead of rendering a blank page, the user will see the empty
form\r\nand will be able to re-insert the right values.\r\n\r\n##
Testing\r\n\r\nNavigate to `Inventory`-> Click on `Settings` on the
topbar -> Start\r\nwriting different metric indices in the Metric
Indices field.\r\n\r\n### Editing Metric Indices
validation\r\n\r\n\r\nhttps://user-images.githubusercontent.com/34506779/203763021-0f4d8926-ffa4-448a-a038-696732158f4e.mov\r\n\r\n###
Missing/Broken configuration
response\r\n\r\n\r\nhttps://user-images.githubusercontent.com/34506779/203763120-ffc91cd3-9bf4-43da-a04f-5561ceabf591.mov\r\n\r\nCo-authored-by:
Marco Antonio Ghiani <[email protected]>\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"ddcbf73284d48b1f1f6eb9322f6f108f312f8e02"}}]}]
BACKPORT-->

Co-authored-by: Marco Antonio Ghiani <[email protected]>
  • Loading branch information
kibanamachine and tonyghiani authored Nov 30, 2022
1 parent c9fe5ac commit 28991d4
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 50 deletions.
2 changes: 2 additions & 0 deletions packages/kbn-io-ts-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
* Side Public License, v 1.
*/

export type { IndexPatternType } from './src/index_pattern_rt';
export type { NonEmptyStringBrand } from './src/non_empty_string_rt';

export { deepExactRt } from './src/deep_exact_rt';
export { indexPatternRt } from './src/index_pattern_rt';
export { jsonRt } from './src/json_rt';
export { mergeRt } from './src/merge_rt';
export { strictKeysRt } from './src/strict_keys_rt';
Expand Down
35 changes: 35 additions & 0 deletions packages/kbn-io-ts-utils/src/index_pattern_rt/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { indexPatternRt } from '.';
import { isRight } from 'fp-ts/lib/Either';

describe('indexPatternRt', () => {
test('passes on valid index pattern strings', () => {
expect(isRight(indexPatternRt.decode('logs-*'))).toBe(true);
expect(isRight(indexPatternRt.decode('logs-*,filebeat-*'))).toBe(true);
});

test('fails if the pattern is an empty string', () => {
expect(isRight(indexPatternRt.decode(''))).toBe(false);
});

test('fails if the pattern contains empty spaces', () => {
expect(isRight(indexPatternRt.decode(' '))).toBe(false);
expect(isRight(indexPatternRt.decode(' logs-*'))).toBe(false);
expect(isRight(indexPatternRt.decode('logs-* '))).toBe(false);
expect(isRight(indexPatternRt.decode('logs-*, filebeat-*'))).toBe(false);
});

test('fails if the pattern contains empty comma-separated entries', () => {
expect(isRight(indexPatternRt.decode(',logs-*'))).toBe(false);
expect(isRight(indexPatternRt.decode('logs-*,'))).toBe(false);
expect(isRight(indexPatternRt.decode('logs-*,,filebeat-*'))).toBe(false);
expect(isRight(indexPatternRt.decode('logs-*,,,filebeat-*'))).toBe(false);
});
});
36 changes: 36 additions & 0 deletions packages/kbn-io-ts-utils/src/index_pattern_rt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as rt from 'io-ts';

export const isEmptyString = (value: string) => value === '';

export const containsSpaces = (value: string) => value.includes(' ');

export const containsEmptyEntries = (value: string) => value.split(',').some(isEmptyString);

export const validateIndexPattern = (indexPattern: string) => {
return (
!isEmptyString(indexPattern) &&
!containsSpaces(indexPattern) &&
!containsEmptyEntries(indexPattern)
);
};

export interface IndexPatternBrand {
readonly IndexPattern: unique symbol;
}

type IndexPattern = rt.Branded<string, IndexPatternBrand>;

export const indexPatternRt = rt.brand(
rt.string,
(pattern): pattern is IndexPattern => validateIndexPattern(pattern),
'IndexPattern'
);

export type IndexPatternType = rt.TypeOf<typeof indexPatternRt>;
6 changes: 6 additions & 0 deletions x-pack/plugins/infra/common/metrics_sources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { indexPatternRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import {
SourceConfigurationRT,
Expand All @@ -28,6 +29,11 @@ export type MetricsSourceConfigurationProperties = rt.TypeOf<
typeof metricsSourceConfigurationPropertiesRT
>;

export const partialMetricsSourceConfigurationReqPayloadRT = rt.partial({
...metricsSourceConfigurationPropertiesRT.type.props,
metricAlias: indexPatternRt,
});

export const partialMetricsSourceConfigurationPropertiesRT = rt.partial({
...metricsSourceConfigurationPropertiesRT.type.props,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
*/

import { ReactNode, useCallback, useMemo, useState } from 'react';

import {
aggregateValidationErrors,
createInputFieldProps,
createInputRangeFieldProps,
validateInputFieldHasNotEmptyEntries,
validateInputFieldHasNotEmptySpaces,
validateInputFieldNotEmpty,
} from './input_fields';

Expand Down Expand Up @@ -41,7 +45,7 @@ export const useIndicesConfigurationFormState = ({
const nameFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.name),
errors: aggregateValidationErrors<string>(validateInputFieldNotEmpty)(formState.name),
name: 'name',
onChange: (name) => setFormStateChanges((changes) => ({ ...changes, name })),
value: formState.name,
Expand All @@ -51,7 +55,11 @@ export const useIndicesConfigurationFormState = ({
const metricAliasFieldProps = useMemo(
() =>
createInputFieldProps({
errors: validateInputFieldNotEmpty(formState.metricAlias),
errors: aggregateValidationErrors<string>(
validateInputFieldNotEmpty,
validateInputFieldHasNotEmptyEntries,
validateInputFieldHasNotEmptySpaces
)(formState.metricAlias),
name: 'metricAlias',
onChange: (metricAlias) => setFormStateChanges((changes) => ({ ...changes, metricAlias })),
value: formState.metricAlias,
Expand All @@ -62,7 +70,7 @@ export const useIndicesConfigurationFormState = ({
const anomalyThresholdFieldProps = useMemo(
() =>
createInputRangeFieldProps({
errors: validateInputFieldNotEmpty(formState.anomalyThreshold),
errors: aggregateValidationErrors(validateInputFieldNotEmpty)(formState.anomalyThreshold),
name: 'anomalyThreshold',
onChange: (anomalyThreshold) =>
setFormStateChanges((changes) => ({ ...changes, anomalyThreshold })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export interface InputFieldProps<

export type FieldErrorMessage = string | JSX.Element;

export type ValidationHandlerList<ValueType> = Array<
(value: ValueType) => FieldErrorMessage | false
>;

export const createInputFieldProps = <
Value extends string = string,
FieldElement extends HTMLInputElement = HTMLInputElement
Expand Down Expand Up @@ -83,12 +87,39 @@ export const createInputRangeFieldProps = <
value,
});

export const validateInputFieldNotEmpty = (value: React.ReactText) =>
value === ''
? [
<FormattedMessage
id="xpack.infra.sourceConfiguration.fieldEmptyErrorMessage"
defaultMessage="The field must not be empty"
/>,
]
: [];
export const aggregateValidationErrors =
<ValueType extends ReactText = ReactText>(
...validationHandlers: ValidationHandlerList<ValueType>
) =>
(value: ValueType) =>
validationHandlers.map((validator) => validator(value)).filter(Boolean) as FieldErrorMessage[];

const isEmptyString = (value: ReactText) => value === '';

const containsSpaces = (value: string) => value.includes(' ');

const containsEmptyEntries = (value: string) => value.split(',').some(isEmptyString);

export const validateInputFieldNotEmpty = (value: ReactText) =>
isEmptyString(value) && (
<FormattedMessage
id="xpack.infra.sourceConfiguration.fieldEmptyErrorMessage"
defaultMessage="The field must not be empty."
/>
);

export const validateInputFieldHasNotEmptyEntries = (value: string) =>
containsEmptyEntries(value) && (
<FormattedMessage
id="xpack.infra.sourceConfiguration.fieldContainEmptyEntryErrorMessage"
defaultMessage="The field must not include empty comma-separated values."
/>
);

export const validateInputFieldHasNotEmptySpaces = (value: string) =>
containsSpaces(value) && (
<FormattedMessage
id="xpack.infra.sourceConfiguration.fieldContainSpacesErrorMessage"
defaultMessage="The field must not include spaces."
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { Prompt } from '@kbn/observability-plugin/public';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useSourceContext } from '../../../containers/metrics_source';
Expand Down Expand Up @@ -75,19 +75,13 @@ export const SourceConfigurationSettings = ({
formStateChanges,
]);

const isWriteable = useMemo(
() => shouldAllowEdit && source && source.origin !== 'internal',
[shouldAllowEdit, source]
);
const isWriteable = shouldAllowEdit && (!Boolean(source) || source?.origin !== 'internal');

const { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext();

if ((isLoading || isUninitialized) && !source) {
return <SourceLoadingPage />;
}
if (!source?.configuration) {
return null;
}

return (
<MetricsPageTemplate
Expand Down
59 changes: 29 additions & 30 deletions x-pack/plugins/infra/server/routes/metrics_sources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { hasData } from '../../lib/sources/has_data';
import { createSearchClient } from '../../lib/create_search_client';
import { AnomalyThresholdRangeError } from '../../lib/sources/errors';
import {
partialMetricsSourceConfigurationPropertiesRT,
metricsSourceConfigurationResponseRT,
MetricsSourceStatus,
partialMetricsSourceConfigurationReqPayloadRT,
} from '../../../common/metrics_sources';

export const initMetricsSourceConfigurationRoutes = (libs: InfraBackendLibs) => {
Expand All @@ -35,24 +35,33 @@ export const initMetricsSourceConfigurationRoutes = (libs: InfraBackendLibs) =>
const { sourceId } = request.params;
const soClient = (await requestContext.core).savedObjects.client;

const [source, metricIndicesExist, indexFields] = await Promise.all([
libs.sources.getSourceConfiguration(soClient, sourceId),
libs.sourceStatus.hasMetricIndices(requestContext, sourceId),
libs.fields.getFields(requestContext, sourceId, 'METRICS'),
]);
try {
const [source, metricIndicesExist, indexFields] = await Promise.all([
libs.sources.getSourceConfiguration(soClient, sourceId),
libs.sourceStatus.hasMetricIndices(requestContext, sourceId),
libs.fields.getFields(requestContext, sourceId, 'METRICS'),
]);

if (!source) {
return response.notFound();
}
if (!source) {
return response.notFound();
}

const status: MetricsSourceStatus = {
metricIndicesExist,
indexFields,
};
const status: MetricsSourceStatus = {
metricIndicesExist,
indexFields,
};

return response.ok({
body: metricsSourceConfigurationResponseRT.encode({ source: { ...source, status } }),
});
return response.ok({
body: metricsSourceConfigurationResponseRT.encode({ source: { ...source, status } }),
});
} catch (error) {
return response.customError({
statusCode: error.statusCode ?? 500,
body: {
message: error.message ?? 'An unexpected error occurred',
},
});
}
}
);

Expand All @@ -64,13 +73,13 @@ export const initMetricsSourceConfigurationRoutes = (libs: InfraBackendLibs) =>
params: schema.object({
sourceId: schema.string(),
}),
body: createValidationFunction(partialMetricsSourceConfigurationPropertiesRT),
body: createValidationFunction(partialMetricsSourceConfigurationReqPayloadRT),
},
},
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
const { sources } = libs;
const { sourceId } = request.params;
const patchedSourceConfigurationProperties = request.body;
const sourceConfigurationPayload = request.body;

try {
const soClient = (await requestContext.core).savedObjects.client;
Expand All @@ -84,18 +93,8 @@ export const initMetricsSourceConfigurationRoutes = (libs: InfraBackendLibs) =>

const sourceConfigurationExists = sourceConfiguration.origin === 'stored';
const patchedSourceConfiguration = await (sourceConfigurationExists
? sources.updateSourceConfiguration(
soClient,
sourceId,
// @ts-ignore
patchedSourceConfigurationProperties
)
: sources.createSourceConfiguration(
soClient,
sourceId,
// @ts-ignore
patchedSourceConfigurationProperties
));
? sources.updateSourceConfiguration(soClient, sourceId, sourceConfigurationPayload)
: sources.createSourceConfiguration(soClient, sourceId, sourceConfigurationPayload));

const [metricIndicesExist, indexFields] = await Promise.all([
libs.sourceStatus.hasMetricIndices(requestContext, sourceId),
Expand Down

0 comments on commit 28991d4

Please sign in to comment.