Skip to content

Commit ddd0d1b

Browse files
authored
[7.x] [Logs UI] Add dataset filter to ML module setup screen (#64470) (#65063)
Backports the following commits to 7.x: - [Logs UI] Add dataset filter to ML module setup screen (#64470)
1 parent c51bf6a commit ddd0d1b

File tree

26 files changed

+991
-271
lines changed

26 files changed

+991
-271
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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_VALIDATE_DATASETS_PATH =
10+
'/api/infra/log_analysis/validation/log_entry_datasets';
11+
12+
/**
13+
* Request types
14+
*/
15+
export const validateLogEntryDatasetsRequestPayloadRT = rt.type({
16+
data: rt.type({
17+
indices: rt.array(rt.string),
18+
timestampField: rt.string,
19+
startTime: rt.number,
20+
endTime: rt.number,
21+
}),
22+
});
23+
24+
export type ValidateLogEntryDatasetsRequestPayload = rt.TypeOf<
25+
typeof validateLogEntryDatasetsRequestPayloadRT
26+
>;
27+
28+
/**
29+
* Response types
30+
* */
31+
const logEntryDatasetsEntryRT = rt.strict({
32+
indexName: rt.string,
33+
datasets: rt.array(rt.string),
34+
});
35+
36+
export const validateLogEntryDatasetsResponsePayloadRT = rt.type({
37+
data: rt.type({
38+
datasets: rt.array(logEntryDatasetsEntryRT),
39+
}),
40+
});
41+
42+
export type ValidateLogEntryDatasetsResponsePayload = rt.TypeOf<
43+
typeof validateLogEntryDatasetsResponsePayloadRT
44+
>;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
export * from './datasets';
78
export * from './log_entry_rate_indices';

x-pack/plugins/infra/common/log_analysis/job_parameters.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,73 @@ export const getJobId = (spaceId: string, sourceId: string, jobType: string) =>
2121
export const getDatafeedId = (spaceId: string, sourceId: string, jobType: string) =>
2222
`datafeed-${getJobId(spaceId, sourceId, jobType)}`;
2323

24-
export const jobSourceConfigurationRT = rt.type({
24+
export const datasetFilterRT = rt.union([
25+
rt.strict({
26+
type: rt.literal('includeAll'),
27+
}),
28+
rt.strict({
29+
type: rt.literal('includeSome'),
30+
datasets: rt.array(rt.string),
31+
}),
32+
]);
33+
34+
export type DatasetFilter = rt.TypeOf<typeof datasetFilterRT>;
35+
36+
export const jobSourceConfigurationRT = rt.partial({
2537
indexPattern: rt.string,
2638
timestampField: rt.string,
2739
bucketSpan: rt.number,
40+
datasetFilter: datasetFilterRT,
2841
});
2942

3043
export type JobSourceConfiguration = rt.TypeOf<typeof jobSourceConfigurationRT>;
3144

3245
export const jobCustomSettingsRT = rt.partial({
3346
job_revision: rt.number,
34-
logs_source_config: rt.partial(jobSourceConfigurationRT.props),
47+
logs_source_config: jobSourceConfigurationRT,
3548
});
3649

3750
export type JobCustomSettings = rt.TypeOf<typeof jobCustomSettingsRT>;
51+
52+
export const combineDatasetFilters = (
53+
firstFilter: DatasetFilter,
54+
secondFilter: DatasetFilter
55+
): DatasetFilter => {
56+
if (firstFilter.type === 'includeAll' && secondFilter.type === 'includeAll') {
57+
return {
58+
type: 'includeAll',
59+
};
60+
}
61+
62+
const includedDatasets = new Set([
63+
...(firstFilter.type === 'includeSome' ? firstFilter.datasets : []),
64+
...(secondFilter.type === 'includeSome' ? secondFilter.datasets : []),
65+
]);
66+
67+
return {
68+
type: 'includeSome',
69+
datasets: [...includedDatasets],
70+
};
71+
};
72+
73+
export const filterDatasetFilter = (
74+
datasetFilter: DatasetFilter,
75+
predicate: (dataset: string) => boolean
76+
): DatasetFilter => {
77+
if (datasetFilter.type === 'includeAll') {
78+
return datasetFilter;
79+
} else {
80+
const newDatasets = datasetFilter.datasets.filter(predicate);
81+
82+
if (newDatasets.length > 0) {
83+
return {
84+
type: 'includeSome',
85+
datasets: newDatasets,
86+
};
87+
} else {
88+
return {
89+
type: 'includeAll',
90+
};
91+
}
92+
}
93+
};

x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx

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

7-
import { EuiCode, EuiDescribedFormGroup, EuiFormRow, EuiCheckbox, EuiToolTip } from '@elastic/eui';
7+
import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
88
import { i18n } from '@kbn/i18n';
99
import { FormattedMessage } from '@kbn/i18n/react';
10-
import React, { useCallback, useMemo } from 'react';
11-
10+
import React, { useCallback } from 'react';
1211
import { LoadingOverlayWrapper } from '../../../loading_overlay_wrapper';
13-
import { ValidatedIndex, ValidationIndicesUIError } from './validation';
12+
import { IndexSetupRow } from './index_setup_row';
13+
import { AvailableIndex } from './validation';
1414

1515
export const AnalysisSetupIndicesForm: React.FunctionComponent<{
1616
disabled?: boolean;
17-
indices: ValidatedIndex[];
17+
indices: AvailableIndex[];
1818
isValidating: boolean;
19-
onChangeSelectedIndices: (selectedIndices: ValidatedIndex[]) => void;
19+
onChangeSelectedIndices: (selectedIndices: AvailableIndex[]) => void;
2020
valid: boolean;
2121
}> = ({ disabled = false, indices, isValidating, onChangeSelectedIndices, valid }) => {
22-
const handleCheckboxChange = useCallback(
23-
(event: React.ChangeEvent<HTMLInputElement>) => {
22+
const changeIsIndexSelected = useCallback(
23+
(indexName: string, isSelected: boolean) => {
2424
onChangeSelectedIndices(
2525
indices.map(index => {
26-
const checkbox = event.currentTarget;
27-
return index.name === checkbox.id ? { ...index, isSelected: checkbox.checked } : index;
26+
return index.name === indexName ? { ...index, isSelected } : index;
2827
})
2928
);
3029
},
3130
[indices, onChangeSelectedIndices]
3231
);
3332

34-
const choices = useMemo(
35-
() =>
36-
indices.map(index => {
37-
const checkbox = (
38-
<EuiCheckbox
39-
key={index.name}
40-
id={index.name}
41-
label={<EuiCode>{index.name}</EuiCode>}
42-
onChange={handleCheckboxChange}
43-
checked={index.validity === 'valid' && index.isSelected}
44-
disabled={disabled || index.validity === 'invalid'}
45-
/>
46-
);
47-
48-
return index.validity === 'valid' ? (
49-
checkbox
50-
) : (
51-
<div key={index.name}>
52-
<EuiToolTip content={formatValidationError(index.errors)}>{checkbox}</EuiToolTip>
53-
</div>
54-
);
55-
}),
56-
[disabled, handleCheckboxChange, indices]
33+
const changeDatasetFilter = useCallback(
34+
(indexName: string, datasetFilter) => {
35+
onChangeSelectedIndices(
36+
indices.map(index => {
37+
return index.name === indexName ? { ...index, datasetFilter } : index;
38+
})
39+
);
40+
},
41+
[indices, onChangeSelectedIndices]
5742
);
5843

5944
return (
@@ -69,13 +54,23 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{
6954
description={
7055
<FormattedMessage
7156
id="xpack.infra.analysisSetup.indicesSelectionDescription"
72-
defaultMessage="By default, Machine Learning analyzes log messages in all log indices configured for the source. You can choose to only analyze a subset of the index names. Every selected index name must match at least one index with log entries."
57+
defaultMessage="By default, Machine Learning analyzes log messages in all log indices configured for the source. You can choose to only analyze a subset of the index names. Every selected index name must match at least one index with log entries. You can also choose to only include a certain subset of datasets. Note that the dataset filter applies to all selected indices."
7358
/>
7459
}
7560
>
7661
<LoadingOverlayWrapper isLoading={isValidating}>
7762
<EuiFormRow fullWidth isInvalid={!valid} label={indicesSelectionLabel} labelType="legend">
78-
<>{choices}</>
63+
<>
64+
{indices.map(index => (
65+
<IndexSetupRow
66+
index={index}
67+
isDisabled={disabled}
68+
key={index.name}
69+
onChangeIsSelected={changeIsIndexSelected}
70+
onChangeDatasetFilter={changeDatasetFilter}
71+
/>
72+
))}
73+
</>
7974
</EuiFormRow>
8075
</LoadingOverlayWrapper>
8176
</EuiDescribedFormGroup>
@@ -85,51 +80,3 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{
8580
const indicesSelectionLabel = i18n.translate('xpack.infra.analysisSetup.indicesSelectionLabel', {
8681
defaultMessage: 'Indices',
8782
});
88-
89-
const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => {
90-
return errors.map(error => {
91-
switch (error.error) {
92-
case 'INDEX_NOT_FOUND':
93-
return (
94-
<p key={`${error.error}-${error.index}`}>
95-
<FormattedMessage
96-
id="xpack.infra.analysisSetup.indicesSelectionIndexNotFound"
97-
defaultMessage="No indices match the pattern {index}"
98-
values={{ index: <EuiCode>{error.index}</EuiCode> }}
99-
/>
100-
</p>
101-
);
102-
103-
case 'FIELD_NOT_FOUND':
104-
return (
105-
<p key={`${error.error}-${error.index}-${error.field}`}>
106-
<FormattedMessage
107-
id="xpack.infra.analysisSetup.indicesSelectionNoTimestampField"
108-
defaultMessage="At least one index matching {index} lacks a required field {field}."
109-
values={{
110-
index: <EuiCode>{error.index}</EuiCode>,
111-
field: <EuiCode>{error.field}</EuiCode>,
112-
}}
113-
/>
114-
</p>
115-
);
116-
117-
case 'FIELD_NOT_VALID':
118-
return (
119-
<p key={`${error.error}-${error.index}-${error.field}`}>
120-
<FormattedMessage
121-
id="xpack.infra.analysisSetup.indicesSelectionTimestampNotValid"
122-
defaultMessage="At least one index matching {index} has a field called {field} without the correct type."
123-
values={{
124-
index: <EuiCode>{error.index}</EuiCode>,
125-
field: <EuiCode>{error.field}</EuiCode>,
126-
}}
127-
/>
128-
</p>
129-
);
130-
131-
default:
132-
return '';
133-
}
134-
});
135-
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 {
8+
EuiFilterButton,
9+
EuiFilterGroup,
10+
EuiPopover,
11+
EuiPopoverTitle,
12+
EuiSelectable,
13+
EuiSelectableOption,
14+
} from '@elastic/eui';
15+
import { FormattedMessage } from '@kbn/i18n/react';
16+
import React, { useCallback, useMemo } from 'react';
17+
import { DatasetFilter } from '../../../../../common/log_analysis';
18+
import { useVisibilityState } from '../../../../utils/use_visibility_state';
19+
20+
export const IndexSetupDatasetFilter: React.FC<{
21+
availableDatasets: string[];
22+
datasetFilter: DatasetFilter;
23+
isDisabled?: boolean;
24+
onChangeDatasetFilter: (datasetFilter: DatasetFilter) => void;
25+
}> = ({ availableDatasets, datasetFilter, isDisabled, onChangeDatasetFilter }) => {
26+
const { isVisible, hide, show } = useVisibilityState(false);
27+
28+
const changeDatasetFilter = useCallback(
29+
(options: EuiSelectableOption[]) => {
30+
const selectedDatasets = options
31+
.filter(({ checked }) => checked === 'on')
32+
.map(({ label }) => label);
33+
34+
onChangeDatasetFilter(
35+
selectedDatasets.length === 0
36+
? { type: 'includeAll' }
37+
: { type: 'includeSome', datasets: selectedDatasets }
38+
);
39+
},
40+
[onChangeDatasetFilter]
41+
);
42+
43+
const selectableOptions: EuiSelectableOption[] = useMemo(
44+
() =>
45+
availableDatasets.map(datasetName => ({
46+
label: datasetName,
47+
checked:
48+
datasetFilter.type === 'includeSome' && datasetFilter.datasets.includes(datasetName)
49+
? 'on'
50+
: undefined,
51+
})),
52+
[availableDatasets, datasetFilter]
53+
);
54+
55+
const datasetFilterButton = (
56+
<EuiFilterButton disabled={isDisabled} isSelected={isVisible} onClick={show}>
57+
<FormattedMessage
58+
id="xpack.infra.analysisSetup.indexDatasetFilterIncludeAllButtonLabel"
59+
defaultMessage="{includeType, select, includeAll {All datasets} includeSome {{includedDatasetCount, plural, one {# dataset} other {# datasets}}}}"
60+
values={{
61+
includeType: datasetFilter.type,
62+
includedDatasetCount:
63+
datasetFilter.type === 'includeSome' ? datasetFilter.datasets.length : 0,
64+
}}
65+
/>
66+
</EuiFilterButton>
67+
);
68+
69+
return (
70+
<EuiFilterGroup>
71+
<EuiPopover
72+
button={datasetFilterButton}
73+
closePopover={hide}
74+
isOpen={isVisible}
75+
panelPaddingSize="none"
76+
>
77+
<EuiSelectable onChange={changeDatasetFilter} options={selectableOptions} searchable>
78+
{(list, search) => (
79+
<div>
80+
<EuiPopoverTitle>{search}</EuiPopoverTitle>
81+
{list}
82+
</div>
83+
)}
84+
</EuiSelectable>
85+
</EuiPopover>
86+
</EuiFilterGroup>
87+
);
88+
};

0 commit comments

Comments
 (0)