Skip to content

Commit a47a4b3

Browse files
Alejandro Fernándezelasticmachine
andauthored
[7.x] [Logs UI] log rate setup index validation (#50008) (#51614)
* Scaffold API endpoint * Implement the API endpoint * Implement API client * Set error messages in `useAnalysisSetupState` * Show validation errors next to the submit button * Check for setup errors regarding the selected indexes * Call validation only once Enrich the `availableIndices` array with validation information to show it later in the form. * Ensure validation runs before showing the indices * Adjust naming conventions - Replace `index_pattern` with `indices`, since it means something different in kibana. - Group validation actions under the `validation` namespace. * Move index error messages to the `InitialConfigurationStep` * Move error messages to the UI layer * Move validation call to `useAnalysisSetupState` * Pass timestamp as a parameter of `useAnalysisSetupState` * Fix regression with the index names in the API response * Use `_field_caps` api * s/timestamp/timestampField/g * Tweak error messages * Move `ValidationIndicesUIError` to `log_analysis_setup_state` * Track validation status It's safer to rely on the state of the promise instead of treating an empty array as "loading" * Handle network errors * Use individual `<EuiCheckbox />` elements for the indices This allows to disable individual checkboxes * Pass the whole `validatedIndices` array to the inner objects This will make easier to determine which indeces have errors in the checkbox list itself and simplify the state we keep track of. * Disable indices with errors Show a tooltip above the disabled index to explain why it cannot be selected. * Pass indices to the API as an array * Show overlay while the validation loads * Wrap tooltips on a `block` element Prevents the checkboxes from collapsing on the same line * Use the right dependencies for `useEffect => validateIndices()` * Restore formatter function name * Simplify mapping of selected indices to errors * s/checked/isSelected/g * Make errors field-generic * Allow multiple errors per index * Simplify code a bit Co-authored-by: Elastic Machine <[email protected]>
1 parent 7b6349a commit a47a4b3

File tree

19 files changed

+487
-104
lines changed

19 files changed

+487
-104
lines changed

x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
*/
66

77
export * from './results';
8+
export * from './validation';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export * from './indices';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import * as rt from 'io-ts';
8+
9+
export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices';
10+
11+
/**
12+
* Request types
13+
*/
14+
export const validationIndicesRequestPayloadRT = rt.type({
15+
data: rt.type({
16+
timestampField: rt.string,
17+
indices: rt.array(rt.string),
18+
}),
19+
});
20+
21+
export type ValidationIndicesRequestPayload = rt.TypeOf<typeof validationIndicesRequestPayloadRT>;
22+
23+
/**
24+
* Response types
25+
* */
26+
export const validationIndicesErrorRT = rt.union([
27+
rt.type({
28+
error: rt.literal('INDEX_NOT_FOUND'),
29+
index: rt.string,
30+
}),
31+
rt.type({
32+
error: rt.literal('FIELD_NOT_FOUND'),
33+
index: rt.string,
34+
field: rt.string,
35+
}),
36+
rt.type({
37+
error: rt.literal('FIELD_NOT_VALID'),
38+
index: rt.string,
39+
field: rt.string,
40+
}),
41+
]);
42+
43+
export type ValidationIndicesError = rt.TypeOf<typeof validationIndicesErrorRT>;
44+
45+
export const validationIndicesResponsePayloadRT = rt.type({
46+
data: rt.type({
47+
errors: rt.array(validationIndicesErrorRT),
48+
}),
49+
});
50+
51+
export type ValidationIndicesResponsePayload = rt.TypeOf<typeof validationIndicesResponsePayloadRT>;

x-pack/legacy/plugins/infra/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
"boom": "7.3.0",
1717
"lodash": "^4.17.15"
1818
}
19-
}
19+
}

x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ const OverlayDiv = euiStyled.div`
4040
position: absolute;
4141
top: 0;
4242
width: 100%;
43+
z-index: ${props => props.theme.eui.euiZLevel1};
4344
`;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { fold } from 'fp-ts/lib/Either';
8+
import { pipe } from 'fp-ts/lib/pipeable';
9+
import { identity } from 'fp-ts/lib/function';
10+
import { kfetch } from 'ui/kfetch';
11+
12+
import {
13+
LOG_ANALYSIS_VALIDATION_INDICES_PATH,
14+
validationIndicesRequestPayloadRT,
15+
validationIndicesResponsePayloadRT,
16+
} from '../../../../../common/http_api';
17+
18+
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
19+
20+
export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => {
21+
const response = await kfetch({
22+
method: 'POST',
23+
pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH,
24+
body: JSON.stringify(
25+
validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } })
26+
),
27+
});
28+
29+
return pipe(
30+
validationIndicesResponsePayloadRT.decode(response),
31+
fold(throwErrors(createPlainError), identity)
32+
);
33+
};

x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ export const useLogAnalysisJobs = ({
9494
dispatch({ type: 'fetchingJobStatuses' });
9595
return await callJobsSummaryAPI(spaceId, sourceId);
9696
},
97-
onResolve: response => {
98-
dispatch({ type: 'fetchedJobStatuses', payload: response, spaceId, sourceId });
97+
onResolve: jobResponse => {
98+
dispatch({ type: 'fetchedJobStatuses', payload: jobResponse, spaceId, sourceId });
9999
},
100100
onReject: err => {
101101
dispatch({ type: 'failedFetchingJobStatuses' });
@@ -158,6 +158,7 @@ export const useLogAnalysisJobs = ({
158158
setup: setupMlModule,
159159
setupMlModuleRequest,
160160
setupStatus: statusState.setupStatus,
161+
timestampField: timeField,
161162
viewSetupForReconfiguration,
162163
viewSetupForUpdate,
163164
viewResults,

x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,91 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { useState, useCallback, useMemo } from 'react';
7+
import { useState, useCallback, useMemo, useEffect } from 'react';
88

99
import { isExampleDataIndex } from '../../../../common/log_analysis';
10+
import {
11+
ValidationIndicesError,
12+
ValidationIndicesResponsePayload,
13+
} from '../../../../common/http_api';
14+
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
15+
import { callIndexPatternsValidate } from './api/index_patterns_validate';
1016

1117
type SetupHandler = (
1218
indices: string[],
1319
startTime: number | undefined,
1420
endTime: number | undefined
1521
) => void;
1622

23+
export type ValidationIndicesUIError =
24+
| ValidationIndicesError
25+
| { error: 'NETWORK_ERROR' }
26+
| { error: 'TOO_FEW_SELECTED_INDICES' };
27+
28+
export interface ValidatedIndex {
29+
index: string;
30+
errors: ValidationIndicesError[];
31+
isSelected: boolean;
32+
}
33+
1734
interface AnalysisSetupStateArguments {
1835
availableIndices: string[];
1936
cleanupAndSetupModule: SetupHandler;
2037
setupModule: SetupHandler;
38+
timestampField: string;
2139
}
2240

23-
type IndicesSelection = Record<string, boolean>;
24-
25-
type ValidationErrors = 'TOO_FEW_SELECTED_INDICES';
26-
2741
const fourWeeksInMs = 86400000 * 7 * 4;
2842

2943
export const useAnalysisSetupState = ({
3044
availableIndices,
3145
cleanupAndSetupModule,
3246
setupModule,
47+
timestampField,
3348
}: AnalysisSetupStateArguments) => {
3449
const [startTime, setStartTime] = useState<number | undefined>(Date.now() - fourWeeksInMs);
3550
const [endTime, setEndTime] = useState<number | undefined>(undefined);
3651

37-
const [selectedIndices, setSelectedIndices] = useState<IndicesSelection>(
38-
availableIndices.reduce(
39-
(indexMap, indexName) => ({
40-
...indexMap,
41-
[indexName]: !(availableIndices.length > 1 && isExampleDataIndex(indexName)),
42-
}),
43-
{}
44-
)
52+
// Prepare the validation
53+
const [validatedIndices, setValidatedIndices] = useState<ValidatedIndex[]>(
54+
availableIndices.map(index => ({
55+
index,
56+
errors: [],
57+
isSelected: false,
58+
}))
59+
);
60+
const [validateIndicesRequest, validateIndices] = useTrackedPromise(
61+
{
62+
cancelPreviousOn: 'resolution',
63+
createPromise: async () => {
64+
return await callIndexPatternsValidate(timestampField, availableIndices);
65+
},
66+
onResolve: ({ data }: ValidationIndicesResponsePayload) => {
67+
setValidatedIndices(
68+
availableIndices.map(index => {
69+
const errors = data.errors.filter(error => error.index === index);
70+
return {
71+
index,
72+
errors,
73+
isSelected: errors.length === 0 && !isExampleDataIndex(index),
74+
};
75+
})
76+
);
77+
},
78+
onReject: () => {
79+
setValidatedIndices([]);
80+
},
81+
},
82+
[availableIndices, timestampField]
4583
);
4684

85+
useEffect(() => {
86+
validateIndices();
87+
}, [validateIndices]);
88+
4789
const selectedIndexNames = useMemo(
48-
() =>
49-
Object.entries(selectedIndices)
50-
.filter(([_indexName, isSelected]) => isSelected)
51-
.map(([indexName]) => indexName),
52-
[selectedIndices]
90+
() => validatedIndices.filter(i => i.isSelected).map(i => i.index),
91+
[validatedIndices]
5392
);
5493

5594
const setup = useCallback(() => {
@@ -60,24 +99,42 @@ export const useAnalysisSetupState = ({
6099
return cleanupAndSetupModule(selectedIndexNames, startTime, endTime);
61100
}, [cleanupAndSetupModule, selectedIndexNames, startTime, endTime]);
62101

63-
const validationErrors: ValidationErrors[] = useMemo(
102+
const isValidating = useMemo(
64103
() =>
65-
Object.values(selectedIndices).some(isSelected => isSelected)
66-
? []
67-
: ['TOO_FEW_SELECTED_INDICES' as const],
68-
[selectedIndices]
104+
validateIndicesRequest.state === 'pending' ||
105+
validateIndicesRequest.state === 'uninitialized',
106+
[validateIndicesRequest.state]
69107
);
70108

109+
const validationErrors = useMemo<ValidationIndicesUIError[]>(() => {
110+
if (isValidating) {
111+
return [];
112+
}
113+
114+
if (validateIndicesRequest.state === 'rejected') {
115+
return [{ error: 'NETWORK_ERROR' }];
116+
}
117+
118+
if (selectedIndexNames.length === 0) {
119+
return [{ error: 'TOO_FEW_SELECTED_INDICES' }];
120+
}
121+
122+
return validatedIndices.reduce<ValidationIndicesUIError[]>((errors, index) => {
123+
return selectedIndexNames.includes(index.index) ? errors.concat(index.errors) : errors;
124+
}, []);
125+
}, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]);
126+
71127
return {
72128
cleanupAndSetup,
73129
endTime,
130+
isValidating,
74131
selectedIndexNames,
75-
selectedIndices,
76132
setEndTime,
77-
setSelectedIndices,
78133
setStartTime,
79134
setup,
80135
startTime,
136+
validatedIndices,
137+
setValidatedIndices,
81138
validationErrors,
82139
};
83140
};

x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const AnalysisPageContent = () => {
2727
lastSetupErrorMessages,
2828
setup,
2929
setupStatus,
30+
timestampField,
3031
viewResults,
3132
} = useContext(LogAnalysisJobs.Context);
3233

@@ -61,6 +62,7 @@ export const AnalysisPageContent = () => {
6162
errorMessages={lastSetupErrorMessages}
6263
setup={setup}
6364
setupStatus={setupStatus}
65+
timestampField={timestampField}
6466
viewResults={viewResults}
6567
/>
6668
);

x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface AnalysisSetupContentProps {
3434
errorMessages: string[];
3535
setup: SetupHandler;
3636
setupStatus: SetupStatus;
37+
timestampField: string;
3738
viewResults: () => void;
3839
}
3940

@@ -43,6 +44,7 @@ export const AnalysisSetupContent: React.FunctionComponent<AnalysisSetupContentP
4344
errorMessages,
4445
setup,
4546
setupStatus,
47+
timestampField,
4648
viewResults,
4749
}) => {
4850
useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' });
@@ -82,6 +84,7 @@ export const AnalysisSetupContent: React.FunctionComponent<AnalysisSetupContentP
8284
errorMessages={errorMessages}
8385
setup={setup}
8486
setupStatus={setupStatus}
87+
timestampField={timestampField}
8588
viewResults={viewResults}
8689
/>
8790
</EuiPageContentBody>

0 commit comments

Comments
 (0)