Revert "[Metrics UI] Add ability to override datafeeds and job config for partition field (#78875)"

This reverts commit ee7672aaf0.
This commit is contained in:
Jonathan Budzenski 2020-10-01 12:42:37 -05:00
parent fd7dd41617
commit 085f8a17ff
7 changed files with 444 additions and 247 deletions

View file

@ -33,11 +33,11 @@ export interface ModuleDescriptor<JobType extends string> {
partitionField?: string partitionField?: string
) => Promise<SetupMlModuleResponsePayload>; ) => Promise<SetupMlModuleResponsePayload>;
cleanUpModule: (spaceId: string, sourceId: string) => Promise<DeleteJobsResponsePayload>; cleanUpModule: (spaceId: string, sourceId: string) => Promise<DeleteJobsResponsePayload>;
validateSetupIndices?: ( validateSetupIndices: (
indices: string[], indices: string[],
timestampField: string timestampField: string
) => Promise<ValidationIndicesResponsePayload>; ) => Promise<ValidationIndicesResponsePayload>;
validateSetupDatasets?: ( validateSetupDatasets: (
indices: string[], indices: string[],
timestampField: string, timestampField: string,
startTime: number, startTime: number,

View file

@ -0,0 +1,289 @@
/*
* 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 { isEqual } from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { usePrevious } from 'react-use';
import {
combineDatasetFilters,
DatasetFilter,
filterDatasetFilter,
isExampleDataIndex,
} from '../../../common/infra_ml';
import {
AvailableIndex,
ValidationIndicesError,
ValidationUIError,
} from '../../components/logging/log_analysis_setup/initial_configuration_step';
import { useTrackedPromise } from '../../utils/use_tracked_promise';
import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types';
type SetupHandler = (
indices: string[],
startTime: number | undefined,
endTime: number | undefined,
datasetFilter: DatasetFilter
) => void;
interface AnalysisSetupStateArguments<JobType extends string> {
cleanUpAndSetUpModule: SetupHandler;
moduleDescriptor: ModuleDescriptor<JobType>;
setUpModule: SetupHandler;
sourceConfiguration: ModuleSourceConfiguration;
}
const fourWeeksInMs = 86400000 * 7 * 4;
export const useAnalysisSetupState = <JobType extends string>({
cleanUpAndSetUpModule,
moduleDescriptor: { validateSetupDatasets, validateSetupIndices },
setUpModule,
sourceConfiguration,
}: AnalysisSetupStateArguments<JobType>) => {
const [startTime, setStartTime] = useState<number | undefined>(Date.now() - fourWeeksInMs);
const [endTime, setEndTime] = useState<number | undefined>(undefined);
const isTimeRangeValid = useMemo(
() => (startTime != null && endTime != null ? startTime < endTime : true),
[endTime, startTime]
);
const [validatedIndices, setValidatedIndices] = useState<AvailableIndex[]>(
sourceConfiguration.indices.map((indexName) => ({
name: indexName,
validity: 'unknown' as const,
}))
);
const updateIndicesWithValidationErrors = useCallback(
(validationErrors: ValidationIndicesError[]) =>
setValidatedIndices((availableIndices) =>
availableIndices.map((previousAvailableIndex) => {
const indexValiationErrors = validationErrors.filter(
({ index }) => index === previousAvailableIndex.name
);
if (indexValiationErrors.length > 0) {
return {
validity: 'invalid',
name: previousAvailableIndex.name,
errors: indexValiationErrors,
};
} else if (previousAvailableIndex.validity === 'valid') {
return {
...previousAvailableIndex,
validity: 'valid',
errors: [],
};
} else {
return {
validity: 'valid',
name: previousAvailableIndex.name,
isSelected: !isExampleDataIndex(previousAvailableIndex.name),
availableDatasets: [],
datasetFilter: {
type: 'includeAll' as const,
},
};
}
})
),
[]
);
const updateIndicesWithAvailableDatasets = useCallback(
(availableDatasets: Array<{ indexName: string; datasets: string[] }>) =>
setValidatedIndices((availableIndices) =>
availableIndices.map((previousAvailableIndex) => {
if (previousAvailableIndex.validity !== 'valid') {
return previousAvailableIndex;
}
const availableDatasetsForIndex = availableDatasets.filter(
({ indexName }) => indexName === previousAvailableIndex.name
);
const newAvailableDatasets = availableDatasetsForIndex.flatMap(
({ datasets }) => datasets
);
// filter out datasets that have disappeared if this index' datasets were updated
const newDatasetFilter: DatasetFilter =
availableDatasetsForIndex.length > 0
? filterDatasetFilter(previousAvailableIndex.datasetFilter, (dataset) =>
newAvailableDatasets.includes(dataset)
)
: previousAvailableIndex.datasetFilter;
return {
...previousAvailableIndex,
availableDatasets: newAvailableDatasets,
datasetFilter: newDatasetFilter,
};
})
),
[]
);
const validIndexNames = useMemo(
() => validatedIndices.filter((index) => index.validity === 'valid').map((index) => index.name),
[validatedIndices]
);
const selectedIndexNames = useMemo(
() =>
validatedIndices
.filter((index) => index.validity === 'valid' && index.isSelected)
.map((i) => i.name),
[validatedIndices]
);
const datasetFilter = useMemo(
() =>
validatedIndices
.flatMap((validatedIndex) =>
validatedIndex.validity === 'valid'
? validatedIndex.datasetFilter
: { type: 'includeAll' as const }
)
.reduce(combineDatasetFilters, { type: 'includeAll' as const }),
[validatedIndices]
);
const [validateIndicesRequest, validateIndices] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
return await validateSetupIndices(
sourceConfiguration.indices,
sourceConfiguration.timestampField
);
},
onResolve: ({ data: { errors } }) => {
updateIndicesWithValidationErrors(errors);
},
onReject: () => {
setValidatedIndices([]);
},
},
[sourceConfiguration.indices, sourceConfiguration.timestampField]
);
const [validateDatasetsRequest, validateDatasets] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
if (validIndexNames.length === 0) {
return { data: { datasets: [] } };
}
return await validateSetupDatasets(
validIndexNames,
sourceConfiguration.timestampField,
startTime ?? 0,
endTime ?? Date.now()
);
},
onResolve: ({ data: { datasets } }) => {
updateIndicesWithAvailableDatasets(datasets);
},
},
[validIndexNames, sourceConfiguration.timestampField, startTime, endTime]
);
const setUp = useCallback(() => {
return setUpModule(selectedIndexNames, startTime, endTime, datasetFilter);
}, [setUpModule, selectedIndexNames, startTime, endTime, datasetFilter]);
const cleanUpAndSetUp = useCallback(() => {
return cleanUpAndSetUpModule(selectedIndexNames, startTime, endTime, datasetFilter);
}, [cleanUpAndSetUpModule, selectedIndexNames, startTime, endTime, datasetFilter]);
const isValidating = useMemo(
() => validateIndicesRequest.state === 'pending' || validateDatasetsRequest.state === 'pending',
[validateDatasetsRequest.state, validateIndicesRequest.state]
);
const validationErrors = useMemo<ValidationUIError[]>(() => {
if (isValidating) {
return [];
}
return [
// validate request status
...(validateIndicesRequest.state === 'rejected' ||
validateDatasetsRequest.state === 'rejected'
? [{ error: 'NETWORK_ERROR' as const }]
: []),
// validation request results
...validatedIndices.reduce<ValidationUIError[]>((errors, index) => {
return index.validity === 'invalid' && selectedIndexNames.includes(index.name)
? [...errors, ...index.errors]
: errors;
}, []),
// index count
...(selectedIndexNames.length === 0 ? [{ error: 'TOO_FEW_SELECTED_INDICES' as const }] : []),
// time range
...(!isTimeRangeValid ? [{ error: 'INVALID_TIME_RANGE' as const }] : []),
];
}, [
isValidating,
validateIndicesRequest.state,
validateDatasetsRequest.state,
validatedIndices,
selectedIndexNames,
isTimeRangeValid,
]);
const prevStartTime = usePrevious(startTime);
const prevEndTime = usePrevious(endTime);
const prevValidIndexNames = usePrevious(validIndexNames);
useEffect(() => {
if (!isTimeRangeValid) {
return;
}
validateIndices();
}, [isTimeRangeValid, validateIndices]);
useEffect(() => {
if (!isTimeRangeValid) {
return;
}
if (
startTime !== prevStartTime ||
endTime !== prevEndTime ||
!isEqual(validIndexNames, prevValidIndexNames)
) {
validateDatasets();
}
}, [
endTime,
isTimeRangeValid,
prevEndTime,
prevStartTime,
prevValidIndexNames,
startTime,
validIndexNames,
validateDatasets,
]);
return {
cleanUpAndSetUp,
datasetFilter,
endTime,
isValidating,
selectedIndexNames,
setEndTime,
setStartTime,
setUp,
startTime,
validatedIndices,
setValidatedIndices,
validationErrors,
};
};

View file

@ -10,27 +10,17 @@ import { cleanUpJobsAndDatafeeds } from '../../infra_ml_cleanup';
import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api'; import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api';
import { callGetMlModuleAPI } from '../../api/ml_get_module'; import { callGetMlModuleAPI } from '../../api/ml_get_module';
import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api'; import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api';
import { callValidateIndicesAPI } from '../../../logs/log_analysis/api/validate_indices';
import { callValidateDatasetsAPI } from '../../../logs/log_analysis/api/validate_datasets';
import { import {
metricsHostsJobTypes, metricsHostsJobTypes,
getJobId, getJobId,
MetricsHostsJobType, MetricsHostsJobType,
DatasetFilter, DatasetFilter,
bucketSpan, bucketSpan,
partitionField,
} from '../../../../../common/infra_ml'; } from '../../../../../common/infra_ml';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import MemoryJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import MemoryDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_memory_usage.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import NetworkInJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import NetworkInDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_in.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import NetworkOutJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import NetworkOutDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_out.json';
type JobType = 'hosts_memory_usage' | 'hosts_network_in' | 'hosts_network_out';
const moduleId = 'metrics_ui_hosts'; const moduleId = 'metrics_ui_hosts';
const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', { const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', {
defaultMessage: 'Metrics anomanly detection', defaultMessage: 'Metrics anomanly detection',
@ -64,68 +54,23 @@ const setUpModule = async (
end: number | undefined, end: number | undefined,
datasetFilter: DatasetFilter, datasetFilter: DatasetFilter,
{ spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration, { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration,
partitionField?: string pField?: string
) => { ) => {
const indexNamePattern = indices.join(','); const indexNamePattern = indices.join(',');
const jobIds: JobType[] = ['hosts_memory_usage', 'hosts_network_in', 'hosts_network_out']; const jobIds = ['hosts_memory_usage', 'hosts_network_in', 'hosts_network_out'];
const jobOverrides = jobIds.map((id) => ({
const jobOverrides = jobIds.map((id) => { job_id: id,
const { job: defaultJobConfig } = getDefaultJobConfigs(id); data_description: {
time_field: timestampField,
// eslint-disable-next-line @typescript-eslint/naming-convention },
const analysis_config = { custom_settings: {
...defaultJobConfig.analysis_config, metrics_source_config: {
}; indexPattern: indexNamePattern,
timestampField,
if (partitionField) { bucketSpan,
analysis_config.detectors[0].partition_field_name = partitionField;
if (analysis_config.influencers.indexOf(partitionField) === -1) {
analysis_config.influencers.push(partitionField);
}
}
return {
job_id: id,
data_description: {
time_field: timestampField,
}, },
analysis_config, },
custom_settings: { }));
metrics_source_config: {
indexPattern: indexNamePattern,
timestampField,
bucketSpan,
},
},
};
});
const datafeedOverrides = jobIds.map((id) => {
const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id);
if (!partitionField || id === 'hosts_memory_usage') {
// Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field
return defaultDatafeedConfig;
}
// If we have a partition field, we need to change the aggregation to do a terms agg at the top level
const aggregations = {
[partitionField]: {
terms: {
field: partitionField,
},
aggregations: {
...defaultDatafeedConfig.aggregations,
},
},
};
return {
...defaultDatafeedConfig,
job_id: id,
aggregations,
};
});
return callSetupMlModuleAPI( return callSetupMlModuleAPI(
moduleId, moduleId,
@ -135,34 +80,36 @@ const setUpModule = async (
sourceId, sourceId,
indexNamePattern, indexNamePattern,
jobOverrides, jobOverrides,
datafeedOverrides []
); );
}; };
const getDefaultJobConfigs = (jobId: JobType) => {
switch (jobId) {
case 'hosts_memory_usage':
return {
datafeed: MemoryDatafeed,
job: MemoryJob,
};
case 'hosts_network_in':
return {
datafeed: NetworkInDatafeed,
job: NetworkInJob,
};
case 'hosts_network_out':
return {
datafeed: NetworkOutDatafeed,
job: NetworkOutJob,
};
}
};
const cleanUpModule = async (spaceId: string, sourceId: string) => { const cleanUpModule = async (spaceId: string, sourceId: string) => {
return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsHostsJobTypes); return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsHostsJobTypes);
}; };
const validateSetupIndices = async (indices: string[], timestampField: string) => {
return await callValidateIndicesAPI(indices, [
{
name: timestampField,
validTypes: ['date'],
},
{
name: partitionField,
validTypes: ['keyword'],
},
]);
};
const validateSetupDatasets = async (
indices: string[],
timestampField: string,
startTime: number,
endTime: number
) => {
return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime);
};
export const metricHostsModule: ModuleDescriptor<MetricsHostsJobType> = { export const metricHostsModule: ModuleDescriptor<MetricsHostsJobType> = {
moduleId, moduleId,
moduleName, moduleName,
@ -174,4 +121,6 @@ export const metricHostsModule: ModuleDescriptor<MetricsHostsJobType> = {
getModuleDefinition, getModuleDefinition,
setUpModule, setUpModule,
cleanUpModule, cleanUpModule,
validateSetupDatasets,
validateSetupIndices,
}; };

View file

@ -10,28 +10,17 @@ import { cleanUpJobsAndDatafeeds } from '../../infra_ml_cleanup';
import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api'; import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api';
import { callGetMlModuleAPI } from '../../api/ml_get_module'; import { callGetMlModuleAPI } from '../../api/ml_get_module';
import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api'; import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api';
import { callValidateIndicesAPI } from '../../../logs/log_analysis/api/validate_indices';
import { callValidateDatasetsAPI } from '../../../logs/log_analysis/api/validate_datasets';
import { import {
metricsK8SJobTypes, metricsK8SJobTypes,
getJobId, getJobId,
MetricK8sJobType, MetricK8sJobType,
DatasetFilter, DatasetFilter,
bucketSpan, bucketSpan,
partitionField,
} from '../../../../../common/infra_ml'; } from '../../../../../common/infra_ml';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import MemoryJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import MemoryDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_memory_usage.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import NetworkInJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import NetworkInDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_in.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import NetworkOutJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import NetworkOutDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_out.json';
type JobType = 'k8s_memory_usage' | 'k8s_network_in' | 'k8s_network_out';
export const DEFAULT_K8S_PARTITION_FIELD = 'kubernetes.namespace';
const moduleId = 'metrics_ui_k8s'; const moduleId = 'metrics_ui_k8s';
const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', { const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', {
defaultMessage: 'Metrics anomanly detection', defaultMessage: 'Metrics anomanly detection',
@ -65,72 +54,26 @@ const setUpModule = async (
end: number | undefined, end: number | undefined,
datasetFilter: DatasetFilter, datasetFilter: DatasetFilter,
{ spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration, { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration,
partitionField?: string pField?: string
) => { ) => {
const indexNamePattern = indices.join(','); const indexNamePattern = indices.join(',');
const jobIds: JobType[] = ['k8s_memory_usage', 'k8s_network_in', 'k8s_network_out']; const jobIds = ['k8s_memory_usage', 'k8s_network_in', 'k8s_network_out'];
const jobOverrides = jobIds.map((id) => { const jobOverrides = jobIds.map((id) => ({
const { job: defaultJobConfig } = getDefaultJobConfigs(id); job_id: id,
analysis_config: {
// eslint-disable-next-line @typescript-eslint/naming-convention bucket_span: `${bucketSpan}ms`,
const analysis_config = { },
...defaultJobConfig.analysis_config, data_description: {
}; time_field: timestampField,
},
if (partitionField) { custom_settings: {
analysis_config.detectors[0].partition_field_name = partitionField; metrics_source_config: {
if (analysis_config.influencers.indexOf(partitionField) === -1) { indexPattern: indexNamePattern,
analysis_config.influencers.push(partitionField); timestampField,
} bucketSpan,
}
return {
job_id: id,
data_description: {
time_field: timestampField,
}, },
analysis_config, },
custom_settings: { }));
metrics_source_config: {
indexPattern: indexNamePattern,
timestampField,
bucketSpan,
},
},
};
});
const datafeedOverrides = jobIds.map((id) => {
const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id);
if (!partitionField || id === 'k8s_memory_usage') {
// Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field
return defaultDatafeedConfig;
}
// Because the ML K8s jobs ship with a default partition field of {kubernetes.namespace}, ignore that agg and wrap it in our own agg.
const innerAggregation =
defaultDatafeedConfig.aggregations[DEFAULT_K8S_PARTITION_FIELD].aggregations;
// If we have a partition field, we need to change the aggregation to do a terms agg to partition the data at the top level
const aggregations = {
[partitionField]: {
terms: {
field: partitionField,
size: 25, // 25 is arbitratry and only used to keep the number of buckets to a managable level in the event that the user choose a high cardinality partition field.
},
aggregations: {
...innerAggregation,
},
},
};
return {
...defaultDatafeedConfig,
job_id: id,
aggregations,
};
});
return callSetupMlModuleAPI( return callSetupMlModuleAPI(
moduleId, moduleId,
@ -140,34 +83,36 @@ const setUpModule = async (
sourceId, sourceId,
indexNamePattern, indexNamePattern,
jobOverrides, jobOverrides,
datafeedOverrides []
); );
}; };
const getDefaultJobConfigs = (jobId: JobType) => {
switch (jobId) {
case 'k8s_memory_usage':
return {
datafeed: MemoryDatafeed,
job: MemoryJob,
};
case 'k8s_network_in':
return {
datafeed: NetworkInDatafeed,
job: NetworkInJob,
};
case 'k8s_network_out':
return {
datafeed: NetworkOutDatafeed,
job: NetworkOutJob,
};
}
};
const cleanUpModule = async (spaceId: string, sourceId: string) => { const cleanUpModule = async (spaceId: string, sourceId: string) => {
return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsK8SJobTypes); return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsK8SJobTypes);
}; };
const validateSetupIndices = async (indices: string[], timestampField: string) => {
return await callValidateIndicesAPI(indices, [
{
name: timestampField,
validTypes: ['date'],
},
{
name: partitionField,
validTypes: ['keyword'],
},
]);
};
const validateSetupDatasets = async (
indices: string[],
timestampField: string,
startTime: number,
endTime: number
) => {
return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime);
};
export const metricHostsModule: ModuleDescriptor<MetricK8sJobType> = { export const metricHostsModule: ModuleDescriptor<MetricK8sJobType> = {
moduleId, moduleId,
moduleName, moduleName,
@ -179,4 +124,6 @@ export const metricHostsModule: ModuleDescriptor<MetricK8sJobType> = {
getModuleDefinition, getModuleDefinition,
setUpModule, setUpModule,
cleanUpModule, cleanUpModule,
validateSetupDatasets,
validateSetupIndices,
}; };

View file

@ -50,10 +50,10 @@ export const AnomalyDetectionFlyout = () => {
return ( return (
<> <>
<EuiButtonEmpty iconSide={'left'} iconType={'inspect'} onClick={openFlyout}> <EuiButtonEmpty iconSide={'right'} onClick={openFlyout}>
<FormattedMessage <FormattedMessage
id="xpack.infra.ml.anomalyDetectionButton" id="xpack.infra.ml.anomalyDetectionButton"
defaultMessage="Anomaly detection" defaultMessage="Anomaly Detection"
/> />
</EuiButtonEmpty> </EuiButtonEmpty>
{showFlyout && ( {showFlyout && (

View file

@ -5,7 +5,7 @@
*/ */
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiSpacer } from '@elastic/eui'; import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -30,7 +30,7 @@ interface Props {
} }
export const FlyoutHome = (props: Props) => { export const FlyoutHome = (props: Props) => {
const [tab] = useState<'jobs' | 'anomalies'>('jobs'); const [tab, setTab] = useState<'jobs' | 'anomalies'>('jobs');
const { goToSetup } = props; const { goToSetup } = props;
const { const {
fetchJobStatus: fetchHostJobStatus, fetchJobStatus: fetchHostJobStatus,
@ -56,10 +56,18 @@ export const FlyoutHome = (props: Props) => {
goToSetup('kubernetes'); goToSetup('kubernetes');
}, [goToSetup]); }, [goToSetup]);
const goToJobs = useCallback(() => {
setTab('jobs');
}, []);
const jobIds = [ const jobIds = [
...(k8sJobSummaries || []).map((k) => k.id), ...(k8sJobSummaries || []).map((k) => k.id),
...(hostJobSummaries || []).map((h) => h.id), ...(hostJobSummaries || []).map((h) => h.id),
]; ];
const anomaliesUrl = useLinkProps({
app: 'ml',
pathname: `/explorer?_g=${createResultsUrl(jobIds)}`,
});
useEffect(() => { useEffect(() => {
if (hasInfraMLReadCapabilities) { if (hasInfraMLReadCapabilities) {
@ -97,24 +105,30 @@ export const FlyoutHome = (props: Props) => {
</EuiFlyoutHeader> </EuiFlyoutHeader>
<EuiFlyoutBody> <EuiFlyoutBody>
<div> <EuiTabs>
<EuiText> <EuiTab isSelected={tab === 'jobs'} onClick={goToJobs}>
<p> <FormattedMessage
<FormattedMessage defaultMessage="Jobs"
defaultMessage="Anomaly detection is powered by machine learning. Machine learning jobs are available for the following resource types. Enable these jobs to begin detecting anomalies in your infrastructure metrics." id="xpack.infra.ml.anomalyFlyout.jobsTabLabel"
id="xpack.infra.ml.anomalyFlyout.create.description" />
/> </EuiTab>
</p> <EuiTab
</EuiText> disabled={jobIds.length === 0}
</div> isSelected={tab === 'anomalies'}
{...anomaliesUrl}
>
<FormattedMessage
defaultMessage="Anomalies"
id="xpack.infra.ml.anomalyFlyout.anomaliesTabLabel"
/>
</EuiTab>
</EuiTabs>
<EuiSpacer size="l" /> <EuiSpacer size="l" />
{hostJobSummaries.length > 0 && ( {hostJobSummaries.length > 0 && (
<> <>
<JobsEnabledCallout <JobsEnabledCallout
hasHostJobs={hostJobSummaries.length > 0} hasHostJobs={hostJobSummaries.length > 0}
hasK8sJobs={k8sJobSummaries.length > 0} hasK8sJobs={k8sJobSummaries.length > 0}
jobIds={jobIds}
/> />
<EuiSpacer size="l" /> <EuiSpacer size="l" />
</> </>
@ -137,7 +151,6 @@ export const FlyoutHome = (props: Props) => {
interface CalloutProps { interface CalloutProps {
hasHostJobs: boolean; hasHostJobs: boolean;
hasK8sJobs: boolean; hasK8sJobs: boolean;
jobIds: string[];
} }
const JobsEnabledCallout = (props: CalloutProps) => { const JobsEnabledCallout = (props: CalloutProps) => {
let target = ''; let target = '';
@ -162,34 +175,8 @@ const JobsEnabledCallout = (props: CalloutProps) => {
pathname: '/jobs', pathname: '/jobs',
}); });
const anomaliesUrl = useLinkProps({
app: 'ml',
pathname: `/explorer?_g=${createResultsUrl(props.jobIds)}`,
});
return ( return (
<> <>
<EuiFlexGroup gutterSize={'s'}>
<EuiFlexItem grow={false}>
<EuiButton {...manageJobsLinkProps} style={{ marginRight: 5 }}>
<FormattedMessage
defaultMessage="Manage jobs"
id="xpack.infra.ml.anomalyFlyout.manageJobs"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton {...anomaliesUrl}>
<FormattedMessage
defaultMessage="View anomalies"
id="xpack.infra.ml.anomalyFlyout.anomaliesTabLabel"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiCallOut <EuiCallOut
size="m" size="m"
color="success" color="success"
@ -202,6 +189,13 @@ const JobsEnabledCallout = (props: CalloutProps) => {
} }
iconType="check" iconType="check"
/> />
<EuiSpacer size="l" />
<EuiButton {...manageJobsLinkProps}>
<FormattedMessage
defaultMessage="Manage Jobs"
id="xpack.infra.ml.anomalyFlyout.manageJobs"
/>
</EuiButton>
</> </>
); );
}; };
@ -217,11 +211,30 @@ interface CreateJobTab {
const CreateJobTab = (props: CreateJobTab) => { const CreateJobTab = (props: CreateJobTab) => {
return ( return (
<> <>
{/* <EuiSpacer size="l" /> */} <div>
<EuiText>
<h3>
<FormattedMessage
defaultMessage="Create ML Jobs"
id="xpack.infra.ml.anomalyFlyout.create.jobsTitle"
/>
</h3>
</EuiText>
<EuiText>
<p>
<FormattedMessage
defaultMessage="Machine Learning jobs are available for the following resource types. Enable these jobs to begin detecting anomalies in your infrastructure metrics"
id="xpack.infra.ml.anomalyFlyout.create.description"
/>
</p>
</EuiText>
</div>
<EuiSpacer size="l" />
<EuiFlexGroup gutterSize={'m'}> <EuiFlexGroup gutterSize={'m'}>
<EuiFlexItem> <EuiFlexItem>
<EuiCard <EuiCard
isDisabled={!props.hasSetupCapabilities} // isDisabled={props.hasSetupCapabilities}
icon={<EuiIcon type={'storage'} />} icon={<EuiIcon type={'storage'} />}
// title="Hosts" // title="Hosts"
title={ title={
@ -232,7 +245,7 @@ const CreateJobTab = (props: CreateJobTab) => {
} }
description={ description={
<FormattedMessage <FormattedMessage
defaultMessage="Detect anomalies for memory usage and network traffic." defaultMessage="Detect anomalies for CPU usage, memory usage, network traffic, and load."
id="xpack.infra.ml.anomalyFlyout.create.hostDescription" id="xpack.infra.ml.anomalyFlyout.create.hostDescription"
/> />
} }
@ -241,7 +254,7 @@ const CreateJobTab = (props: CreateJobTab) => {
{props.hasHostJobs && ( {props.hasHostJobs && (
<EuiButtonEmpty onClick={props.createHosts}> <EuiButtonEmpty onClick={props.createHosts}>
<FormattedMessage <FormattedMessage
defaultMessage="Recreate jobs" defaultMessage="Recreate Jobs"
id="xpack.infra.ml.anomalyFlyout.create.recreateButton" id="xpack.infra.ml.anomalyFlyout.create.recreateButton"
/> />
</EuiButtonEmpty> </EuiButtonEmpty>
@ -249,7 +262,7 @@ const CreateJobTab = (props: CreateJobTab) => {
{!props.hasHostJobs && ( {!props.hasHostJobs && (
<EuiButton onClick={props.createHosts}> <EuiButton onClick={props.createHosts}>
<FormattedMessage <FormattedMessage
defaultMessage="Enable" defaultMessage="Create Jobs"
id="xpack.infra.ml.anomalyFlyout.create.createButton" id="xpack.infra.ml.anomalyFlyout.create.createButton"
/> />
</EuiButton> </EuiButton>
@ -260,7 +273,7 @@ const CreateJobTab = (props: CreateJobTab) => {
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem> <EuiFlexItem>
<EuiCard <EuiCard
isDisabled={!props.hasSetupCapabilities} // isDisabled={props.hasSetupCapabilities}
icon={<EuiIcon type={'logoKubernetes'} />} icon={<EuiIcon type={'logoKubernetes'} />}
title={ title={
<FormattedMessage <FormattedMessage
@ -270,7 +283,7 @@ const CreateJobTab = (props: CreateJobTab) => {
} }
description={ description={
<FormattedMessage <FormattedMessage
defaultMessage="Detect anomalies for memory usage and network traffic." defaultMessage="Detect anomalies for CPU usage, memory usage, network traffic, and load."
id="xpack.infra.ml.anomalyFlyout.create.k8sDescription" id="xpack.infra.ml.anomalyFlyout.create.k8sDescription"
/> />
} }
@ -279,7 +292,7 @@ const CreateJobTab = (props: CreateJobTab) => {
{props.hasK8sJobs && ( {props.hasK8sJobs && (
<EuiButtonEmpty onClick={props.createK8s}> <EuiButtonEmpty onClick={props.createK8s}>
<FormattedMessage <FormattedMessage
defaultMessage="Recreate jobs" defaultMessage="Recreate Jobs"
id="xpack.infra.ml.anomalyFlyout.create.recreateButton" id="xpack.infra.ml.anomalyFlyout.create.recreateButton"
/> />
</EuiButtonEmpty> </EuiButtonEmpty>
@ -287,7 +300,7 @@ const CreateJobTab = (props: CreateJobTab) => {
{!props.hasK8sJobs && ( {!props.hasK8sJobs && (
<EuiButton onClick={props.createK8s}> <EuiButton onClick={props.createK8s}>
<FormattedMessage <FormattedMessage
defaultMessage="Enable" defaultMessage="Create Jobs"
id="xpack.infra.ml.anomalyFlyout.create.createButton" id="xpack.infra.ml.anomalyFlyout.create.createButton"
/> />
</EuiButton> </EuiButton>

View file

@ -20,7 +20,6 @@ import { useSourceViaHttp } from '../../../../../../containers/source/use_source
import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module'; import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module';
import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module'; import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module';
import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; import { FixedDatePicker } from '../../../../../../components/fixed_datepicker';
import { DEFAULT_K8S_PARTITION_FIELD } from '../../../../../../containers/ml/modules/metrics_k8s/module_descriptor';
interface Props { interface Props {
jobType: 'hosts' | 'kubernetes'; jobType: 'hosts' | 'kubernetes';
@ -108,7 +107,7 @@ export const JobSetupScreen = (props: Props) => {
useEffect(() => { useEffect(() => {
if (props.jobType === 'kubernetes') { if (props.jobType === 'kubernetes') {
setPartitionField([DEFAULT_K8S_PARTITION_FIELD]); setPartitionField(['kubernetes.namespace']);
} }
}, [props.jobType]); }, [props.jobType]);