mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Uptime] Ml detection of duration anomalies (#59785)
* add flyout * add state * update state * ad job * update * updat * add ml analyze button * update api * use differential colors for duration chart * remove duration chart gql * update type * type fix * fix tyoe * update translation * update test * update conflicts * update anomaly record * chart * added annotations * update error handling * update * update types * fixed types * fix types * update types * update * update * remove unnecessary change * remove unnecessary change * fix type * update * save * update pr * update tets * update job deletion * update * update tets * upadte tests * fix types * update title text * update types * fixed tests * update tests and types * updated types * fix PR feedback * unit test * update more types * update test and manage job * resolve conflicts * types * remove unnecessary change * revert ml code * revert ml code * fixed formatting issues pointed by pr feedback
This commit is contained in:
parent
4dbcb3c0e9
commit
d31e5f524f
69 changed files with 2522 additions and 180 deletions
|
@ -26,6 +26,7 @@ export * from './field_icon';
|
|||
export * from './table_list_view';
|
||||
export * from './split_panel';
|
||||
export { ValidatedDualRange } from './validated_range';
|
||||
export * from './notifications';
|
||||
export { Markdown, MarkdownSimple } from './markdown';
|
||||
export { reactToUiComponent, uiToReactComponent } from './adapters';
|
||||
export { useUrlTracker } from './use_url_tracker';
|
||||
|
|
|
@ -16,4 +16,10 @@ export enum API_URLS {
|
|||
PING_HISTOGRAM = `/api/uptime/ping/histogram`,
|
||||
SNAPSHOT_COUNT = `/api/uptime/snapshot/count`,
|
||||
FILTERS = `/api/uptime/filters`,
|
||||
|
||||
ML_MODULE_JOBS = `/api/ml/modules/jobs_exist/`,
|
||||
ML_SETUP_MODULE = '/api/ml/modules/setup/',
|
||||
ML_DELETE_JOB = `/api/ml/jobs/delete_jobs`,
|
||||
ML_CAPABILITIES = '/api/ml/ml_capabilities',
|
||||
ML_ANOMALIES_RESULT = `/api/ml/results/anomalies_table_data`,
|
||||
}
|
||||
|
|
|
@ -15,6 +15,10 @@ export enum STATUS {
|
|||
DOWN = 'down',
|
||||
}
|
||||
|
||||
export const ML_JOB_ID = 'high_latency_by_geo';
|
||||
|
||||
export const ML_MODULE_ID = 'uptime_heartbeat';
|
||||
|
||||
export const UNNAMED_LOCATION = 'Unnamed-location';
|
||||
|
||||
export const SHORT_TS_LOCALE = 'en-short-locale';
|
||||
|
|
|
@ -7,10 +7,21 @@
|
|||
import React, { useContext, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useUrlParams } from '../../../hooks';
|
||||
import { getMonitorDurationAction } from '../../../state/actions';
|
||||
import {
|
||||
getAnomalyRecordsAction,
|
||||
getMLCapabilitiesAction,
|
||||
getMonitorDurationAction,
|
||||
} from '../../../state/actions';
|
||||
import { DurationChartComponent } from '../../functional/charts';
|
||||
import { selectDurationLines } from '../../../state/selectors';
|
||||
import {
|
||||
anomaliesSelector,
|
||||
hasMLFeatureAvailable,
|
||||
hasMLJobSelector,
|
||||
selectDurationLines,
|
||||
} from '../../../state/selectors';
|
||||
import { UptimeRefreshContext } from '../../../contexts';
|
||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
||||
import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer';
|
||||
|
||||
interface Props {
|
||||
monitorId: string;
|
||||
|
@ -18,24 +29,58 @@ interface Props {
|
|||
|
||||
export const DurationChart: React.FC<Props> = ({ monitorId }: Props) => {
|
||||
const [getUrlParams] = useUrlParams();
|
||||
const { dateRangeStart, dateRangeEnd } = getUrlParams();
|
||||
const {
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
absoluteDateRangeStart,
|
||||
absoluteDateRangeEnd,
|
||||
} = getUrlParams();
|
||||
|
||||
const { monitor_duration, loading } = useSelector(selectDurationLines);
|
||||
const { durationLines, loading } = useSelector(selectDurationLines);
|
||||
|
||||
const isMLAvailable = useSelector(hasMLFeatureAvailable);
|
||||
|
||||
const { data: mlJobs, loading: jobsLoading } = useSelector(hasMLJobSelector);
|
||||
|
||||
const hasMLJob =
|
||||
!!mlJobs?.jobsExist &&
|
||||
!!mlJobs.jobs.find((job: JobStat) => job.id === getMLJobId(monitorId as string));
|
||||
|
||||
const anomalies = useSelector(anomaliesSelector);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { lastRefresh } = useContext(UptimeRefreshContext);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getMonitorDurationAction({ monitorId, dateStart: dateRangeStart, dateEnd: dateRangeEnd })
|
||||
);
|
||||
if (isMLAvailable) {
|
||||
const anomalyParams = {
|
||||
listOfMonitorIds: [monitorId],
|
||||
dateStart: absoluteDateRangeStart,
|
||||
dateEnd: absoluteDateRangeEnd,
|
||||
};
|
||||
|
||||
dispatch(getAnomalyRecordsAction.get(anomalyParams));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId, isMLAvailable]);
|
||||
|
||||
useEffect(() => {
|
||||
const params = { monitorId, dateStart: dateRangeStart, dateEnd: dateRangeEnd };
|
||||
dispatch(getMonitorDurationAction(params));
|
||||
}, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getMLCapabilitiesAction.get());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<DurationChartComponent
|
||||
locationDurationLines={monitor_duration?.locationDurationLines ?? []}
|
||||
loading={loading}
|
||||
anomalies={anomalies}
|
||||
hasMLJob={hasMLJob}
|
||||
loading={loading || jobsLoading}
|
||||
locationDurationLines={durationLines?.locationDurationLines ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { indexStatusSelector } from '../../../state/selectors';
|
|||
import { EmptyStateComponent } from '../../functional/empty_state/empty_state';
|
||||
|
||||
export const EmptyState: React.FC = ({ children }) => {
|
||||
const { data, loading, errors } = useSelector(indexStatusSelector);
|
||||
const { data, loading, error } = useSelector(indexStatusSelector);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -23,7 +23,7 @@ export const EmptyState: React.FC = ({ children }) => {
|
|||
<EmptyStateComponent
|
||||
statesIndexStatus={data}
|
||||
loading={loading}
|
||||
errors={errors}
|
||||
errors={error ? [error] : undefined}
|
||||
children={children as React.ReactElement}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -22,7 +22,8 @@ interface StateProps {
|
|||
}
|
||||
|
||||
interface DispatchProps {
|
||||
loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => void;
|
||||
loadMonitorStatus: typeof getMonitorStatusAction;
|
||||
loadSelectedMonitor: typeof getSelectedMonitorAction;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
|
@ -33,6 +34,7 @@ type Props = OwnProps & StateProps & DispatchProps;
|
|||
|
||||
const Container: React.FC<Props> = ({
|
||||
loadMonitorStatus,
|
||||
loadSelectedMonitor,
|
||||
monitorId,
|
||||
monitorStatus,
|
||||
monitorLocations,
|
||||
|
@ -43,8 +45,9 @@ const Container: React.FC<Props> = ({
|
|||
const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams();
|
||||
|
||||
useEffect(() => {
|
||||
loadMonitorStatus(dateStart, dateEnd, monitorId);
|
||||
}, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh]);
|
||||
loadMonitorStatus({ dateStart, dateEnd, monitorId });
|
||||
loadSelectedMonitor({ monitorId });
|
||||
}, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh, loadSelectedMonitor]);
|
||||
|
||||
return (
|
||||
<MonitorStatusBarComponent
|
||||
|
@ -61,20 +64,8 @@ const mapStateToProps = (state: AppState, ownProps: OwnProps) => ({
|
|||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch<any>): DispatchProps => ({
|
||||
loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => {
|
||||
dispatch(
|
||||
getMonitorStatusAction({
|
||||
monitorId,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
getSelectedMonitorAction({
|
||||
monitorId,
|
||||
})
|
||||
);
|
||||
},
|
||||
loadSelectedMonitor: params => dispatch(getSelectedMonitorAction(params)),
|
||||
loadMonitorStatus: params => dispatch(getMonitorStatusAction(params)),
|
||||
});
|
||||
|
||||
// @ts-ignore TODO: Investigate typescript issues here
|
||||
|
|
|
@ -52,6 +52,8 @@ exports[`MonitorCharts component renders the component without errors 1`] = `
|
|||
}
|
||||
>
|
||||
<DurationChartComponent
|
||||
anomalies={null}
|
||||
hasMLJob={false}
|
||||
loading={false}
|
||||
locationDurationLines={
|
||||
Array [
|
||||
|
|
|
@ -64,8 +64,10 @@ describe('MonitorCharts component', () => {
|
|||
it('renders the component without errors', () => {
|
||||
const component = shallowWithRouter(
|
||||
<DurationChartComponent
|
||||
locationDurationLines={chartResponse.monitorChartsData.locationDurationLines}
|
||||
loading={false}
|
||||
hasMLJob={false}
|
||||
anomalies={null}
|
||||
locationDurationLines={chartResponse.monitorChartsData.locationDurationLines}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
const Header = styled.div`
|
||||
font-weight: bold;
|
||||
padding-left: 4px;
|
||||
`;
|
||||
|
||||
const RecordSeverity = styled.div`
|
||||
font-weight: bold;
|
||||
border-left: 4px solid ${props => props.color};
|
||||
padding-left: 2px;
|
||||
`;
|
||||
|
||||
const TimeDiv = styled.div`
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid gray;
|
||||
padding-bottom: 2px;
|
||||
`;
|
||||
|
||||
export const AnnotationTooltip = ({ details }: { details: string }) => {
|
||||
const data = JSON.parse(details);
|
||||
|
||||
function capitalizeFirstLetter(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimeDiv>{moment(data.time).format('lll')}</TimeDiv>
|
||||
<Header>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.charts.mlAnnotation.header"
|
||||
defaultMessage="Score: {score}"
|
||||
values={{ score: data.score.toFixed(2) }}
|
||||
/>
|
||||
</Header>
|
||||
<RecordSeverity color={data.color}>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.charts.mlAnnotation.severity"
|
||||
defaultMessage="Severity: {severity}"
|
||||
values={{ severity: capitalizeFirstLetter(data.severity) }}
|
||||
/>
|
||||
</RecordSeverity>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -4,12 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Axis, Chart, Position, timeFormatter, Settings } from '@elastic/charts';
|
||||
import { EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { Axis, Chart, Position, timeFormatter, Settings } from '@elastic/charts';
|
||||
import { SeriesIdentifier } from '@elastic/charts/dist/chart_types/xy_chart/utils/series';
|
||||
import { getChartDateLabel } from '../../../lib/helper';
|
||||
import { LocationDurationLine } from '../../../../common/types';
|
||||
import { DurationLineSeriesList } from './duration_line_series_list';
|
||||
|
@ -17,6 +18,9 @@ import { ChartWrapper } from './chart_wrapper';
|
|||
import { useUrlParams } from '../../../hooks';
|
||||
import { getTickFormat } from './get_tick_format';
|
||||
import { ChartEmptyState } from './chart_empty_state';
|
||||
import { DurationAnomaliesBar } from './duration_line_bar_list';
|
||||
import { MLIntegrationComponent } from '../../monitor_details/ml/ml_integeration';
|
||||
import { AnomalyRecords } from '../../../state/actions';
|
||||
|
||||
interface DurationChartProps {
|
||||
/**
|
||||
|
@ -29,6 +33,10 @@ interface DurationChartProps {
|
|||
* To represent the loading spinner on chart
|
||||
*/
|
||||
loading: boolean;
|
||||
|
||||
hasMLJob: boolean;
|
||||
|
||||
anomalies: AnomalyRecords | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,29 +45,64 @@ interface DurationChartProps {
|
|||
* milliseconds.
|
||||
* @param props The props required for this component to render properly
|
||||
*/
|
||||
export const DurationChartComponent = ({ locationDurationLines, loading }: DurationChartProps) => {
|
||||
export const DurationChartComponent = ({
|
||||
locationDurationLines,
|
||||
anomalies,
|
||||
loading,
|
||||
hasMLJob,
|
||||
}: DurationChartProps) => {
|
||||
const hasLines = locationDurationLines.length > 0;
|
||||
const [getUrlParams, updateUrlParams] = useUrlParams();
|
||||
const { absoluteDateRangeStart: min, absoluteDateRangeEnd: max } = getUrlParams();
|
||||
|
||||
const [hiddenLegends, setHiddenLegends] = useState<string[]>([]);
|
||||
|
||||
const onBrushEnd = (minX: number, maxX: number) => {
|
||||
updateUrlParams({
|
||||
dateRangeStart: moment(minX).toISOString(),
|
||||
dateRangeEnd: moment(maxX).toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
const legendToggleVisibility = (legendItem: SeriesIdentifier | null) => {
|
||||
if (legendItem) {
|
||||
setHiddenLegends(prevState => {
|
||||
if (prevState.includes(legendItem.specId)) {
|
||||
return [...prevState.filter(item => item !== legendItem.specId)];
|
||||
} else {
|
||||
return [...prevState, legendItem.specId];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel paddingSize="m">
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorCharts.monitorDuration.titleLabel"
|
||||
defaultMessage="Monitor duration"
|
||||
description="The 'ms' is an abbreviation for milliseconds."
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
{hasMLJob ? (
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorCharts.monitorDuration.titleLabelWithAnomaly"
|
||||
defaultMessage="Monitor duration (Anomalies: {noOfAnomalies})"
|
||||
values={{ noOfAnomalies: anomalies?.anomalies?.length ?? 0 }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorCharts.monitorDuration.titleLabel"
|
||||
defaultMessage="Monitor duration"
|
||||
/>
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MLIntegrationComponent />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<ChartWrapper height="400px" loading={loading}>
|
||||
{hasLines ? (
|
||||
<Chart>
|
||||
|
@ -69,6 +112,7 @@ export const DurationChartComponent = ({ locationDurationLines, loading }: Durat
|
|||
showLegendExtra
|
||||
legendPosition={Position.Bottom}
|
||||
onBrushEnd={onBrushEnd}
|
||||
onLegendItemClick={legendToggleVisibility}
|
||||
/>
|
||||
<Axis
|
||||
id="bottom"
|
||||
|
@ -89,6 +133,7 @@ export const DurationChartComponent = ({ locationDurationLines, loading }: Durat
|
|||
})}
|
||||
/>
|
||||
<DurationLineSeriesList lines={locationDurationLines} />
|
||||
<DurationAnomaliesBar anomalies={anomalies} hiddenLegends={hiddenLegends} />
|
||||
</Chart>
|
||||
) : (
|
||||
<ChartEmptyState
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import moment from 'moment';
|
||||
import { AnnotationTooltipFormatter, RectAnnotation } from '@elastic/charts';
|
||||
import { RectAnnotationDatum } from '@elastic/charts/dist/chart_types/xy_chart/utils/specs';
|
||||
import { AnnotationTooltip } from './annotation_tooltip';
|
||||
import { ANOMALY_SEVERITY } from '../../../../../../../plugins/ml/common/constants/anomalies';
|
||||
import {
|
||||
getSeverityColor,
|
||||
getSeverityType,
|
||||
} from '../../../../../../../plugins/ml/common/util/anomaly_utils';
|
||||
|
||||
interface Props {
|
||||
anomalies: any;
|
||||
hiddenLegends: string[];
|
||||
}
|
||||
|
||||
export const DurationAnomaliesBar = ({ anomalies, hiddenLegends }: Props) => {
|
||||
const anomalyAnnotations: Map<string, { rect: RectAnnotationDatum[]; color: string }> = new Map();
|
||||
|
||||
Object.keys(ANOMALY_SEVERITY).forEach(severityLevel => {
|
||||
anomalyAnnotations.set(severityLevel.toLowerCase(), { rect: [], color: '' });
|
||||
});
|
||||
|
||||
if (anomalies?.anomalies) {
|
||||
const records = anomalies.anomalies;
|
||||
records.forEach((record: any) => {
|
||||
let recordObsvLoc = record.source['observer.geo.name']?.[0] ?? 'N/A';
|
||||
if (recordObsvLoc === '') {
|
||||
recordObsvLoc = 'N/A';
|
||||
}
|
||||
if (hiddenLegends.length && hiddenLegends.includes(`loc-avg-${recordObsvLoc}`)) {
|
||||
return;
|
||||
}
|
||||
const severityLevel = getSeverityType(record.severity);
|
||||
|
||||
const tooltipData = {
|
||||
time: record.source.timestamp,
|
||||
score: record.severity,
|
||||
severity: severityLevel,
|
||||
color: getSeverityColor(record.severity),
|
||||
};
|
||||
|
||||
const anomalyRect = {
|
||||
coordinates: {
|
||||
x0: moment(record.source.timestamp).valueOf(),
|
||||
x1: moment(record.source.timestamp)
|
||||
.add(record.source.bucket_span, 's')
|
||||
.valueOf(),
|
||||
},
|
||||
details: JSON.stringify(tooltipData),
|
||||
};
|
||||
anomalyAnnotations.get(severityLevel)!.rect.push(anomalyRect);
|
||||
anomalyAnnotations.get(severityLevel)!.color = getSeverityColor(record.severity);
|
||||
});
|
||||
}
|
||||
|
||||
const getRectStyle = (color: string) => {
|
||||
return {
|
||||
fill: color,
|
||||
opacity: 1,
|
||||
strokeWidth: 2,
|
||||
stroke: color,
|
||||
};
|
||||
};
|
||||
|
||||
const tooltipFormatter: AnnotationTooltipFormatter = (details?: string) => {
|
||||
return <AnnotationTooltip details={details || ''} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.from(anomalyAnnotations).map(([keyIndex, rectAnnotation]) => {
|
||||
return rectAnnotation.rect.length > 0 ? (
|
||||
<RectAnnotation
|
||||
dataValues={rectAnnotation.rect}
|
||||
key={keyIndex}
|
||||
id={keyIndex}
|
||||
style={getRectStyle(rectAnnotation.color)}
|
||||
renderTooltip={tooltipFormatter}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -749,17 +749,7 @@ exports[`EmptyState component renders error message when an error occurs 1`] = `
|
|||
<EmptyStateComponent
|
||||
errors={
|
||||
Array [
|
||||
Object {
|
||||
"extensions": undefined,
|
||||
"locations": undefined,
|
||||
"message": "An error occurred",
|
||||
"name": "foo",
|
||||
"nodes": undefined,
|
||||
"originalError": undefined,
|
||||
"path": undefined,
|
||||
"positions": undefined,
|
||||
"source": undefined,
|
||||
},
|
||||
[error: There was an error fetching your data.],
|
||||
]
|
||||
}
|
||||
intl={
|
||||
|
@ -870,17 +860,7 @@ exports[`EmptyState component renders error message when an error occurs 1`] = `
|
|||
<EmptyStateError
|
||||
errors={
|
||||
Array [
|
||||
Object {
|
||||
"extensions": undefined,
|
||||
"locations": undefined,
|
||||
"message": "An error occurred",
|
||||
"name": "foo",
|
||||
"nodes": undefined,
|
||||
"originalError": undefined,
|
||||
"path": undefined,
|
||||
"positions": undefined,
|
||||
"source": undefined,
|
||||
},
|
||||
[error: There was an error fetching your data.],
|
||||
]
|
||||
}
|
||||
>
|
||||
|
@ -904,7 +884,7 @@ exports[`EmptyState component renders error message when an error occurs 1`] = `
|
|||
body={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
An error occurred
|
||||
There was an error fetching your data.
|
||||
</p>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
@ -971,9 +951,9 @@ exports[`EmptyState component renders error message when an error occurs 1`] = `
|
|||
className="euiText euiText--medium"
|
||||
>
|
||||
<p
|
||||
key="An error occurred"
|
||||
key="There was an error fetching your data."
|
||||
>
|
||||
An error occurred
|
||||
There was an error fetching your data.
|
||||
</p>
|
||||
</div>
|
||||
</EuiText>
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { EmptyStateComponent } from '../empty_state';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { StatesIndexStatus } from '../../../../../common/runtime_types';
|
||||
import { IHttpFetchError } from '../../../../../../../../../target/types/core/public/http';
|
||||
import { HttpFetchError } from '../../../../../../../../../src/core/public/http/http_fetch_error';
|
||||
|
||||
describe('EmptyState component', () => {
|
||||
let statesIndexStatus: StatesIndexStatus;
|
||||
|
@ -41,18 +42,8 @@ describe('EmptyState component', () => {
|
|||
});
|
||||
|
||||
it(`renders error message when an error occurs`, () => {
|
||||
const errors: GraphQLError[] = [
|
||||
{
|
||||
message: 'An error occurred',
|
||||
locations: undefined,
|
||||
path: undefined,
|
||||
nodes: undefined,
|
||||
source: undefined,
|
||||
positions: undefined,
|
||||
originalError: undefined,
|
||||
extensions: undefined,
|
||||
name: 'foo',
|
||||
},
|
||||
const errors: IHttpFetchError[] = [
|
||||
new HttpFetchError('There was an error fetching your data.', 'error', {} as any),
|
||||
];
|
||||
const component = mountWithIntl(
|
||||
<EmptyStateComponent statesIndexStatus={null} errors={errors} loading={false}>
|
||||
|
|
|
@ -10,12 +10,13 @@ import { EmptyStateError } from './empty_state_error';
|
|||
import { EmptyStateLoading } from './empty_state_loading';
|
||||
import { DataMissing } from './data_missing';
|
||||
import { StatesIndexStatus } from '../../../../common/runtime_types';
|
||||
import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http';
|
||||
|
||||
interface EmptyStateProps {
|
||||
children: JSX.Element[] | JSX.Element;
|
||||
statesIndexStatus: StatesIndexStatus | null;
|
||||
loading: boolean;
|
||||
errors?: Error[];
|
||||
errors?: IHttpFetchError[];
|
||||
}
|
||||
|
||||
export const EmptyStateComponent = ({
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Fragment } from 'react';
|
||||
import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http';
|
||||
|
||||
interface EmptyStateErrorProps {
|
||||
errors: Error[];
|
||||
errors: IHttpFetchError[];
|
||||
}
|
||||
|
||||
export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => {
|
||||
|
|
|
@ -64,3 +64,10 @@ export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel'
|
|||
export const DOWN = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', {
|
||||
defaultMessage: 'Down',
|
||||
});
|
||||
|
||||
export const RESPONSE_ANOMALY_SCORE = i18n.translate(
|
||||
'xpack.uptime.monitorList.anomalyColumn.label',
|
||||
{
|
||||
defaultMessage: 'Response Anomaly Score',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ import { Ping, PingResults } from '../../../../common/graphql/types';
|
|||
import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper';
|
||||
import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order';
|
||||
import { pingsQuery } from '../../../queries';
|
||||
import { LocationName } from './../location_name';
|
||||
import { LocationName } from './location_name';
|
||||
import { Pagination } from './../monitor_list';
|
||||
import { PingListExpandedRowComponent } from './expanded_row';
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ML Confirm Job Delete shallow renders without errors 1`] = `
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
buttonColor="danger"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
defaultFocusedButton="confirm"
|
||||
onCancel={[MockFunction]}
|
||||
onConfirm={[MockFunction]}
|
||||
title="Delete anomaly detection job?"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Are you sure you want to delete this job?"
|
||||
id="xpack.uptime.monitorDetails.ml.confirmDeleteMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Deleting a job can be time consuming. It will be deleted in the background and data may not disappear instantly."
|
||||
id="xpack.uptime.monitorDetails.ml.deleteJobWarning"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
`;
|
||||
|
||||
exports[`ML Confirm Job Delete shallow renders without errors while loading 1`] = `
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
buttonColor="danger"
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
defaultFocusedButton="confirm"
|
||||
onCancel={[MockFunction]}
|
||||
onConfirm={[MockFunction]}
|
||||
title="Delete anomaly detection job?"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Deleting jobs..."
|
||||
id="xpack.uptime.monitorDetails.ml.deleteMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
)
|
||||
</p>
|
||||
<EuiLoadingSpinner
|
||||
size="xl"
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
`;
|
|
@ -0,0 +1,73 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ShowLicenseInfo renders without errors 1`] = `
|
||||
Array [
|
||||
<div
|
||||
class="euiCallOut euiCallOut--primary license-info-trial"
|
||||
>
|
||||
<div
|
||||
class="euiCallOutHeader"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="euiCallOutHeader__icon"
|
||||
data-euiicon-type="help"
|
||||
/>
|
||||
<span
|
||||
class="euiCallOutHeader__title"
|
||||
>
|
||||
Start free 14-day trial
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="euiText euiText--small"
|
||||
>
|
||||
<p>
|
||||
In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license.
|
||||
</p>
|
||||
<a
|
||||
class="euiButton euiButton--primary"
|
||||
href="/app/kibana#/management/elasticsearch/license_management/home"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Start free 14-day trial
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
class="euiSpacer euiSpacer--l"
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ShowLicenseInfo shallow renders without errors 1`] = `
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
className="license-info-trial"
|
||||
color="primary"
|
||||
iconType="help"
|
||||
title="Start free 14-day trial"
|
||||
>
|
||||
<p>
|
||||
In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license.
|
||||
</p>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
href="/app/kibana#/management/elasticsearch/license_management/home"
|
||||
target="_blank"
|
||||
>
|
||||
Start free 14-day trial
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,240 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ML Flyout component renders without errors 1`] = `
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
hideCloseButton={false}
|
||||
maxWidth={false}
|
||||
onClose={[Function]}
|
||||
ownFocus={false}
|
||||
size="s"
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
Enable anomaly detection
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<ShowLicenseInfo />
|
||||
<EuiText>
|
||||
<p>
|
||||
Here you can create a machine learning job to calculate anomaly scores on
|
||||
response durations for Uptime Monitor. Once enabled, the monitor duration chart on the details page
|
||||
will show the expected bounds and annotate the graph with anomalies. You can also potentially
|
||||
identify periods of increased latency across geographical regions.
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Once a job is created, you can manage it and see more details in the {mlJobsPageLink}."
|
||||
id="xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription"
|
||||
values={
|
||||
Object {
|
||||
"mlJobsPageLink": <ForwardRef
|
||||
href="/app/ml"
|
||||
>
|
||||
Machine Learning jobs management page
|
||||
</ForwardRef>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
Note: It might take a few minutes for the job to begin calculating results.
|
||||
</em>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexEnd"
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
disabled={true}
|
||||
fill={true}
|
||||
isLoading={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Create new job
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
`;
|
||||
|
||||
exports[`ML Flyout component shows license info if no ml available 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width:1px;height:0px;padding:0;overflow:hidden;position:fixed;top:1px;left:1px"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width:1px;height:0px;padding:0;overflow:hidden;position:fixed;top:1px;left:1px"
|
||||
tabindex="1"
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="false"
|
||||
>
|
||||
<div
|
||||
class="euiFlyout euiFlyout--small"
|
||||
role="dialog"
|
||||
tabindex="0"
|
||||
>
|
||||
<button
|
||||
aria-label="Closes this dialog"
|
||||
class="euiButtonIcon euiButtonIcon--text euiFlyout__closeButton"
|
||||
data-test-subj="euiFlyoutCloseButton"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
data-euiicon-type="cross"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="euiFlyoutHeader"
|
||||
>
|
||||
<h2
|
||||
class="euiTitle euiTitle--medium"
|
||||
>
|
||||
Enable anomaly detection
|
||||
</h2>
|
||||
<div
|
||||
class="euiSpacer euiSpacer--s"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlyoutBody"
|
||||
>
|
||||
<div
|
||||
class="euiFlyoutBody__overflow"
|
||||
>
|
||||
<div
|
||||
class="euiFlyoutBody__overflowContent"
|
||||
>
|
||||
<div
|
||||
class="euiCallOut euiCallOut--primary license-info-trial"
|
||||
>
|
||||
<div
|
||||
class="euiCallOutHeader"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="euiCallOutHeader__icon"
|
||||
data-euiicon-type="help"
|
||||
/>
|
||||
<span
|
||||
class="euiCallOutHeader__title"
|
||||
>
|
||||
Start free 14-day trial
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="euiText euiText--small"
|
||||
>
|
||||
<p>
|
||||
In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license.
|
||||
</p>
|
||||
<a
|
||||
class="euiButton euiButton--primary"
|
||||
href="/app/kibana#/management/elasticsearch/license_management/home"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Start free 14-day trial
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiSpacer euiSpacer--l"
|
||||
/>
|
||||
<div
|
||||
class="euiText euiText--medium"
|
||||
>
|
||||
<p>
|
||||
Here you can create a machine learning job to calculate anomaly scores on
|
||||
response durations for Uptime Monitor. Once enabled, the monitor duration chart on the details page
|
||||
will show the expected bounds and annotate the graph with anomalies. You can also potentially
|
||||
identify periods of increased latency across geographical regions.
|
||||
</p>
|
||||
<p>
|
||||
Once a job is created, you can manage it and see more details in the
|
||||
<a
|
||||
class="euiLink euiLink--primary"
|
||||
href="/app/ml"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Machine Learning jobs management page
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
Note: It might take a few minutes for the job to begin calculating results.
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="euiSpacer euiSpacer--l"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlyoutFooter"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsFlexEnd euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--fill"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Create new job
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width:1px;height:0px;padding:0;overflow:hidden;position:fixed;top:1px;left:1px"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,86 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ML Integrations renders without errors 1`] = `
|
||||
<div
|
||||
class="euiPopover euiPopover--anchorDownCenter"
|
||||
>
|
||||
<div
|
||||
class="euiPopover__anchor"
|
||||
>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="euiButtonEmpty__icon"
|
||||
data-euiicon-type="machineLearningApp"
|
||||
/>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
Enable anomaly detection
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ML Integrations shallow renders without errors 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<MLIntegrationComponent />
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,82 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ML JobLink renders without errors 1`] = `
|
||||
<a
|
||||
class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small"
|
||||
href="/app/ml#/explorer?_g=(ml:(jobIds:!(testMonitor_high_latency_by_geo)),refreshInterval:(pause:!t,value:0),time:(from:'',to:''))&_a=(mlExplorerFilter:(filterActive:!t,filteredFields:!(monitor.id,testMonitor)),mlExplorerSwimlane:(viewByFieldName:observer.geo.name))"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
exports[`ML JobLink shallow renders without errors 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<MLJobLink
|
||||
basePath=""
|
||||
dateRange={
|
||||
Object {
|
||||
"from": "",
|
||||
"to": "",
|
||||
}
|
||||
}
|
||||
monitorId="testMonitor"
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,90 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Manage ML Job renders without errors 1`] = `
|
||||
<div
|
||||
class="euiPopover euiPopover--anchorDownCenter"
|
||||
>
|
||||
<div
|
||||
class="euiPopover__anchor"
|
||||
>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--iconRight"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="euiButtonEmpty__icon"
|
||||
data-euiicon-type="arrowDown"
|
||||
/>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
Anomaly detection
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Manage ML Job shallow renders without errors 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ManageMLJobComponent
|
||||
hasMLJob={true}
|
||||
onEnableJob={[MockFunction]}
|
||||
onJobDelete={[MockFunction]}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { ConfirmJobDeletion } from '../confirm_delete';
|
||||
|
||||
describe('ML Confirm Job Delete', () => {
|
||||
it('shallow renders without errors', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ConfirmJobDeletion loading={false} onConfirm={jest.fn()} onCancel={jest.fn()} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shallow renders without errors while loading', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ConfirmJobDeletion loading={true} onConfirm={jest.fn()} onCancel={jest.fn()} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { ShowLicenseInfo } from '../license_info';
|
||||
|
||||
describe('ShowLicenseInfo', () => {
|
||||
it('shallow renders without errors', () => {
|
||||
const wrapper = shallowWithIntl(<ShowLicenseInfo />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
const wrapper = renderWithIntl(<ShowLicenseInfo />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { MLFlyoutView } from '../ml_flyout';
|
||||
import { UptimeSettingsContext } from '../../../../contexts';
|
||||
import { CLIENT_DEFAULTS } from '../../../../../common/constants';
|
||||
import { License } from '../../../../../../../../plugins/licensing/common/license';
|
||||
|
||||
const expiredLicense = new License({
|
||||
signature: 'test signature',
|
||||
license: {
|
||||
expiryDateInMillis: 0,
|
||||
mode: 'platinum',
|
||||
status: 'expired',
|
||||
type: 'platinum',
|
||||
uid: '1',
|
||||
},
|
||||
features: {
|
||||
ml: {
|
||||
isAvailable: false,
|
||||
isEnabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const validLicense = new License({
|
||||
signature: 'test signature',
|
||||
license: {
|
||||
expiryDateInMillis: 30000,
|
||||
mode: 'platinum',
|
||||
status: 'active',
|
||||
type: 'platinum',
|
||||
uid: '2',
|
||||
},
|
||||
features: {
|
||||
ml: {
|
||||
isAvailable: true,
|
||||
isEnabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('ML Flyout component', () => {
|
||||
const createJob = () => {};
|
||||
const onClose = () => {};
|
||||
const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS;
|
||||
|
||||
it('renders without errors', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<MLFlyoutView
|
||||
isCreatingJob={false}
|
||||
onClickCreate={createJob}
|
||||
onClose={onClose}
|
||||
canCreateMLJob={true}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('shows license info if no ml available', () => {
|
||||
const value = {
|
||||
license: expiredLicense,
|
||||
basePath: '',
|
||||
dateRangeStart: DATE_RANGE_START,
|
||||
dateRangeEnd: DATE_RANGE_END,
|
||||
isApmAvailable: true,
|
||||
isInfraAvailable: true,
|
||||
isLogsAvailable: true,
|
||||
};
|
||||
const wrapper = renderWithIntl(
|
||||
<UptimeSettingsContext.Provider value={value}>
|
||||
<MLFlyoutView
|
||||
isCreatingJob={false}
|
||||
onClickCreate={createJob}
|
||||
onClose={onClose}
|
||||
canCreateMLJob={true}
|
||||
/>
|
||||
</UptimeSettingsContext.Provider>
|
||||
);
|
||||
const licenseComponent = wrapper.find('.license-info-trial');
|
||||
expect(licenseComponent.length).toBe(1);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('able to create job if valid license is available', () => {
|
||||
const value = {
|
||||
license: validLicense,
|
||||
basePath: '',
|
||||
dateRangeStart: DATE_RANGE_START,
|
||||
dateRangeEnd: DATE_RANGE_END,
|
||||
isApmAvailable: true,
|
||||
isInfraAvailable: true,
|
||||
isLogsAvailable: true,
|
||||
};
|
||||
const wrapper = renderWithIntl(
|
||||
<UptimeSettingsContext.Provider value={value}>
|
||||
<MLFlyoutView
|
||||
isCreatingJob={false}
|
||||
onClickCreate={createJob}
|
||||
onClose={onClose}
|
||||
canCreateMLJob={true}
|
||||
/>
|
||||
</UptimeSettingsContext.Provider>
|
||||
);
|
||||
|
||||
const licenseComponent = wrapper.find('.license-info-trial');
|
||||
expect(licenseComponent.length).toBe(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { MLIntegrationComponent } from '../ml_integeration';
|
||||
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
||||
import * as redux from 'react-redux';
|
||||
|
||||
describe('ML Integrations', () => {
|
||||
beforeEach(() => {
|
||||
const spy = jest.spyOn(redux, 'useDispatch');
|
||||
spy.mockReturnValue(jest.fn());
|
||||
|
||||
const spy1 = jest.spyOn(redux, 'useSelector');
|
||||
spy1.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('shallow renders without errors', () => {
|
||||
const wrapper = shallowWithRouter(<MLIntegrationComponent />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
const wrapper = renderWithRouter(<MLIntegrationComponent />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
||||
import { MLJobLink } from '../ml_job_link';
|
||||
|
||||
describe('ML JobLink', () => {
|
||||
it('shallow renders without errors', () => {
|
||||
const wrapper = shallowWithRouter(
|
||||
<MLJobLink dateRange={{ to: '', from: '' }} basePath="" monitorId="testMonitor" />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
const wrapper = renderWithRouter(
|
||||
<MLJobLink dateRange={{ to: '', from: '' }} basePath="" monitorId="testMonitor" />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { ManageMLJobComponent } from '../manage_ml_job';
|
||||
import * as redux from 'react-redux';
|
||||
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
||||
|
||||
describe('Manage ML Job', () => {
|
||||
it('shallow renders without errors', () => {
|
||||
const spy = jest.spyOn(redux, 'useSelector');
|
||||
spy.mockReturnValue(true);
|
||||
|
||||
const wrapper = shallowWithRouter(
|
||||
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
const spy = jest.spyOn(redux, 'useSelector');
|
||||
spy.mockReturnValue(true);
|
||||
|
||||
const wrapper = renderWithRouter(
|
||||
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiOverlayMask, EuiConfirmModal, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import * as labels from './translations';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmJobDeletion: React.FC<Props> = ({ loading, onConfirm, onCancel }) => {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={labels.JOB_DELETION_CONFIRMATION}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
{!loading ? (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorDetails.ml.confirmDeleteMessage"
|
||||
defaultMessage="Are you sure you want to delete this job?"
|
||||
/>
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorDetails.ml.deleteMessage"
|
||||
defaultMessage="Deleting jobs..."
|
||||
/>
|
||||
)
|
||||
</p>
|
||||
)}
|
||||
{!loading ? (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorDetails.ml.deleteJobWarning"
|
||||
defaultMessage="Deleting a job can be time consuming.
|
||||
It will be deleted in the background and data may not disappear instantly."
|
||||
/>
|
||||
</p>
|
||||
) : (
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
)}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 React, { useContext } from 'react';
|
||||
import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { UptimeSettingsContext } from '../../../contexts';
|
||||
import * as labels from './translations';
|
||||
|
||||
export const ShowLicenseInfo = () => {
|
||||
const { basePath } = useContext(UptimeSettingsContext);
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
className="license-info-trial"
|
||||
title={labels.START_TRAIL}
|
||||
color="primary"
|
||||
iconType="help"
|
||||
>
|
||||
<p>{labels.START_TRAIL_DESC}</p>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
href={basePath + `/app/kibana#/management/elasticsearch/license_management/home`}
|
||||
target="_blank"
|
||||
>
|
||||
{labels.START_TRAIL}
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 React, { useContext, useState } from 'react';
|
||||
|
||||
import { EuiButtonEmpty, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { canDeleteMLJobSelector } from '../../../state/selectors';
|
||||
import { UptimeSettingsContext } from '../../../contexts';
|
||||
import * as labels from './translations';
|
||||
import { getMLJobLinkHref } from './ml_job_link';
|
||||
import { useUrlParams } from '../../../hooks';
|
||||
|
||||
interface Props {
|
||||
hasMLJob: boolean;
|
||||
onEnableJob: () => void;
|
||||
onJobDelete: () => void;
|
||||
}
|
||||
|
||||
export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Props) => {
|
||||
const [isPopOverOpen, setIsPopOverOpen] = useState(false);
|
||||
|
||||
const { basePath } = useContext(UptimeSettingsContext);
|
||||
|
||||
const canDeleteMLJob = useSelector(canDeleteMLJobSelector);
|
||||
|
||||
const [getUrlParams] = useUrlParams();
|
||||
const { dateRangeStart, dateRangeEnd } = getUrlParams();
|
||||
|
||||
let { monitorId } = useParams();
|
||||
monitorId = atob(monitorId || '');
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
iconType={hasMLJob ? 'arrowDown' : 'machineLearningApp'}
|
||||
iconSide={hasMLJob ? 'right' : 'left'}
|
||||
onClick={hasMLJob ? () => setIsPopOverOpen(true) : onEnableJob}
|
||||
disabled={hasMLJob && !canDeleteMLJob}
|
||||
>
|
||||
{hasMLJob ? labels.ANOMALY_DETECTION : labels.ENABLE_ANOMALY_DETECTION}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const panels = [
|
||||
{
|
||||
id: 0,
|
||||
title: labels.MANAGE_ANOMALY_DETECTION,
|
||||
items: [
|
||||
{
|
||||
name: labels.EXPLORE_IN_ML_APP,
|
||||
icon: <EuiIcon type="dataVisualizer" size="m" />,
|
||||
href: getMLJobLinkHref({
|
||||
basePath,
|
||||
monitorId,
|
||||
dateRange: { from: dateRangeStart, to: dateRangeEnd },
|
||||
}),
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
name: labels.DISABLE_ANOMALY_DETECTION,
|
||||
icon: <EuiIcon type="trash" size="m" />,
|
||||
onClick: () => {
|
||||
setIsPopOverOpen(false);
|
||||
onJobDelete();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover button={button} isOpen={isPopOverOpen} closePopover={() => setIsPopOverOpen(false)}>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 React, { useContext } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import * as labels from './translations';
|
||||
import { UptimeSettingsContext } from '../../../contexts';
|
||||
import { ShowLicenseInfo } from './license_info';
|
||||
|
||||
interface Props {
|
||||
isCreatingJob: boolean;
|
||||
onClickCreate: () => void;
|
||||
onClose: () => void;
|
||||
canCreateMLJob: boolean;
|
||||
}
|
||||
|
||||
export function MLFlyoutView({ isCreatingJob, onClickCreate, onClose, canCreateMLJob }: Props) {
|
||||
const { basePath, license } = useContext(UptimeSettingsContext);
|
||||
|
||||
const isLoadingMLJob = false;
|
||||
|
||||
const hasPlatinumLicense = license?.getFeature('ml')?.isAvailable;
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} size="s">
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle>
|
||||
<h2>{labels.ENABLE_ANOMALY_DETECTION}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{!hasPlatinumLicense && <ShowLicenseInfo />}
|
||||
<EuiText>
|
||||
<p>{labels.CREAT_ML_JOB_DESC}</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription"
|
||||
defaultMessage="Once a job is created, you can manage it and see more details in the {mlJobsPageLink}."
|
||||
values={{
|
||||
mlJobsPageLink: (
|
||||
<EuiLink href={basePath + '/app/ml'}>{labels.ML_MANAGEMENT_PAGE}</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<em>{labels.TAKE_SOME_TIME_TEXT}</em>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => onClickCreate()}
|
||||
fill
|
||||
isLoading={isCreatingJob}
|
||||
disabled={isCreatingJob || isLoadingMLJob || !hasPlatinumLicense || !canCreateMLJob}
|
||||
>
|
||||
{labels.CREATE_NEW_JOB}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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 React, { useContext, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
canCreateMLJobSelector,
|
||||
hasMLJobSelector,
|
||||
hasNewMLJobSelector,
|
||||
isMLJobCreatingSelector,
|
||||
} from '../../../state/selectors';
|
||||
import { createMLJobAction, getExistingMLJobAction } from '../../../state/actions';
|
||||
import { MLJobLink } from './ml_job_link';
|
||||
import * as labels from './translations';
|
||||
import {
|
||||
useKibana,
|
||||
KibanaReactNotifications,
|
||||
} from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { MLFlyoutView } from './ml_flyout';
|
||||
import { ML_JOB_ID } from '../../../../common/constants';
|
||||
import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts';
|
||||
import { useUrlParams } from '../../../hooks';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const showMLJobNotification = (
|
||||
notifications: KibanaReactNotifications,
|
||||
monitorId: string,
|
||||
basePath: string,
|
||||
range: { to: string; from: string },
|
||||
success: boolean,
|
||||
message = ''
|
||||
) => {
|
||||
if (success) {
|
||||
notifications.toasts.success({
|
||||
title: <p>{labels.JOB_CREATED_SUCCESS_TITLE}</p>,
|
||||
body: (
|
||||
<p>
|
||||
{labels.JOB_CREATED_SUCCESS_MESSAGE}
|
||||
<MLJobLink monitorId={monitorId} basePath={basePath} dateRange={range}>
|
||||
{labels.VIEW_JOB}
|
||||
</MLJobLink>
|
||||
</p>
|
||||
),
|
||||
toastLifeTimeMs: 5000,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.warning({
|
||||
title: <p>{labels.JOB_CREATION_FAILED}</p>,
|
||||
body: message ?? <p>{labels.JOB_CREATION_FAILED_MESSAGE}</p>,
|
||||
toastLifeTimeMs: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
||||
const { notifications } = useKibana();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { data: hasMLJob, error } = useSelector(hasNewMLJobSelector);
|
||||
const isMLJobCreating = useSelector(isMLJobCreatingSelector);
|
||||
const { basePath } = useContext(UptimeSettingsContext);
|
||||
|
||||
const { refreshApp } = useContext(UptimeRefreshContext);
|
||||
|
||||
let { monitorId } = useParams();
|
||||
monitorId = atob(monitorId || '');
|
||||
|
||||
const createMLJob = () => dispatch(createMLJobAction.get({ monitorId: monitorId as string }));
|
||||
|
||||
const canCreateMLJob = useSelector(canCreateMLJobSelector);
|
||||
|
||||
const { data: uptimeJobs } = useSelector(hasMLJobSelector);
|
||||
|
||||
const hasExistingMLJob = !!uptimeJobs?.jobsExist;
|
||||
|
||||
const [isCreatingJob, setIsCreatingJob] = useState(false);
|
||||
|
||||
const [getUrlParams] = useUrlParams();
|
||||
const { dateRangeStart, dateRangeEnd } = getUrlParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreatingJob && !isMLJobCreating) {
|
||||
if (hasMLJob) {
|
||||
showMLJobNotification(
|
||||
notifications,
|
||||
monitorId as string,
|
||||
basePath,
|
||||
{ to: dateRangeEnd, from: dateRangeStart },
|
||||
true
|
||||
);
|
||||
const loadMLJob = (jobId: string) =>
|
||||
dispatch(getExistingMLJobAction.get({ monitorId: monitorId as string }));
|
||||
|
||||
loadMLJob(ML_JOB_ID);
|
||||
|
||||
refreshApp();
|
||||
} else {
|
||||
showMLJobNotification(
|
||||
notifications,
|
||||
monitorId as string,
|
||||
basePath,
|
||||
{ to: dateRangeEnd, from: dateRangeStart },
|
||||
false,
|
||||
error?.body?.message
|
||||
);
|
||||
}
|
||||
setIsCreatingJob(false);
|
||||
onClose();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
hasMLJob,
|
||||
notifications,
|
||||
onClose,
|
||||
isCreatingJob,
|
||||
error,
|
||||
isMLJobCreating,
|
||||
monitorId,
|
||||
dispatch,
|
||||
basePath,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExistingMLJob) {
|
||||
setIsCreatingJob(true);
|
||||
dispatch(createMLJobAction.get({ monitorId: monitorId as string }));
|
||||
}
|
||||
}, [dispatch, hasExistingMLJob, monitorId]);
|
||||
|
||||
if (hasExistingMLJob) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createAnomalyJob = () => {
|
||||
setIsCreatingJob(true);
|
||||
createMLJob();
|
||||
};
|
||||
|
||||
return (
|
||||
<MLFlyoutView
|
||||
canCreateMLJob={!!canCreateMLJob}
|
||||
isCreatingJob={isMLJobCreating}
|
||||
onClickCreate={createAnomalyJob}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { MachineLearningFlyout } from './ml_flyout_container';
|
||||
import {
|
||||
hasMLFeatureAvailable,
|
||||
hasMLJobSelector,
|
||||
isMLJobDeletedSelector,
|
||||
isMLJobDeletingSelector,
|
||||
} from '../../../state/selectors';
|
||||
import { deleteMLJobAction, getExistingMLJobAction, resetMLState } from '../../../state/actions';
|
||||
import { ConfirmJobDeletion } from './confirm_delete';
|
||||
import { UptimeRefreshContext } from '../../../contexts';
|
||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
||||
import * as labels from './translations';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ManageMLJobComponent } from './manage_ml_job';
|
||||
import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer';
|
||||
|
||||
export const MLIntegrationComponent = () => {
|
||||
const [isMlFlyoutOpen, setIsMlFlyoutOpen] = useState(false);
|
||||
const [isConfirmDeleteJobOpen, setIsConfirmDeleteJobOpen] = useState(false);
|
||||
|
||||
const { lastRefresh, refreshApp } = useContext(UptimeRefreshContext);
|
||||
|
||||
const { notifications } = useKibana();
|
||||
|
||||
let { monitorId } = useParams();
|
||||
monitorId = atob(monitorId || '');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isMLAvailable = useSelector(hasMLFeatureAvailable);
|
||||
|
||||
const deleteMLJob = () => dispatch(deleteMLJobAction.get({ monitorId: monitorId as string }));
|
||||
const isMLJobDeleting = useSelector(isMLJobDeletingSelector);
|
||||
const { data: jobDeletionSuccess } = useSelector(isMLJobDeletedSelector);
|
||||
|
||||
const { data: uptimeJobs } = useSelector(hasMLJobSelector);
|
||||
|
||||
const hasMLJob =
|
||||
!!uptimeJobs?.jobsExist &&
|
||||
!!uptimeJobs.jobs.find((job: JobStat) => job.id === getMLJobId(monitorId as string));
|
||||
|
||||
useEffect(() => {
|
||||
if (isMLAvailable) {
|
||||
dispatch(getExistingMLJobAction.get({ monitorId: monitorId as string }));
|
||||
}
|
||||
}, [dispatch, isMLAvailable, monitorId, lastRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConfirmDeleteJobOpen && jobDeletionSuccess?.[getMLJobId(monitorId as string)]?.deleted) {
|
||||
setIsConfirmDeleteJobOpen(false);
|
||||
notifications.toasts.success({
|
||||
title: <p>{labels.JOB_DELETION}</p>,
|
||||
body: <p>{labels.JOB_DELETION_SUCCESS}</p>,
|
||||
toastLifeTimeMs: 3000,
|
||||
});
|
||||
dispatch(resetMLState());
|
||||
|
||||
refreshApp();
|
||||
}
|
||||
}, [
|
||||
isMLJobDeleting,
|
||||
isConfirmDeleteJobOpen,
|
||||
jobDeletionSuccess,
|
||||
monitorId,
|
||||
refreshApp,
|
||||
notifications.toasts,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
const onEnableJobClick = () => {
|
||||
setIsMlFlyoutOpen(true);
|
||||
};
|
||||
|
||||
const closeFlyout = () => {
|
||||
setIsMlFlyoutOpen(false);
|
||||
};
|
||||
|
||||
const confirmDeleteMLJob = () => {
|
||||
setIsConfirmDeleteJobOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ManageMLJobComponent
|
||||
hasMLJob={hasMLJob as boolean}
|
||||
onEnableJob={onEnableJobClick}
|
||||
onJobDelete={confirmDeleteMLJob}
|
||||
/>
|
||||
{isMlFlyoutOpen && <MachineLearningFlyout onClose={closeFlyout} />}
|
||||
{isConfirmDeleteJobOpen && (
|
||||
<ConfirmJobDeletion
|
||||
onConfirm={deleteMLJob}
|
||||
loading={isMLJobDeleting}
|
||||
onCancel={() => {
|
||||
setIsConfirmDeleteJobOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import url from 'url';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import { getMLJobId } from '../../../state/api/ml_anomaly';
|
||||
|
||||
interface Props {
|
||||
monitorId: string;
|
||||
basePath: string;
|
||||
dateRange: {
|
||||
to: string;
|
||||
from: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const getMLJobLinkHref = ({ basePath, monitorId, dateRange }: Props) => {
|
||||
const query = {
|
||||
ml: { jobIds: [getMLJobId(monitorId)] },
|
||||
refreshInterval: { pause: true, value: 0 },
|
||||
time: dateRange,
|
||||
};
|
||||
|
||||
const queryParams = {
|
||||
mlExplorerFilter: {
|
||||
filterActive: true,
|
||||
filteredFields: ['monitor.id', monitorId],
|
||||
},
|
||||
mlExplorerSwimlane: {
|
||||
viewByFieldName: 'observer.geo.name',
|
||||
},
|
||||
};
|
||||
|
||||
const path = '/explorer';
|
||||
|
||||
return url.format({
|
||||
pathname: basePath + '/app/ml',
|
||||
hash:
|
||||
`${path}?_g=${rison.encode(query as RisonValue)}` +
|
||||
(monitorId ? `&_a=${rison.encode(queryParams as RisonValue)}` : ''),
|
||||
});
|
||||
};
|
||||
|
||||
export const MLJobLink: React.FC<Props> = ({ basePath, monitorId, dateRange, children }) => {
|
||||
const href = getMLJobLinkHref({ basePath, monitorId, dateRange });
|
||||
return <EuiButtonEmpty children={children} size="s" href={href} target="_blank" />;
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const JOB_CREATED_SUCCESS_TITLE = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreatedNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Job successfully created',
|
||||
}
|
||||
);
|
||||
|
||||
export const JOB_CREATED_SUCCESS_MESSAGE = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreatedNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The analysis is now running for response duration chart. It might take a while before results are added to the response times graph.',
|
||||
}
|
||||
);
|
||||
|
||||
export const JOB_CREATION_FAILED = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Job creation failed',
|
||||
}
|
||||
);
|
||||
|
||||
export const JOB_CREATION_FAILED_MESSAGE = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreationFailedNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Your current license may not allow for creating machine learning jobs, or this job may already exist.',
|
||||
}
|
||||
);
|
||||
|
||||
export const JOB_DELETION = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobDeletionNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Job deleted',
|
||||
}
|
||||
);
|
||||
|
||||
export const JOB_DELETION_SUCCESS = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobDeletionSuccessNotificationText',
|
||||
{
|
||||
defaultMessage: 'Job is successfully deleted',
|
||||
}
|
||||
);
|
||||
|
||||
export const JOB_DELETION_CONFIRMATION = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobDeletionConfirmLabel',
|
||||
{
|
||||
defaultMessage: 'Delete anomaly detection job?',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_JOB = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText',
|
||||
{
|
||||
defaultMessage: 'View job',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPLORE_IN_ML_APP = i18n.translate('xpack.uptime.ml.durationChart.exploreInMlApp', {
|
||||
defaultMessage: 'Explore in ML App',
|
||||
});
|
||||
|
||||
export const ENABLE_ANOMALY_DETECTION = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle',
|
||||
{
|
||||
defaultMessage: 'Enable anomaly detection',
|
||||
}
|
||||
);
|
||||
|
||||
export const ANOMALY_DETECTION = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.anomalyDetectionTitle',
|
||||
{
|
||||
defaultMessage: 'Anomaly detection',
|
||||
}
|
||||
);
|
||||
|
||||
export const DISABLE_ANOMALY_DETECTION = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyDetectionTitle',
|
||||
{
|
||||
defaultMessage: 'Disable anomaly detection',
|
||||
}
|
||||
);
|
||||
|
||||
export const MANAGE_ANOMALY_DETECTION = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.manageAnomalyDetectionTitle',
|
||||
{
|
||||
defaultMessage: 'Manage anomaly detection',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_EXISTING_JOB = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText',
|
||||
{
|
||||
defaultMessage: 'View existing job',
|
||||
}
|
||||
);
|
||||
|
||||
export const ML_MANAGEMENT_PAGE = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText',
|
||||
{
|
||||
defaultMessage: 'Machine Learning jobs management page',
|
||||
}
|
||||
);
|
||||
|
||||
export const TAKE_SOME_TIME_TEXT = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.manageMLJobDescription.noteText',
|
||||
{
|
||||
defaultMessage: 'Note: It might take a few minutes for the job to begin calculating results.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_NEW_JOB = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.createNewJobButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create new job',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREAT_ML_JOB_DESC = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.createMLJobDescription',
|
||||
{
|
||||
defaultMessage: `Here you can create a machine learning job to calculate anomaly scores on
|
||||
response durations for Uptime Monitor. Once enabled, the monitor duration chart on the details page
|
||||
will show the expected bounds and annotate the graph with anomalies. You can also potentially
|
||||
identify periods of increased latency across geographical regions.`,
|
||||
}
|
||||
);
|
||||
|
||||
export const START_TRAIL = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.startTrial',
|
||||
{
|
||||
defaultMessage: 'Start free 14-day trial',
|
||||
}
|
||||
);
|
||||
|
||||
export const START_TRAIL_DESC = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.startTrialDesc',
|
||||
{
|
||||
defaultMessage:
|
||||
'In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license.',
|
||||
}
|
||||
);
|
|
@ -9,9 +9,11 @@ import { UptimeAppProps } from '../uptime_app';
|
|||
import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../common/constants';
|
||||
import { CommonlyUsedRange } from '../components/functional/uptime_date_picker';
|
||||
import { useUrlParams } from '../hooks';
|
||||
import { ILicense } from '../../../../../plugins/licensing/common/types';
|
||||
|
||||
export interface UptimeSettingsContextValues {
|
||||
basePath: string;
|
||||
license?: ILicense | null;
|
||||
dateRangeStart: string;
|
||||
dateRangeEnd: string;
|
||||
isApmAvailable: boolean;
|
||||
|
@ -39,14 +41,29 @@ const defaultContext: UptimeSettingsContextValues = {
|
|||
export const UptimeSettingsContext = createContext(defaultContext);
|
||||
|
||||
export const UptimeSettingsContextProvider: React.FC<UptimeAppProps> = ({ children, ...props }) => {
|
||||
const { basePath, isApmAvailable, isInfraAvailable, isLogsAvailable, commonlyUsedRanges } = props;
|
||||
const {
|
||||
basePath,
|
||||
isApmAvailable,
|
||||
isInfraAvailable,
|
||||
isLogsAvailable,
|
||||
commonlyUsedRanges,
|
||||
plugins,
|
||||
} = props;
|
||||
|
||||
const [getUrlParams] = useUrlParams();
|
||||
|
||||
const { dateRangeStart, dateRangeEnd } = getUrlParams();
|
||||
|
||||
let license: ILicense | null = null;
|
||||
|
||||
// @ts-ignore
|
||||
plugins.licensing.license$.subscribe((licenseItem: ILicense) => {
|
||||
license = licenseItem;
|
||||
});
|
||||
|
||||
const value = useMemo(() => {
|
||||
return {
|
||||
license,
|
||||
basePath,
|
||||
isApmAvailable,
|
||||
isInfraAvailable,
|
||||
|
@ -56,6 +73,7 @@ export const UptimeSettingsContextProvider: React.FC<UptimeAppProps> = ({ childr
|
|||
dateRangeEnd: dateRangeEnd ?? DATE_RANGE_END,
|
||||
};
|
||||
}, [
|
||||
license,
|
||||
basePath,
|
||||
isApmAvailable,
|
||||
isInfraAvailable,
|
||||
|
|
|
@ -10,5 +10,6 @@ export * from './ui';
|
|||
export * from './monitor_status';
|
||||
export * from './index_patternts';
|
||||
export * from './ping';
|
||||
export * from './ml_anomaly';
|
||||
export * from './monitor_duration';
|
||||
export * from './index_status';
|
||||
|
|
|
@ -7,4 +7,4 @@
|
|||
import { createAsyncAction } from './utils';
|
||||
import { StatesIndexStatus } from '../../../common/runtime_types';
|
||||
|
||||
export const indexStatusAction = createAsyncAction<StatesIndexStatus & Error>('GET INDEX STATUS');
|
||||
export const indexStatusAction = createAsyncAction<any, StatesIndexStatus>('GET INDEX STATUS');
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { createAction } from 'redux-actions';
|
||||
import { createAsyncAction } from './utils';
|
||||
import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges';
|
||||
import { AnomaliesTableRecord } from '../../../../../../plugins/ml/common/types/anomalies';
|
||||
import { CreateMLJobSuccess, DeleteJobResults, MonitorIdParam } from './types';
|
||||
import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer';
|
||||
|
||||
export const resetMLState = createAction('RESET_ML_STATE');
|
||||
|
||||
export const getExistingMLJobAction = createAsyncAction<MonitorIdParam, JobExistResult>(
|
||||
'GET_EXISTING_ML_JOB'
|
||||
);
|
||||
|
||||
export const createMLJobAction = createAsyncAction<MonitorIdParam, CreateMLJobSuccess | null>(
|
||||
'CREATE_ML_JOB'
|
||||
);
|
||||
|
||||
export const getMLCapabilitiesAction = createAsyncAction<any, PrivilegesResponse>(
|
||||
'GET_ML_CAPABILITIES'
|
||||
);
|
||||
|
||||
export const deleteMLJobAction = createAsyncAction<MonitorIdParam, DeleteJobResults>(
|
||||
'DELETE_ML_JOB'
|
||||
);
|
||||
|
||||
export interface AnomalyRecordsParams {
|
||||
dateStart: number;
|
||||
dateEnd: number;
|
||||
listOfMonitorIds: string[];
|
||||
anomalyThreshold?: number;
|
||||
}
|
||||
|
||||
export interface AnomalyRecords {
|
||||
anomalies: AnomaliesTableRecord[];
|
||||
interval: string;
|
||||
}
|
||||
|
||||
export const getAnomalyRecordsAction = createAsyncAction<AnomalyRecordsParams, AnomalyRecords>(
|
||||
'GET_ANOMALY_RECORDS'
|
||||
);
|
|
@ -7,6 +7,7 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { QueryParams } from './types';
|
||||
import { MonitorDurationResult } from '../../../common/types';
|
||||
import { IHttpFetchError } from '../../../../../../../target/types/core/public/http';
|
||||
|
||||
type MonitorQueryParams = QueryParams & { monitorId: string };
|
||||
|
||||
|
@ -14,4 +15,6 @@ export const getMonitorDurationAction = createAction<MonitorQueryParams>('GET_MO
|
|||
export const getMonitorDurationActionSuccess = createAction<MonitorDurationResult>(
|
||||
'GET_MONITOR_DURATION_SUCCESS'
|
||||
);
|
||||
export const getMonitorDurationActionFail = createAction<Error>('GET_MONITOR_DURATION_FAIL');
|
||||
export const getMonitorDurationActionFail = createAction<IHttpFetchError>(
|
||||
'GET_MONITOR_DURATION_FAIL'
|
||||
);
|
||||
|
|
|
@ -5,11 +5,21 @@
|
|||
*/
|
||||
|
||||
import { Action } from 'redux-actions';
|
||||
import { IHttpFetchError } from '../../../../../../../target/types/core/public/http';
|
||||
|
||||
export interface AsyncAction {
|
||||
get: (payload?: any) => Action<any>;
|
||||
success: (payload?: any) => Action<any>;
|
||||
fail: (payload?: any) => Action<any>;
|
||||
export interface AsyncAction<Payload, SuccessPayload> {
|
||||
get: (payload: Payload) => Action<Payload>;
|
||||
success: (payload: SuccessPayload) => Action<SuccessPayload>;
|
||||
fail: (payload: IHttpFetchError) => Action<IHttpFetchError>;
|
||||
}
|
||||
export interface AsyncAction1<Payload, SuccessPayload> {
|
||||
get: (payload?: Payload) => Action<Payload>;
|
||||
success: (payload: SuccessPayload) => Action<SuccessPayload>;
|
||||
fail: (payload: IHttpFetchError) => Action<IHttpFetchError>;
|
||||
}
|
||||
|
||||
export interface MonitorIdParam {
|
||||
monitorId: string;
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
|
@ -27,3 +37,15 @@ export interface MonitorDetailsActionPayload {
|
|||
dateEnd: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface CreateMLJobSuccess {
|
||||
count: number;
|
||||
jobId: string;
|
||||
}
|
||||
|
||||
export interface DeleteJobResults {
|
||||
[id: string]: {
|
||||
[status: string]: boolean;
|
||||
error?: any;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,12 +5,18 @@
|
|||
*/
|
||||
|
||||
import { createAction } from 'redux-actions';
|
||||
import { AsyncAction } from './types';
|
||||
import { AsyncAction, AsyncAction1 } from './types';
|
||||
import { IHttpFetchError } from '../../../../../../../target/types/core/public/http';
|
||||
|
||||
export function createAsyncAction<Payload>(actionStr: string): AsyncAction {
|
||||
export function createAsyncAction<Payload, SuccessPayload>(
|
||||
actionStr: string
|
||||
): AsyncAction1<Payload, SuccessPayload>;
|
||||
export function createAsyncAction<Payload, SuccessPayload>(
|
||||
actionStr: string
|
||||
): AsyncAction<Payload, SuccessPayload> {
|
||||
return {
|
||||
get: createAction<Payload>(actionStr),
|
||||
success: createAction<Payload>(`${actionStr}_SUCCESS`),
|
||||
fail: createAction<Payload>(`${actionStr}_FAIL`),
|
||||
success: createAction<SuccessPayload>(`${actionStr}_SUCCESS`),
|
||||
fail: createAction<IHttpFetchError>(`${actionStr}_FAIL`),
|
||||
};
|
||||
}
|
||||
|
|
88
x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts
Normal file
88
x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { apiService } from './utils';
|
||||
import { AnomalyRecords, AnomalyRecordsParams } from '../actions';
|
||||
import { API_URLS, INDEX_NAMES, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants';
|
||||
import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges';
|
||||
import { CreateMLJobSuccess, DeleteJobResults, MonitorIdParam } from '../actions/types';
|
||||
import { DataRecognizerConfigResponse } from '../../../../../../plugins/ml/common/types/modules';
|
||||
import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer';
|
||||
|
||||
export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`;
|
||||
|
||||
export const getMLCapabilities = async (): Promise<PrivilegesResponse> => {
|
||||
return await apiService.get(API_URLS.ML_CAPABILITIES);
|
||||
};
|
||||
|
||||
export const getExistingJobs = async (): Promise<JobExistResult> => {
|
||||
return await apiService.get(API_URLS.ML_MODULE_JOBS + ML_MODULE_ID);
|
||||
};
|
||||
|
||||
export const createMLJob = async ({
|
||||
monitorId,
|
||||
}: MonitorIdParam): Promise<CreateMLJobSuccess | null> => {
|
||||
const url = API_URLS.ML_SETUP_MODULE + ML_MODULE_ID;
|
||||
|
||||
const data = {
|
||||
prefix: `${monitorId}_`,
|
||||
useDedicatedIndex: false,
|
||||
startDatafeed: true,
|
||||
start: moment()
|
||||
.subtract(24, 'h')
|
||||
.valueOf(),
|
||||
indexPatternName: INDEX_NAMES.HEARTBEAT,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
'monitor.id': monitorId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response: DataRecognizerConfigResponse = await apiService.post(url, data);
|
||||
if (response?.jobs?.[0]?.id === getMLJobId(monitorId) && response?.jobs?.[0]?.success) {
|
||||
return {
|
||||
count: 1,
|
||||
jobId: response?.jobs?.[0]?.id,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteMLJob = async ({ monitorId }: MonitorIdParam): Promise<DeleteJobResults> => {
|
||||
const data = { jobIds: [getMLJobId(monitorId)] };
|
||||
|
||||
return await apiService.post(API_URLS.ML_DELETE_JOB, data);
|
||||
};
|
||||
|
||||
export const fetchAnomalyRecords = async ({
|
||||
dateStart,
|
||||
dateEnd,
|
||||
listOfMonitorIds,
|
||||
anomalyThreshold,
|
||||
}: AnomalyRecordsParams): Promise<AnomalyRecords> => {
|
||||
const data = {
|
||||
jobIds: listOfMonitorIds.map((monitorId: string) => getMLJobId(monitorId)),
|
||||
criteriaFields: [],
|
||||
influencers: [],
|
||||
aggregationInterval: 'auto',
|
||||
threshold: anomalyThreshold ?? 25,
|
||||
earliestMs: dateStart,
|
||||
latestMs: dateEnd,
|
||||
dateFormatTz: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
maxRecords: 500,
|
||||
maxExamples: 10,
|
||||
};
|
||||
return apiService.post(API_URLS.ML_ANOMALIES_RESULT, data);
|
||||
};
|
|
@ -7,6 +7,9 @@
|
|||
import { call, put } from 'redux-saga/effects';
|
||||
import { fetchEffectFactory } from '../fetch_effect';
|
||||
import { indexStatusAction } from '../../actions';
|
||||
import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error';
|
||||
import { StatesIndexStatus } from '../../../../common/runtime_types';
|
||||
import { fetchIndexStatus } from '../../api';
|
||||
|
||||
describe('fetch saga effect factory', () => {
|
||||
const asyncAction = indexStatusAction;
|
||||
|
@ -15,7 +18,7 @@ describe('fetch saga effect factory', () => {
|
|||
|
||||
it('works with success workflow', () => {
|
||||
const indexStatusResult = { indexExists: true, docCount: 2712532 };
|
||||
const fetchStatus = async () => {
|
||||
const fetchStatus = async (): Promise<StatesIndexStatus> => {
|
||||
return { indexExists: true, docCount: 2712532 };
|
||||
};
|
||||
fetchEffect = fetchEffectFactory(
|
||||
|
@ -25,6 +28,7 @@ describe('fetch saga effect factory', () => {
|
|||
)(calledAction);
|
||||
let next = fetchEffect.next();
|
||||
|
||||
// @ts-ignore TODO, dig deeper for TS issues here
|
||||
expect(next.value).toEqual(call(fetchStatus, calledAction.payload));
|
||||
|
||||
const successResult = put(asyncAction.success(indexStatusResult));
|
||||
|
@ -35,18 +39,21 @@ describe('fetch saga effect factory', () => {
|
|||
});
|
||||
|
||||
it('works with error workflow', () => {
|
||||
const indexStatusResultError = new Error('no heartbeat index found');
|
||||
const fetchStatus = async () => {
|
||||
return indexStatusResultError;
|
||||
};
|
||||
const indexStatusResultError = new HttpFetchError(
|
||||
'No heartbeat index found.',
|
||||
'error',
|
||||
{} as any
|
||||
);
|
||||
|
||||
fetchEffect = fetchEffectFactory(
|
||||
fetchStatus,
|
||||
fetchIndexStatus,
|
||||
asyncAction.success,
|
||||
asyncAction.fail
|
||||
)(calledAction);
|
||||
let next = fetchEffect.next();
|
||||
|
||||
expect(next.value).toEqual(call(fetchStatus, calledAction.payload));
|
||||
// @ts-ignore TODO, dig deeper for TS issues here
|
||||
expect(next.value).toEqual(call(fetchIndexStatus, calledAction.payload));
|
||||
|
||||
const errorResult = put(asyncAction.fail(indexStatusResultError));
|
||||
|
||||
|
@ -56,18 +63,21 @@ describe('fetch saga effect factory', () => {
|
|||
});
|
||||
|
||||
it('works with throw error workflow', () => {
|
||||
const unExpectedError = new Error('no url found, so throw error');
|
||||
const fetchStatus = async () => {
|
||||
return await fetch('/some/url');
|
||||
};
|
||||
const unExpectedError = new HttpFetchError(
|
||||
'No url found for the call, so throw error.',
|
||||
'error',
|
||||
{} as any
|
||||
);
|
||||
|
||||
fetchEffect = fetchEffectFactory(
|
||||
fetchStatus,
|
||||
fetchIndexStatus,
|
||||
asyncAction.success,
|
||||
asyncAction.fail
|
||||
)(calledAction);
|
||||
let next = fetchEffect.next();
|
||||
|
||||
expect(next.value).toEqual(call(fetchStatus, calledAction.payload));
|
||||
// @ts-ignore TODO, dig deeper for TS issues here
|
||||
expect(next.value).toEqual(call(fetchIndexStatus, calledAction.payload));
|
||||
|
||||
const unexpectedErrorResult = put(asyncAction.fail(unExpectedError));
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { Action } from 'redux-actions';
|
||||
import { IHttpFetchError } from '../../../../../../../target/types/core/public/http';
|
||||
|
||||
/**
|
||||
* Factory function for a fetch effect. It expects three action creators,
|
||||
|
@ -21,24 +22,23 @@ import { Action } from 'redux-actions';
|
|||
export function fetchEffectFactory<T, R, S, F>(
|
||||
fetch: (request: T) => Promise<R>,
|
||||
success: (response: R) => Action<S>,
|
||||
fail: (error: Error) => Action<F>
|
||||
fail: (error: IHttpFetchError) => Action<F>
|
||||
) {
|
||||
return function*(action: Action<T>) {
|
||||
try {
|
||||
const response = yield call(fetch, action.payload);
|
||||
|
||||
if (response instanceof Error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(response);
|
||||
|
||||
yield put(fail(response));
|
||||
yield put(fail(response as IHttpFetchError));
|
||||
} else {
|
||||
yield put(success(response));
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
yield put(fail(error));
|
||||
yield put(fail(error as IHttpFetchError));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { fetchDynamicSettingsEffect, setDynamicSettingsEffect } from './dynamic_
|
|||
import { fetchIndexPatternEffect } from './index_pattern';
|
||||
import { fetchPingHistogramEffect } from './ping';
|
||||
import { fetchMonitorDurationEffect } from './monitor_duration';
|
||||
import { fetchMLJobEffect } from './ml_anomaly';
|
||||
import { fetchIndexStatusEffect } from './index_status';
|
||||
|
||||
export function* rootEffect() {
|
||||
|
@ -24,6 +25,7 @@ export function* rootEffect() {
|
|||
yield fork(setDynamicSettingsEffect);
|
||||
yield fork(fetchIndexPatternEffect);
|
||||
yield fork(fetchPingHistogramEffect);
|
||||
yield fork(fetchMLJobEffect);
|
||||
yield fork(fetchMonitorDurationEffect);
|
||||
yield fork(fetchIndexStatusEffect);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { takeLatest } from 'redux-saga/effects';
|
||||
import {
|
||||
getMLCapabilitiesAction,
|
||||
getExistingMLJobAction,
|
||||
createMLJobAction,
|
||||
getAnomalyRecordsAction,
|
||||
deleteMLJobAction,
|
||||
} from '../actions';
|
||||
import { fetchEffectFactory } from './fetch_effect';
|
||||
import {
|
||||
getExistingJobs,
|
||||
createMLJob,
|
||||
fetchAnomalyRecords,
|
||||
deleteMLJob,
|
||||
getMLCapabilities,
|
||||
} from '../api/ml_anomaly';
|
||||
|
||||
export function* fetchMLJobEffect() {
|
||||
yield takeLatest(
|
||||
getExistingMLJobAction.get,
|
||||
fetchEffectFactory(getExistingJobs, getExistingMLJobAction.success, getExistingMLJobAction.fail)
|
||||
);
|
||||
yield takeLatest(
|
||||
createMLJobAction.get,
|
||||
fetchEffectFactory(createMLJob, createMLJobAction.success, createMLJobAction.fail)
|
||||
);
|
||||
yield takeLatest(
|
||||
getAnomalyRecordsAction.get,
|
||||
fetchEffectFactory(
|
||||
fetchAnomalyRecords,
|
||||
getAnomalyRecordsAction.success,
|
||||
getAnomalyRecordsAction.fail
|
||||
)
|
||||
);
|
||||
yield takeLatest(
|
||||
deleteMLJobAction.get,
|
||||
fetchEffectFactory(deleteMLJob, deleteMLJobAction.success, deleteMLJobAction.fail)
|
||||
);
|
||||
yield takeLatest(
|
||||
getMLCapabilitiesAction.get,
|
||||
fetchEffectFactory(
|
||||
getMLCapabilities,
|
||||
getMLCapabilitiesAction.success,
|
||||
getMLCapabilitiesAction.fail
|
||||
)
|
||||
);
|
||||
}
|
|
@ -15,6 +15,7 @@ import { indexPatternReducer } from './index_pattern';
|
|||
import { pingReducer } from './ping';
|
||||
import { monitorDurationReducer } from './monitor_duration';
|
||||
import { indexStatusReducer } from './index_status';
|
||||
import { mlJobsReducer } from './ml_anomaly';
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
monitor: monitorReducer,
|
||||
|
@ -25,6 +26,7 @@ export const rootReducer = combineReducers({
|
|||
dynamicSettings: dynamicSettingsReducer,
|
||||
indexPattern: indexPatternReducer,
|
||||
ping: pingReducer,
|
||||
ml: mlJobsReducer,
|
||||
monitorDuration: monitorDurationReducer,
|
||||
indexStatus: indexStatusReducer,
|
||||
});
|
||||
|
|
|
@ -6,25 +6,23 @@
|
|||
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { indexStatusAction } from '../actions';
|
||||
import { handleAsyncAction } from './utils';
|
||||
import { IReducerState } from './types';
|
||||
import { getAsyncInitialState, handleAsyncAction } from './utils';
|
||||
import { AsyncInitialState } from './types';
|
||||
import { StatesIndexStatus } from '../../../common/runtime_types';
|
||||
|
||||
export interface IndexStatusState extends IReducerState {
|
||||
data: StatesIndexStatus | null;
|
||||
export interface IndexStatusState {
|
||||
indexStatus: AsyncInitialState<StatesIndexStatus | null>;
|
||||
}
|
||||
|
||||
const initialState: IndexStatusState = {
|
||||
data: null,
|
||||
loading: false,
|
||||
errors: [],
|
||||
indexStatus: getAsyncInitialState(),
|
||||
};
|
||||
|
||||
type PayLoad = StatesIndexStatus & Error;
|
||||
|
||||
export const indexStatusReducer = handleActions<IndexStatusState, PayLoad>(
|
||||
{
|
||||
...handleAsyncAction('data', indexStatusAction),
|
||||
...handleAsyncAction('indexStatus', indexStatusAction),
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { handleActions } from 'redux-actions';
|
||||
import {
|
||||
getExistingMLJobAction,
|
||||
createMLJobAction,
|
||||
getAnomalyRecordsAction,
|
||||
deleteMLJobAction,
|
||||
resetMLState,
|
||||
AnomalyRecords,
|
||||
getMLCapabilitiesAction,
|
||||
} from '../actions';
|
||||
import { getAsyncInitialState, handleAsyncAction } from './utils';
|
||||
import { IHttpFetchError } from '../../../../../../../target/types/core/public/http';
|
||||
import { AsyncInitialState } from './types';
|
||||
import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges';
|
||||
import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types';
|
||||
import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer';
|
||||
|
||||
export interface MLJobState {
|
||||
mlJob: AsyncInitialState<JobExistResult>;
|
||||
createJob: AsyncInitialState<CreateMLJobSuccess>;
|
||||
deleteJob: AsyncInitialState<DeleteJobResults>;
|
||||
anomalies: AsyncInitialState<AnomalyRecords>;
|
||||
mlCapabilities: AsyncInitialState<PrivilegesResponse>;
|
||||
}
|
||||
|
||||
const initialState: MLJobState = {
|
||||
mlJob: getAsyncInitialState(),
|
||||
createJob: getAsyncInitialState(),
|
||||
deleteJob: getAsyncInitialState(),
|
||||
anomalies: getAsyncInitialState(),
|
||||
mlCapabilities: getAsyncInitialState(),
|
||||
};
|
||||
|
||||
type Payload = IHttpFetchError;
|
||||
|
||||
export const mlJobsReducer = handleActions<MLJobState>(
|
||||
{
|
||||
...handleAsyncAction<MLJobState, Payload>('mlJob', getExistingMLJobAction),
|
||||
...handleAsyncAction<MLJobState, Payload>('mlCapabilities', getMLCapabilitiesAction),
|
||||
...handleAsyncAction<MLJobState, Payload>('createJob', createMLJobAction),
|
||||
...handleAsyncAction<MLJobState, Payload>('deleteJob', deleteMLJobAction),
|
||||
...handleAsyncAction<MLJobState, Payload>('anomalies', getAnomalyRecordsAction),
|
||||
...{
|
||||
[String(resetMLState)]: state => ({
|
||||
...state,
|
||||
mlJob: {
|
||||
loading: false,
|
||||
data: null,
|
||||
error: null,
|
||||
},
|
||||
createJob: {
|
||||
data: null,
|
||||
error: null,
|
||||
loading: false,
|
||||
},
|
||||
deleteJob: {
|
||||
data: null,
|
||||
error: null,
|
||||
loading: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
initialState
|
||||
);
|
|
@ -13,13 +13,13 @@ import {
|
|||
import { MonitorDurationResult } from '../../../common/types';
|
||||
|
||||
export interface MonitorDuration {
|
||||
monitor_duration: MonitorDurationResult | null;
|
||||
durationLines: MonitorDurationResult | null;
|
||||
errors: any[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const initialState: MonitorDuration = {
|
||||
monitor_duration: null,
|
||||
durationLines: null,
|
||||
loading: false,
|
||||
errors: [],
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ export const monitorDurationReducer = handleActions<MonitorDuration, PayLoad>(
|
|||
) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
monitor_duration: { ...action.payload },
|
||||
durationLines: { ...action.payload },
|
||||
}),
|
||||
|
||||
[String(getMonitorDurationActionFail)]: (state: MonitorDuration, action: Action<Error>) => ({
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface IReducerState {
|
||||
errors: Error[];
|
||||
import { IHttpFetchError } from '../../../../../../../target/types/core/public/http';
|
||||
|
||||
export interface AsyncInitialState<ReduceStateType> {
|
||||
data: ReduceStateType | null;
|
||||
loading: boolean;
|
||||
error?: IHttpFetchError | null;
|
||||
}
|
||||
|
|
|
@ -6,28 +6,45 @@
|
|||
|
||||
import { Action } from 'redux-actions';
|
||||
import { AsyncAction } from '../actions/types';
|
||||
import { IReducerState } from './types';
|
||||
|
||||
export function handleAsyncAction<ReducerState extends IReducerState, Payload>(
|
||||
export function handleAsyncAction<ReducerState, Payload>(
|
||||
storeKey: string,
|
||||
asyncAction: AsyncAction
|
||||
asyncAction: AsyncAction<any, any>
|
||||
) {
|
||||
return {
|
||||
[String(asyncAction.get)]: (state: ReducerState) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
[storeKey]: {
|
||||
...(state as any)[storeKey],
|
||||
loading: true,
|
||||
},
|
||||
}),
|
||||
|
||||
[String(asyncAction.success)]: (state: ReducerState, action: Action<any>) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
[storeKey]: action.payload === null ? action.payload : { ...action.payload },
|
||||
[storeKey]: {
|
||||
...(state as any)[storeKey],
|
||||
data: action.payload === null ? action.payload : { ...action.payload },
|
||||
loading: false,
|
||||
},
|
||||
}),
|
||||
|
||||
[String(asyncAction.fail)]: (state: ReducerState, action: Action<any>) => ({
|
||||
...state,
|
||||
errors: [...state.errors, action.payload],
|
||||
loading: false,
|
||||
[storeKey]: {
|
||||
...(state as any)[storeKey],
|
||||
data: null,
|
||||
error: action.payload,
|
||||
loading: false,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function getAsyncInitialState(initialData = null) {
|
||||
return {
|
||||
data: initialData,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -60,14 +60,28 @@ describe('state selectors', () => {
|
|||
errors: [],
|
||||
},
|
||||
monitorDuration: {
|
||||
monitor_duration: null,
|
||||
durationLines: null,
|
||||
loading: false,
|
||||
errors: [],
|
||||
},
|
||||
ml: {
|
||||
mlJob: {
|
||||
data: null,
|
||||
loading: false,
|
||||
},
|
||||
createJob: { data: null, loading: false },
|
||||
deleteJob: { data: null, loading: false },
|
||||
mlCapabilities: { data: null, loading: false },
|
||||
anomalies: {
|
||||
data: null,
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
indexStatus: {
|
||||
loading: false,
|
||||
data: null,
|
||||
errors: [],
|
||||
indexStatus: {
|
||||
data: null,
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { AppState } from '../../state';
|
||||
|
||||
// UI Selectors
|
||||
|
@ -21,13 +22,9 @@ export const monitorLocationsSelector = (state: AppState, monitorId: string) =>
|
|||
return state.monitor.monitorLocationsList?.get(monitorId);
|
||||
};
|
||||
|
||||
export const selectSelectedMonitor = (state: AppState) => {
|
||||
return state.monitorStatus.monitor;
|
||||
};
|
||||
export const selectSelectedMonitor = (state: AppState) => state.monitorStatus.monitor;
|
||||
|
||||
export const selectMonitorStatus = (state: AppState) => {
|
||||
return state.monitorStatus.status;
|
||||
};
|
||||
export const selectMonitorStatus = (state: AppState) => state.monitorStatus.status;
|
||||
|
||||
export const selectDynamicSettings = (state: AppState) => {
|
||||
return state.dynamicSettings;
|
||||
|
@ -46,6 +43,36 @@ export const selectPingHistogram = ({ ping, ui }: AppState) => {
|
|||
};
|
||||
};
|
||||
|
||||
const mlCapabilitiesSelector = (state: AppState) => state.ml.mlCapabilities.data;
|
||||
|
||||
export const hasMLFeatureAvailable = createSelector(
|
||||
mlCapabilitiesSelector,
|
||||
mlCapabilities =>
|
||||
mlCapabilities?.isPlatinumOrTrialLicense && mlCapabilities?.mlFeatureEnabledInSpace
|
||||
);
|
||||
|
||||
export const canCreateMLJobSelector = createSelector(
|
||||
mlCapabilitiesSelector,
|
||||
mlCapabilities => mlCapabilities?.capabilities.canCreateJob
|
||||
);
|
||||
|
||||
export const canDeleteMLJobSelector = createSelector(
|
||||
mlCapabilitiesSelector,
|
||||
mlCapabilities => mlCapabilities?.capabilities.canDeleteJob
|
||||
);
|
||||
|
||||
export const hasMLJobSelector = ({ ml }: AppState) => ml.mlJob;
|
||||
|
||||
export const hasNewMLJobSelector = ({ ml }: AppState) => ml.createJob;
|
||||
|
||||
export const isMLJobCreatingSelector = ({ ml }: AppState) => ml.createJob.loading;
|
||||
|
||||
export const isMLJobDeletingSelector = ({ ml }: AppState) => ml.deleteJob.loading;
|
||||
|
||||
export const isMLJobDeletedSelector = ({ ml }: AppState) => ml.deleteJob;
|
||||
|
||||
export const anomaliesSelector = ({ ml }: AppState) => ml.anomalies.data;
|
||||
|
||||
export const selectDurationLines = ({ monitorDuration }: AppState) => {
|
||||
return monitorDuration;
|
||||
};
|
||||
|
@ -60,5 +87,5 @@ export const selectMonitorStatusAlert = ({ indexPattern, overviewFilters, ui }:
|
|||
});
|
||||
|
||||
export const indexStatusSelector = ({ indexStatus }: AppState) => {
|
||||
return indexStatus;
|
||||
return indexStatus.indexStatus;
|
||||
};
|
||||
|
|
|
@ -326,7 +326,7 @@
|
|||
"redux": "^4.0.5",
|
||||
"redux-actions": "^2.6.5",
|
||||
"redux-observable": "^1.2.0",
|
||||
"redux-saga": "^0.16.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"redux-thunks": "^1.0.0",
|
||||
"request": "^2.88.0",
|
||||
|
|
|
@ -36,3 +36,20 @@ export interface AnomalyRecordDoc {
|
|||
// TODO provide the causes resource interface.
|
||||
causes?: any[];
|
||||
}
|
||||
|
||||
export interface AnomaliesTableRecord {
|
||||
time: number;
|
||||
source: AnomalyRecordDoc;
|
||||
rowId: string;
|
||||
jobId: string;
|
||||
detectorIndex: number;
|
||||
severity: number;
|
||||
entityName?: string;
|
||||
entityValue?: any;
|
||||
influencers?: Array<{ [key: string]: any }>;
|
||||
actual?: number[];
|
||||
actualSort?: any;
|
||||
typical?: number[];
|
||||
typicalSort?: any;
|
||||
metricDescriptionSort?: number;
|
||||
}
|
||||
|
|
17
x-pack/plugins/ml/common/types/data_recognizer.ts
Normal file
17
x-pack/plugins/ml/common/types/data_recognizer.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 interface JobStat {
|
||||
id: string;
|
||||
earliestTimestampMs: number;
|
||||
latestTimestampMs: number;
|
||||
latestResultsTimestampMs: number;
|
||||
}
|
||||
|
||||
export interface JobExistResult {
|
||||
jobsExist: boolean;
|
||||
jobs: JobStat[];
|
||||
}
|
|
@ -35,6 +35,7 @@ import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_lim
|
|||
import { fieldsServiceProvider } from '../fields_service';
|
||||
import { jobServiceProvider } from '../job_service';
|
||||
import { resultsServiceProvider } from '../results_service';
|
||||
import { JobExistResult, JobStat } from '../../../common/types/data_recognizer';
|
||||
|
||||
const ML_DIR = 'ml';
|
||||
const KIBANA_DIR = 'kibana';
|
||||
|
@ -80,18 +81,6 @@ export interface RecognizeResult {
|
|||
logo: { icon: string } | null;
|
||||
}
|
||||
|
||||
interface JobStat {
|
||||
id: string;
|
||||
earliestTimestampMs: number;
|
||||
latestTimestampMs: number;
|
||||
latestResultsTimestampMs: number;
|
||||
}
|
||||
|
||||
interface JobExistResult {
|
||||
jobsExist: boolean;
|
||||
jobs: JobStat[];
|
||||
}
|
||||
|
||||
interface ObjectExistResult {
|
||||
id: string;
|
||||
type: string;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"icon": "uptimeApp"
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"id": "uptime_heartbeat",
|
||||
"title": "Uptime: Heartbeat",
|
||||
"description": "Detect latency issues in heartbeat monitors.",
|
||||
"type": "Heartbeat data",
|
||||
"logoFile": "logo.json",
|
||||
"defaultIndexPattern": "heartbeat-*",
|
||||
"query": {
|
||||
"bool": {
|
||||
"filter": [{ "term": { "agent.type": "heartbeat" } }]
|
||||
}
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"id": "high_latency_by_geo",
|
||||
"file": "high_latency_by_geo.json"
|
||||
}
|
||||
],
|
||||
"datafeeds": [
|
||||
{
|
||||
"id": "datafeed-high_latency_by_geo",
|
||||
"file": "datafeed_high_latency_by_geo.json",
|
||||
"job_id": "high_latency_by_geo"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"job_id": "JOB_ID",
|
||||
"indices": ["INDEX_PATTERN_NAME"],
|
||||
"max_empty_searches": 10,
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": [
|
||||
{ "bool": { "filter": { "term": { "agent.type": "heartbeat" } } } },
|
||||
{ "bool": { "filter": { "term": { "event.dataset": "uptime" } } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"job_type": "anomaly_detector",
|
||||
"groups": ["uptime"],
|
||||
"description": "Uptime Heartbeat: Identify periods of increased latency across geographical regions.",
|
||||
"analysis_config": {
|
||||
"bucket_span": "15m",
|
||||
"detectors": [
|
||||
{
|
||||
"detector_description": "increased latency",
|
||||
"function": "high_mean",
|
||||
"field_name": "monitor.duration.us",
|
||||
"partition_field_name": "observer.geo.name",
|
||||
"use_null": true
|
||||
}
|
||||
],
|
||||
"influencers": ["monitor.id", "monitor.name", "observer.geo.name"]
|
||||
},
|
||||
"allow_lazy_open": true,
|
||||
"analysis_limits": {
|
||||
"model_memory_limit": "32mb"
|
||||
},
|
||||
"data_description": {
|
||||
"time_field": "@timestamp"
|
||||
},
|
||||
"custom_settings": {
|
||||
"created_by": "ml-module-uptime-heartbeat",
|
||||
"custom_urls": []
|
||||
}
|
||||
}
|
|
@ -4,24 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AnomalyRecordDoc } from '../../../common/types/anomalies';
|
||||
|
||||
export interface AnomaliesTableRecord {
|
||||
time: number;
|
||||
source: AnomalyRecordDoc;
|
||||
rowId: string;
|
||||
jobId: string;
|
||||
detectorIndex: number;
|
||||
severity: number;
|
||||
entityName?: string;
|
||||
entityValue?: any;
|
||||
influencers?: Array<{ [key: string]: any }>;
|
||||
actual?: number[];
|
||||
actualSort?: any;
|
||||
typical?: number[];
|
||||
typicalSort?: any;
|
||||
metricDescriptionSort?: number;
|
||||
}
|
||||
import { AnomaliesTableRecord } from '../../../common/types/anomalies';
|
||||
|
||||
export function buildAnomalyTableItems(
|
||||
anomalyRecords: any,
|
||||
|
|
|
@ -8,11 +8,11 @@ import _ from 'lodash';
|
|||
import moment from 'moment';
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
import { APICaller } from 'kibana/server';
|
||||
import { buildAnomalyTableItems, AnomaliesTableRecord } from './build_anomaly_table_items';
|
||||
import { buildAnomalyTableItems } from './build_anomaly_table_items';
|
||||
import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns';
|
||||
import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search';
|
||||
import { getPartitionFieldsValuesFactory } from './get_partition_fields_values';
|
||||
import { AnomalyRecordDoc } from '../../../common/types/anomalies';
|
||||
import { AnomaliesTableRecord, AnomalyRecordDoc } from '../../../common/types/anomalies';
|
||||
|
||||
// Service for carrying out Elasticsearch queries to obtain data for the
|
||||
// ML Results dashboards.
|
||||
|
|
73
yarn.lock
73
yarn.lock
|
@ -3078,6 +3078,50 @@
|
|||
react-lifecycles-compat "^3.0.4"
|
||||
warning "^3.0.0"
|
||||
|
||||
"@redux-saga/core@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4"
|
||||
integrity sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.3"
|
||||
"@redux-saga/deferred" "^1.1.2"
|
||||
"@redux-saga/delay-p" "^1.1.2"
|
||||
"@redux-saga/is" "^1.1.2"
|
||||
"@redux-saga/symbols" "^1.1.2"
|
||||
"@redux-saga/types" "^1.1.0"
|
||||
redux "^4.0.4"
|
||||
typescript-tuple "^2.2.1"
|
||||
|
||||
"@redux-saga/deferred@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/deferred/-/deferred-1.1.2.tgz#59937a0eba71fff289f1310233bc518117a71888"
|
||||
integrity sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ==
|
||||
|
||||
"@redux-saga/delay-p@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/delay-p/-/delay-p-1.1.2.tgz#8f515f4b009b05b02a37a7c3d0ca9ddc157bb355"
|
||||
integrity sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==
|
||||
dependencies:
|
||||
"@redux-saga/symbols" "^1.1.2"
|
||||
|
||||
"@redux-saga/is@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/is/-/is-1.1.2.tgz#ae6c8421f58fcba80faf7cadb7d65b303b97e58e"
|
||||
integrity sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==
|
||||
dependencies:
|
||||
"@redux-saga/symbols" "^1.1.2"
|
||||
"@redux-saga/types" "^1.1.0"
|
||||
|
||||
"@redux-saga/symbols@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/symbols/-/symbols-1.1.2.tgz#216a672a487fc256872b8034835afc22a2d0595d"
|
||||
integrity sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ==
|
||||
|
||||
"@redux-saga/types@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.1.0.tgz#0e81ce56b4883b4b2a3001ebe1ab298b84237204"
|
||||
integrity sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==
|
||||
|
||||
"@samverschueren/stream-to-observable@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
|
||||
|
@ -25312,10 +25356,12 @@ redux-observable@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.2.0.tgz#ff51b6c6be2598e9b5e89fc36639186bb0e669c7"
|
||||
integrity sha512-yeR90RP2WzZzCxxnQPlh2uFzyfFLsfXu8ROh53jGDPXVqj71uNDMmvi/YKQkd9ofiVoO4OYb1snbowO49tCEMg==
|
||||
|
||||
redux-saga@^0.16.0:
|
||||
version "0.16.2"
|
||||
resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-0.16.2.tgz#993662e86bc945d8509ac2b8daba3a8c615cc971"
|
||||
integrity sha512-iIjKnRThI5sKPEASpUvySemjzwqwI13e3qP7oLub+FycCRDysLSAOwt958niZW6LhxfmS6Qm1BzbU70w/Koc4w==
|
||||
redux-saga@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.1.3.tgz#9f3e6aebd3c994bbc0f6901a625f9a42b51d1112"
|
||||
integrity sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==
|
||||
dependencies:
|
||||
"@redux-saga/core" "^1.1.3"
|
||||
|
||||
redux-thunk@2.2.0:
|
||||
version "2.2.0"
|
||||
|
@ -29889,6 +29935,13 @@ typedarray@^0.0.6, typedarray@~0.0.5:
|
|||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript-compare@^0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript-compare/-/typescript-compare-0.0.2.tgz#7ee40a400a406c2ea0a7e551efd3309021d5f425"
|
||||
integrity sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==
|
||||
dependencies:
|
||||
typescript-logic "^0.0.0"
|
||||
|
||||
typescript-fsa-reducers@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-fsa-reducers/-/typescript-fsa-reducers-1.2.1.tgz#2af1a85f7b88fb0dfb9fa59d5da51a5d7ac6543f"
|
||||
|
@ -29899,6 +29952,18 @@ typescript-fsa@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-3.0.0.tgz#3ad1cb915a67338e013fc21f67c9b3e0e110c912"
|
||||
integrity sha512-xiXAib35i0QHl/+wMobzPibjAH5TJLDj+qGq5jwVLG9qR4FUswZURBw2qihBm0m06tHoyb3FzpnJs1GRhRwVag==
|
||||
|
||||
typescript-logic@^0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-logic/-/typescript-logic-0.0.0.tgz#66ebd82a2548f2b444a43667bec120b496890196"
|
||||
integrity sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==
|
||||
|
||||
typescript-tuple@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-tuple/-/typescript-tuple-2.2.1.tgz#7d9813fb4b355f69ac55032e0363e8bb0f04dad2"
|
||||
integrity sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==
|
||||
dependencies:
|
||||
typescript-compare "^0.0.2"
|
||||
|
||||
typescript@3.7.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.7.2:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue