mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Change Point Detection (#144093)
## Summary Adds a Change point detection page in the AIOps labs. _Note:_ This page will be hidden under the hardcoded feature flag for 8.6. <img width="1775" alt="image" src="https://user-images.githubusercontent.com/5236598/199506277-f0d71104-3098-4e15-a697-35f5eec5c110.png"> ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
parent
aef304e147
commit
3e22323e00
46 changed files with 1805 additions and 66 deletions
|
@ -12,6 +12,7 @@
|
|||
"requiredPlugins": [
|
||||
"charts",
|
||||
"data",
|
||||
"lens",
|
||||
"licensing"
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
|
|
|
@ -204,7 +204,7 @@ export function getEsQueryFromSavedSearch({
|
|||
};
|
||||
}
|
||||
|
||||
// If saved search available, merge saved search with latest user query or filters
|
||||
// If saved search available, merge saved search with the latest user query or filters
|
||||
// which might differ from extracted saved search data
|
||||
if (savedSearchData) {
|
||||
const globalFilters = filterManager?.getGlobalFilters();
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { type DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { startWith } from 'rxjs';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import type { Query, Filter } from '@kbn/es-query';
|
||||
import {
|
||||
createMergedEsQuery,
|
||||
getEsQueryFromSavedSearch,
|
||||
} from '../../application/utils/search_utils';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import { useTimefilter, useTimeRangeUpdates } from '../../hooks/use_time_filter';
|
||||
import { useChangePointResults } from './use_change_point_agg_request';
|
||||
import { type TimeBuckets, TimeBucketsInterval } from '../../../common/time_buckets';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { usePageUrlState } from '../../hooks/use_url_state';
|
||||
import { useTimeBuckets } from '../../hooks/use_time_buckets';
|
||||
|
||||
export interface ChangePointDetectionRequestParams {
|
||||
fn: string;
|
||||
splitField: string;
|
||||
metricField: string;
|
||||
interval: string;
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
export const ChangePointDetectionContext = createContext<{
|
||||
timeBuckets: TimeBuckets;
|
||||
bucketInterval: TimeBucketsInterval;
|
||||
requestParams: ChangePointDetectionRequestParams;
|
||||
metricFieldOptions: DataViewField[];
|
||||
splitFieldsOptions: DataViewField[];
|
||||
updateRequestParams: (update: Partial<ChangePointDetectionRequestParams>) => void;
|
||||
isLoading: boolean;
|
||||
annotations: ChangePointAnnotation[];
|
||||
resultFilters: Filter[];
|
||||
updateFilters: (update: Filter[]) => void;
|
||||
resultQuery: Query;
|
||||
progress: number;
|
||||
pagination: {
|
||||
activePage: number;
|
||||
pageCount: number;
|
||||
updatePagination: (newPage: number) => void;
|
||||
};
|
||||
}>({
|
||||
isLoading: false,
|
||||
splitFieldsOptions: [],
|
||||
metricFieldOptions: [],
|
||||
requestParams: {} as ChangePointDetectionRequestParams,
|
||||
timeBuckets: {} as TimeBuckets,
|
||||
bucketInterval: {} as TimeBucketsInterval,
|
||||
updateRequestParams: () => {},
|
||||
annotations: [],
|
||||
resultFilters: [],
|
||||
updateFilters: () => {},
|
||||
resultQuery: { query: '', language: 'kuery' },
|
||||
progress: 0,
|
||||
pagination: {
|
||||
activePage: 0,
|
||||
pageCount: 1,
|
||||
updatePagination: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
export type ChangePointType =
|
||||
| 'dip'
|
||||
| 'spike'
|
||||
| 'distribution_change'
|
||||
| 'step_change'
|
||||
| 'trend_change'
|
||||
| 'stationary'
|
||||
| 'non_stationary'
|
||||
| 'indeterminable';
|
||||
|
||||
export interface ChangePointAnnotation {
|
||||
label: string;
|
||||
reason: string;
|
||||
timestamp: string;
|
||||
group_field: string;
|
||||
type: ChangePointType;
|
||||
p_value: number;
|
||||
}
|
||||
|
||||
const DEFAULT_AGG_FUNCTION = 'min';
|
||||
|
||||
export const ChangePointDetectionContextProvider: FC = ({ children }) => {
|
||||
const { dataView, savedSearch } = useDataSource();
|
||||
const {
|
||||
uiSettings,
|
||||
data: {
|
||||
query: { filterManager },
|
||||
},
|
||||
} = useAiopsAppContext();
|
||||
|
||||
const savedSearchQuery = useMemo(() => {
|
||||
return getEsQueryFromSavedSearch({
|
||||
dataView,
|
||||
uiSettings,
|
||||
savedSearch,
|
||||
filterManager,
|
||||
});
|
||||
}, [dataView, savedSearch, uiSettings, filterManager]);
|
||||
|
||||
const timefilter = useTimefilter();
|
||||
const timeBuckets = useTimeBuckets();
|
||||
const [resultFilters, setResultFilter] = useState<Filter[]>([]);
|
||||
|
||||
const [bucketInterval, setBucketInterval] = useState<TimeBucketsInterval>();
|
||||
|
||||
const timeRange = useTimeRangeUpdates();
|
||||
|
||||
useMount(function updateIntervalOnTimeBoundsChange() {
|
||||
const timeUpdateSubscription = timefilter
|
||||
.getTimeUpdate$()
|
||||
.pipe(startWith(timefilter.getTime()))
|
||||
.subscribe(() => {
|
||||
const activeBounds = timefilter.getActiveBounds();
|
||||
if (!activeBounds) {
|
||||
throw new Error('Time bound not available');
|
||||
}
|
||||
timeBuckets.setInterval('auto');
|
||||
timeBuckets.setBounds(activeBounds);
|
||||
setBucketInterval(timeBuckets.getInterval());
|
||||
});
|
||||
return () => {
|
||||
timeUpdateSubscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
const metricFieldOptions = useMemo<DataViewField[]>(() => {
|
||||
return dataView.fields.filter(({ aggregatable, type }) => aggregatable && type === 'number');
|
||||
}, [dataView]);
|
||||
|
||||
const splitFieldsOptions = useMemo<DataViewField[]>(() => {
|
||||
return dataView.fields.filter(
|
||||
({ aggregatable, esTypes, displayName }) =>
|
||||
aggregatable &&
|
||||
esTypes &&
|
||||
esTypes.includes('keyword') &&
|
||||
!['_id', '_index'].includes(displayName)
|
||||
);
|
||||
}, [dataView]);
|
||||
|
||||
const [requestParamsFromUrl, updateRequestParams] =
|
||||
usePageUrlState<ChangePointDetectionRequestParams>('changePoint');
|
||||
|
||||
const resultQuery = useMemo<Query>(() => {
|
||||
return (
|
||||
requestParamsFromUrl.query ?? {
|
||||
query: savedSearchQuery?.searchString ?? '',
|
||||
language: savedSearchQuery?.queryLanguage ?? 'kuery',
|
||||
}
|
||||
);
|
||||
}, [savedSearchQuery, requestParamsFromUrl.query]);
|
||||
|
||||
const requestParams = useMemo(() => {
|
||||
const params = { ...requestParamsFromUrl };
|
||||
if (!params.fn) {
|
||||
params.fn = DEFAULT_AGG_FUNCTION;
|
||||
}
|
||||
if (!params.metricField && metricFieldOptions.length > 0) {
|
||||
params.metricField = metricFieldOptions[0].name;
|
||||
}
|
||||
if (!params.splitField && splitFieldsOptions.length > 0) {
|
||||
params.splitField = splitFieldsOptions[0].name;
|
||||
}
|
||||
params.interval = bucketInterval?.expression!;
|
||||
return params;
|
||||
}, [requestParamsFromUrl, metricFieldOptions, splitFieldsOptions, bucketInterval]);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
(update: Filter[]) => {
|
||||
filterManager.setFilters(update);
|
||||
},
|
||||
[filterManager]
|
||||
);
|
||||
|
||||
useMount(() => {
|
||||
setResultFilter(filterManager.getFilters());
|
||||
const sub = filterManager.getUpdates$().subscribe(() => {
|
||||
setResultFilter(filterManager.getFilters());
|
||||
});
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(
|
||||
function syncFilters() {
|
||||
const globalFilters = filterManager?.getGlobalFilters();
|
||||
if (requestParamsFromUrl.filters) {
|
||||
filterManager.setFilters(requestParamsFromUrl.filters);
|
||||
}
|
||||
if (globalFilters) {
|
||||
filterManager?.addFilters(globalFilters);
|
||||
}
|
||||
},
|
||||
[requestParamsFromUrl.filters, filterManager]
|
||||
);
|
||||
|
||||
const combinedQuery = useMemo(() => {
|
||||
const mergedQuery = createMergedEsQuery(resultQuery, resultFilters, dataView, uiSettings);
|
||||
if (!Array.isArray(mergedQuery.bool?.filter)) {
|
||||
if (!mergedQuery.bool) {
|
||||
mergedQuery.bool = {};
|
||||
}
|
||||
mergedQuery.bool.filter = [];
|
||||
}
|
||||
|
||||
mergedQuery.bool!.filter.push({
|
||||
range: {
|
||||
[dataView.timeFieldName!]: {
|
||||
from: timeRange.from,
|
||||
to: timeRange.to,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return mergedQuery;
|
||||
}, [resultFilters, resultQuery, uiSettings, dataView, timeRange]);
|
||||
|
||||
const {
|
||||
results: annotations,
|
||||
isLoading: annotationsLoading,
|
||||
progress,
|
||||
pagination,
|
||||
} = useChangePointResults(requestParams, combinedQuery);
|
||||
|
||||
if (!bucketInterval) return null;
|
||||
|
||||
const value = {
|
||||
isLoading: annotationsLoading,
|
||||
progress,
|
||||
timeBuckets,
|
||||
requestParams,
|
||||
updateRequestParams,
|
||||
metricFieldOptions,
|
||||
splitFieldsOptions,
|
||||
annotations,
|
||||
bucketInterval,
|
||||
resultFilters,
|
||||
updateFilters,
|
||||
resultQuery,
|
||||
pagination,
|
||||
};
|
||||
|
||||
return (
|
||||
<ChangePointDetectionContext.Provider value={value}>
|
||||
{children}
|
||||
</ChangePointDetectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useChangePointDetectionContext() {
|
||||
return useContext(ChangePointDetectionContext);
|
||||
}
|
||||
|
||||
export function useRequestParams() {
|
||||
return useChangePointDetectionContext().requestParams;
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiDescriptionList,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiPagination,
|
||||
EuiPanel,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { SearchBarWrapper } from './search_bar';
|
||||
import { useChangePointDetectionContext } from './change_point_detection_context';
|
||||
import { MetricFieldSelector } from './metric_field_selector';
|
||||
import { SplitFieldSelector } from './split_field_selector';
|
||||
import { FunctionPicker } from './function_picker';
|
||||
import { ChartComponent } from './chart_component';
|
||||
|
||||
export const ChangePointDetectionPage: FC = () => {
|
||||
const {
|
||||
requestParams,
|
||||
updateRequestParams,
|
||||
annotations,
|
||||
resultFilters,
|
||||
updateFilters,
|
||||
resultQuery,
|
||||
progress,
|
||||
pagination,
|
||||
} = useChangePointDetectionContext();
|
||||
|
||||
const setFn = useCallback(
|
||||
(fn: string) => {
|
||||
updateRequestParams({ fn });
|
||||
},
|
||||
[updateRequestParams]
|
||||
);
|
||||
|
||||
const setSplitField = useCallback(
|
||||
(splitField: string) => {
|
||||
updateRequestParams({ splitField });
|
||||
},
|
||||
[updateRequestParams]
|
||||
);
|
||||
|
||||
const setMetricField = useCallback(
|
||||
(metricField: string) => {
|
||||
updateRequestParams({ metricField });
|
||||
},
|
||||
[updateRequestParams]
|
||||
);
|
||||
|
||||
const setQuery = useCallback(
|
||||
(query: Query) => {
|
||||
updateRequestParams({ query });
|
||||
},
|
||||
[updateRequestParams]
|
||||
);
|
||||
|
||||
const selectControlCss = { width: '200px' };
|
||||
|
||||
return (
|
||||
<div data-test-subj="aiopsChangePointDetectionPage">
|
||||
<SearchBarWrapper
|
||||
query={resultQuery}
|
||||
onQueryChange={setQuery}
|
||||
filters={resultFilters}
|
||||
onFiltersChange={updateFilters}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup alignItems={'center'}>
|
||||
<EuiFlexItem grow={false} css={selectControlCss}>
|
||||
<FunctionPicker value={requestParams.fn} onChange={setFn} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={selectControlCss}>
|
||||
<MetricFieldSelector value={requestParams.metricField} onChange={setMetricField} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={selectControlCss}>
|
||||
<SplitFieldSelector value={requestParams.splitField} onChange={setSplitField} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem css={{ visibility: progress === 100 ? 'hidden' : 'visible' }} grow={false}>
|
||||
<EuiProgress
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.progressBarLabel"
|
||||
defaultMessage="Fetching change points"
|
||||
/>
|
||||
}
|
||||
value={progress}
|
||||
max={100}
|
||||
valueText
|
||||
size="m"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{annotations.length === 0 && progress === 100 ? (
|
||||
<>
|
||||
<EuiEmptyPrompt
|
||||
iconType="search"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.noChangePointsFoundTitle"
|
||||
defaultMessage="No change points found"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.noChangePointsFoundMessage"
|
||||
defaultMessage="Try to extend the time range or update the query"
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<EuiFlexGrid columns={annotations.length >= 2 ? 2 : 1} responsive gutterSize={'m'}>
|
||||
{annotations.map((v) => {
|
||||
return (
|
||||
<EuiFlexItem key={v.group_field}>
|
||||
<EuiPanel paddingSize="s" hasBorder hasShadow={false}>
|
||||
<EuiFlexGroup justifyContent={'spaceBetween'} alignItems={'center'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems={'center'} gutterSize={'s'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs">
|
||||
<h3>{v.group_field}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{v.reason ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip position="top" content={v.reason}>
|
||||
<EuiIcon
|
||||
tabIndex={0}
|
||||
color={'warning'}
|
||||
type="alert"
|
||||
title={i18n.translate(
|
||||
'xpack.aiops.changePointDetection.notResultsWarning',
|
||||
{
|
||||
defaultMessage: 'No change point agg results warning',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">{v.type}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{v.p_value !== undefined ? (
|
||||
<EuiDescriptionList
|
||||
type="inline"
|
||||
listItems={[{ title: 'p-value', description: v.p_value.toPrecision(3) }]}
|
||||
/>
|
||||
) : null}
|
||||
<ChartComponent annotation={v} />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGrid>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{pagination.pageCount > 1 ? (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPagination
|
||||
pageCount={pagination.pageCount}
|
||||
activePage={pagination.activePage}
|
||||
onPageClick={pagination.updatePagination}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import React, { FC } from 'react';
|
||||
import { PageHeader } from '../page_header';
|
||||
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
|
||||
import { DataSourceContext } from '../../hooks/use_data_source';
|
||||
import { UrlStateProvider } from '../../hooks/use_url_state';
|
||||
import { SavedSearchSavedObject } from '../../application/utils/search_utils';
|
||||
import { AiopsAppContext, AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
|
||||
import { ChangePointDetectionPage } from './change_point_detection_page';
|
||||
|
||||
export interface ChangePointDetectionAppStateProps {
|
||||
dataView: DataView;
|
||||
savedSearch: SavedSearch | SavedSearchSavedObject | null;
|
||||
appDependencies: AiopsAppDependencies;
|
||||
}
|
||||
|
||||
export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps> = ({
|
||||
dataView,
|
||||
savedSearch,
|
||||
appDependencies,
|
||||
}) => {
|
||||
return (
|
||||
<AiopsAppContext.Provider value={appDependencies}>
|
||||
<UrlStateProvider>
|
||||
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
|
||||
<PageHeader />
|
||||
<ChangePointDetectionContextProvider>
|
||||
<ChangePointDetectionPage />
|
||||
</ChangePointDetectionContextProvider>
|
||||
</DataSourceContext.Provider>
|
||||
</UrlStateProvider>
|
||||
</AiopsAppContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { type TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { useChangePointDetectionContext } from './change_point_detection_context';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import { useTimeRangeUpdates } from '../../hooks/use_time_filter';
|
||||
import { fnOperationTypeMapping } from './constants';
|
||||
|
||||
export interface ChartComponentProps {
|
||||
annotation: {
|
||||
group_field: string;
|
||||
label: string;
|
||||
timestamp: string;
|
||||
reason: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ChartComponent: FC<ChartComponentProps> = React.memo(({ annotation }) => {
|
||||
const {
|
||||
lens: { EmbeddableComponent },
|
||||
} = useAiopsAppContext();
|
||||
|
||||
const timeRange = useTimeRangeUpdates();
|
||||
const { dataView } = useDataSource();
|
||||
const { requestParams, bucketInterval } = useChangePointDetectionContext();
|
||||
|
||||
const filters = useMemo(
|
||||
() => [
|
||||
{
|
||||
meta: {
|
||||
index: dataView.id!,
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: requestParams.splitField,
|
||||
params: {
|
||||
query: annotation.group_field,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
[requestParams.splitField]: annotation.group_field,
|
||||
},
|
||||
},
|
||||
$state: {
|
||||
store: FilterStateStore.APP_STATE,
|
||||
},
|
||||
},
|
||||
],
|
||||
[dataView.id, requestParams.splitField, annotation.group_field]
|
||||
);
|
||||
|
||||
// @ts-ignore incorrect types for attributes
|
||||
const attributes = useMemo<TypedLensByValueInput['attributes']>(() => {
|
||||
return {
|
||||
title: annotation.group_field,
|
||||
description: '',
|
||||
visualizationType: 'lnsXY',
|
||||
type: 'lens',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: dataView.id!,
|
||||
name: 'indexpattern-datasource-layer-2d61a885-abb0-4d4e-a5f9-c488caec3c22',
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: dataView.id!,
|
||||
name: 'xy-visualization-layer-8d26ab67-b841-4877-9d02-55bf270f9caf',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
visualization: {
|
||||
legend: {
|
||||
isVisible: false,
|
||||
position: 'right',
|
||||
},
|
||||
valueLabels: 'hide',
|
||||
fittingFunction: 'None',
|
||||
axisTitlesVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
tickLabelsVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
labelsOrientation: {
|
||||
x: 0,
|
||||
yLeft: 0,
|
||||
yRight: 0,
|
||||
},
|
||||
gridlinesVisibilitySettings: {
|
||||
x: true,
|
||||
yLeft: true,
|
||||
yRight: true,
|
||||
},
|
||||
preferredSeriesType: 'line',
|
||||
layers: [
|
||||
{
|
||||
layerId: '2d61a885-abb0-4d4e-a5f9-c488caec3c22',
|
||||
accessors: ['e9f26d17-fb36-4982-8539-03f1849cbed0'],
|
||||
position: 'top',
|
||||
seriesType: 'line',
|
||||
showGridlines: false,
|
||||
layerType: 'data',
|
||||
xAccessor: '877e6638-bfaa-43ec-afb9-2241dc8e1c86',
|
||||
},
|
||||
...(annotation.timestamp
|
||||
? [
|
||||
{
|
||||
layerId: '8d26ab67-b841-4877-9d02-55bf270f9caf',
|
||||
layerType: 'annotations',
|
||||
annotations: [
|
||||
{
|
||||
type: 'manual',
|
||||
label: annotation.label,
|
||||
icon: 'triangle',
|
||||
textVisibility: true,
|
||||
key: {
|
||||
type: 'point_in_time',
|
||||
timestamp: annotation.timestamp,
|
||||
},
|
||||
id: 'a8fb297c-8d96-4011-93c0-45af110d5302',
|
||||
isHidden: false,
|
||||
color: '#F04E98',
|
||||
lineStyle: 'solid',
|
||||
lineWidth: 2,
|
||||
outside: false,
|
||||
},
|
||||
],
|
||||
ignoreGlobalFilters: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
filters,
|
||||
datasourceStates: {
|
||||
formBased: {
|
||||
layers: {
|
||||
'2d61a885-abb0-4d4e-a5f9-c488caec3c22': {
|
||||
columns: {
|
||||
'877e6638-bfaa-43ec-afb9-2241dc8e1c86': {
|
||||
label: dataView.timeFieldName,
|
||||
dataType: 'date',
|
||||
operationType: 'date_histogram',
|
||||
sourceField: dataView.timeFieldName,
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: {
|
||||
interval: bucketInterval.expression,
|
||||
includeEmptyRows: true,
|
||||
dropPartials: false,
|
||||
},
|
||||
},
|
||||
'e9f26d17-fb36-4982-8539-03f1849cbed0': {
|
||||
label: `${requestParams.fn}(${requestParams.metricField})`,
|
||||
dataType: 'number',
|
||||
operationType: fnOperationTypeMapping[requestParams.fn],
|
||||
sourceField: requestParams.metricField,
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
params: {
|
||||
emptyAsNull: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columnOrder: [
|
||||
'877e6638-bfaa-43ec-afb9-2241dc8e1c86',
|
||||
'e9f26d17-fb36-4982-8539-03f1849cbed0',
|
||||
],
|
||||
incompleteColumns: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
textBased: {
|
||||
layers: {},
|
||||
},
|
||||
},
|
||||
internalReferences: [],
|
||||
adHocDataViews: {},
|
||||
},
|
||||
};
|
||||
}, [dataView.id, dataView.timeFieldName, annotation, requestParams, filters, bucketInterval]);
|
||||
|
||||
return (
|
||||
<EmbeddableComponent
|
||||
id={`changePointChart_${annotation.group_field}`}
|
||||
style={{ height: 350 }}
|
||||
timeRange={timeRange}
|
||||
attributes={attributes}
|
||||
renderMode={'view'}
|
||||
executionContext={{
|
||||
type: 'aiops_change_point_detection_chart',
|
||||
name: 'Change point detection',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const fnOperationTypeMapping: Record<string, string> = {
|
||||
min: 'min',
|
||||
max: 'max',
|
||||
sum: 'sum',
|
||||
avg: 'average',
|
||||
} as const;
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC } from 'react';
|
||||
import { fnOperationTypeMapping } from './constants';
|
||||
|
||||
interface FunctionPickerProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const FunctionPicker: FC<FunctionPickerProps> = React.memo(({ value, onChange }) => {
|
||||
const options = Object.keys(fnOperationTypeMapping).map((v) => {
|
||||
return {
|
||||
value: v,
|
||||
text: v,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
prepend={i18n.translate('xpack.aiops.changePointDetection.selectFunctionLabel', {
|
||||
defaultMessage: 'Function',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { type ChangePointDetectionAppStateProps } from './change_point_detetion_root';
|
||||
|
||||
import { ChangePointDetectionAppState } from './change_point_detetion_root';
|
||||
|
||||
// required for dynamic import using React.lazy()
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ChangePointDetectionAppState;
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui';
|
||||
import { useChangePointDetectionContext } from './change_point_detection_context';
|
||||
|
||||
interface MetricFieldSelectorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const MetricFieldSelector: FC<MetricFieldSelectorProps> = React.memo(
|
||||
({ value, onChange }) => {
|
||||
const { metricFieldOptions } = useChangePointDetectionContext();
|
||||
|
||||
const options = useMemo<EuiSelectOption[]>(() => {
|
||||
return metricFieldOptions.map((v) => ({ value: v.name, text: v.displayName }));
|
||||
}, [metricFieldOptions]);
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiSelect
|
||||
prepend={i18n.translate('xpack.aiops.changePointDetection.selectMetricFieldLabel', {
|
||||
defaultMessage: 'Metric field',
|
||||
})}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { type Filter, fromKueryExpression, type Query } from '@kbn/es-query';
|
||||
import { type SearchBarOwnProps } from '@kbn/unified-search-plugin/public/search_bar';
|
||||
import { EuiSpacer, EuiTextColor } from '@elastic/eui';
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
||||
export interface SearchBarProps {
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
onQueryChange: (update: Query) => void;
|
||||
onFiltersChange: (update: Filter[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable search bar component for the AIOps app.
|
||||
*
|
||||
* @param query
|
||||
* @param filters
|
||||
* @param onQueryChange
|
||||
* @param onFiltersChange
|
||||
* @constructor
|
||||
*/
|
||||
export const SearchBarWrapper: FC<SearchBarProps> = ({
|
||||
query,
|
||||
filters,
|
||||
onQueryChange,
|
||||
onFiltersChange,
|
||||
}) => {
|
||||
const { dataView } = useDataSource();
|
||||
const {
|
||||
unifiedSearch: {
|
||||
ui: { SearchBar },
|
||||
},
|
||||
} = useAiopsAppContext();
|
||||
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
|
||||
const onQuerySubmit: SearchBarOwnProps['onQuerySubmit'] = useCallback(
|
||||
(payload, isUpdate) => {
|
||||
if (payload.query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
|
||||
try {
|
||||
// Validates the query
|
||||
fromKueryExpression(payload.query.query);
|
||||
setError(undefined);
|
||||
onQueryChange(payload.query);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onQueryChange]
|
||||
);
|
||||
|
||||
const onFiltersUpdated = useCallback(
|
||||
(updatedFilters: Filter[]) => {
|
||||
onFiltersChange(updatedFilters);
|
||||
},
|
||||
[onFiltersChange]
|
||||
);
|
||||
|
||||
const resultQuery = query ?? { query: '', language: 'kuery' };
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBar
|
||||
showSubmitButton={false}
|
||||
appName={'aiops'}
|
||||
showFilterBar
|
||||
showDatePicker={false}
|
||||
showQueryInput
|
||||
query={resultQuery}
|
||||
filters={filters ?? []}
|
||||
onQuerySubmit={onQuerySubmit}
|
||||
indexPatterns={[dataView]}
|
||||
placeholder={i18n.translate('xpack.aiops.searchPanel.queryBarPlaceholderText', {
|
||||
defaultMessage: 'Search… (e.g. status:200 AND extension:"PHP")',
|
||||
})}
|
||||
displayStyle={'inPage'}
|
||||
isClearable
|
||||
onFiltersUpdated={onFiltersUpdated}
|
||||
/>
|
||||
{error ? (
|
||||
<>
|
||||
<EuiSpacer size={'s'} />
|
||||
<EuiTextColor color="danger">{error}</EuiTextColor>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiSelect, type EuiSelectOption } from '@elastic/eui';
|
||||
import { useChangePointDetectionContext } from './change_point_detection_context';
|
||||
|
||||
interface SplitFieldSelectorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const SplitFieldSelector: FC<SplitFieldSelectorProps> = React.memo(({ value, onChange }) => {
|
||||
const { splitFieldsOptions } = useChangePointDetectionContext();
|
||||
|
||||
const options = useMemo<EuiSelectOption[]>(() => {
|
||||
return splitFieldsOptions.map((v) => ({ value: v.name, text: v.displayName }));
|
||||
}, [splitFieldsOptions]);
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiSelect
|
||||
prepend={i18n.translate('xpack.aiops.changePointDetection.selectSpitFieldLabel', {
|
||||
defaultMessage: 'Split field',
|
||||
})}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useState, useMemo } from 'react';
|
||||
import { type QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import {
|
||||
ChangePointAnnotation,
|
||||
ChangePointDetectionRequestParams,
|
||||
ChangePointType,
|
||||
} from './change_point_detection_context';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useCancellableSearch } from '../../hooks/use_cancellable_search';
|
||||
import { useSplitFieldCardinality } from './use_split_field_cardinality';
|
||||
|
||||
interface RequestOptions {
|
||||
index: string;
|
||||
fn: string;
|
||||
metricField: string;
|
||||
splitField: string;
|
||||
timeField: string;
|
||||
timeInterval: string;
|
||||
afterKey?: string;
|
||||
}
|
||||
|
||||
export const COMPOSITE_AGG_SIZE = 500;
|
||||
|
||||
function getChangePointDetectionRequestBody(
|
||||
{ index, fn, metricField, splitField, timeInterval, timeField, afterKey }: RequestOptions,
|
||||
query: QueryDslQueryContainer
|
||||
) {
|
||||
return {
|
||||
params: {
|
||||
index,
|
||||
size: 0,
|
||||
body: {
|
||||
query,
|
||||
aggregations: {
|
||||
groupings: {
|
||||
composite: {
|
||||
size: COMPOSITE_AGG_SIZE,
|
||||
...(afterKey !== undefined ? { after: { splitFieldTerm: afterKey } } : {}),
|
||||
sources: [
|
||||
{
|
||||
splitFieldTerm: {
|
||||
terms: {
|
||||
field: splitField,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
aggregations: {
|
||||
over_time: {
|
||||
date_histogram: {
|
||||
field: timeField,
|
||||
fixed_interval: timeInterval,
|
||||
},
|
||||
aggs: {
|
||||
function_value: {
|
||||
[fn]: {
|
||||
field: metricField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
change_point_request: {
|
||||
change_point: {
|
||||
buckets_path: 'over_time>function_value',
|
||||
},
|
||||
},
|
||||
select: {
|
||||
bucket_selector: {
|
||||
buckets_path: { p_value: 'change_point_request.p_value' },
|
||||
script: 'params.p_value < 1',
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
bucket_sort: {
|
||||
sort: [{ 'change_point_request.p_value': { order: 'asc' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const CHARTS_PER_PAGE = 6;
|
||||
|
||||
export function useChangePointResults(
|
||||
requestParams: ChangePointDetectionRequestParams,
|
||||
query: QueryDslQueryContainer
|
||||
) {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useAiopsAppContext();
|
||||
|
||||
const { dataView } = useDataSource();
|
||||
|
||||
const [results, setResults] = useState<ChangePointAnnotation[]>([]);
|
||||
const [activePage, setActivePage] = useState<number>(0);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
const splitFieldCardinality = useSplitFieldCardinality(requestParams.splitField, query);
|
||||
|
||||
const { runRequest, cancelRequest, isLoading } = useCancellableSearch();
|
||||
|
||||
const reset = useCallback(() => {
|
||||
cancelRequest();
|
||||
setProgress(0);
|
||||
setActivePage(0);
|
||||
setResults([]);
|
||||
}, [cancelRequest]);
|
||||
|
||||
const fetchResults = useCallback(
|
||||
async (afterKey?: string, prevBucketsCount?: number) => {
|
||||
try {
|
||||
if (!splitFieldCardinality) {
|
||||
setProgress(100);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestPayload = getChangePointDetectionRequestBody(
|
||||
{
|
||||
index: dataView.getIndexPattern(),
|
||||
fn: requestParams.fn,
|
||||
timeInterval: requestParams.interval,
|
||||
metricField: requestParams.metricField,
|
||||
timeField: dataView.timeFieldName!,
|
||||
splitField: requestParams.splitField,
|
||||
afterKey,
|
||||
},
|
||||
query
|
||||
);
|
||||
const result = await runRequest<
|
||||
typeof requestPayload,
|
||||
{ rawResponse: ChangePointAggResponse }
|
||||
>(requestPayload);
|
||||
|
||||
if (result === null) {
|
||||
setProgress(100);
|
||||
return;
|
||||
}
|
||||
|
||||
const buckets = result.rawResponse.aggregations.groupings.buckets;
|
||||
|
||||
setProgress(
|
||||
Math.min(
|
||||
Math.round(((buckets.length + (prevBucketsCount ?? 0)) / splitFieldCardinality) * 100),
|
||||
100
|
||||
)
|
||||
);
|
||||
|
||||
const groups = buckets.map((v) => {
|
||||
const changePointType = Object.keys(v.change_point_request.type)[0] as ChangePointType;
|
||||
const timeAsString = v.change_point_request.bucket?.key;
|
||||
const rawPValue = v.change_point_request.type[changePointType].p_value;
|
||||
|
||||
return {
|
||||
group_field: v.key.splitFieldTerm,
|
||||
type: changePointType,
|
||||
p_value: rawPValue,
|
||||
timestamp: timeAsString,
|
||||
label: changePointType,
|
||||
reason: v.change_point_request.type[changePointType].reason,
|
||||
} as ChangePointAnnotation;
|
||||
});
|
||||
|
||||
setResults((prev) => {
|
||||
return (
|
||||
(prev ?? [])
|
||||
.concat(groups)
|
||||
// Lower p_value indicates a bigger change point, hence the acs sorting
|
||||
.sort((a, b) => a.p_value - b.p_value)
|
||||
);
|
||||
});
|
||||
|
||||
if (result.rawResponse.aggregations.groupings.after_key?.splitFieldTerm) {
|
||||
await fetchResults(
|
||||
result.rawResponse.aggregations.groupings.after_key.splitFieldTerm,
|
||||
buckets.length + (prevBucketsCount ?? 0)
|
||||
);
|
||||
} else {
|
||||
setProgress(100);
|
||||
}
|
||||
} catch (e) {
|
||||
toasts.addError(e, {
|
||||
title: i18n.translate('xpack.aiops.changePointDetection.fetchErrorTitle', {
|
||||
defaultMessage: 'Failed to fetch change points',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
[runRequest, requestParams, query, dataView, splitFieldCardinality, toasts]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function fetchResultsOnInputChange() {
|
||||
reset();
|
||||
fetchResults();
|
||||
|
||||
return () => {
|
||||
cancelRequest();
|
||||
};
|
||||
},
|
||||
[requestParams, query, splitFieldCardinality, fetchResults, reset, cancelRequest]
|
||||
);
|
||||
|
||||
const pagination = useMemo(() => {
|
||||
return {
|
||||
activePage,
|
||||
pageCount: Math.round((results.length ?? 0) / CHARTS_PER_PAGE),
|
||||
updatePagination: setActivePage,
|
||||
};
|
||||
}, [activePage, results.length]);
|
||||
|
||||
const resultPerPage = useMemo(() => {
|
||||
const start = activePage * CHARTS_PER_PAGE;
|
||||
return results.slice(start, start + CHARTS_PER_PAGE);
|
||||
}, [results, activePage]);
|
||||
|
||||
return { results: resultPerPage, isLoading, reset, progress, pagination };
|
||||
}
|
||||
|
||||
interface ChangePointAggResponse {
|
||||
took: number;
|
||||
timed_out: boolean;
|
||||
_shards: { total: number; failed: number; successful: number; skipped: number };
|
||||
hits: { hits: any[]; total: number; max_score: null };
|
||||
aggregations: {
|
||||
groupings: {
|
||||
after_key?: {
|
||||
splitFieldTerm: string;
|
||||
};
|
||||
buckets: Array<{
|
||||
key: { splitFieldTerm: string };
|
||||
doc_count: number;
|
||||
over_time: {
|
||||
buckets: Array<{
|
||||
key_as_string: string;
|
||||
doc_count: number;
|
||||
function_value: { value: number };
|
||||
key: number;
|
||||
}>;
|
||||
};
|
||||
change_point_request: {
|
||||
bucket?: { doc_count: number; function_value: { value: number }; key: string };
|
||||
type: {
|
||||
[key in ChangePointType]: { p_value: number; change_point: number; reason?: string };
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type {
|
||||
QueryDslQueryContainer,
|
||||
AggregationsCardinalityAggregate,
|
||||
SearchResponseBody,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { useCancellableSearch } from '../../hooks/use_cancellable_search';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
|
||||
/**
|
||||
* Gets the cardinality of the selected split field
|
||||
* @param splitField
|
||||
* @param query
|
||||
*/
|
||||
export function useSplitFieldCardinality(splitField: string, query: QueryDslQueryContainer) {
|
||||
const [cardinality, setCardinality] = useState<number>();
|
||||
const { dataView } = useDataSource();
|
||||
|
||||
const requestPayload = useMemo(() => {
|
||||
return {
|
||||
params: {
|
||||
index: dataView.getIndexPattern(),
|
||||
size: 0,
|
||||
body: {
|
||||
query,
|
||||
aggregations: {
|
||||
fieldCount: {
|
||||
cardinality: {
|
||||
field: splitField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [splitField, dataView, query]);
|
||||
|
||||
const { runRequest: getSplitFieldCardinality, cancelRequest } = useCancellableSearch();
|
||||
|
||||
useEffect(
|
||||
function performCardinalityCheck() {
|
||||
cancelRequest();
|
||||
|
||||
getSplitFieldCardinality<
|
||||
typeof requestPayload,
|
||||
{
|
||||
rawResponse: SearchResponseBody<
|
||||
unknown,
|
||||
{ fieldCount: AggregationsCardinalityAggregate }
|
||||
>;
|
||||
}
|
||||
>(requestPayload).then((response) => {
|
||||
if (response?.rawResponse.aggregations) {
|
||||
setCardinality(response.rawResponse.aggregations.fieldCount.value);
|
||||
}
|
||||
});
|
||||
},
|
||||
[getSplitFieldCardinality, requestPayload, cancelRequest]
|
||||
);
|
||||
|
||||
return cardinality;
|
||||
}
|
|
@ -21,13 +21,12 @@ import {
|
|||
OnTimeChangeProps,
|
||||
} from '@elastic/eui';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { TimefilterContract, TimeHistoryContract, UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import { TimeHistoryContract, UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
|
||||
import { useRefreshIntervalUpdates, useTimeRangeUpdates } from '../../hooks/use_time_filter';
|
||||
import { useUrlState } from '../../hooks/use_url_state';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import { aiopsRefresh$ } from '../../application/services/timefilter_refresh_service';
|
||||
|
@ -67,21 +66,6 @@ function updateLastRefresh(timeRange?: OnRefreshProps) {
|
|||
aiopsRefresh$.next({ lastRefresh: Date.now(), timeRange });
|
||||
}
|
||||
|
||||
export const useRefreshIntervalUpdates = (timefilter: TimefilterContract) => {
|
||||
return useObservable(
|
||||
timefilter.getRefreshIntervalUpdate$().pipe(map(timefilter.getRefreshInterval)),
|
||||
timefilter.getRefreshInterval()
|
||||
);
|
||||
};
|
||||
|
||||
export const useTimeRangeUpdates = (timefilter: TimefilterContract, absolute = false) => {
|
||||
const getTimeCallback = absolute
|
||||
? timefilter.getAbsoluteTime.bind(timefilter)
|
||||
: timefilter.getTime.bind(timefilter);
|
||||
|
||||
return useObservable(timefilter.getTimeUpdate$().pipe(map(getTimeCallback)), getTimeCallback());
|
||||
};
|
||||
|
||||
export const DatePickerWrapper: FC = () => {
|
||||
const services = useAiopsAppContext();
|
||||
const { toasts } = services.notifications;
|
||||
|
@ -93,8 +77,8 @@ export const DatePickerWrapper: FC = () => {
|
|||
const [globalState, setGlobalState] = useUrlState('_g');
|
||||
const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(history);
|
||||
|
||||
const timeFilterRefreshInterval = useRefreshIntervalUpdates(timefilter);
|
||||
const time = useTimeRangeUpdates(timefilter);
|
||||
const timeFilterRefreshInterval = useRefreshIntervalUpdates();
|
||||
const time = useTimeRangeUpdates();
|
||||
|
||||
useEffect(
|
||||
function syncTimRangeFromUrlState() {
|
||||
|
@ -257,13 +241,10 @@ export const DatePickerWrapper: FC = () => {
|
|||
}
|
||||
|
||||
return isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled ? (
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
className="mlNavigationMenu__datePickerWrapper"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" css={{ width: '100%', minWidth: 0 }}>
|
||||
<EuiFlexItem grow={true} css={{ width: '100%' }}>
|
||||
<EuiSuperDatePicker
|
||||
css={{ width: '100%', minWidth: 0 }}
|
||||
start={time.from}
|
||||
end={time.to}
|
||||
isPaused={refreshInterval.pause}
|
||||
|
|
|
@ -26,15 +26,18 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import { setFullTimeRange } from './full_time_range_selector_service';
|
||||
import {
|
||||
type GetTimeFieldRangeResponse,
|
||||
setFullTimeRange,
|
||||
} from './full_time_range_selector_service';
|
||||
import { AIOPS_FROZEN_TIER_PREFERENCE, useStorage } from '../../hooks/use_storage';
|
||||
|
||||
interface Props {
|
||||
export interface FullTimeRangeSelectorProps {
|
||||
timefilter: TimefilterContract;
|
||||
dataView: DataView;
|
||||
disabled: boolean;
|
||||
query?: QueryDslQueryContainer;
|
||||
callback?: (a: any) => void;
|
||||
callback?: (a: GetTimeFieldRangeResponse) => void;
|
||||
}
|
||||
|
||||
const FROZEN_TIER_PREFERENCE = {
|
||||
|
@ -44,7 +47,7 @@ const FROZEN_TIER_PREFERENCE = {
|
|||
|
||||
type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
|
||||
|
||||
export const FullTimeRangeSelector: FC<Props> = ({
|
||||
export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
|
||||
timefilter,
|
||||
dataView,
|
||||
query,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { PageHeader } from './page_header';
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
|
||||
} from '@elastic/eui';
|
||||
import { FullTimeRangeSelectorProps } from '../full_time_range_selector/full_time_range_selector';
|
||||
import { useUrlState } from '../../hooks/use_url_state';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useTimefilter } from '../../hooks/use_time_filter';
|
||||
import { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
import { DatePickerWrapper } from '../date_picker_wrapper';
|
||||
|
||||
export const PageHeader: FC = () => {
|
||||
const [, setGlobalState] = useUrlState('_g');
|
||||
const { dataView } = useDataSource();
|
||||
|
||||
const timefilter = useTimefilter({
|
||||
timeRangeSelector: dataView.timeFieldName !== undefined,
|
||||
autoRefreshSelector: true,
|
||||
});
|
||||
|
||||
const updateTimeState: FullTimeRangeSelectorProps['callback'] = useCallback(
|
||||
(update) => {
|
||||
setGlobalState({ time: { from: update.start.string, to: update.end.string } });
|
||||
},
|
||||
[setGlobalState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiPageContentHeader className="aiopsPageHeader">
|
||||
<EuiPageContentHeaderSection>
|
||||
<div className="dataViewTitleHeader">
|
||||
<EuiTitle size="s">
|
||||
<h2>{dataView.getName()}</h2>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="s"
|
||||
data-test-subj="aiopsTimeRangeSelectorSection"
|
||||
>
|
||||
{dataView.timeFieldName !== undefined && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FullTimeRangeSelector
|
||||
dataView={dataView}
|
||||
query={undefined}
|
||||
disabled={false}
|
||||
timefilter={timefilter}
|
||||
callback={updateTimeState}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeader>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -16,6 +16,7 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
|||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { CoreStart, CoreSetup, HttpStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import type { ThemeServiceStart } from '@kbn/core/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
|
||||
export interface AiopsAppDependencies {
|
||||
application: CoreStart['application'];
|
||||
|
@ -29,6 +30,7 @@ export interface AiopsAppDependencies {
|
|||
uiSettings: IUiSettingsClient;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
share: SharePluginStart;
|
||||
lens: LensPublicStart;
|
||||
}
|
||||
|
||||
export const AiopsAppContext = createContext<AiopsAppDependencies | undefined>(undefined);
|
||||
|
|
67
x-pack/plugins/aiops/public/hooks/use_cancellable_search.ts
Normal file
67
x-pack/plugins/aiops/public/hooks/use_cancellable_search.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
type IKibanaSearchResponse,
|
||||
isCompleteResponse,
|
||||
isErrorResponse,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { useAiopsAppContext } from './use_aiops_app_context';
|
||||
|
||||
export function useCancellableSearch() {
|
||||
const { data } = useAiopsAppContext();
|
||||
const abortController = useRef(new AbortController());
|
||||
const [isLoading, setIsFetching] = useState<boolean>(false);
|
||||
|
||||
const runRequest = useCallback(
|
||||
<RequestBody, ResponseType extends IKibanaSearchResponse>(
|
||||
requestBody: RequestBody
|
||||
): Promise<ResponseType | null> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
data.search
|
||||
.search<RequestBody, ResponseType>(requestBody, {
|
||||
abortSignal: abortController.current.signal,
|
||||
})
|
||||
.pipe(
|
||||
tap(() => {
|
||||
setIsFetching(true);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
if (isCompleteResponse(result)) {
|
||||
setIsFetching(false);
|
||||
resolve(result);
|
||||
} else if (isErrorResponse(result)) {
|
||||
reject(result);
|
||||
} else {
|
||||
// partial results
|
||||
// Ignore partial results for now.
|
||||
// An issue with the search function means partial results are not being returned correctly.
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
if (error.name === 'AbortError') {
|
||||
return resolve(null);
|
||||
}
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[data.search]
|
||||
);
|
||||
|
||||
const cancelRequest = useCallback(() => {
|
||||
abortController.current.abort();
|
||||
abortController.current = new AbortController();
|
||||
}, []);
|
||||
|
||||
return { runRequest, cancelRequest, isLoading };
|
||||
}
|
|
@ -9,12 +9,11 @@ import { useEffect, useMemo, useState } from 'react';
|
|||
import { merge } from 'rxjs';
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import type { ChangePoint } from '@kbn/ml-agg-utils';
|
||||
|
||||
import type { SavedSearch } from '@kbn/discover-plugin/public';
|
||||
|
||||
import { TimeBuckets } from '../../common/time_buckets';
|
||||
import { useTimeBuckets } from './use_time_buckets';
|
||||
|
||||
import { useAiopsAppContext } from './use_aiops_app_context';
|
||||
import { aiopsRefresh$ } from '../application/services/timefilter_refresh_service';
|
||||
|
@ -96,14 +95,7 @@ export const useData = (
|
|||
lastRefresh,
|
||||
]);
|
||||
|
||||
const _timeBuckets = useMemo(() => {
|
||||
return new TimeBuckets({
|
||||
[UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
|
||||
[UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
|
||||
dateFormat: uiSettings.get('dateFormat'),
|
||||
'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
|
||||
});
|
||||
}, [uiSettings]);
|
||||
const _timeBuckets = useTimeBuckets();
|
||||
|
||||
const timefilter = useTimefilter({
|
||||
timeRangeSelector: currentDataView?.timeFieldName !== undefined,
|
||||
|
|
25
x-pack/plugins/aiops/public/hooks/use_data_source.ts
Normal file
25
x-pack/plugins/aiops/public/hooks/use_data_source.ts
Normal file
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { SavedSearchSavedObject } from '../application/utils/search_utils';
|
||||
|
||||
export const DataSourceContext = createContext<{
|
||||
dataView: DataView | never;
|
||||
savedSearch: SavedSearch | SavedSearchSavedObject | null;
|
||||
}>({
|
||||
get dataView(): never {
|
||||
throw new Error('Context is not implemented');
|
||||
},
|
||||
savedSearch: null,
|
||||
});
|
||||
|
||||
export function useDataSource() {
|
||||
return useContext(DataSourceContext);
|
||||
}
|
24
x-pack/plugins/aiops/public/hooks/use_time_buckets.ts
Normal file
24
x-pack/plugins/aiops/public/hooks/use_time_buckets.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import { TimeBuckets } from '../../common/time_buckets';
|
||||
import { useAiopsAppContext } from './use_aiops_app_context';
|
||||
|
||||
export const useTimeBuckets = () => {
|
||||
const { uiSettings } = useAiopsAppContext();
|
||||
|
||||
return useMemo(() => {
|
||||
return new TimeBuckets({
|
||||
[UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
|
||||
[UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
|
||||
dateFormat: uiSettings.get('dateFormat'),
|
||||
'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
|
||||
});
|
||||
}, [uiSettings]);
|
||||
};
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useAiopsAppContext } from './use_aiops_app_context';
|
||||
|
||||
interface UseTimefilterOptions {
|
||||
|
@ -41,3 +44,31 @@ export const useTimefilter = ({
|
|||
|
||||
return timefilter;
|
||||
};
|
||||
|
||||
export const useRefreshIntervalUpdates = () => {
|
||||
const timefilter = useTimefilter();
|
||||
|
||||
const refreshIntervalObservable$ = useMemo(
|
||||
() => timefilter.getRefreshIntervalUpdate$().pipe(map(timefilter.getRefreshInterval)),
|
||||
[timefilter]
|
||||
);
|
||||
|
||||
return useObservable(refreshIntervalObservable$, timefilter.getRefreshInterval());
|
||||
};
|
||||
|
||||
export const useTimeRangeUpdates = (absolute = false) => {
|
||||
const timefilter = useTimefilter();
|
||||
|
||||
const getTimeCallback = useMemo(() => {
|
||||
return absolute
|
||||
? timefilter.getAbsoluteTime.bind(timefilter)
|
||||
: timefilter.getTime.bind(timefilter);
|
||||
}, [absolute, timefilter]);
|
||||
|
||||
const timeChangeObservable$ = useMemo(
|
||||
() => timefilter.getTimeUpdate$().pipe(map(getTimeCallback), distinctUntilChanged(isEqual)),
|
||||
[timefilter, getTimeCallback]
|
||||
);
|
||||
|
||||
return useObservable(timeChangeObservable$, getTimeCallback());
|
||||
};
|
||||
|
|
|
@ -184,12 +184,12 @@ export const useUrlState = (accessor: Accessor) => {
|
|||
};
|
||||
|
||||
export const AppStateKey = 'AIOPS_INDEX_VIEWER';
|
||||
|
||||
export const ChangePointStateKey = 'changePoint' as const;
|
||||
/**
|
||||
* Hook for managing the URL state of the page.
|
||||
*/
|
||||
export const usePageUrlState = <PageUrlState extends {}>(
|
||||
pageKey: typeof AppStateKey,
|
||||
pageKey: typeof AppStateKey | typeof ChangePointStateKey,
|
||||
defaultState?: PageUrlState
|
||||
): [PageUrlState, (update: Partial<PageUrlState>, replaceState?: boolean) => void] => {
|
||||
const [appState, setAppState] = useUrlState('_a');
|
||||
|
|
|
@ -13,4 +13,8 @@ export function plugin() {
|
|||
return new AiopsPlugin();
|
||||
}
|
||||
|
||||
export { ExplainLogRateSpikes, LogCategorization } from './shared_lazy_components';
|
||||
export {
|
||||
ExplainLogRateSpikes,
|
||||
LogCategorization,
|
||||
ChangePointDetection,
|
||||
} from './shared_lazy_components';
|
||||
|
|
|
@ -15,7 +15,7 @@ const ExplainLogRateSpikesAppStateLazy = React.lazy(
|
|||
() => import('./components/explain_log_rate_spikes')
|
||||
);
|
||||
|
||||
const ExplainLogRateSpikesLazyWrapper: FC = ({ children }) => (
|
||||
const LazyWrapper: FC = ({ children }) => (
|
||||
<EuiErrorBoundary>
|
||||
<Suspense fallback={<EuiLoadingContent lines={3} />}>{children}</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
|
@ -26,25 +26,30 @@ const ExplainLogRateSpikesLazyWrapper: FC = ({ children }) => (
|
|||
* @param {ExplainLogRateSpikesAppStateProps} props - properties specifying the data on which to run the analysis.
|
||||
*/
|
||||
export const ExplainLogRateSpikes: FC<ExplainLogRateSpikesAppStateProps> = (props) => (
|
||||
<ExplainLogRateSpikesLazyWrapper>
|
||||
<LazyWrapper>
|
||||
<ExplainLogRateSpikesAppStateLazy {...props} />
|
||||
</ExplainLogRateSpikesLazyWrapper>
|
||||
</LazyWrapper>
|
||||
);
|
||||
|
||||
const LogCategorizationAppStateLazy = React.lazy(() => import('./components/log_categorization'));
|
||||
|
||||
const LogCategorizationLazyWrapper: FC = ({ children }) => (
|
||||
<EuiErrorBoundary>
|
||||
<Suspense fallback={<EuiLoadingContent lines={3} />}>{children}</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
|
||||
/**
|
||||
* Lazy-wrapped LogCategorizationAppStateProps React component
|
||||
* @param {LogCategorizationAppStateProps} props - properties specifying the data on which to run the analysis.
|
||||
*/
|
||||
export const LogCategorization: FC<LogCategorizationAppStateProps> = (props) => (
|
||||
<LogCategorizationLazyWrapper>
|
||||
<LazyWrapper>
|
||||
<LogCategorizationAppStateLazy {...props} />
|
||||
</LogCategorizationLazyWrapper>
|
||||
</LazyWrapper>
|
||||
);
|
||||
|
||||
const ChangePointDetectionLazy = React.lazy(() => import('./components/change_point_detection'));
|
||||
/**
|
||||
* Lazy-wrapped LogCategorizationAppStateProps React component
|
||||
* @param {LogCategorizationAppStateProps} props - properties specifying the data on which to run the analysis.
|
||||
*/
|
||||
export const ChangePointDetection: FC<LogCategorizationAppStateProps> = (props) => (
|
||||
<LazyWrapper>
|
||||
<ChangePointDetectionLazy {...props} />
|
||||
</LazyWrapper>
|
||||
);
|
||||
|
|
|
@ -25,5 +25,6 @@
|
|||
{ "path": "../security/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/charts/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/discover/tsconfig.json" },
|
||||
{ "path": "../lens/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -62,6 +62,8 @@ export const ML_PAGES = {
|
|||
AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT: 'aiops/explain_log_rate_spikes_index_select',
|
||||
AIOPS_LOG_CATEGORIZATION: 'aiops/log_categorization',
|
||||
AIOPS_LOG_CATEGORIZATION_INDEX_SELECT: 'aiops/log_categorization_index_select',
|
||||
AIOPS_CHANGE_POINT_DETECTION: 'aiops/change_point_detection',
|
||||
AIOPS_CHANGE_POINT_DETECTION_INDEX_SELECT: 'aiops/change_point_detection_index_select',
|
||||
} as const;
|
||||
|
||||
export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES];
|
||||
|
|
|
@ -65,7 +65,9 @@ export type MlGenericUrlState = MLPageState<
|
|||
| typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES
|
||||
| typeof ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT
|
||||
| typeof ML_PAGES.AIOPS_LOG_CATEGORIZATION
|
||||
| typeof ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT,
|
||||
| typeof ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT
|
||||
| typeof ML_PAGES.AIOPS_CHANGE_POINT_DETECTION_INDEX_SELECT
|
||||
| typeof ML_PAGES.AIOPS_CHANGE_POINT_DETECTION,
|
||||
MlGenericUrlPageState | undefined
|
||||
>;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"embeddable",
|
||||
"features",
|
||||
"fieldFormats",
|
||||
"lens",
|
||||
"licensing",
|
||||
"share",
|
||||
"taskManager",
|
||||
|
@ -28,7 +29,6 @@
|
|||
"alerting",
|
||||
"dashboard",
|
||||
"home",
|
||||
"lens",
|
||||
"licenseManagement",
|
||||
"management",
|
||||
"maps",
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { ChangePointDetection } from '@kbn/aiops-plugin/public';
|
||||
|
||||
import { useMlContext } from '../contexts/ml';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import { HelpMenu } from '../components/help_menu';
|
||||
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';
|
||||
|
||||
import { MlPageHeader } from '../components/page_header';
|
||||
|
||||
export const ChangePointDetectionPage: FC = () => {
|
||||
const { services } = useMlKibana();
|
||||
|
||||
const context = useMlContext();
|
||||
const dataView = context.currentDataView;
|
||||
const savedSearch = context.currentSavedSearch;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MlPageHeader>
|
||||
<EuiFlexGroup responsive={false} wrap={false} alignItems={'center'} gutterSize={'m'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.changePointDetection.pageHeader"
|
||||
defaultMessage="Change point detection"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TechnicalPreviewBadge />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</MlPageHeader>
|
||||
{dataView ? (
|
||||
<ChangePointDetection
|
||||
dataView={dataView}
|
||||
savedSearch={savedSearch}
|
||||
appDependencies={pick(services, [
|
||||
'application',
|
||||
'data',
|
||||
'charts',
|
||||
'fieldFormats',
|
||||
'http',
|
||||
'notifications',
|
||||
'share',
|
||||
'storage',
|
||||
'uiSettings',
|
||||
'unifiedSearch',
|
||||
'theme',
|
||||
'lens',
|
||||
])}
|
||||
/>
|
||||
) : null}
|
||||
<HelpMenu docLink={services.docLinks.links.ml.guide} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -58,6 +58,7 @@ export const ExplainLogRateSpikesPage: FC = () => {
|
|||
'uiSettings',
|
||||
'unifiedSearch',
|
||||
'theme',
|
||||
'lens',
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export { ExplainLogRateSpikesPage } from './explain_log_rate_spikes';
|
||||
export { ChangePointDetectionPage } from './change_point_detection';
|
||||
|
|
|
@ -58,6 +58,7 @@ export const LogCategorizationPage: FC = () => {
|
|||
'uiSettings',
|
||||
'unifiedSearch',
|
||||
'theme',
|
||||
'lens',
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -93,6 +93,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
cases: deps.cases,
|
||||
unifiedSearch: deps.unifiedSearch,
|
||||
licensing: deps.licensing,
|
||||
lens: deps.lens,
|
||||
...coreStart,
|
||||
};
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ export interface Tab {
|
|||
onClick?: () => Promise<void>;
|
||||
/** Indicates if item should be marked as active with nested routes */
|
||||
highlightNestedRoutes?: boolean;
|
||||
/** List of route IDs related to the side nav entry */
|
||||
relatedRouteIds?: string[];
|
||||
}
|
||||
|
||||
export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
||||
|
@ -252,6 +254,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
|||
}),
|
||||
disabled: disableLinks,
|
||||
testSubj: 'mlMainTab explainLogRateSpikes',
|
||||
relatedRouteIds: ['explain_log_rate_spikes'],
|
||||
},
|
||||
{
|
||||
id: 'logCategorization',
|
||||
|
@ -261,6 +264,17 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
|||
}),
|
||||
disabled: disableLinks,
|
||||
testSubj: 'mlMainTab logCategorization',
|
||||
relatedRouteIds: ['log_categorization'],
|
||||
},
|
||||
{
|
||||
id: 'changePointDetection',
|
||||
pathId: ML_PAGES.AIOPS_CHANGE_POINT_DETECTION_INDEX_SELECT,
|
||||
name: i18n.translate('xpack.ml.navMenu.changePointDetectionLinkText', {
|
||||
defaultMessage: 'Change Point Detection',
|
||||
}),
|
||||
disabled: disableLinks,
|
||||
testSubj: 'mlMainTab changePointDetection',
|
||||
relatedRouteIds: ['change_point_detection'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -271,13 +285,24 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
|||
|
||||
const getTabItem: (tab: Tab) => EuiSideNavItemType<unknown> = useCallback(
|
||||
(tab: Tab) => {
|
||||
const { id, disabled, items, onClick, pathId, name, testSubj, highlightNestedRoutes } = tab;
|
||||
const {
|
||||
id,
|
||||
disabled,
|
||||
items,
|
||||
onClick,
|
||||
pathId,
|
||||
name,
|
||||
testSubj,
|
||||
highlightNestedRoutes,
|
||||
relatedRouteIds,
|
||||
} = tab;
|
||||
|
||||
const onClickCallback = onClick ?? (pathId ? redirectToTab.bind(null, pathId) : undefined);
|
||||
|
||||
const isSelected =
|
||||
`/${pathId}` === activeRoute?.path ||
|
||||
(!!highlightNestedRoutes && activeRoute?.path.includes(`${pathId}/`));
|
||||
(!!highlightNestedRoutes && activeRoute?.path.includes(`${pathId}/`)) ||
|
||||
(Array.isArray(relatedRouteIds) && relatedRouteIds.includes(activeRoute?.id!));
|
||||
|
||||
return {
|
||||
id,
|
||||
|
@ -290,7 +315,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) {
|
|||
forceOpen: true,
|
||||
};
|
||||
},
|
||||
[activeRoute?.path, redirectToTab]
|
||||
[activeRoute, redirectToTab]
|
||||
);
|
||||
|
||||
return useMemo(() => tabsDefinition.map(getTabItem), [tabsDefinition, getTabItem]);
|
||||
|
|
|
@ -24,6 +24,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
|||
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import type { CasesUiStart } from '@kbn/cases-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
import type { MlServicesContext } from '../../app';
|
||||
|
||||
interface StartPlugins {
|
||||
|
@ -45,6 +46,7 @@ interface StartPlugins {
|
|||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
core: CoreStart;
|
||||
appName: string;
|
||||
lens: LensPublicStart;
|
||||
}
|
||||
export type StartServices = CoreStart &
|
||||
StartPlugins & {
|
||||
|
|
|
@ -71,6 +71,13 @@ export const AIOPS_BREADCRUMB_LOG_PATTERN_ANALYSIS: ChromeBreadcrumb = Object.fr
|
|||
href: '/aiops/log_categorization_index_select',
|
||||
});
|
||||
|
||||
export const AIOPS_BREADCRUMB_CHANGE_POINT_DETECTION: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbLabel', {
|
||||
defaultMessage: 'AIOps Labs',
|
||||
}),
|
||||
href: '/aiops/change_point_detection_index_select',
|
||||
});
|
||||
|
||||
export const EXPLAIN_LOG_RATE_SPIKES: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.aiops.explainLogRateSpikesBreadcrumbLabel', {
|
||||
defaultMessage: 'Explain Log Rate Spikes',
|
||||
|
@ -85,6 +92,13 @@ export const LOG_PATTERN_ANALYSIS: ChromeBreadcrumb = Object.freeze({
|
|||
href: '/aiops/log_categorization_index_select',
|
||||
});
|
||||
|
||||
export const CHANGE_POINT_DETECTION: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.aiops.changePointDetectionBreadcrumbLabel', {
|
||||
defaultMessage: 'Change Point Detection',
|
||||
}),
|
||||
href: '/aiops/change_point_detection_index_select',
|
||||
});
|
||||
|
||||
export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
|
||||
text: i18n.translate('xpack.ml.createJobsBreadcrumbLabel', {
|
||||
defaultMessage: 'Create job',
|
||||
|
@ -115,8 +129,10 @@ const breadcrumbs = {
|
|||
DATA_VISUALIZER_BREADCRUMB,
|
||||
AIOPS_BREADCRUMB_EXPLAIN_LOG_RATE_SPIKES,
|
||||
AIOPS_BREADCRUMB_LOG_PATTERN_ANALYSIS,
|
||||
AIOPS_BREADCRUMB_CHANGE_POINT_DETECTION,
|
||||
EXPLAIN_LOG_RATE_SPIKES,
|
||||
LOG_PATTERN_ANALYSIS,
|
||||
CHANGE_POINT_DETECTION,
|
||||
CREATE_JOB_BREADCRUMB,
|
||||
CALENDAR_MANAGEMENT_BREADCRUMB,
|
||||
FILTER_LISTS_BREADCRUMB,
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AIOPS_ENABLED } from '@kbn/aiops-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC } from 'react';
|
||||
import { parse } from 'query-string';
|
||||
import { NavigateToPath } from '../../../contexts/kibana';
|
||||
import { MlRoute } from '../..';
|
||||
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
|
||||
import { PageLoader, PageProps } from '../../router';
|
||||
import { useResolver } from '../../use_resolver';
|
||||
import { checkBasicLicense } from '../../../license';
|
||||
import { cacheDataViewsContract } from '../../../util/index_utils';
|
||||
import { ChangePointDetectionPage as Page } from '../../../aiops';
|
||||
|
||||
export const changePointDetectionRouteFactory = (
|
||||
navigateToPath: NavigateToPath,
|
||||
basePath: string
|
||||
): MlRoute => ({
|
||||
id: 'change_point_detection',
|
||||
path: '/aiops/change_point_detection',
|
||||
title: i18n.translate('xpack.ml.aiops.changePointDetection.docTitle', {
|
||||
defaultMessage: 'Change point detection',
|
||||
}),
|
||||
render: (props, deps) => <PageWrapper {...props} deps={deps} />,
|
||||
breadcrumbs: [
|
||||
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB_CHANGE_POINT_DETECTION', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.changePointDetectionLabel', {
|
||||
defaultMessage: 'Change point detection',
|
||||
}),
|
||||
},
|
||||
],
|
||||
disabled: !AIOPS_ENABLED,
|
||||
});
|
||||
|
||||
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
|
||||
const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false });
|
||||
const { context } = useResolver(index, savedSearchId, deps.config, deps.dataViewsContract, {
|
||||
checkBasicLicense,
|
||||
cacheDataViewsContract: () => cacheDataViewsContract(deps.dataViewsContract),
|
||||
});
|
||||
|
||||
return (
|
||||
<PageLoader context={context}>
|
||||
<Page />
|
||||
</PageLoader>
|
||||
);
|
||||
};
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export * from './explain_log_rate_spikes';
|
||||
export * from './log_categorization';
|
||||
export * from './change_point_detection';
|
||||
|
|
|
@ -55,7 +55,7 @@ const getExplainLogRateSpikesBreadcrumbs = (navigateToPath: NavigateToPath, base
|
|||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB_EXPLAIN_LOG_RATE_SPIKES', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('EXPLAIN_LOG_RATE_SPIKES', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.selectDateViewLabel', {
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.selectDataViewLabel', {
|
||||
defaultMessage: 'Select Data View',
|
||||
}),
|
||||
},
|
||||
|
@ -66,7 +66,18 @@ const getLogCategorizationBreadcrumbs = (navigateToPath: NavigateToPath, basePat
|
|||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB_LOG_PATTERN_ANALYSIS', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('LOG_PATTERN_ANALYSIS', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.selectDateViewLabel', {
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.selectDataViewLabel', {
|
||||
defaultMessage: 'Select Data View',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const getChangePointDetectionBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
|
||||
getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('AIOPS_BREADCRUMB_CHANGE_POINT_DETECTION', navigateToPath, basePath),
|
||||
getBreadcrumbWithUrlForApp('CHANGE_POINT_DETECTION', navigateToPath, basePath),
|
||||
{
|
||||
text: i18n.translate('xpack.ml.aiopsBreadcrumbs.selectDataViewLabel', {
|
||||
defaultMessage: 'Select Data View',
|
||||
}),
|
||||
},
|
||||
|
@ -148,6 +159,26 @@ export const logCategorizationIndexOrSearchRouteFactory = (
|
|||
breadcrumbs: getLogCategorizationBreadcrumbs(navigateToPath, basePath),
|
||||
});
|
||||
|
||||
export const changePointDetectionIndexOrSearchRouteFactory = (
|
||||
navigateToPath: NavigateToPath,
|
||||
basePath: string
|
||||
): MlRoute => ({
|
||||
id: 'data_view_change_point_detection',
|
||||
path: '/aiops/change_point_detection_index_select',
|
||||
title: i18n.translate('xpack.ml.selectDataViewLabel', {
|
||||
defaultMessage: 'Select Data View',
|
||||
}),
|
||||
render: (props, deps) => (
|
||||
<PageWrapper
|
||||
{...props}
|
||||
nextStepPath="aiops/change_point_detection"
|
||||
deps={deps}
|
||||
mode={MODE.DATAVISUALIZER}
|
||||
/>
|
||||
),
|
||||
breadcrumbs: getChangePointDetectionBreadcrumbs(navigateToPath, basePath),
|
||||
});
|
||||
|
||||
const PageWrapper: FC<IndexOrSearchPageProps> = ({ nextStepPath, deps, mode }) => {
|
||||
const {
|
||||
services: {
|
||||
|
|
|
@ -90,6 +90,8 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
|
|||
case ML_PAGES.AIOPS_EXPLAIN_LOG_RATE_SPIKES_INDEX_SELECT:
|
||||
case ML_PAGES.AIOPS_LOG_CATEGORIZATION:
|
||||
case ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT:
|
||||
case ML_PAGES.AIOPS_CHANGE_POINT_DETECTION:
|
||||
case ML_PAGES.AIOPS_CHANGE_POINT_DETECTION_INDEX_SELECT:
|
||||
case ML_PAGES.OVERVIEW:
|
||||
case ML_PAGES.SETTINGS:
|
||||
case ML_PAGES.FILTER_LISTS_MANAGE:
|
||||
|
|
|
@ -19475,7 +19475,6 @@
|
|||
"xpack.ml.aiops.explainLogRateSpikes.docTitle": "Expliquer les pics de taux de log",
|
||||
"xpack.ml.aiopsBreadcrumbLabel": "AIOps",
|
||||
"xpack.ml.aiopsBreadcrumbs.explainLogRateSpikesLabel": "Expliquer les pics de taux de log",
|
||||
"xpack.ml.aiopsBreadcrumbs.selectDateViewLabel": "Vue de données",
|
||||
"xpack.ml.alertConditionValidation.title": "La condition d'alerte contient les problèmes suivants :",
|
||||
"xpack.ml.alertContext.anomalyExplorerUrlDescription": "URL pour ouvrir dans Anomaly Explorer",
|
||||
"xpack.ml.alertContext.isInterimDescription": "Indique si les premiers résultats contiennent des résultats temporaires",
|
||||
|
|
|
@ -19456,7 +19456,6 @@
|
|||
"xpack.ml.aiops.explainLogRateSpikes.docTitle": "ログレートスパイクを説明",
|
||||
"xpack.ml.aiopsBreadcrumbLabel": "AIOps",
|
||||
"xpack.ml.aiopsBreadcrumbs.explainLogRateSpikesLabel": "ログレートスパイクを説明",
|
||||
"xpack.ml.aiopsBreadcrumbs.selectDateViewLabel": "データビュー",
|
||||
"xpack.ml.alertConditionValidation.title": "アラート条件には次の問題が含まれます。",
|
||||
"xpack.ml.alertContext.anomalyExplorerUrlDescription": "異常エクスプローラーを開くURL",
|
||||
"xpack.ml.alertContext.isInterimDescription": "上位の一致に中間結果が含まれるかどうかを示します",
|
||||
|
|
|
@ -19486,7 +19486,6 @@
|
|||
"xpack.ml.aiops.explainLogRateSpikes.docTitle": "解释日志速率峰值",
|
||||
"xpack.ml.aiopsBreadcrumbLabel": "AIOps",
|
||||
"xpack.ml.aiopsBreadcrumbs.explainLogRateSpikesLabel": "解释日志速率峰值",
|
||||
"xpack.ml.aiopsBreadcrumbs.selectDateViewLabel": "数据视图",
|
||||
"xpack.ml.alertConditionValidation.title": "告警条件包含以下问题:",
|
||||
"xpack.ml.alertContext.anomalyExplorerUrlDescription": "要在 Anomaly Explorer 中打开的 URL",
|
||||
"xpack.ml.alertContext.isInterimDescription": "表示排名靠前的命中是否包含中间结果",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue