[ML] Anomaly Explorer swim lane pagination (#70063)
* [ML] use explorer service * [ML] WIP pagination * [ML] add to dashboard without the limit * [ML] WIP * [ML] loading states * [ML] viewBySwimlaneDataLoading on field change * [ML] fix dashboard control * [ML] universal swim lane container, embeddable pagination * [ML] fix css issue * [ML] rename anomalyTimelineService * [ML] rename callback * [ML] rename container component * [ML] empty state, increase pagination margin * [ML] check for loading * [ML] fix i18n * [ML] fix unit test * [ML] improve selected cells * [ML] fix overall selection with changing job selection * [ML] required props for pagination component * [ML] move RESIZE_IGNORED_DIFF_PX * [ML] jest tests * [ML] add test subject * [ML] SWIM_LANE_DEFAULT_PAGE_SIZE * [ML] change empty state styling * [ML] fix agg size for influencer filters * [ML] remove debounce * [ML] SCSS variables, rename swim lane class * [ML] job selector using context * [ML] set padding for embeddable panel * [ML] adjust pagination styles * [ML] replace custom time range subject with timefilter * [ML] change loading indicator to mono * [ML] use swim lane type constant * [ML] change context naming * [ML] update jest snapshot * [ML] fix tests
This commit is contained in:
parent
335c9bb148
commit
854e7a5204
55 changed files with 1367 additions and 1293 deletions
|
@ -7,7 +7,7 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { AppMountParameters, CoreStart } from 'kibana/public';
|
import { AppMountParameters, CoreStart, HttpStart } from 'kibana/public';
|
||||||
|
|
||||||
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@ import { setLicenseCache } from './license';
|
||||||
import { MlSetupDependencies, MlStartDependencies } from '../plugin';
|
import { MlSetupDependencies, MlStartDependencies } from '../plugin';
|
||||||
|
|
||||||
import { MlRouter } from './routing';
|
import { MlRouter } from './routing';
|
||||||
|
import { mlApiServicesProvider } from './services/ml_api_service';
|
||||||
|
import { HttpService } from './services/http_service';
|
||||||
|
|
||||||
type MlDependencies = MlSetupDependencies & MlStartDependencies;
|
type MlDependencies = MlSetupDependencies & MlStartDependencies;
|
||||||
|
|
||||||
|
@ -27,6 +29,23 @@ interface AppProps {
|
||||||
|
|
||||||
const localStorage = new Storage(window.localStorage);
|
const localStorage = new Storage(window.localStorage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides global services available across the entire ML app.
|
||||||
|
*/
|
||||||
|
export function getMlGlobalServices(httpStart: HttpStart) {
|
||||||
|
const httpService = new HttpService(httpStart);
|
||||||
|
return {
|
||||||
|
httpService,
|
||||||
|
mlApiServices: mlApiServicesProvider(httpService),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MlServicesContext {
|
||||||
|
mlServices: MlGlobalServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MlGlobalServices = ReturnType<typeof getMlGlobalServices>;
|
||||||
|
|
||||||
const App: FC<AppProps> = ({ coreStart, deps }) => {
|
const App: FC<AppProps> = ({ coreStart, deps }) => {
|
||||||
const pageDeps = {
|
const pageDeps = {
|
||||||
indexPatterns: deps.data.indexPatterns,
|
indexPatterns: deps.data.indexPatterns,
|
||||||
|
@ -47,7 +66,9 @@ const App: FC<AppProps> = ({ coreStart, deps }) => {
|
||||||
const I18nContext = coreStart.i18n.Context;
|
const I18nContext = coreStart.i18n.Context;
|
||||||
return (
|
return (
|
||||||
<I18nContext>
|
<I18nContext>
|
||||||
<KibanaContextProvider services={services}>
|
<KibanaContextProvider
|
||||||
|
services={{ ...services, mlServices: getMlGlobalServices(coreStart.http) }}
|
||||||
|
>
|
||||||
<MlRouter pageDeps={pageDeps} />
|
<MlRouter pageDeps={pageDeps} />
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
</I18nContext>
|
</I18nContext>
|
||||||
|
|
|
@ -27,7 +27,6 @@ import {
|
||||||
normalizeTimes,
|
normalizeTimes,
|
||||||
} from './job_select_service_utils';
|
} from './job_select_service_utils';
|
||||||
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';
|
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';
|
||||||
import { ml } from '../../services/ml_api_service';
|
|
||||||
import { useMlKibana } from '../../contexts/kibana';
|
import { useMlKibana } from '../../contexts/kibana';
|
||||||
import { JobSelectionMaps } from './job_selector';
|
import { JobSelectionMaps } from './job_selector';
|
||||||
|
|
||||||
|
@ -66,7 +65,10 @@ export const JobSelectorFlyout: FC<JobSelectorFlyoutProps> = ({
|
||||||
withTimeRangeSelector = true,
|
withTimeRangeSelector = true,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
services: { notifications },
|
services: {
|
||||||
|
notifications,
|
||||||
|
mlServices: { mlApiServices },
|
||||||
|
},
|
||||||
} = useMlKibana();
|
} = useMlKibana();
|
||||||
|
|
||||||
const [newSelection, setNewSelection] = useState(selectedIds);
|
const [newSelection, setNewSelection] = useState(selectedIds);
|
||||||
|
@ -151,7 +153,7 @@ export const JobSelectorFlyout: FC<JobSelectorFlyoutProps> = ({
|
||||||
|
|
||||||
async function fetchJobs() {
|
async function fetchJobs() {
|
||||||
try {
|
try {
|
||||||
const resp = await ml.jobs.jobsWithTimerange(dateFormatTz);
|
const resp = await mlApiServices.jobs.jobsWithTimerange(dateFormatTz);
|
||||||
const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH);
|
const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH);
|
||||||
const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs);
|
const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs);
|
||||||
setJobs(normalizedJobs);
|
setJobs(normalizedJobs);
|
||||||
|
|
|
@ -9,10 +9,7 @@ import { Subscription } from 'rxjs';
|
||||||
import { EuiSuperDatePicker, OnRefreshProps } from '@elastic/eui';
|
import { EuiSuperDatePicker, OnRefreshProps } from '@elastic/eui';
|
||||||
import { TimeRange, TimeHistoryContract } from 'src/plugins/data/public';
|
import { TimeRange, TimeHistoryContract } from 'src/plugins/data/public';
|
||||||
|
|
||||||
import {
|
import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service';
|
||||||
mlTimefilterRefresh$,
|
|
||||||
mlTimefilterTimeChange$,
|
|
||||||
} from '../../../services/timefilter_refresh_service';
|
|
||||||
import { useUrlState } from '../../../util/url_state';
|
import { useUrlState } from '../../../util/url_state';
|
||||||
import { useMlKibana } from '../../../contexts/kibana';
|
import { useMlKibana } from '../../../contexts/kibana';
|
||||||
|
|
||||||
|
@ -108,7 +105,6 @@ export const DatePickerWrapper: FC = () => {
|
||||||
timefilter.setTime(newTime);
|
timefilter.setTime(newTime);
|
||||||
setTime(newTime);
|
setTime(newTime);
|
||||||
setRecentlyUsedRanges(getRecentlyUsedRanges());
|
setRecentlyUsedRanges(getRecentlyUsedRanges());
|
||||||
mlTimefilterTimeChange$.next({ lastRefresh: Date.now(), timeRange: { start, end } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateInterval({
|
function updateInterval({
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { SecurityPluginSetup } from '../../../../../security/public';
|
import { SecurityPluginSetup } from '../../../../../security/public';
|
||||||
import { LicenseManagementUIPluginSetup } from '../../../../../license_management/public';
|
import { LicenseManagementUIPluginSetup } from '../../../../../license_management/public';
|
||||||
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
|
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
|
||||||
|
import { MlServicesContext } from '../../app';
|
||||||
|
|
||||||
interface StartPlugins {
|
interface StartPlugins {
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
|
@ -20,7 +21,8 @@ interface StartPlugins {
|
||||||
licenseManagement?: LicenseManagementUIPluginSetup;
|
licenseManagement?: LicenseManagementUIPluginSetup;
|
||||||
share: SharePluginStart;
|
share: SharePluginStart;
|
||||||
}
|
}
|
||||||
export type StartServices = CoreStart & StartPlugins & { kibanaVersion: string };
|
export type StartServices = CoreStart &
|
||||||
|
StartPlugins & { kibanaVersion: string } & MlServicesContext;
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
export const useMlKibana = () => useKibana<StartServices>();
|
export const useMlKibana = () => useKibana<StartServices>();
|
||||||
export type MlKibanaReactContextValue = KibanaReactContextValue<StartServices>;
|
export type MlKibanaReactContextValue = KibanaReactContextValue<StartServices>;
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public';
|
import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public';
|
||||||
import { SavedSearchSavedObject } from '../../../../common/types/kibana';
|
import { SavedSearchSavedObject } from '../../../../common/types/kibana';
|
||||||
|
import { MlServicesContext } from '../../app';
|
||||||
|
|
||||||
export interface MlContextValue {
|
export interface MlContextValue {
|
||||||
combinedQuery: any;
|
combinedQuery: any;
|
||||||
|
@ -34,4 +35,4 @@ export type SavedSearchQuery = object;
|
||||||
// Multiple custom hooks can be created to access subsets of
|
// Multiple custom hooks can be created to access subsets of
|
||||||
// the overall context value if necessary too,
|
// the overall context value if necessary too,
|
||||||
// see useCurrentIndexPattern() for example.
|
// see useCurrentIndexPattern() for example.
|
||||||
export const MlContext = React.createContext<Partial<MlContextValue>>({});
|
export const MlContext = React.createContext<Partial<MlContextValue & MlServicesContext>>({});
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
$borderRadius: $euiBorderRadius / 2;
|
||||||
|
|
||||||
.ml-swimlane-selector {
|
.ml-swimlane-selector {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
@ -104,10 +106,9 @@
|
||||||
|
|
||||||
// SASSTODO: This entire selector needs to be rewritten.
|
// SASSTODO: This entire selector needs to be rewritten.
|
||||||
// It looks extremely brittle with very specific sizing units
|
// It looks extremely brittle with very specific sizing units
|
||||||
.ml-explorer-swimlane {
|
.mlExplorerSwimlane {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: $euiSizeS;
|
|
||||||
|
|
||||||
line.gridLine {
|
line.gridLine {
|
||||||
stroke: $euiBorderColor;
|
stroke: $euiBorderColor;
|
||||||
|
@ -218,17 +219,20 @@
|
||||||
div.lane {
|
div.lane {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-bottom: 0px;
|
border-bottom: 0px;
|
||||||
border-radius: 2px;
|
border-radius: $borderRadius;
|
||||||
margin-top: -1px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
div.lane-label {
|
div.lane-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 13px;
|
font-size: $euiFontSizeXS;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border-radius: 2px;
|
border-radius: $borderRadius;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
@ -261,7 +265,7 @@
|
||||||
.sl-cell-inner-dragselect {
|
.sl-cell-inner-dragselect {
|
||||||
height: 26px;
|
height: 26px;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
border-radius: 2px;
|
border-radius: $borderRadius;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +297,7 @@
|
||||||
.sl-cell-inner,
|
.sl-cell-inner,
|
||||||
.sl-cell-inner-dragselect {
|
.sl-cell-inner-dragselect {
|
||||||
border: 2px solid $euiColorDarkShade;
|
border: 2px solid $euiColorDarkShade;
|
||||||
border-radius: 2px;
|
border-radius: $borderRadius;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import useObservable from 'react-use/lib/useObservable';
|
||||||
import { forkJoin, of, Observable, Subject } from 'rxjs';
|
import { forkJoin, of, Observable, Subject } from 'rxjs';
|
||||||
import { mergeMap, switchMap, tap } from 'rxjs/operators';
|
import { mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import { anomalyDataChange } from '../explorer_charts/explorer_charts_container_service';
|
import { anomalyDataChange } from '../explorer_charts/explorer_charts_container_service';
|
||||||
import { explorerService } from '../explorer_dashboard_service';
|
import { explorerService } from '../explorer_dashboard_service';
|
||||||
import {
|
import {
|
||||||
|
@ -22,15 +23,17 @@ import {
|
||||||
loadAnomaliesTableData,
|
loadAnomaliesTableData,
|
||||||
loadDataForCharts,
|
loadDataForCharts,
|
||||||
loadFilteredTopInfluencers,
|
loadFilteredTopInfluencers,
|
||||||
loadOverallData,
|
|
||||||
loadTopInfluencers,
|
loadTopInfluencers,
|
||||||
loadViewBySwimlane,
|
|
||||||
loadViewByTopFieldValuesForSelectedTime,
|
|
||||||
AppStateSelectedCells,
|
AppStateSelectedCells,
|
||||||
ExplorerJob,
|
ExplorerJob,
|
||||||
TimeRangeBounds,
|
TimeRangeBounds,
|
||||||
} from '../explorer_utils';
|
} from '../explorer_utils';
|
||||||
import { ExplorerState } from '../reducers';
|
import { ExplorerState } from '../reducers';
|
||||||
|
import { useMlKibana, useTimefilter } from '../../contexts/kibana';
|
||||||
|
import { AnomalyTimelineService } from '../../services/anomaly_timeline_service';
|
||||||
|
import { mlResultsServiceProvider } from '../../services/results_service';
|
||||||
|
import { isViewBySwimLaneData } from '../swimlane_container';
|
||||||
|
import { ANOMALY_SWIM_LANE_HARD_LIMIT } from '../explorer_constants';
|
||||||
|
|
||||||
// Memoize the data fetching methods.
|
// Memoize the data fetching methods.
|
||||||
// wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument
|
// wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument
|
||||||
|
@ -39,13 +42,13 @@ import { ExplorerState } from '../reducers';
|
||||||
// about this parameter. The generic type T retains and returns the type information of
|
// about this parameter. The generic type T retains and returns the type information of
|
||||||
// the original function.
|
// the original function.
|
||||||
const memoizeIsEqual = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs);
|
const memoizeIsEqual = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs);
|
||||||
const wrapWithLastRefreshArg = <T extends (...a: any[]) => any>(func: T) => {
|
const wrapWithLastRefreshArg = <T extends (...a: any[]) => any>(func: T, context: any = null) => {
|
||||||
return function (lastRefresh: number, ...args: Parameters<T>): ReturnType<T> {
|
return function (lastRefresh: number, ...args: Parameters<T>): ReturnType<T> {
|
||||||
return func.apply(null, args);
|
return func.apply(context, args);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const memoize = <T extends (...a: any[]) => any>(func: T) => {
|
const memoize = <T extends (...a: any[]) => any>(func: T, context?: any) => {
|
||||||
return memoizeOne(wrapWithLastRefreshArg<T>(func), memoizeIsEqual);
|
return memoizeOne(wrapWithLastRefreshArg<T>(func, context), memoizeIsEqual);
|
||||||
};
|
};
|
||||||
|
|
||||||
const memoizedAnomalyDataChange = memoize<typeof anomalyDataChange>(anomalyDataChange);
|
const memoizedAnomalyDataChange = memoize<typeof anomalyDataChange>(anomalyDataChange);
|
||||||
|
@ -56,9 +59,7 @@ const memoizedLoadDataForCharts = memoize<typeof loadDataForCharts>(loadDataForC
|
||||||
const memoizedLoadFilteredTopInfluencers = memoize<typeof loadFilteredTopInfluencers>(
|
const memoizedLoadFilteredTopInfluencers = memoize<typeof loadFilteredTopInfluencers>(
|
||||||
loadFilteredTopInfluencers
|
loadFilteredTopInfluencers
|
||||||
);
|
);
|
||||||
const memoizedLoadOverallData = memoize(loadOverallData);
|
|
||||||
const memoizedLoadTopInfluencers = memoize(loadTopInfluencers);
|
const memoizedLoadTopInfluencers = memoize(loadTopInfluencers);
|
||||||
const memoizedLoadViewBySwimlane = memoize(loadViewBySwimlane);
|
|
||||||
const memoizedLoadAnomaliesTableData = memoize(loadAnomaliesTableData);
|
const memoizedLoadAnomaliesTableData = memoize(loadAnomaliesTableData);
|
||||||
|
|
||||||
export interface LoadExplorerDataConfig {
|
export interface LoadExplorerDataConfig {
|
||||||
|
@ -73,6 +74,9 @@ export interface LoadExplorerDataConfig {
|
||||||
tableInterval: string;
|
tableInterval: string;
|
||||||
tableSeverity: number;
|
tableSeverity: number;
|
||||||
viewBySwimlaneFieldName: string;
|
viewBySwimlaneFieldName: string;
|
||||||
|
viewByFromPage: number;
|
||||||
|
viewByPerPage: number;
|
||||||
|
swimlaneContainerWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfig => {
|
export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfig => {
|
||||||
|
@ -87,183 +91,213 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the data necessary for the Anomaly Explorer using observables.
|
* Fetches the data necessary for the Anomaly Explorer using observables.
|
||||||
*
|
|
||||||
* @param config LoadExplorerDataConfig
|
|
||||||
*
|
|
||||||
* @return Partial<ExplorerState>
|
|
||||||
*/
|
*/
|
||||||
function loadExplorerData(config: LoadExplorerDataConfig): Observable<Partial<ExplorerState>> {
|
const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService) => {
|
||||||
if (!isLoadExplorerDataConfig(config)) {
|
const memoizedLoadOverallData = memoize(
|
||||||
return of({});
|
anomalyTimelineService.loadOverallData,
|
||||||
}
|
anomalyTimelineService
|
||||||
|
|
||||||
const {
|
|
||||||
bounds,
|
|
||||||
lastRefresh,
|
|
||||||
influencersFilterQuery,
|
|
||||||
noInfluencersConfigured,
|
|
||||||
selectedCells,
|
|
||||||
selectedJobs,
|
|
||||||
swimlaneBucketInterval,
|
|
||||||
swimlaneLimit,
|
|
||||||
tableInterval,
|
|
||||||
tableSeverity,
|
|
||||||
viewBySwimlaneFieldName,
|
|
||||||
} = config;
|
|
||||||
|
|
||||||
const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName);
|
|
||||||
const jobIds = getSelectionJobIds(selectedCells, selectedJobs);
|
|
||||||
const timerange = getSelectionTimeRange(
|
|
||||||
selectedCells,
|
|
||||||
swimlaneBucketInterval.asSeconds(),
|
|
||||||
bounds
|
|
||||||
);
|
);
|
||||||
|
const memoizedLoadViewBySwimlane = memoize(
|
||||||
|
anomalyTimelineService.loadViewBySwimlane,
|
||||||
|
anomalyTimelineService
|
||||||
|
);
|
||||||
|
return (config: LoadExplorerDataConfig): Observable<Partial<ExplorerState>> => {
|
||||||
|
if (!isLoadExplorerDataConfig(config)) {
|
||||||
|
return of({});
|
||||||
|
}
|
||||||
|
|
||||||
const dateFormatTz = getDateFormatTz();
|
const {
|
||||||
|
bounds,
|
||||||
// First get the data where we have all necessary args at hand using forkJoin:
|
|
||||||
// annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues
|
|
||||||
return forkJoin({
|
|
||||||
annotationsData: memoizedLoadAnnotationsTableData(
|
|
||||||
lastRefresh,
|
lastRefresh,
|
||||||
|
influencersFilterQuery,
|
||||||
|
noInfluencersConfigured,
|
||||||
selectedCells,
|
selectedCells,
|
||||||
selectedJobs,
|
selectedJobs,
|
||||||
swimlaneBucketInterval.asSeconds(),
|
|
||||||
bounds
|
|
||||||
),
|
|
||||||
anomalyChartRecords: memoizedLoadDataForCharts(
|
|
||||||
lastRefresh,
|
|
||||||
jobIds,
|
|
||||||
timerange.earliestMs,
|
|
||||||
timerange.latestMs,
|
|
||||||
selectionInfluencers,
|
|
||||||
selectedCells,
|
|
||||||
influencersFilterQuery
|
|
||||||
),
|
|
||||||
influencers:
|
|
||||||
selectionInfluencers.length === 0
|
|
||||||
? memoizedLoadTopInfluencers(
|
|
||||||
lastRefresh,
|
|
||||||
jobIds,
|
|
||||||
timerange.earliestMs,
|
|
||||||
timerange.latestMs,
|
|
||||||
[],
|
|
||||||
noInfluencersConfigured,
|
|
||||||
influencersFilterQuery
|
|
||||||
)
|
|
||||||
: Promise.resolve({}),
|
|
||||||
overallState: memoizedLoadOverallData(
|
|
||||||
lastRefresh,
|
|
||||||
selectedJobs,
|
|
||||||
swimlaneBucketInterval,
|
swimlaneBucketInterval,
|
||||||
bounds
|
swimlaneLimit,
|
||||||
),
|
|
||||||
tableData: memoizedLoadAnomaliesTableData(
|
|
||||||
lastRefresh,
|
|
||||||
selectedCells,
|
|
||||||
selectedJobs,
|
|
||||||
dateFormatTz,
|
|
||||||
swimlaneBucketInterval.asSeconds(),
|
|
||||||
bounds,
|
|
||||||
viewBySwimlaneFieldName,
|
|
||||||
tableInterval,
|
tableInterval,
|
||||||
tableSeverity,
|
tableSeverity,
|
||||||
influencersFilterQuery
|
viewBySwimlaneFieldName,
|
||||||
),
|
swimlaneContainerWidth,
|
||||||
topFieldValues:
|
viewByFromPage,
|
||||||
selectedCells !== undefined && selectedCells.showTopFieldValues === true
|
viewByPerPage,
|
||||||
? loadViewByTopFieldValuesForSelectedTime(
|
} = config;
|
||||||
|
|
||||||
|
const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName);
|
||||||
|
const jobIds = getSelectionJobIds(selectedCells, selectedJobs);
|
||||||
|
const timerange = getSelectionTimeRange(
|
||||||
|
selectedCells,
|
||||||
|
swimlaneBucketInterval.asSeconds(),
|
||||||
|
bounds
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateFormatTz = getDateFormatTz();
|
||||||
|
|
||||||
|
// First get the data where we have all necessary args at hand using forkJoin:
|
||||||
|
// annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues
|
||||||
|
return forkJoin({
|
||||||
|
annotationsData: memoizedLoadAnnotationsTableData(
|
||||||
|
lastRefresh,
|
||||||
|
selectedCells,
|
||||||
|
selectedJobs,
|
||||||
|
swimlaneBucketInterval.asSeconds(),
|
||||||
|
bounds
|
||||||
|
),
|
||||||
|
anomalyChartRecords: memoizedLoadDataForCharts(
|
||||||
|
lastRefresh,
|
||||||
|
jobIds,
|
||||||
|
timerange.earliestMs,
|
||||||
|
timerange.latestMs,
|
||||||
|
selectionInfluencers,
|
||||||
|
selectedCells,
|
||||||
|
influencersFilterQuery
|
||||||
|
),
|
||||||
|
influencers:
|
||||||
|
selectionInfluencers.length === 0
|
||||||
|
? memoizedLoadTopInfluencers(
|
||||||
|
lastRefresh,
|
||||||
|
jobIds,
|
||||||
|
timerange.earliestMs,
|
||||||
|
timerange.latestMs,
|
||||||
|
[],
|
||||||
|
noInfluencersConfigured,
|
||||||
|
influencersFilterQuery
|
||||||
|
)
|
||||||
|
: Promise.resolve({}),
|
||||||
|
overallState: memoizedLoadOverallData(lastRefresh, selectedJobs, swimlaneContainerWidth),
|
||||||
|
tableData: memoizedLoadAnomaliesTableData(
|
||||||
|
lastRefresh,
|
||||||
|
selectedCells,
|
||||||
|
selectedJobs,
|
||||||
|
dateFormatTz,
|
||||||
|
swimlaneBucketInterval.asSeconds(),
|
||||||
|
bounds,
|
||||||
|
viewBySwimlaneFieldName,
|
||||||
|
tableInterval,
|
||||||
|
tableSeverity,
|
||||||
|
influencersFilterQuery
|
||||||
|
),
|
||||||
|
topFieldValues:
|
||||||
|
selectedCells !== undefined && selectedCells.showTopFieldValues === true
|
||||||
|
? anomalyTimelineService.loadViewByTopFieldValuesForSelectedTime(
|
||||||
|
timerange.earliestMs,
|
||||||
|
timerange.latestMs,
|
||||||
|
selectedJobs,
|
||||||
|
viewBySwimlaneFieldName,
|
||||||
|
swimlaneLimit,
|
||||||
|
viewByPerPage,
|
||||||
|
viewByFromPage,
|
||||||
|
swimlaneContainerWidth
|
||||||
|
)
|
||||||
|
: Promise.resolve([]),
|
||||||
|
}).pipe(
|
||||||
|
// Trigger a side-effect action to reset view-by swimlane,
|
||||||
|
// show the view-by loading indicator
|
||||||
|
// and pass on the data we already fetched.
|
||||||
|
tap(explorerService.setViewBySwimlaneLoading),
|
||||||
|
// Trigger a side-effect to update the charts.
|
||||||
|
tap(({ anomalyChartRecords }) => {
|
||||||
|
if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) {
|
||||||
|
memoizedAnomalyDataChange(
|
||||||
|
lastRefresh,
|
||||||
|
anomalyChartRecords,
|
||||||
timerange.earliestMs,
|
timerange.earliestMs,
|
||||||
timerange.latestMs,
|
timerange.latestMs,
|
||||||
selectedJobs,
|
tableSeverity
|
||||||
viewBySwimlaneFieldName,
|
);
|
||||||
swimlaneLimit,
|
} else {
|
||||||
noInfluencersConfigured
|
memoizedAnomalyDataChange(
|
||||||
)
|
|
||||||
: Promise.resolve([]),
|
|
||||||
}).pipe(
|
|
||||||
// Trigger a side-effect action to reset view-by swimlane,
|
|
||||||
// show the view-by loading indicator
|
|
||||||
// and pass on the data we already fetched.
|
|
||||||
tap(explorerService.setViewBySwimlaneLoading),
|
|
||||||
// Trigger a side-effect to update the charts.
|
|
||||||
tap(({ anomalyChartRecords }) => {
|
|
||||||
if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) {
|
|
||||||
memoizedAnomalyDataChange(
|
|
||||||
lastRefresh,
|
|
||||||
anomalyChartRecords,
|
|
||||||
timerange.earliestMs,
|
|
||||||
timerange.latestMs,
|
|
||||||
tableSeverity
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
memoizedAnomalyDataChange(
|
|
||||||
lastRefresh,
|
|
||||||
[],
|
|
||||||
timerange.earliestMs,
|
|
||||||
timerange.latestMs,
|
|
||||||
tableSeverity
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// Load view-by swimlane data and filtered top influencers.
|
|
||||||
// mergeMap is used to have access to the already fetched data and act on it in arg #1.
|
|
||||||
// In arg #2 of mergeMap we combine the data and pass it on in the action format
|
|
||||||
// which can be consumed by explorerReducer() later on.
|
|
||||||
mergeMap(
|
|
||||||
({ anomalyChartRecords, influencers, overallState, topFieldValues }) =>
|
|
||||||
forkJoin({
|
|
||||||
influencers:
|
|
||||||
(selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) &&
|
|
||||||
anomalyChartRecords !== undefined &&
|
|
||||||
anomalyChartRecords.length > 0
|
|
||||||
? memoizedLoadFilteredTopInfluencers(
|
|
||||||
lastRefresh,
|
|
||||||
jobIds,
|
|
||||||
timerange.earliestMs,
|
|
||||||
timerange.latestMs,
|
|
||||||
anomalyChartRecords,
|
|
||||||
selectionInfluencers,
|
|
||||||
noInfluencersConfigured,
|
|
||||||
influencersFilterQuery
|
|
||||||
)
|
|
||||||
: Promise.resolve(influencers),
|
|
||||||
viewBySwimlaneState: memoizedLoadViewBySwimlane(
|
|
||||||
lastRefresh,
|
lastRefresh,
|
||||||
topFieldValues,
|
[],
|
||||||
{
|
timerange.earliestMs,
|
||||||
earliest: overallState.overallSwimlaneData.earliest,
|
timerange.latestMs,
|
||||||
latest: overallState.overallSwimlaneData.latest,
|
tableSeverity
|
||||||
},
|
);
|
||||||
selectedJobs,
|
}
|
||||||
viewBySwimlaneFieldName,
|
}),
|
||||||
swimlaneLimit,
|
// Load view-by swimlane data and filtered top influencers.
|
||||||
influencersFilterQuery,
|
// mergeMap is used to have access to the already fetched data and act on it in arg #1.
|
||||||
noInfluencersConfigured
|
// In arg #2 of mergeMap we combine the data and pass it on in the action format
|
||||||
),
|
// which can be consumed by explorerReducer() later on.
|
||||||
}),
|
mergeMap(
|
||||||
(
|
({ anomalyChartRecords, influencers, overallState, topFieldValues }) =>
|
||||||
{ annotationsData, overallState, tableData },
|
forkJoin({
|
||||||
{ influencers, viewBySwimlaneState }
|
influencers:
|
||||||
): Partial<ExplorerState> => {
|
(selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) &&
|
||||||
return {
|
anomalyChartRecords !== undefined &&
|
||||||
annotationsData,
|
anomalyChartRecords.length > 0
|
||||||
influencers,
|
? memoizedLoadFilteredTopInfluencers(
|
||||||
...overallState,
|
lastRefresh,
|
||||||
...viewBySwimlaneState,
|
jobIds,
|
||||||
tableData,
|
timerange.earliestMs,
|
||||||
};
|
timerange.latestMs,
|
||||||
}
|
anomalyChartRecords,
|
||||||
)
|
selectionInfluencers,
|
||||||
);
|
noInfluencersConfigured,
|
||||||
}
|
influencersFilterQuery
|
||||||
|
)
|
||||||
const loadExplorerData$ = new Subject<LoadExplorerDataConfig>();
|
: Promise.resolve(influencers),
|
||||||
const explorerData$ = loadExplorerData$.pipe(
|
viewBySwimlaneState: memoizedLoadViewBySwimlane(
|
||||||
switchMap((config: LoadExplorerDataConfig) => loadExplorerData(config))
|
lastRefresh,
|
||||||
);
|
topFieldValues,
|
||||||
|
{
|
||||||
export const useExplorerData = (): [Partial<ExplorerState> | undefined, (d: any) => void] => {
|
earliest: overallState.earliest,
|
||||||
const explorerData = useObservable(explorerData$);
|
latest: overallState.latest,
|
||||||
return [explorerData, (c) => loadExplorerData$.next(c)];
|
},
|
||||||
|
selectedJobs,
|
||||||
|
viewBySwimlaneFieldName,
|
||||||
|
ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||||
|
viewByPerPage,
|
||||||
|
viewByFromPage,
|
||||||
|
swimlaneContainerWidth,
|
||||||
|
influencersFilterQuery
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
(
|
||||||
|
{ annotationsData, overallState, tableData },
|
||||||
|
{ influencers, viewBySwimlaneState }
|
||||||
|
): Partial<ExplorerState> => {
|
||||||
|
return {
|
||||||
|
annotationsData,
|
||||||
|
influencers,
|
||||||
|
loading: false,
|
||||||
|
viewBySwimlaneDataLoading: false,
|
||||||
|
overallSwimlaneData: overallState,
|
||||||
|
viewBySwimlaneData: viewBySwimlaneState,
|
||||||
|
tableData,
|
||||||
|
swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState)
|
||||||
|
? viewBySwimlaneState.cardinality
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const useExplorerData = (): [Partial<ExplorerState> | undefined, (d: any) => void] => {
|
||||||
|
const timefilter = useTimefilter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
services: {
|
||||||
|
mlServices: { mlApiServices },
|
||||||
|
uiSettings,
|
||||||
|
},
|
||||||
|
} = useMlKibana();
|
||||||
|
const loadExplorerData = useMemo(() => {
|
||||||
|
const service = new AnomalyTimelineService(
|
||||||
|
timefilter,
|
||||||
|
uiSettings,
|
||||||
|
mlResultsServiceProvider(mlApiServices)
|
||||||
|
);
|
||||||
|
return loadExplorerDataProvider(service);
|
||||||
|
}, []);
|
||||||
|
const loadExplorerData$ = useMemo(() => new Subject<LoadExplorerDataConfig>(), []);
|
||||||
|
const explorerData$ = useMemo(() => loadExplorerData$.pipe(switchMap(loadExplorerData)), []);
|
||||||
|
const explorerData = useObservable(explorerData$);
|
||||||
|
|
||||||
|
const update = useCallback((c) => {
|
||||||
|
loadExplorerData$.next(c);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [explorerData, update];
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,7 +52,6 @@ function getDefaultEmbeddablepaPanelConfig(jobIds: JobId[]) {
|
||||||
interface AddToDashboardControlProps {
|
interface AddToDashboardControlProps {
|
||||||
jobIds: JobId[];
|
jobIds: JobId[];
|
||||||
viewBy: string;
|
viewBy: string;
|
||||||
limit: number;
|
|
||||||
onClose: (callback?: () => Promise<any>) => void;
|
onClose: (callback?: () => Promise<any>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +62,6 @@ export const AddToDashboardControl: FC<AddToDashboardControlProps> = ({
|
||||||
onClose,
|
onClose,
|
||||||
jobIds,
|
jobIds,
|
||||||
viewBy,
|
viewBy,
|
||||||
limit,
|
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
notifications: { toasts },
|
notifications: { toasts },
|
||||||
|
@ -141,7 +139,6 @@ export const AddToDashboardControl: FC<AddToDashboardControlProps> = ({
|
||||||
jobIds,
|
jobIds,
|
||||||
swimlaneType,
|
swimlaneType,
|
||||||
viewBy,
|
viewBy,
|
||||||
limit,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -206,8 +203,8 @@ export const AddToDashboardControl: FC<AddToDashboardControlProps> = ({
|
||||||
{
|
{
|
||||||
id: SWIMLANE_TYPE.VIEW_BY,
|
id: SWIMLANE_TYPE.VIEW_BY,
|
||||||
label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', {
|
label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', {
|
||||||
defaultMessage: 'View by {viewByField}, up to {limit} rows',
|
defaultMessage: 'View by {viewByField}',
|
||||||
values: { viewByField: viewBy, limit },
|
values: { viewByField: viewBy },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -22,12 +22,11 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { DRAG_SELECT_ACTION, VIEW_BY_JOB_LABEL } from './explorer_constants';
|
import { DRAG_SELECT_ACTION, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants';
|
||||||
import { AddToDashboardControl } from './add_to_dashboard_control';
|
import { AddToDashboardControl } from './add_to_dashboard_control';
|
||||||
import { useMlKibana } from '../contexts/kibana';
|
import { useMlKibana } from '../contexts/kibana';
|
||||||
import { TimeBuckets } from '../util/time_buckets';
|
import { TimeBuckets } from '../util/time_buckets';
|
||||||
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
|
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
|
||||||
import { SelectLimit } from './select_limit';
|
|
||||||
import {
|
import {
|
||||||
ALLOW_CELL_RANGE_SELECTION,
|
ALLOW_CELL_RANGE_SELECTION,
|
||||||
dragSelect$,
|
dragSelect$,
|
||||||
|
@ -36,9 +35,9 @@ import {
|
||||||
import { ExplorerState } from './reducers/explorer_reducer';
|
import { ExplorerState } from './reducers/explorer_reducer';
|
||||||
import { hasMatchingPoints } from './has_matching_points';
|
import { hasMatchingPoints } from './has_matching_points';
|
||||||
import { ExplorerNoInfluencersFound } from './components/explorer_no_influencers_found/explorer_no_influencers_found';
|
import { ExplorerNoInfluencersFound } from './components/explorer_no_influencers_found/explorer_no_influencers_found';
|
||||||
import { LoadingIndicator } from '../components/loading_indicator';
|
|
||||||
import { SwimlaneContainer } from './swimlane_container';
|
import { SwimlaneContainer } from './swimlane_container';
|
||||||
import { OverallSwimlaneData } from './explorer_utils';
|
import { OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils';
|
||||||
|
import { NoOverallData } from './components/no_overall_data';
|
||||||
|
|
||||||
function mapSwimlaneOptionsToEuiOptions(options: string[]) {
|
function mapSwimlaneOptionsToEuiOptions(options: string[]) {
|
||||||
return options.map((option) => ({
|
return options.map((option) => ({
|
||||||
|
@ -132,8 +131,11 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
||||||
viewBySwimlaneDataLoading,
|
viewBySwimlaneDataLoading,
|
||||||
viewBySwimlaneFieldName,
|
viewBySwimlaneFieldName,
|
||||||
viewBySwimlaneOptions,
|
viewBySwimlaneOptions,
|
||||||
swimlaneLimit,
|
|
||||||
selectedJobs,
|
selectedJobs,
|
||||||
|
viewByFromPage,
|
||||||
|
viewByPerPage,
|
||||||
|
swimlaneLimit,
|
||||||
|
loading,
|
||||||
} = explorerState;
|
} = explorerState;
|
||||||
|
|
||||||
const setSwimlaneSelectActive = useCallback((active: boolean) => {
|
const setSwimlaneSelectActive = useCallback((active: boolean) => {
|
||||||
|
@ -159,25 +161,18 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Listener for click events in the swimlane to load corresponding anomaly data.
|
// Listener for click events in the swimlane to load corresponding anomaly data.
|
||||||
const swimlaneCellClick = useCallback((selectedCellsUpdate: any) => {
|
const swimlaneCellClick = useCallback(
|
||||||
// If selectedCells is an empty object we clear any existing selection,
|
(selectedCellsUpdate: any) => {
|
||||||
// otherwise we save the new selection in AppState and update the Explorer.
|
// If selectedCells is an empty object we clear any existing selection,
|
||||||
if (Object.keys(selectedCellsUpdate).length === 0) {
|
// otherwise we save the new selection in AppState and update the Explorer.
|
||||||
setSelectedCells();
|
if (Object.keys(selectedCellsUpdate).length === 0) {
|
||||||
} else {
|
setSelectedCells();
|
||||||
setSelectedCells(selectedCellsUpdate);
|
} else {
|
||||||
}
|
setSelectedCells(selectedCellsUpdate);
|
||||||
}, []);
|
}
|
||||||
|
},
|
||||||
const showOverallSwimlane =
|
[setSelectedCells]
|
||||||
overallSwimlaneData !== null &&
|
);
|
||||||
overallSwimlaneData.laneLabels &&
|
|
||||||
overallSwimlaneData.laneLabels.length > 0;
|
|
||||||
|
|
||||||
const showViewBySwimlane =
|
|
||||||
viewBySwimlaneData !== null &&
|
|
||||||
viewBySwimlaneData.laneLabels &&
|
|
||||||
viewBySwimlaneData.laneLabels.length > 0;
|
|
||||||
|
|
||||||
const menuItems = useMemo(() => {
|
const menuItems = useMemo(() => {
|
||||||
const items = [];
|
const items = [];
|
||||||
|
@ -235,21 +230,6 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiFormRow
|
|
||||||
label={
|
|
||||||
<span className="eui-textNoWrap">
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.ml.explorer.limitLabel"
|
|
||||||
defaultMessage="Limit"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
display={'columnCompressed'}
|
|
||||||
>
|
|
||||||
<SelectLimit />
|
|
||||||
</EuiFormRow>
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
||||||
<div className="panel-sub-title">
|
<div className="panel-sub-title">
|
||||||
{viewByLoadedForTimeFormatted && (
|
{viewByLoadedForTimeFormatted && (
|
||||||
|
@ -305,68 +285,84 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="ml-explorer-swimlane euiText"
|
className="mlExplorerSwimlane euiText"
|
||||||
onMouseEnter={onSwimlaneEnterHandler}
|
onMouseEnter={onSwimlaneEnterHandler}
|
||||||
onMouseLeave={onSwimlaneLeaveHandler}
|
onMouseLeave={onSwimlaneLeaveHandler}
|
||||||
data-test-subj="mlAnomalyExplorerSwimlaneOverall"
|
data-test-subj="mlAnomalyExplorerSwimlaneOverall"
|
||||||
>
|
>
|
||||||
{showOverallSwimlane && (
|
<SwimlaneContainer
|
||||||
<SwimlaneContainer
|
filterActive={filterActive}
|
||||||
filterActive={filterActive}
|
maskAll={maskAll}
|
||||||
maskAll={maskAll}
|
timeBuckets={timeBuckets}
|
||||||
timeBuckets={timeBuckets}
|
swimlaneCellClick={swimlaneCellClick}
|
||||||
swimlaneCellClick={swimlaneCellClick}
|
swimlaneData={overallSwimlaneData as OverallSwimlaneData}
|
||||||
swimlaneData={overallSwimlaneData as OverallSwimlaneData}
|
swimlaneType={'overall'}
|
||||||
swimlaneType={'overall'}
|
selection={selectedCells}
|
||||||
selection={selectedCells}
|
swimlaneRenderDoneListener={swimlaneRenderDoneListener}
|
||||||
swimlaneRenderDoneListener={swimlaneRenderDoneListener}
|
onResize={(width) => explorerService.setSwimlaneContainerWidth(width)}
|
||||||
onResize={(width) => explorerService.setSwimlaneContainerWidth(width)}
|
isLoading={loading}
|
||||||
/>
|
noDataWarning={<NoOverallData />}
|
||||||
)}
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
{viewBySwimlaneOptions.length > 0 && (
|
{viewBySwimlaneOptions.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{showViewBySwimlane && (
|
<>
|
||||||
<>
|
<div
|
||||||
<EuiSpacer size="m" />
|
className="mlExplorerSwimlane euiText"
|
||||||
<div
|
onMouseEnter={onSwimlaneEnterHandler}
|
||||||
className="ml-explorer-swimlane euiText"
|
onMouseLeave={onSwimlaneLeaveHandler}
|
||||||
onMouseEnter={onSwimlaneEnterHandler}
|
data-test-subj="mlAnomalyExplorerSwimlaneViewBy"
|
||||||
onMouseLeave={onSwimlaneLeaveHandler}
|
>
|
||||||
data-test-subj="mlAnomalyExplorerSwimlaneViewBy"
|
<SwimlaneContainer
|
||||||
>
|
filterActive={filterActive}
|
||||||
<SwimlaneContainer
|
maskAll={
|
||||||
filterActive={filterActive}
|
maskAll &&
|
||||||
maskAll={
|
!hasMatchingPoints({
|
||||||
maskAll &&
|
filteredFields,
|
||||||
!hasMatchingPoints({
|
swimlaneData: viewBySwimlaneData,
|
||||||
filteredFields,
|
})
|
||||||
swimlaneData: viewBySwimlaneData,
|
}
|
||||||
})
|
timeBuckets={timeBuckets}
|
||||||
|
swimlaneCellClick={swimlaneCellClick}
|
||||||
|
swimlaneData={viewBySwimlaneData as ViewBySwimLaneData}
|
||||||
|
swimlaneType={SWIMLANE_TYPE.VIEW_BY}
|
||||||
|
selection={selectedCells}
|
||||||
|
swimlaneRenderDoneListener={swimlaneRenderDoneListener}
|
||||||
|
onResize={(width) => explorerService.setSwimlaneContainerWidth(width)}
|
||||||
|
fromPage={viewByFromPage}
|
||||||
|
perPage={viewByPerPage}
|
||||||
|
swimlaneLimit={swimlaneLimit}
|
||||||
|
onPaginationChange={({ perPage: perPageUpdate, fromPage: fromPageUpdate }) => {
|
||||||
|
if (perPageUpdate) {
|
||||||
|
explorerService.setViewByPerPage(perPageUpdate);
|
||||||
}
|
}
|
||||||
timeBuckets={timeBuckets}
|
if (fromPageUpdate) {
|
||||||
swimlaneCellClick={swimlaneCellClick}
|
explorerService.setViewByFromPage(fromPageUpdate);
|
||||||
swimlaneData={viewBySwimlaneData as OverallSwimlaneData}
|
}
|
||||||
swimlaneType={'viewBy'}
|
}}
|
||||||
selection={selectedCells}
|
isLoading={loading || viewBySwimlaneDataLoading}
|
||||||
swimlaneRenderDoneListener={swimlaneRenderDoneListener}
|
noDataWarning={
|
||||||
onResize={(width) => explorerService.setSwimlaneContainerWidth(width)}
|
typeof viewBySwimlaneFieldName === 'string' ? (
|
||||||
/>
|
viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL ? (
|
||||||
</div>
|
<FormattedMessage
|
||||||
</>
|
id="xpack.ml.explorer.noResultForSelectedJobsMessage"
|
||||||
)}
|
defaultMessage="No results found for selected {jobsCount, plural, one {job} other {jobs}}"
|
||||||
|
values={{ jobsCount: selectedJobs?.length ?? 1 }}
|
||||||
{viewBySwimlaneDataLoading && <LoadingIndicator />}
|
/>
|
||||||
|
) : (
|
||||||
{!showViewBySwimlane &&
|
<ExplorerNoInfluencersFound
|
||||||
!viewBySwimlaneDataLoading &&
|
viewBySwimlaneFieldName={viewBySwimlaneFieldName}
|
||||||
typeof viewBySwimlaneFieldName === 'string' && (
|
showFilterMessage={filterActive === true}
|
||||||
<ExplorerNoInfluencersFound
|
/>
|
||||||
viewBySwimlaneFieldName={viewBySwimlaneFieldName}
|
)
|
||||||
showFilterMessage={filterActive === true}
|
) : null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
</>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
|
@ -380,7 +376,6 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
||||||
}}
|
}}
|
||||||
jobIds={selectedJobs.map(({ id }) => id)}
|
jobIds={selectedJobs.map(({ id }) => id)}
|
||||||
viewBy={viewBySwimlaneFieldName!}
|
viewBy={viewBySwimlaneFieldName!}
|
||||||
limit={swimlaneLimit}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`ExplorerNoInfluencersFound snapshot 1`] = `
|
exports[`ExplorerNoInfluencersFound snapshot 1`] = `
|
||||||
<EuiEmptyPrompt
|
<FormattedMessage
|
||||||
title={
|
defaultMessage="No {viewBySwimlaneFieldName} influencers found"
|
||||||
<h2>
|
id="xpack.ml.explorer.noInfluencersFoundTitle"
|
||||||
<FormattedMessage
|
values={
|
||||||
defaultMessage="No {viewBySwimlaneFieldName} influencers found"
|
Object {
|
||||||
id="xpack.ml.explorer.noInfluencersFoundTitle"
|
"viewBySwimlaneFieldName": "field_name",
|
||||||
values={
|
}
|
||||||
Object {
|
|
||||||
"viewBySwimlaneFieldName": "field_name",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</h2>
|
|
||||||
}
|
}
|
||||||
titleSize="xs"
|
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* React component for rendering EuiEmptyPrompt when no influencers were found.
|
* React component for rendering EuiEmptyPrompt when no influencers were found.
|
||||||
|
@ -15,26 +14,17 @@ import { EuiEmptyPrompt } from '@elastic/eui';
|
||||||
export const ExplorerNoInfluencersFound: FC<{
|
export const ExplorerNoInfluencersFound: FC<{
|
||||||
viewBySwimlaneFieldName: string;
|
viewBySwimlaneFieldName: string;
|
||||||
showFilterMessage?: boolean;
|
showFilterMessage?: boolean;
|
||||||
}> = ({ viewBySwimlaneFieldName, showFilterMessage = false }) => (
|
}> = ({ viewBySwimlaneFieldName, showFilterMessage = false }) =>
|
||||||
<EuiEmptyPrompt
|
showFilterMessage === false ? (
|
||||||
titleSize="xs"
|
<FormattedMessage
|
||||||
title={
|
id="xpack.ml.explorer.noInfluencersFoundTitle"
|
||||||
<h2>
|
defaultMessage="No {viewBySwimlaneFieldName} influencers found"
|
||||||
{showFilterMessage === false && (
|
values={{ viewBySwimlaneFieldName }}
|
||||||
<FormattedMessage
|
/>
|
||||||
id="xpack.ml.explorer.noInfluencersFoundTitle"
|
) : (
|
||||||
defaultMessage="No {viewBySwimlaneFieldName} influencers found"
|
<FormattedMessage
|
||||||
values={{ viewBySwimlaneFieldName }}
|
id="xpack.ml.explorer.noInfluencersFoundTitleFilterMessage"
|
||||||
/>
|
defaultMessage="No {viewBySwimlaneFieldName} influencers found for specified filter"
|
||||||
)}
|
values={{ viewBySwimlaneFieldName }}
|
||||||
{showFilterMessage === true && (
|
/>
|
||||||
<FormattedMessage
|
);
|
||||||
id="xpack.ml.explorer.noInfluencersFoundTitleFilterMessage"
|
|
||||||
defaultMessage="No {viewBySwimlaneFieldName} influencers found for specified filter"
|
|
||||||
values={{ viewBySwimlaneFieldName }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
|
||||||
|
export const NoOverallData: FC = () => {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.ml.anomalySwimLane.noOverallDataMessage"
|
||||||
|
defaultMessage="No overall data found"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -12,8 +12,6 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
|
@ -27,6 +25,7 @@ import {
|
||||||
EuiPageHeaderSection,
|
EuiPageHeaderSection,
|
||||||
EuiSpacer,
|
EuiSpacer,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
|
EuiLoadingContent,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { AnnotationFlyout } from '../components/annotations/annotation_flyout';
|
import { AnnotationFlyout } from '../components/annotations/annotation_flyout';
|
||||||
|
@ -36,12 +35,10 @@ import { DatePickerWrapper } from '../components/navigation_menu/date_picker_wra
|
||||||
import { InfluencersList } from '../components/influencers_list';
|
import { InfluencersList } from '../components/influencers_list';
|
||||||
import { explorerService } from './explorer_dashboard_service';
|
import { explorerService } from './explorer_dashboard_service';
|
||||||
import { AnomalyResultsViewSelector } from '../components/anomaly_results_view_selector';
|
import { AnomalyResultsViewSelector } from '../components/anomaly_results_view_selector';
|
||||||
import { LoadingIndicator } from '../components/loading_indicator/loading_indicator';
|
|
||||||
import { NavigationMenu } from '../components/navigation_menu';
|
import { NavigationMenu } from '../components/navigation_menu';
|
||||||
import { CheckboxShowCharts } from '../components/controls/checkbox_showcharts';
|
import { CheckboxShowCharts } from '../components/controls/checkbox_showcharts';
|
||||||
import { JobSelector } from '../components/job_selector';
|
import { JobSelector } from '../components/job_selector';
|
||||||
import { SelectInterval } from '../components/controls/select_interval/select_interval';
|
import { SelectInterval } from '../components/controls/select_interval/select_interval';
|
||||||
import { limit$ } from './select_limit/select_limit';
|
|
||||||
import { SelectSeverity } from '../components/controls/select_severity/select_severity';
|
import { SelectSeverity } from '../components/controls/select_severity/select_severity';
|
||||||
import {
|
import {
|
||||||
ExplorerQueryBar,
|
ExplorerQueryBar,
|
||||||
|
@ -142,19 +139,6 @@ export class Explorer extends React.Component {
|
||||||
|
|
||||||
state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG };
|
state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG };
|
||||||
|
|
||||||
_unsubscribeAll = new Subject();
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
limit$.pipe(takeUntil(this._unsubscribeAll)).subscribe(explorerService.setSwimlaneLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this._unsubscribeAll.next();
|
|
||||||
this._unsubscribeAll.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
viewByChangeHandler = (e) => explorerService.setViewBySwimlaneFieldName(e.target.value);
|
|
||||||
|
|
||||||
// Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes
|
// Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes
|
||||||
// and will cause a syntax error when called with getKqlQueryValues
|
// and will cause a syntax error when called with getKqlQueryValues
|
||||||
applyFilter = (fieldName, fieldValue, action) => {
|
applyFilter = (fieldName, fieldValue, action) => {
|
||||||
|
@ -240,29 +224,7 @@ export class Explorer extends React.Component {
|
||||||
const noJobsFound = selectedJobs === null || selectedJobs.length === 0;
|
const noJobsFound = selectedJobs === null || selectedJobs.length === 0;
|
||||||
const hasResults = overallSwimlaneData.points && overallSwimlaneData.points.length > 0;
|
const hasResults = overallSwimlaneData.points && overallSwimlaneData.points.length > 0;
|
||||||
|
|
||||||
if (loading === true) {
|
if (noJobsFound && !loading) {
|
||||||
return (
|
|
||||||
<ExplorerPage
|
|
||||||
jobSelectorProps={jobSelectorProps}
|
|
||||||
noInfluencersConfigured={noInfluencersConfigured}
|
|
||||||
influencers={influencers}
|
|
||||||
filterActive={filterActive}
|
|
||||||
filterPlaceHolder={filterPlaceHolder}
|
|
||||||
filterIconTriggeredQuery={this.state.filterIconTriggeredQuery}
|
|
||||||
indexPattern={indexPattern}
|
|
||||||
queryString={queryString}
|
|
||||||
updateLanguage={this.updateLanguage}
|
|
||||||
>
|
|
||||||
<LoadingIndicator
|
|
||||||
label={i18n.translate('xpack.ml.explorer.loadingLabel', {
|
|
||||||
defaultMessage: 'Loading',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</ExplorerPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noJobsFound) {
|
|
||||||
return (
|
return (
|
||||||
<ExplorerPage jobSelectorProps={jobSelectorProps}>
|
<ExplorerPage jobSelectorProps={jobSelectorProps}>
|
||||||
<ExplorerNoJobsFound />
|
<ExplorerNoJobsFound />
|
||||||
|
@ -270,7 +232,7 @@ export class Explorer extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noJobsFound && hasResults === false) {
|
if (noJobsFound && hasResults === false && !loading) {
|
||||||
return (
|
return (
|
||||||
<ExplorerPage jobSelectorProps={jobSelectorProps}>
|
<ExplorerPage jobSelectorProps={jobSelectorProps}>
|
||||||
<ExplorerNoResultsFound />
|
<ExplorerNoResultsFound />
|
||||||
|
@ -320,7 +282,11 @@ export class Explorer extends React.Component {
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
<InfluencersList influencers={influencers} influencerFilter={this.applyFilter} />
|
{loading ? (
|
||||||
|
<EuiLoadingContent lines={10} />
|
||||||
|
) : (
|
||||||
|
<InfluencersList influencers={influencers} influencerFilter={this.applyFilter} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -352,59 +318,59 @@ export class Explorer extends React.Component {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<EuiTitle className="panel-title">
|
{loading === false && (
|
||||||
<h2>
|
<>
|
||||||
<FormattedMessage
|
<EuiTitle className="panel-title">
|
||||||
id="xpack.ml.explorer.anomaliesTitle"
|
<h2>
|
||||||
defaultMessage="Anomalies"
|
<FormattedMessage
|
||||||
|
id="xpack.ml.explorer.anomaliesTitle"
|
||||||
|
defaultMessage="Anomalies"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
<EuiFlexGroup
|
||||||
|
direction="row"
|
||||||
|
gutterSize="l"
|
||||||
|
responsive={true}
|
||||||
|
className="ml-anomalies-controls"
|
||||||
|
>
|
||||||
|
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
||||||
|
<EuiFormRow
|
||||||
|
label={i18n.translate('xpack.ml.explorer.severityThresholdLabel', {
|
||||||
|
defaultMessage: 'Severity threshold',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SelectSeverity />
|
||||||
|
</EuiFormRow>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
||||||
|
<EuiFormRow
|
||||||
|
label={i18n.translate('xpack.ml.explorer.intervalLabel', {
|
||||||
|
defaultMessage: 'Interval',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SelectInterval />
|
||||||
|
</EuiFormRow>
|
||||||
|
</EuiFlexItem>
|
||||||
|
{chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && (
|
||||||
|
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
||||||
|
<EuiFormRow label="​">
|
||||||
|
<CheckboxShowCharts />
|
||||||
|
</EuiFormRow>
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
<div className="euiText explorer-charts">
|
||||||
|
{showCharts && <ExplorerChartsContainer {...{ ...chartsData, severity }} />}
|
||||||
|
</div>
|
||||||
|
<AnomaliesTable
|
||||||
|
bounds={bounds}
|
||||||
|
tableData={tableData}
|
||||||
|
influencerFilter={this.applyFilter}
|
||||||
/>
|
/>
|
||||||
</h2>
|
</>
|
||||||
</EuiTitle>
|
)}
|
||||||
|
|
||||||
<EuiFlexGroup
|
|
||||||
direction="row"
|
|
||||||
gutterSize="l"
|
|
||||||
responsive={true}
|
|
||||||
className="ml-anomalies-controls"
|
|
||||||
>
|
|
||||||
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
|
||||||
<EuiFormRow
|
|
||||||
label={i18n.translate('xpack.ml.explorer.severityThresholdLabel', {
|
|
||||||
defaultMessage: 'Severity threshold',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<SelectSeverity />
|
|
||||||
</EuiFormRow>
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
|
||||||
<EuiFormRow
|
|
||||||
label={i18n.translate('xpack.ml.explorer.intervalLabel', {
|
|
||||||
defaultMessage: 'Interval',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<SelectInterval />
|
|
||||||
</EuiFormRow>
|
|
||||||
</EuiFlexItem>
|
|
||||||
{chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && (
|
|
||||||
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
|
||||||
<EuiFormRow label="​">
|
|
||||||
<CheckboxShowCharts />
|
|
||||||
</EuiFormRow>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
</EuiFlexGroup>
|
|
||||||
|
|
||||||
<EuiSpacer size="m" />
|
|
||||||
|
|
||||||
<div className="euiText explorer-charts">
|
|
||||||
{showCharts && <ExplorerChartsContainer {...{ ...chartsData, severity }} />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnomaliesTable
|
|
||||||
bounds={bounds}
|
|
||||||
tableData={tableData}
|
|
||||||
influencerFilter={this.applyFilter}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ExplorerPage>
|
</ExplorerPage>
|
||||||
|
|
|
@ -27,9 +27,10 @@ export const EXPLORER_ACTION = {
|
||||||
SET_INFLUENCER_FILTER_SETTINGS: 'setInfluencerFilterSettings',
|
SET_INFLUENCER_FILTER_SETTINGS: 'setInfluencerFilterSettings',
|
||||||
SET_SELECTED_CELLS: 'setSelectedCells',
|
SET_SELECTED_CELLS: 'setSelectedCells',
|
||||||
SET_SWIMLANE_CONTAINER_WIDTH: 'setSwimlaneContainerWidth',
|
SET_SWIMLANE_CONTAINER_WIDTH: 'setSwimlaneContainerWidth',
|
||||||
SET_SWIMLANE_LIMIT: 'setSwimlaneLimit',
|
|
||||||
SET_VIEW_BY_SWIMLANE_FIELD_NAME: 'setViewBySwimlaneFieldName',
|
SET_VIEW_BY_SWIMLANE_FIELD_NAME: 'setViewBySwimlaneFieldName',
|
||||||
SET_VIEW_BY_SWIMLANE_LOADING: 'setViewBySwimlaneLoading',
|
SET_VIEW_BY_SWIMLANE_LOADING: 'setViewBySwimlaneLoading',
|
||||||
|
SET_VIEW_BY_PER_PAGE: 'setViewByPerPage',
|
||||||
|
SET_VIEW_BY_FROM_PAGE: 'setViewByFromPage',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FILTER_ACTION = {
|
export const FILTER_ACTION = {
|
||||||
|
@ -51,9 +52,23 @@ export const CHART_TYPE = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAX_CATEGORY_EXAMPLES = 10;
|
export const MAX_CATEGORY_EXAMPLES = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum amount of top influencer to fetch.
|
||||||
|
*/
|
||||||
export const MAX_INFLUENCER_FIELD_VALUES = 10;
|
export const MAX_INFLUENCER_FIELD_VALUES = 10;
|
||||||
export const MAX_INFLUENCER_FIELD_NAMES = 50;
|
export const MAX_INFLUENCER_FIELD_NAMES = 50;
|
||||||
|
|
||||||
export const VIEW_BY_JOB_LABEL = i18n.translate('xpack.ml.explorer.jobIdLabel', {
|
export const VIEW_BY_JOB_LABEL = i18n.translate('xpack.ml.explorer.jobIdLabel', {
|
||||||
defaultMessage: 'job ID',
|
defaultMessage: 'job ID',
|
||||||
});
|
});
|
||||||
|
/**
|
||||||
|
* Hard limitation for the size of terms
|
||||||
|
* aggregations on influencers values.
|
||||||
|
*/
|
||||||
|
export const ANOMALY_SWIM_LANE_HARD_LIMIT = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default page size fot the anomaly swim lane.
|
||||||
|
*/
|
||||||
|
export const SWIM_LANE_DEFAULT_PAGE_SIZE = 10;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
import { from, isObservable, Observable, Subject } from 'rxjs';
|
import { from, isObservable, Observable, Subject } from 'rxjs';
|
||||||
import { distinctUntilChanged, flatMap, map, scan } from 'rxjs/operators';
|
import { distinctUntilChanged, flatMap, map, scan, shareReplay } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DeepPartial } from '../../../common/types/common';
|
import { DeepPartial } from '../../../common/types/common';
|
||||||
|
|
||||||
|
@ -49,7 +49,9 @@ const explorerFilteredAction$ = explorerAction$.pipe(
|
||||||
|
|
||||||
// applies action and returns state
|
// applies action and returns state
|
||||||
const explorerState$: Observable<ExplorerState> = explorerFilteredAction$.pipe(
|
const explorerState$: Observable<ExplorerState> = explorerFilteredAction$.pipe(
|
||||||
scan(explorerReducer, getExplorerDefaultState())
|
scan(explorerReducer, getExplorerDefaultState()),
|
||||||
|
// share the last emitted value among new subscribers
|
||||||
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ExplorerAppState {
|
interface ExplorerAppState {
|
||||||
|
@ -59,6 +61,8 @@ interface ExplorerAppState {
|
||||||
selectedTimes?: number[];
|
selectedTimes?: number[];
|
||||||
showTopFieldValues?: boolean;
|
showTopFieldValues?: boolean;
|
||||||
viewByFieldName?: string;
|
viewByFieldName?: string;
|
||||||
|
viewByPerPage?: number;
|
||||||
|
viewByFromPage?: number;
|
||||||
};
|
};
|
||||||
mlExplorerFilter: {
|
mlExplorerFilter: {
|
||||||
influencersFilterQuery?: unknown;
|
influencersFilterQuery?: unknown;
|
||||||
|
@ -88,6 +92,14 @@ const explorerAppState$: Observable<ExplorerAppState> = explorerState$.pipe(
|
||||||
appState.mlExplorerSwimlane.viewByFieldName = state.viewBySwimlaneFieldName;
|
appState.mlExplorerSwimlane.viewByFieldName = state.viewBySwimlaneFieldName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.viewByFromPage !== undefined) {
|
||||||
|
appState.mlExplorerSwimlane.viewByFromPage = state.viewByFromPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.viewByPerPage !== undefined) {
|
||||||
|
appState.mlExplorerSwimlane.viewByPerPage = state.viewByPerPage;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.filterActive) {
|
if (state.filterActive) {
|
||||||
appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery;
|
appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery;
|
||||||
appState.mlExplorerFilter.filterActive = state.filterActive;
|
appState.mlExplorerFilter.filterActive = state.filterActive;
|
||||||
|
@ -153,13 +165,16 @@ export const explorerService = {
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setSwimlaneLimit: (payload: number) => {
|
|
||||||
explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIMLANE_LIMIT, payload });
|
|
||||||
},
|
|
||||||
setViewBySwimlaneFieldName: (payload: string) => {
|
setViewBySwimlaneFieldName: (payload: string) => {
|
||||||
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME, payload });
|
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME, payload });
|
||||||
},
|
},
|
||||||
setViewBySwimlaneLoading: (payload: any) => {
|
setViewBySwimlaneLoading: (payload: any) => {
|
||||||
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_LOADING, payload });
|
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_LOADING, payload });
|
||||||
},
|
},
|
||||||
|
setViewByFromPage: (payload: number) => {
|
||||||
|
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_FROM_PAGE, payload });
|
||||||
|
},
|
||||||
|
setViewByPerPage: (payload: number) => {
|
||||||
|
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE, payload });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,7 @@ import {
|
||||||
ChartTooltipService,
|
ChartTooltipService,
|
||||||
ChartTooltipValue,
|
ChartTooltipValue,
|
||||||
} from '../components/chart_tooltip/chart_tooltip_service';
|
} from '../components/chart_tooltip/chart_tooltip_service';
|
||||||
import { OverallSwimlaneData } from './explorer_utils';
|
import { OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils';
|
||||||
|
|
||||||
const SCSS = {
|
const SCSS = {
|
||||||
mlDragselectDragging: 'mlDragselectDragging',
|
mlDragselectDragging: 'mlDragselectDragging',
|
||||||
|
@ -57,7 +57,7 @@ export interface ExplorerSwimlaneProps {
|
||||||
maskAll?: boolean;
|
maskAll?: boolean;
|
||||||
timeBuckets: InstanceType<typeof TimeBucketsClass>;
|
timeBuckets: InstanceType<typeof TimeBucketsClass>;
|
||||||
swimlaneCellClick?: Function;
|
swimlaneCellClick?: Function;
|
||||||
swimlaneData: OverallSwimlaneData;
|
swimlaneData: OverallSwimlaneData | ViewBySwimLaneData;
|
||||||
swimlaneType: SwimlaneType;
|
swimlaneType: SwimlaneType;
|
||||||
selection?: {
|
selection?: {
|
||||||
lanes: any[];
|
lanes: any[];
|
||||||
|
@ -211,7 +211,7 @@ export class ExplorerSwimlane extends React.Component<ExplorerSwimlaneProps> {
|
||||||
const { swimlaneType } = this.props;
|
const { swimlaneType } = this.props;
|
||||||
|
|
||||||
// This selects both overall and viewby swimlane
|
// This selects both overall and viewby swimlane
|
||||||
const wrapper = d3.selectAll('.ml-explorer-swimlane');
|
const wrapper = d3.selectAll('.mlExplorerSwimlane');
|
||||||
|
|
||||||
wrapper.selectAll('.lane-label').classed('lane-label-masked', true);
|
wrapper.selectAll('.lane-label').classed('lane-label-masked', true);
|
||||||
wrapper
|
wrapper
|
||||||
|
@ -242,7 +242,7 @@ export class ExplorerSwimlane extends React.Component<ExplorerSwimlaneProps> {
|
||||||
maskIrrelevantSwimlanes(maskAll: boolean) {
|
maskIrrelevantSwimlanes(maskAll: boolean) {
|
||||||
if (maskAll === true) {
|
if (maskAll === true) {
|
||||||
// This selects both overall and viewby swimlane
|
// This selects both overall and viewby swimlane
|
||||||
const allSwimlanes = d3.selectAll('.ml-explorer-swimlane');
|
const allSwimlanes = d3.selectAll('.mlExplorerSwimlane');
|
||||||
allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true);
|
allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true);
|
||||||
allSwimlanes
|
allSwimlanes
|
||||||
.selectAll('.sl-cell-inner,.sl-cell-inner-dragselect')
|
.selectAll('.sl-cell-inner,.sl-cell-inner-dragselect')
|
||||||
|
@ -258,7 +258,7 @@ export class ExplorerSwimlane extends React.Component<ExplorerSwimlaneProps> {
|
||||||
|
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
// This selects both overall and viewby swimlane
|
// This selects both overall and viewby swimlane
|
||||||
const wrapper = d3.selectAll('.ml-explorer-swimlane');
|
const wrapper = d3.selectAll('.mlExplorerSwimlane');
|
||||||
|
|
||||||
wrapper.selectAll('.lane-label').classed('lane-label-masked', false);
|
wrapper.selectAll('.lane-label').classed('lane-label-masked', false);
|
||||||
wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false);
|
wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false);
|
||||||
|
|
|
@ -8,8 +8,6 @@ import { Moment } from 'moment';
|
||||||
|
|
||||||
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
|
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
|
||||||
|
|
||||||
import { TimeBucketsInterval } from '../util/time_buckets';
|
|
||||||
|
|
||||||
interface ClearedSelectedAnomaliesState {
|
interface ClearedSelectedAnomaliesState {
|
||||||
selectedCells: undefined;
|
selectedCells: undefined;
|
||||||
viewByLoadedForTimeFormatted: null;
|
viewByLoadedForTimeFormatted: null;
|
||||||
|
@ -35,6 +33,10 @@ export declare interface OverallSwimlaneData extends SwimlaneData {
|
||||||
latest: number;
|
latest: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ViewBySwimLaneData extends OverallSwimlaneData {
|
||||||
|
cardinality: number;
|
||||||
|
}
|
||||||
|
|
||||||
export declare const getDateFormatTz: () => any;
|
export declare const getDateFormatTz: () => any;
|
||||||
|
|
||||||
export declare const getDefaultSwimlaneData: () => SwimlaneData;
|
export declare const getDefaultSwimlaneData: () => SwimlaneData;
|
||||||
|
@ -163,22 +165,6 @@ declare interface LoadOverallDataResponse {
|
||||||
overallSwimlaneData: OverallSwimlaneData;
|
overallSwimlaneData: OverallSwimlaneData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare const loadOverallData: (
|
|
||||||
selectedJobs: ExplorerJob[],
|
|
||||||
interval: TimeBucketsInterval,
|
|
||||||
bounds: TimeRangeBounds
|
|
||||||
) => Promise<LoadOverallDataResponse>;
|
|
||||||
|
|
||||||
export declare const loadViewBySwimlane: (
|
|
||||||
fieldValues: string[],
|
|
||||||
bounds: SwimlaneBounds,
|
|
||||||
selectedJobs: ExplorerJob[],
|
|
||||||
viewBySwimlaneFieldName: string,
|
|
||||||
swimlaneLimit: number,
|
|
||||||
influencersFilterQuery: any,
|
|
||||||
noInfluencersConfigured: boolean
|
|
||||||
) => Promise<any>;
|
|
||||||
|
|
||||||
export declare const loadViewByTopFieldValuesForSelectedTime: (
|
export declare const loadViewByTopFieldValuesForSelectedTime: (
|
||||||
earliestMs: number,
|
earliestMs: number,
|
||||||
latestMs: number,
|
latestMs: number,
|
||||||
|
|
|
@ -8,11 +8,9 @@
|
||||||
* utils for Anomaly Explorer.
|
* utils for Anomaly Explorer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { chain, each, get, union, uniq } from 'lodash';
|
import { chain, get, union, uniq } from 'lodash';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE,
|
ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE,
|
||||||
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
|
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
|
||||||
|
@ -27,7 +25,7 @@ import { parseInterval } from '../../../common/util/parse_interval';
|
||||||
import { ml } from '../services/ml_api_service';
|
import { ml } from '../services/ml_api_service';
|
||||||
import { mlJobService } from '../services/job_service';
|
import { mlJobService } from '../services/job_service';
|
||||||
import { mlResultsService } from '../services/results_service';
|
import { mlResultsService } from '../services/results_service';
|
||||||
import { getBoundsRoundedToInterval, getTimeBucketsFromCache } from '../util/time_buckets';
|
import { getTimeBucketsFromCache } from '../util/time_buckets';
|
||||||
import { getTimefilter, getUiSettings } from '../util/dependency_cache';
|
import { getTimefilter, getUiSettings } from '../util/dependency_cache';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -36,7 +34,6 @@ import {
|
||||||
SWIMLANE_TYPE,
|
SWIMLANE_TYPE,
|
||||||
VIEW_BY_JOB_LABEL,
|
VIEW_BY_JOB_LABEL,
|
||||||
} from './explorer_constants';
|
} from './explorer_constants';
|
||||||
import { getSwimlaneContainerWidth } from './legacy_utils';
|
|
||||||
|
|
||||||
// create new job objects based on standard job config objects
|
// create new job objects based on standard job config objects
|
||||||
// new job objects just contain job id, bucket span in seconds and a selected flag.
|
// new job objects just contain job id, bucket span in seconds and a selected flag.
|
||||||
|
@ -51,6 +48,7 @@ export function getClearedSelectedAnomaliesState() {
|
||||||
return {
|
return {
|
||||||
selectedCells: undefined,
|
selectedCells: undefined,
|
||||||
viewByLoadedForTimeFormatted: null,
|
viewByLoadedForTimeFormatted: null,
|
||||||
|
swimlaneLimit: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,58 +265,6 @@ export function getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth)
|
||||||
return buckets.getInterval();
|
return buckets.getInterval();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadViewByTopFieldValuesForSelectedTime(
|
|
||||||
earliestMs,
|
|
||||||
latestMs,
|
|
||||||
selectedJobs,
|
|
||||||
viewBySwimlaneFieldName,
|
|
||||||
swimlaneLimit,
|
|
||||||
noInfluencersConfigured
|
|
||||||
) {
|
|
||||||
const selectedJobIds = selectedJobs.map((d) => d.id);
|
|
||||||
|
|
||||||
// Find the top field values for the selected time, and then load the 'view by'
|
|
||||||
// swimlane over the full time range for those specific field values.
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
|
|
||||||
mlResultsService
|
|
||||||
.getTopInfluencers(selectedJobIds, earliestMs, latestMs, swimlaneLimit)
|
|
||||||
.then((resp) => {
|
|
||||||
if (resp.influencers[viewBySwimlaneFieldName] === undefined) {
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const topFieldValues = [];
|
|
||||||
const topInfluencers = resp.influencers[viewBySwimlaneFieldName];
|
|
||||||
if (Array.isArray(topInfluencers)) {
|
|
||||||
topInfluencers.forEach((influencerData) => {
|
|
||||||
if (influencerData.maxAnomalyScore > 0) {
|
|
||||||
topFieldValues.push(influencerData.influencerFieldValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
resolve(topFieldValues);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
mlResultsService
|
|
||||||
.getScoresByBucket(
|
|
||||||
selectedJobIds,
|
|
||||||
earliestMs,
|
|
||||||
latestMs,
|
|
||||||
getSwimlaneBucketInterval(
|
|
||||||
selectedJobs,
|
|
||||||
getSwimlaneContainerWidth(noInfluencersConfigured)
|
|
||||||
).asSeconds() + 's',
|
|
||||||
swimlaneLimit
|
|
||||||
)
|
|
||||||
.then((resp) => {
|
|
||||||
const topFieldValues = Object.keys(resp.results);
|
|
||||||
resolve(topFieldValues);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain the list of 'View by' fields per job and viewBySwimlaneFieldName
|
// Obtain the list of 'View by' fields per job and viewBySwimlaneFieldName
|
||||||
export function getViewBySwimlaneOptions({
|
export function getViewBySwimlaneOptions({
|
||||||
currentViewBySwimlaneFieldName,
|
currentViewBySwimlaneFieldName,
|
||||||
|
@ -435,105 +381,6 @@ export function getViewBySwimlaneOptions({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processOverallResults(scoresByTime, searchBounds, interval) {
|
|
||||||
const overallLabel = i18n.translate('xpack.ml.explorer.overallLabel', {
|
|
||||||
defaultMessage: 'Overall',
|
|
||||||
});
|
|
||||||
const dataset = {
|
|
||||||
laneLabels: [overallLabel],
|
|
||||||
points: [],
|
|
||||||
interval,
|
|
||||||
earliest: searchBounds.min.valueOf() / 1000,
|
|
||||||
latest: searchBounds.max.valueOf() / 1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.keys(scoresByTime).length > 0) {
|
|
||||||
// Store the earliest and latest times of the data returned by the ES aggregations,
|
|
||||||
// These will be used for calculating the earliest and latest times for the swimlane charts.
|
|
||||||
each(scoresByTime, (score, timeMs) => {
|
|
||||||
const time = timeMs / 1000;
|
|
||||||
dataset.points.push({
|
|
||||||
laneLabel: overallLabel,
|
|
||||||
time,
|
|
||||||
value: score,
|
|
||||||
});
|
|
||||||
|
|
||||||
dataset.earliest = Math.min(time, dataset.earliest);
|
|
||||||
dataset.latest = Math.max(time + dataset.interval, dataset.latest);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataset;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processViewByResults(
|
|
||||||
scoresByInfluencerAndTime,
|
|
||||||
sortedLaneValues,
|
|
||||||
bounds,
|
|
||||||
viewBySwimlaneFieldName,
|
|
||||||
interval
|
|
||||||
) {
|
|
||||||
// Processes the scores for the 'view by' swimlane.
|
|
||||||
// Sorts the lanes according to the supplied array of lane
|
|
||||||
// values in the order in which they should be displayed,
|
|
||||||
// or pass an empty array to sort lanes according to max score over all time.
|
|
||||||
const dataset = {
|
|
||||||
fieldName: viewBySwimlaneFieldName,
|
|
||||||
points: [],
|
|
||||||
interval,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set the earliest and latest to be the same as the overall swimlane.
|
|
||||||
dataset.earliest = bounds.earliest;
|
|
||||||
dataset.latest = bounds.latest;
|
|
||||||
|
|
||||||
const laneLabels = [];
|
|
||||||
const maxScoreByLaneLabel = {};
|
|
||||||
|
|
||||||
each(scoresByInfluencerAndTime, (influencerData, influencerFieldValue) => {
|
|
||||||
laneLabels.push(influencerFieldValue);
|
|
||||||
maxScoreByLaneLabel[influencerFieldValue] = 0;
|
|
||||||
|
|
||||||
each(influencerData, (anomalyScore, timeMs) => {
|
|
||||||
const time = timeMs / 1000;
|
|
||||||
dataset.points.push({
|
|
||||||
laneLabel: influencerFieldValue,
|
|
||||||
time,
|
|
||||||
value: anomalyScore,
|
|
||||||
});
|
|
||||||
maxScoreByLaneLabel[influencerFieldValue] = Math.max(
|
|
||||||
maxScoreByLaneLabel[influencerFieldValue],
|
|
||||||
anomalyScore
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortValuesLength = sortedLaneValues.length;
|
|
||||||
if (sortValuesLength === 0) {
|
|
||||||
// Sort lanes in descending order of max score.
|
|
||||||
// Note the keys in scoresByInfluencerAndTime received from the ES request
|
|
||||||
// are not guaranteed to be sorted by score if they can be parsed as numbers
|
|
||||||
// (e.g. if viewing by HTTP response code).
|
|
||||||
dataset.laneLabels = laneLabels.sort((a, b) => {
|
|
||||||
return maxScoreByLaneLabel[b] - maxScoreByLaneLabel[a];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Sort lanes according to supplied order
|
|
||||||
// e.g. when a cell in the overall swimlane has been selected.
|
|
||||||
// Find the index of each lane label from the actual data set,
|
|
||||||
// rather than using sortedLaneValues as-is, just in case they differ.
|
|
||||||
dataset.laneLabels = laneLabels.sort((a, b) => {
|
|
||||||
let aIndex = sortedLaneValues.indexOf(a);
|
|
||||||
let bIndex = sortedLaneValues.indexOf(b);
|
|
||||||
aIndex = aIndex > -1 ? aIndex : sortValuesLength;
|
|
||||||
bIndex = bIndex > -1 ? bIndex : sortValuesLength;
|
|
||||||
return aIndex - bIndex;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataset;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, bounds) {
|
export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, bounds) {
|
||||||
const jobIds =
|
const jobIds =
|
||||||
selectedCells !== undefined && selectedCells.viewByFieldName === VIEW_BY_JOB_LABEL
|
selectedCells !== undefined && selectedCells.viewByFieldName === VIEW_BY_JOB_LABEL
|
||||||
|
@ -723,138 +570,6 @@ export async function loadDataForCharts(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadOverallData(selectedJobs, interval, bounds) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Loads the overall data components i.e. the overall swimlane and influencers list.
|
|
||||||
if (selectedJobs === null) {
|
|
||||||
resolve({
|
|
||||||
loading: false,
|
|
||||||
hasResuts: false,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the search bounds align to the bucketing interval used in the swimlane so
|
|
||||||
// that the first and last buckets are complete.
|
|
||||||
const searchBounds = getBoundsRoundedToInterval(bounds, interval, false);
|
|
||||||
const selectedJobIds = selectedJobs.map((d) => d.id);
|
|
||||||
|
|
||||||
// Load the overall bucket scores by time.
|
|
||||||
// Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets
|
|
||||||
// which wouldn't be the case if e.g. '1M' was used.
|
|
||||||
// Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works
|
|
||||||
// to ensure the search is inclusive of end time.
|
|
||||||
const overallBucketsBounds = getBoundsRoundedToInterval(bounds, interval, true);
|
|
||||||
mlResultsService
|
|
||||||
.getOverallBucketScores(
|
|
||||||
selectedJobIds,
|
|
||||||
// Note there is an optimization for when top_n == 1.
|
|
||||||
// If top_n > 1, we should test what happens when the request takes long
|
|
||||||
// and refactor the loading calls, if necessary, to avoid delays in loading other components.
|
|
||||||
1,
|
|
||||||
overallBucketsBounds.min.valueOf(),
|
|
||||||
overallBucketsBounds.max.valueOf(),
|
|
||||||
interval.asSeconds() + 's'
|
|
||||||
)
|
|
||||||
.then((resp) => {
|
|
||||||
const overallSwimlaneData = processOverallResults(
|
|
||||||
resp.results,
|
|
||||||
searchBounds,
|
|
||||||
interval.asSeconds()
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
loading: false,
|
|
||||||
overallSwimlaneData,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadViewBySwimlane(
|
|
||||||
fieldValues,
|
|
||||||
bounds,
|
|
||||||
selectedJobs,
|
|
||||||
viewBySwimlaneFieldName,
|
|
||||||
swimlaneLimit,
|
|
||||||
influencersFilterQuery,
|
|
||||||
noInfluencersConfigured
|
|
||||||
) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const finish = (resp) => {
|
|
||||||
if (resp !== undefined) {
|
|
||||||
const viewBySwimlaneData = processViewByResults(
|
|
||||||
resp.results,
|
|
||||||
fieldValues,
|
|
||||||
bounds,
|
|
||||||
viewBySwimlaneFieldName,
|
|
||||||
getSwimlaneBucketInterval(
|
|
||||||
selectedJobs,
|
|
||||||
getSwimlaneContainerWidth(noInfluencersConfigured)
|
|
||||||
).asSeconds()
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
viewBySwimlaneData,
|
|
||||||
viewBySwimlaneDataLoading: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve({ viewBySwimlaneDataLoading: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (selectedJobs === undefined || viewBySwimlaneFieldName === undefined) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// Ensure the search bounds align to the bucketing interval used in the swimlane so
|
|
||||||
// that the first and last buckets are complete.
|
|
||||||
const timefilter = getTimefilter();
|
|
||||||
const timefilterBounds = timefilter.getActiveBounds();
|
|
||||||
const searchBounds = getBoundsRoundedToInterval(
|
|
||||||
timefilterBounds,
|
|
||||||
getSwimlaneBucketInterval(selectedJobs, getSwimlaneContainerWidth(noInfluencersConfigured)),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
const selectedJobIds = selectedJobs.map((d) => d.id);
|
|
||||||
|
|
||||||
// load scores by influencer/jobId value and time.
|
|
||||||
// Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets
|
|
||||||
// which wouldn't be the case if e.g. '1M' was used.
|
|
||||||
const interval = `${getSwimlaneBucketInterval(
|
|
||||||
selectedJobs,
|
|
||||||
getSwimlaneContainerWidth(noInfluencersConfigured)
|
|
||||||
).asSeconds()}s`;
|
|
||||||
if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
|
|
||||||
mlResultsService
|
|
||||||
.getInfluencerValueMaxScoreByTime(
|
|
||||||
selectedJobIds,
|
|
||||||
viewBySwimlaneFieldName,
|
|
||||||
fieldValues,
|
|
||||||
searchBounds.min.valueOf(),
|
|
||||||
searchBounds.max.valueOf(),
|
|
||||||
interval,
|
|
||||||
swimlaneLimit,
|
|
||||||
influencersFilterQuery
|
|
||||||
)
|
|
||||||
.then(finish);
|
|
||||||
} else {
|
|
||||||
const jobIds =
|
|
||||||
fieldValues !== undefined && fieldValues.length > 0 ? fieldValues : selectedJobIds;
|
|
||||||
mlResultsService
|
|
||||||
.getScoresByBucket(
|
|
||||||
jobIds,
|
|
||||||
searchBounds.min.valueOf(),
|
|
||||||
searchBounds.max.valueOf(),
|
|
||||||
interval,
|
|
||||||
swimlaneLimit
|
|
||||||
)
|
|
||||||
.then(finish);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadTopInfluencers(
|
export async function loadTopInfluencers(
|
||||||
selectedJobIds,
|
selectedJobIds,
|
||||||
earliestMs,
|
earliestMs,
|
||||||
|
@ -871,6 +586,8 @@ export async function loadTopInfluencers(
|
||||||
earliestMs,
|
earliestMs,
|
||||||
latestMs,
|
latestMs,
|
||||||
MAX_INFLUENCER_FIELD_VALUES,
|
MAX_INFLUENCER_FIELD_VALUES,
|
||||||
|
10,
|
||||||
|
1,
|
||||||
influencers,
|
influencers,
|
||||||
influencersFilterQuery
|
influencersFilterQuery
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useUrlState } from '../../util/url_state';
|
import { useUrlState } from '../../util/url_state';
|
||||||
import { SWIMLANE_TYPE } from '../explorer_constants';
|
import { SWIMLANE_TYPE } from '../explorer_constants';
|
||||||
import { AppStateSelectedCells } from '../explorer_utils';
|
import { AppStateSelectedCells } from '../explorer_utils';
|
||||||
|
@ -14,55 +15,55 @@ export const useSelectedCells = (): [
|
||||||
] => {
|
] => {
|
||||||
const [appState, setAppState] = useUrlState('_a');
|
const [appState, setAppState] = useUrlState('_a');
|
||||||
|
|
||||||
let selectedCells: AppStateSelectedCells | undefined;
|
|
||||||
|
|
||||||
// keep swimlane selection, restore selectedCells from AppState
|
// keep swimlane selection, restore selectedCells from AppState
|
||||||
if (
|
const selectedCells = useMemo(() => {
|
||||||
appState &&
|
return appState?.mlExplorerSwimlane?.selectedType !== undefined
|
||||||
appState.mlExplorerSwimlane &&
|
? {
|
||||||
appState.mlExplorerSwimlane.selectedType !== undefined
|
type: appState.mlExplorerSwimlane.selectedType,
|
||||||
) {
|
lanes: appState.mlExplorerSwimlane.selectedLanes,
|
||||||
selectedCells = {
|
times: appState.mlExplorerSwimlane.selectedTimes,
|
||||||
type: appState.mlExplorerSwimlane.selectedType,
|
showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues,
|
||||||
lanes: appState.mlExplorerSwimlane.selectedLanes,
|
viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName,
|
||||||
times: appState.mlExplorerSwimlane.selectedTimes,
|
}
|
||||||
showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues,
|
: undefined;
|
||||||
viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName,
|
// TODO fix appState to use memoization
|
||||||
};
|
}, [JSON.stringify(appState?.mlExplorerSwimlane)]);
|
||||||
}
|
|
||||||
|
|
||||||
const setSelectedCells = (swimlaneSelectedCells: AppStateSelectedCells) => {
|
const setSelectedCells = useCallback(
|
||||||
const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane };
|
(swimlaneSelectedCells: AppStateSelectedCells) => {
|
||||||
|
const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane };
|
||||||
|
|
||||||
if (swimlaneSelectedCells !== undefined) {
|
if (swimlaneSelectedCells !== undefined) {
|
||||||
swimlaneSelectedCells.showTopFieldValues = false;
|
swimlaneSelectedCells.showTopFieldValues = false;
|
||||||
|
|
||||||
const currentSwimlaneType = selectedCells?.type;
|
const currentSwimlaneType = selectedCells?.type;
|
||||||
const currentShowTopFieldValues = selectedCells?.showTopFieldValues;
|
const currentShowTopFieldValues = selectedCells?.showTopFieldValues;
|
||||||
const newSwimlaneType = swimlaneSelectedCells?.type;
|
const newSwimlaneType = swimlaneSelectedCells?.type;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(currentSwimlaneType === SWIMLANE_TYPE.OVERALL &&
|
(currentSwimlaneType === SWIMLANE_TYPE.OVERALL &&
|
||||||
newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) ||
|
newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) ||
|
||||||
newSwimlaneType === SWIMLANE_TYPE.OVERALL ||
|
newSwimlaneType === SWIMLANE_TYPE.OVERALL ||
|
||||||
currentShowTopFieldValues === true
|
currentShowTopFieldValues === true
|
||||||
) {
|
) {
|
||||||
swimlaneSelectedCells.showTopFieldValues = true;
|
swimlaneSelectedCells.showTopFieldValues = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type;
|
||||||
|
mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes;
|
||||||
|
mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times;
|
||||||
|
mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues;
|
||||||
|
setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
|
||||||
|
} else {
|
||||||
|
delete mlExplorerSwimlane.selectedType;
|
||||||
|
delete mlExplorerSwimlane.selectedLanes;
|
||||||
|
delete mlExplorerSwimlane.selectedTimes;
|
||||||
|
delete mlExplorerSwimlane.showTopFieldValues;
|
||||||
|
setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type;
|
[appState?.mlExplorerSwimlane, selectedCells]
|
||||||
mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes;
|
);
|
||||||
mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times;
|
|
||||||
mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues;
|
|
||||||
setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
|
|
||||||
} else {
|
|
||||||
delete mlExplorerSwimlane.selectedType;
|
|
||||||
delete mlExplorerSwimlane.selectedLanes;
|
|
||||||
delete mlExplorerSwimlane.selectedTimes;
|
|
||||||
delete mlExplorerSwimlane.showTopFieldValues;
|
|
||||||
setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return [selectedCells, setSelectedCells];
|
return [selectedCells, setSelectedCells];
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,8 +11,3 @@ export function getChartContainerWidth() {
|
||||||
const chartContainer = document.querySelector('.explorer-charts');
|
const chartContainer = document.querySelector('.explorer-charts');
|
||||||
return Math.floor((chartContainer && chartContainer.clientWidth) || 0);
|
return Math.floor((chartContainer && chartContainer.clientWidth) || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSwimlaneContainerWidth() {
|
|
||||||
const explorerContainer = document.querySelector('.ml-explorer');
|
|
||||||
return (explorerContainer && explorerContainer.clientWidth) || 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,5 +19,6 @@ export function clearInfluencerFilterSettings(state: ExplorerState): ExplorerSta
|
||||||
queryString: '',
|
queryString: '',
|
||||||
tableQueryString: '',
|
tableQueryString: '',
|
||||||
...getClearedSelectedAnomaliesState(),
|
...getClearedSelectedAnomaliesState(),
|
||||||
|
viewByFromPage: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload)
|
||||||
noInfluencersConfigured: getInfluencers(selectedJobs).length === 0,
|
noInfluencersConfigured: getInfluencers(selectedJobs).length === 0,
|
||||||
overallSwimlaneData: getDefaultSwimlaneData(),
|
overallSwimlaneData: getDefaultSwimlaneData(),
|
||||||
selectedJobs,
|
selectedJobs,
|
||||||
|
viewByFromPage: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// clear filter if selected jobs have no influencers
|
// clear filter if selected jobs have no influencers
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder';
|
||||||
export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => {
|
export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => {
|
||||||
const { type, payload } = nextAction;
|
const { type, payload } = nextAction;
|
||||||
|
|
||||||
let nextState;
|
let nextState: ExplorerState;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS:
|
case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS:
|
||||||
|
@ -39,6 +39,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
||||||
...state,
|
...state,
|
||||||
...getClearedSelectedAnomaliesState(),
|
...getClearedSelectedAnomaliesState(),
|
||||||
loading: false,
|
loading: false,
|
||||||
|
viewByFromPage: 1,
|
||||||
selectedJobs: [],
|
selectedJobs: [],
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
@ -82,22 +83,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH:
|
case EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH:
|
||||||
if (state.noInfluencersConfigured === true) {
|
nextState = { ...state, swimlaneContainerWidth: payload };
|
||||||
// swimlane is full width, minus 30 for the 'no influencers' info icon,
|
|
||||||
// minus 170 for the lane labels, minus 50 padding
|
|
||||||
nextState = { ...state, swimlaneContainerWidth: payload - 250 };
|
|
||||||
} else {
|
|
||||||
// swimlane width is 5 sixths of the window,
|
|
||||||
// minus 170 for the lane labels, minus 50 padding
|
|
||||||
nextState = { ...state, swimlaneContainerWidth: (payload / 6) * 5 - 220 };
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EXPLORER_ACTION.SET_SWIMLANE_LIMIT:
|
|
||||||
nextState = {
|
|
||||||
...state,
|
|
||||||
swimlaneLimit: payload,
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME:
|
case EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME:
|
||||||
|
@ -117,6 +103,9 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
||||||
...getClearedSelectedAnomaliesState(),
|
...getClearedSelectedAnomaliesState(),
|
||||||
maskAll,
|
maskAll,
|
||||||
viewBySwimlaneFieldName,
|
viewBySwimlaneFieldName,
|
||||||
|
viewBySwimlaneData: getDefaultSwimlaneData(),
|
||||||
|
viewByFromPage: 1,
|
||||||
|
viewBySwimlaneDataLoading: true,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -125,7 +114,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
||||||
nextState = {
|
nextState = {
|
||||||
...state,
|
...state,
|
||||||
annotationsData,
|
annotationsData,
|
||||||
...overallState,
|
overallSwimlaneData: overallState,
|
||||||
tableData,
|
tableData,
|
||||||
viewBySwimlaneData: {
|
viewBySwimlaneData: {
|
||||||
...getDefaultSwimlaneData(),
|
...getDefaultSwimlaneData(),
|
||||||
|
@ -134,6 +123,22 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EXPLORER_ACTION.SET_VIEW_BY_FROM_PAGE:
|
||||||
|
nextState = {
|
||||||
|
...state,
|
||||||
|
viewByFromPage: payload,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE:
|
||||||
|
nextState = {
|
||||||
|
...state,
|
||||||
|
// reset current page on the page size change
|
||||||
|
viewByFromPage: 1,
|
||||||
|
viewByPerPage: payload,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
nextState = state;
|
nextState = state;
|
||||||
}
|
}
|
||||||
|
@ -155,7 +160,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
||||||
filteredFields: nextState.filteredFields,
|
filteredFields: nextState.filteredFields,
|
||||||
isAndOperator: nextState.isAndOperator,
|
isAndOperator: nextState.isAndOperator,
|
||||||
selectedJobs: nextState.selectedJobs,
|
selectedJobs: nextState.selectedJobs,
|
||||||
selectedCells: nextState.selectedCells,
|
selectedCells: nextState.selectedCells!,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { bounds, selectedCells } = nextState;
|
const { bounds, selectedCells } = nextState;
|
||||||
|
|
|
@ -57,5 +57,6 @@ export function setInfluencerFilterSettings(
|
||||||
filteredFields.includes(selectedViewByFieldName) === false,
|
filteredFields.includes(selectedViewByFieldName) === false,
|
||||||
viewBySwimlaneFieldName: selectedViewByFieldName,
|
viewBySwimlaneFieldName: selectedViewByFieldName,
|
||||||
viewBySwimlaneOptions: filteredViewBySwimlaneOptions,
|
viewBySwimlaneOptions: filteredViewBySwimlaneOptions,
|
||||||
|
viewByFromPage: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,9 @@ import {
|
||||||
TimeRangeBounds,
|
TimeRangeBounds,
|
||||||
OverallSwimlaneData,
|
OverallSwimlaneData,
|
||||||
SwimlaneData,
|
SwimlaneData,
|
||||||
|
ViewBySwimLaneData,
|
||||||
} from '../../explorer_utils';
|
} from '../../explorer_utils';
|
||||||
|
import { SWIM_LANE_DEFAULT_PAGE_SIZE } from '../../explorer_constants';
|
||||||
|
|
||||||
export interface ExplorerState {
|
export interface ExplorerState {
|
||||||
annotationsData: any[];
|
annotationsData: any[];
|
||||||
|
@ -42,14 +44,16 @@ export interface ExplorerState {
|
||||||
selectedJobs: ExplorerJob[] | null;
|
selectedJobs: ExplorerJob[] | null;
|
||||||
swimlaneBucketInterval: any;
|
swimlaneBucketInterval: any;
|
||||||
swimlaneContainerWidth: number;
|
swimlaneContainerWidth: number;
|
||||||
swimlaneLimit: number;
|
|
||||||
tableData: AnomaliesTableData;
|
tableData: AnomaliesTableData;
|
||||||
tableQueryString: string;
|
tableQueryString: string;
|
||||||
viewByLoadedForTimeFormatted: string | null;
|
viewByLoadedForTimeFormatted: string | null;
|
||||||
viewBySwimlaneData: SwimlaneData | OverallSwimlaneData;
|
viewBySwimlaneData: SwimlaneData | ViewBySwimLaneData;
|
||||||
viewBySwimlaneDataLoading: boolean;
|
viewBySwimlaneDataLoading: boolean;
|
||||||
viewBySwimlaneFieldName?: string;
|
viewBySwimlaneFieldName?: string;
|
||||||
|
viewByPerPage: number;
|
||||||
|
viewByFromPage: number;
|
||||||
viewBySwimlaneOptions: string[];
|
viewBySwimlaneOptions: string[];
|
||||||
|
swimlaneLimit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultIndexPattern() {
|
function getDefaultIndexPattern() {
|
||||||
|
@ -78,7 +82,6 @@ export function getExplorerDefaultState(): ExplorerState {
|
||||||
selectedJobs: null,
|
selectedJobs: null,
|
||||||
swimlaneBucketInterval: undefined,
|
swimlaneBucketInterval: undefined,
|
||||||
swimlaneContainerWidth: 0,
|
swimlaneContainerWidth: 0,
|
||||||
swimlaneLimit: 10,
|
|
||||||
tableData: {
|
tableData: {
|
||||||
anomalies: [],
|
anomalies: [],
|
||||||
examplesByJobId: [''],
|
examplesByJobId: [''],
|
||||||
|
@ -92,5 +95,8 @@ export function getExplorerDefaultState(): ExplorerState {
|
||||||
viewBySwimlaneDataLoading: false,
|
viewBySwimlaneDataLoading: false,
|
||||||
viewBySwimlaneFieldName: undefined,
|
viewBySwimlaneFieldName: undefined,
|
||||||
viewBySwimlaneOptions: [],
|
viewBySwimlaneOptions: [],
|
||||||
|
viewByPerPage: SWIM_LANE_DEFAULT_PAGE_SIZE,
|
||||||
|
viewByFromPage: 1,
|
||||||
|
swimlaneLimit: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 { useSwimlaneLimit, SelectLimit } from './select_limit';
|
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 { act } from 'react-dom/test-utils';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { SelectLimit } from './select_limit';
|
|
||||||
|
|
||||||
describe('SelectLimit', () => {
|
|
||||||
test('creates correct initial selected value', () => {
|
|
||||||
const wrapper = shallow(<SelectLimit />);
|
|
||||||
expect(wrapper.props().value).toEqual(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('state for currently selected value is updated correctly on click', () => {
|
|
||||||
const wrapper = shallow(<SelectLimit />);
|
|
||||||
expect(wrapper.props().value).toEqual(10);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
wrapper.simulate('change', { target: { value: 25 } });
|
|
||||||
});
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(wrapper.props().value).toEqual(10);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* React component for rendering a select element with limit options.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
|
||||||
|
|
||||||
import { EuiSelect } from '@elastic/eui';
|
|
||||||
|
|
||||||
const limitOptions = [5, 10, 25, 50];
|
|
||||||
|
|
||||||
const euiOptions = limitOptions.map((limit) => ({
|
|
||||||
value: limit,
|
|
||||||
text: `${limit}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const defaultLimit = limitOptions[1];
|
|
||||||
export const limit$ = new BehaviorSubject<number>(defaultLimit);
|
|
||||||
|
|
||||||
export const useSwimlaneLimit = (): [number, (newLimit: number) => void] => {
|
|
||||||
const limit = useObservable(limit$, defaultLimit);
|
|
||||||
|
|
||||||
return [limit!, (newLimit: number) => limit$.next(newLimit)];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectLimit = () => {
|
|
||||||
const [limit, setLimit] = useSwimlaneLimit();
|
|
||||||
|
|
||||||
function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
|
||||||
setLimit(parseInt(e.target.value, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
return <EuiSelect compressed options={euiOptions} onChange={onChange} value={limit} />;
|
|
||||||
};
|
|
|
@ -5,7 +5,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useCallback, useState } from 'react';
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
import { EuiResizeObserver, EuiText } from '@elastic/eui';
|
import {
|
||||||
|
EuiText,
|
||||||
|
EuiLoadingChart,
|
||||||
|
EuiResizeObserver,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiEmptyPrompt,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import {
|
import {
|
||||||
|
@ -14,48 +21,139 @@ import {
|
||||||
} from '../../application/explorer/explorer_swimlane';
|
} from '../../application/explorer/explorer_swimlane';
|
||||||
|
|
||||||
import { MlTooltipComponent } from '../../application/components/chart_tooltip';
|
import { MlTooltipComponent } from '../../application/components/chart_tooltip';
|
||||||
|
import { SwimLanePagination } from './swimlane_pagination';
|
||||||
|
import { SWIMLANE_TYPE } from './explorer_constants';
|
||||||
|
import { ViewBySwimLaneData } from './explorer_utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ignore insignificant resize, e.g. browser scrollbar appearance.
|
||||||
|
*/
|
||||||
|
const RESIZE_IGNORED_DIFF_PX = 20;
|
||||||
const RESIZE_THROTTLE_TIME_MS = 500;
|
const RESIZE_THROTTLE_TIME_MS = 500;
|
||||||
|
|
||||||
|
export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData {
|
||||||
|
return arg && arg.hasOwnProperty('cardinality');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anomaly swim lane container responsible for handling resizing, pagination and injecting
|
||||||
|
* tooltip service.
|
||||||
|
*
|
||||||
|
* @param children
|
||||||
|
* @param onResize
|
||||||
|
* @param perPage
|
||||||
|
* @param fromPage
|
||||||
|
* @param swimlaneLimit
|
||||||
|
* @param onPaginationChange
|
||||||
|
* @param props
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export const SwimlaneContainer: FC<
|
export const SwimlaneContainer: FC<
|
||||||
Omit<ExplorerSwimlaneProps, 'chartWidth' | 'tooltipService'> & {
|
Omit<ExplorerSwimlaneProps, 'chartWidth' | 'tooltipService'> & {
|
||||||
onResize: (width: number) => void;
|
onResize: (width: number) => void;
|
||||||
|
fromPage?: number;
|
||||||
|
perPage?: number;
|
||||||
|
swimlaneLimit?: number;
|
||||||
|
onPaginationChange?: (arg: { perPage?: number; fromPage?: number }) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
noDataWarning: string | JSX.Element | null;
|
||||||
}
|
}
|
||||||
> = ({ children, onResize, ...props }) => {
|
> = ({
|
||||||
|
children,
|
||||||
|
onResize,
|
||||||
|
perPage,
|
||||||
|
fromPage,
|
||||||
|
swimlaneLimit,
|
||||||
|
onPaginationChange,
|
||||||
|
isLoading,
|
||||||
|
noDataWarning,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const [chartWidth, setChartWidth] = useState<number>(0);
|
const [chartWidth, setChartWidth] = useState<number>(0);
|
||||||
|
|
||||||
const resizeHandler = useCallback(
|
const resizeHandler = useCallback(
|
||||||
throttle((e: { width: number; height: number }) => {
|
throttle((e: { width: number; height: number }) => {
|
||||||
const labelWidth = 200;
|
const labelWidth = 200;
|
||||||
setChartWidth(e.width - labelWidth);
|
const resultNewWidth = e.width - labelWidth;
|
||||||
onResize(e.width);
|
if (Math.abs(resultNewWidth - chartWidth) > RESIZE_IGNORED_DIFF_PX) {
|
||||||
|
setChartWidth(resultNewWidth);
|
||||||
|
onResize(resultNewWidth);
|
||||||
|
}
|
||||||
}, RESIZE_THROTTLE_TIME_MS),
|
}, RESIZE_THROTTLE_TIME_MS),
|
||||||
[]
|
[chartWidth]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showSwimlane =
|
||||||
|
props.swimlaneData &&
|
||||||
|
props.swimlaneData.laneLabels &&
|
||||||
|
props.swimlaneData.laneLabels.length > 0 &&
|
||||||
|
props.swimlaneData.points.length > 0;
|
||||||
|
|
||||||
|
const isPaginationVisible =
|
||||||
|
(showSwimlane || isLoading) &&
|
||||||
|
swimlaneLimit !== undefined &&
|
||||||
|
onPaginationChange &&
|
||||||
|
props.swimlaneType === SWIMLANE_TYPE.VIEW_BY &&
|
||||||
|
fromPage &&
|
||||||
|
perPage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiResizeObserver onResize={resizeHandler}>
|
<>
|
||||||
{(resizeRef) => (
|
<EuiResizeObserver onResize={resizeHandler}>
|
||||||
<div
|
{(resizeRef) => (
|
||||||
ref={(el) => {
|
<EuiFlexGroup
|
||||||
resizeRef(el);
|
gutterSize={'none'}
|
||||||
}}
|
direction={'column'}
|
||||||
>
|
style={{ width: '100%', height: '100%', overflow: 'hidden' }}
|
||||||
<div style={{ width: '100%' }}>
|
ref={(el) => {
|
||||||
<EuiText color="subdued" size="s">
|
resizeRef(el);
|
||||||
<MlTooltipComponent>
|
}}
|
||||||
{(tooltipService) => (
|
data-test-subj="mlSwimLaneContainer"
|
||||||
<ExplorerSwimlane
|
>
|
||||||
chartWidth={chartWidth}
|
<EuiFlexItem style={{ width: '100%', overflowY: 'auto' }} grow={false}>
|
||||||
tooltipService={tooltipService}
|
<EuiText color="subdued" size="s">
|
||||||
{...props}
|
{showSwimlane && !isLoading && (
|
||||||
|
<MlTooltipComponent>
|
||||||
|
{(tooltipService) => (
|
||||||
|
<ExplorerSwimlane
|
||||||
|
chartWidth={chartWidth}
|
||||||
|
tooltipService={tooltipService}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MlTooltipComponent>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<EuiText textAlign={'center'}>
|
||||||
|
<EuiLoadingChart
|
||||||
|
size="xl"
|
||||||
|
mono={true}
|
||||||
|
data-test-subj="mlSwimLaneLoadingIndicator"
|
||||||
|
/>
|
||||||
|
</EuiText>
|
||||||
|
)}
|
||||||
|
{!isLoading && !showSwimlane && (
|
||||||
|
<EuiEmptyPrompt
|
||||||
|
titleSize="xs"
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
title={<h2>{noDataWarning}</h2>}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</MlTooltipComponent>
|
</EuiText>
|
||||||
</EuiText>
|
</EuiFlexItem>
|
||||||
</div>
|
{isPaginationVisible && (
|
||||||
</div>
|
<EuiFlexItem grow={false}>
|
||||||
)}
|
<SwimLanePagination
|
||||||
</EuiResizeObserver>
|
cardinality={swimlaneLimit!}
|
||||||
|
fromPage={fromPage!}
|
||||||
|
perPage={perPage!}
|
||||||
|
onPaginationChange={onPaginationChange!}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
)}
|
||||||
|
</EuiResizeObserver>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* 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, { FC, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiPopover,
|
||||||
|
EuiContextMenuPanel,
|
||||||
|
EuiPagination,
|
||||||
|
EuiContextMenuItem,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
interface SwimLanePaginationProps {
|
||||||
|
fromPage: number;
|
||||||
|
perPage: number;
|
||||||
|
cardinality: number;
|
||||||
|
onPaginationChange: (arg: { perPage?: number; fromPage?: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwimLanePagination: FC<SwimLanePaginationProps> = ({
|
||||||
|
cardinality,
|
||||||
|
fromPage,
|
||||||
|
perPage,
|
||||||
|
onPaginationChange,
|
||||||
|
}) => {
|
||||||
|
const componentFromPage = fromPage - 1;
|
||||||
|
|
||||||
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
|
|
||||||
|
const onButtonClick = () => setIsPopoverOpen(() => !isPopoverOpen);
|
||||||
|
const closePopover = () => setIsPopoverOpen(false);
|
||||||
|
|
||||||
|
const goToPage = useCallback((pageNumber: number) => {
|
||||||
|
onPaginationChange({ fromPage: pageNumber + 1 });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setPerPage = useCallback((perPageUpdate: number) => {
|
||||||
|
onPaginationChange({ perPage: perPageUpdate });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const pageCount = Math.ceil(cardinality / perPage);
|
||||||
|
|
||||||
|
const items = [5, 10, 20, 50, 100].map((v) => {
|
||||||
|
return (
|
||||||
|
<EuiContextMenuItem
|
||||||
|
key={`${v}_rows`}
|
||||||
|
icon={v === perPage ? 'check' : 'empty'}
|
||||||
|
onClick={() => {
|
||||||
|
closePopover();
|
||||||
|
setPerPage(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.ml.explorer.swimLaneSelectRowsPerPage"
|
||||||
|
defaultMessage="{rowsCount} rows"
|
||||||
|
values={{ rowsCount: v }}
|
||||||
|
/>
|
||||||
|
</EuiContextMenuItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiPopover
|
||||||
|
button={
|
||||||
|
<EuiButtonEmpty
|
||||||
|
size="xs"
|
||||||
|
color="text"
|
||||||
|
iconType="arrowDown"
|
||||||
|
iconSide="right"
|
||||||
|
onClick={onButtonClick}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.ml.explorer.swimLaneRowsPerPage"
|
||||||
|
defaultMessage="Rows per page: {rowsCount}"
|
||||||
|
values={{ rowsCount: perPage }}
|
||||||
|
/>
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
}
|
||||||
|
isOpen={isPopoverOpen}
|
||||||
|
closePopover={closePopover}
|
||||||
|
panelPaddingSize="none"
|
||||||
|
>
|
||||||
|
<EuiContextMenuPanel items={items} />
|
||||||
|
</EuiPopover>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiPagination
|
||||||
|
aria-label={i18n.translate('xpack.ml.explorer.swimLanePagination', {
|
||||||
|
defaultMessage: 'Anomaly swim lane pagination',
|
||||||
|
})}
|
||||||
|
pageCount={pageCount}
|
||||||
|
activePage={componentFromPage}
|
||||||
|
onPageClick={goToPage}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
};
|
|
@ -22,7 +22,6 @@ import { ml } from '../../services/ml_api_service';
|
||||||
import { useExplorerData } from '../../explorer/actions';
|
import { useExplorerData } from '../../explorer/actions';
|
||||||
import { explorerService } from '../../explorer/explorer_dashboard_service';
|
import { explorerService } from '../../explorer/explorer_dashboard_service';
|
||||||
import { getDateFormatTz } from '../../explorer/explorer_utils';
|
import { getDateFormatTz } from '../../explorer/explorer_utils';
|
||||||
import { useSwimlaneLimit } from '../../explorer/select_limit';
|
|
||||||
import { useJobSelection } from '../../components/job_selector/use_job_selection';
|
import { useJobSelection } from '../../components/job_selector/use_job_selection';
|
||||||
import { useShowCharts } from '../../components/controls/checkbox_showcharts';
|
import { useShowCharts } from '../../components/controls/checkbox_showcharts';
|
||||||
import { useTableInterval } from '../../components/controls/select_interval';
|
import { useTableInterval } from '../../components/controls/select_interval';
|
||||||
|
@ -30,6 +29,7 @@ import { useTableSeverity } from '../../components/controls/select_severity';
|
||||||
import { useUrlState } from '../../util/url_state';
|
import { useUrlState } from '../../util/url_state';
|
||||||
import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs';
|
import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs';
|
||||||
import { useTimefilter } from '../../contexts/kibana';
|
import { useTimefilter } from '../../contexts/kibana';
|
||||||
|
import { isViewBySwimLaneData } from '../../explorer/swimlane_container';
|
||||||
|
|
||||||
const breadcrumbs = [
|
const breadcrumbs = [
|
||||||
ML_BREADCRUMB,
|
ML_BREADCRUMB,
|
||||||
|
@ -151,10 +151,6 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
|
||||||
const [showCharts] = useShowCharts();
|
const [showCharts] = useShowCharts();
|
||||||
const [tableInterval] = useTableInterval();
|
const [tableInterval] = useTableInterval();
|
||||||
const [tableSeverity] = useTableSeverity();
|
const [tableSeverity] = useTableSeverity();
|
||||||
const [swimlaneLimit] = useSwimlaneLimit();
|
|
||||||
useEffect(() => {
|
|
||||||
explorerService.setSwimlaneLimit(swimlaneLimit);
|
|
||||||
}, [swimlaneLimit]);
|
|
||||||
|
|
||||||
const [selectedCells, setSelectedCells] = useSelectedCells();
|
const [selectedCells, setSelectedCells] = useSelectedCells();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -170,14 +166,26 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
|
||||||
selectedCells,
|
selectedCells,
|
||||||
selectedJobs: explorerState.selectedJobs,
|
selectedJobs: explorerState.selectedJobs,
|
||||||
swimlaneBucketInterval: explorerState.swimlaneBucketInterval,
|
swimlaneBucketInterval: explorerState.swimlaneBucketInterval,
|
||||||
swimlaneLimit: explorerState.swimlaneLimit,
|
|
||||||
tableInterval: tableInterval.val,
|
tableInterval: tableInterval.val,
|
||||||
tableSeverity: tableSeverity.val,
|
tableSeverity: tableSeverity.val,
|
||||||
viewBySwimlaneFieldName: explorerState.viewBySwimlaneFieldName,
|
viewBySwimlaneFieldName: explorerState.viewBySwimlaneFieldName,
|
||||||
|
swimlaneContainerWidth: explorerState.swimlaneContainerWidth,
|
||||||
|
viewByPerPage: explorerState.viewByPerPage,
|
||||||
|
viewByFromPage: explorerState.viewByFromPage,
|
||||||
}) ||
|
}) ||
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadExplorerData(loadExplorerDataConfig);
|
if (explorerState && explorerState.swimlaneContainerWidth > 0) {
|
||||||
|
loadExplorerData({
|
||||||
|
...loadExplorerDataConfig,
|
||||||
|
swimlaneLimit:
|
||||||
|
explorerState?.viewBySwimlaneData &&
|
||||||
|
isViewBySwimLaneData(explorerState?.viewBySwimlaneData)
|
||||||
|
? explorerState?.viewBySwimlaneData.cardinality
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [JSON.stringify(loadExplorerDataConfig)]);
|
}, [JSON.stringify(loadExplorerDataConfig)]);
|
||||||
|
|
||||||
if (explorerState === undefined || refresh === undefined || showCharts === undefined) {
|
if (explorerState === undefined || refresh === undefined || showCharts === undefined) {
|
||||||
|
|
|
@ -12,41 +12,47 @@ import { I18nProvider } from '@kbn/i18n/react';
|
||||||
|
|
||||||
import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer';
|
import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer';
|
||||||
|
|
||||||
jest.mock('../../contexts/kibana/kibana_context', () => ({
|
jest.mock('../../contexts/kibana/kibana_context', () => {
|
||||||
useMlKibana: () => {
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
return {
|
const { of } = require('rxjs');
|
||||||
services: {
|
return {
|
||||||
uiSettings: { get: jest.fn() },
|
useMlKibana: () => {
|
||||||
data: {
|
return {
|
||||||
query: {
|
services: {
|
||||||
timefilter: {
|
uiSettings: { get: jest.fn() },
|
||||||
|
data: {
|
||||||
|
query: {
|
||||||
timefilter: {
|
timefilter: {
|
||||||
disableTimeRangeSelector: jest.fn(),
|
timefilter: {
|
||||||
disableAutoRefreshSelector: jest.fn(),
|
disableTimeRangeSelector: jest.fn(),
|
||||||
enableTimeRangeSelector: jest.fn(),
|
disableAutoRefreshSelector: jest.fn(),
|
||||||
enableAutoRefreshSelector: jest.fn(),
|
enableTimeRangeSelector: jest.fn(),
|
||||||
getRefreshInterval: jest.fn(),
|
enableAutoRefreshSelector: jest.fn(),
|
||||||
setRefreshInterval: jest.fn(),
|
getRefreshInterval: jest.fn(),
|
||||||
getTime: jest.fn(),
|
setRefreshInterval: jest.fn(),
|
||||||
isAutoRefreshSelectorEnabled: jest.fn(),
|
getTime: jest.fn(),
|
||||||
isTimeRangeSelectorEnabled: jest.fn(),
|
isAutoRefreshSelectorEnabled: jest.fn(),
|
||||||
getRefreshIntervalUpdate$: jest.fn(),
|
isTimeRangeSelectorEnabled: jest.fn(),
|
||||||
getTimeUpdate$: jest.fn(),
|
getRefreshIntervalUpdate$: jest.fn(),
|
||||||
getEnabledUpdated$: jest.fn(),
|
getTimeUpdate$: jest.fn(() => {
|
||||||
|
return of();
|
||||||
|
}),
|
||||||
|
getEnabledUpdated$: jest.fn(),
|
||||||
|
},
|
||||||
|
history: { get: jest.fn() },
|
||||||
},
|
},
|
||||||
history: { get: jest.fn() },
|
},
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
toasts: {
|
||||||
|
addDanger: () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notifications: {
|
};
|
||||||
toasts: {
|
},
|
||||||
addDanger: () => {},
|
};
|
||||||
},
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../../util/dependency_cache', () => ({
|
jest.mock('../../util/dependency_cache', () => ({
|
||||||
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
|
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
|
||||||
|
|
|
@ -5,26 +5,40 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
import { merge, Observable } from 'rxjs';
|
import { merge } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map, skip } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { annotationsRefresh$ } from '../services/annotations_service';
|
import { annotationsRefresh$ } from '../services/annotations_service';
|
||||||
import {
|
import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
|
||||||
mlTimefilterRefresh$,
|
import { useTimefilter } from '../contexts/kibana';
|
||||||
mlTimefilterTimeChange$,
|
|
||||||
} from '../services/timefilter_refresh_service';
|
|
||||||
|
|
||||||
export interface Refresh {
|
export interface Refresh {
|
||||||
lastRefresh: number;
|
lastRefresh: number;
|
||||||
timeRange?: { start: string; end: string };
|
timeRange?: { start: string; end: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
const refresh$: Observable<Refresh> = merge(
|
/**
|
||||||
mlTimefilterRefresh$,
|
* Hook that provides the latest refresh timestamp
|
||||||
mlTimefilterTimeChange$,
|
* and the most recent applied time range.
|
||||||
annotationsRefresh$.pipe(map((d) => ({ lastRefresh: d })))
|
*/
|
||||||
);
|
|
||||||
|
|
||||||
export const useRefresh = () => {
|
export const useRefresh = () => {
|
||||||
|
const timefilter = useTimefilter();
|
||||||
|
|
||||||
|
const refresh$ = useMemo(() => {
|
||||||
|
return merge(
|
||||||
|
mlTimefilterRefresh$,
|
||||||
|
timefilter.getTimeUpdate$().pipe(
|
||||||
|
// skip initially emitted value
|
||||||
|
skip(1),
|
||||||
|
map((_) => {
|
||||||
|
const { from, to } = timefilter.getTime();
|
||||||
|
return { lastRefresh: Date.now(), timeRange: { start: from, end: to } };
|
||||||
|
})
|
||||||
|
),
|
||||||
|
annotationsRefresh$.pipe(map((d) => ({ lastRefresh: d })))
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return useObservable<Refresh>(refresh$);
|
return useObservable<Refresh>(refresh$);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,14 +12,19 @@ import {
|
||||||
UI_SETTINGS,
|
UI_SETTINGS,
|
||||||
} from '../../../../../../src/plugins/data/public';
|
} from '../../../../../../src/plugins/data/public';
|
||||||
import { getBoundsRoundedToInterval, TimeBuckets, TimeRangeBounds } from '../util/time_buckets';
|
import { getBoundsRoundedToInterval, TimeBuckets, TimeRangeBounds } from '../util/time_buckets';
|
||||||
import { ExplorerJob, OverallSwimlaneData, SwimlaneData } from '../explorer/explorer_utils';
|
import {
|
||||||
|
ExplorerJob,
|
||||||
|
OverallSwimlaneData,
|
||||||
|
SwimlaneData,
|
||||||
|
ViewBySwimLaneData,
|
||||||
|
} from '../explorer/explorer_utils';
|
||||||
import { VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants';
|
import { VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants';
|
||||||
import { MlResultsService } from './results_service';
|
import { MlResultsService } from './results_service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anomaly Explorer Service
|
* Service for retrieving anomaly swim lanes data.
|
||||||
*/
|
*/
|
||||||
export class ExplorerService {
|
export class AnomalyTimelineService {
|
||||||
private timeBuckets: TimeBuckets;
|
private timeBuckets: TimeBuckets;
|
||||||
private _customTimeRange: TimeRange | undefined;
|
private _customTimeRange: TimeRange | undefined;
|
||||||
|
|
||||||
|
@ -130,12 +135,27 @@ export class ExplorerService {
|
||||||
return overallSwimlaneData;
|
return overallSwimlaneData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches view by swim lane data.
|
||||||
|
*
|
||||||
|
* @param fieldValues
|
||||||
|
* @param bounds
|
||||||
|
* @param selectedJobs
|
||||||
|
* @param viewBySwimlaneFieldName
|
||||||
|
* @param swimlaneLimit
|
||||||
|
* @param perPage
|
||||||
|
* @param fromPage
|
||||||
|
* @param swimlaneContainerWidth
|
||||||
|
* @param influencersFilterQuery
|
||||||
|
*/
|
||||||
public async loadViewBySwimlane(
|
public async loadViewBySwimlane(
|
||||||
fieldValues: string[],
|
fieldValues: string[],
|
||||||
bounds: { earliest: number; latest: number },
|
bounds: { earliest: number; latest: number },
|
||||||
selectedJobs: ExplorerJob[],
|
selectedJobs: ExplorerJob[],
|
||||||
viewBySwimlaneFieldName: string,
|
viewBySwimlaneFieldName: string,
|
||||||
swimlaneLimit: number,
|
swimlaneLimit: number,
|
||||||
|
perPage: number,
|
||||||
|
fromPage: number,
|
||||||
swimlaneContainerWidth: number,
|
swimlaneContainerWidth: number,
|
||||||
influencersFilterQuery?: any
|
influencersFilterQuery?: any
|
||||||
): Promise<SwimlaneData | undefined> {
|
): Promise<SwimlaneData | undefined> {
|
||||||
|
@ -172,7 +192,8 @@ export class ExplorerService {
|
||||||
searchBounds.min.valueOf(),
|
searchBounds.min.valueOf(),
|
||||||
searchBounds.max.valueOf(),
|
searchBounds.max.valueOf(),
|
||||||
interval,
|
interval,
|
||||||
swimlaneLimit
|
perPage,
|
||||||
|
fromPage
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
response = await this.mlResultsService.getInfluencerValueMaxScoreByTime(
|
response = await this.mlResultsService.getInfluencerValueMaxScoreByTime(
|
||||||
|
@ -183,6 +204,8 @@ export class ExplorerService {
|
||||||
searchBounds.max.valueOf(),
|
searchBounds.max.valueOf(),
|
||||||
interval,
|
interval,
|
||||||
swimlaneLimit,
|
swimlaneLimit,
|
||||||
|
perPage,
|
||||||
|
fromPage,
|
||||||
influencersFilterQuery
|
influencersFilterQuery
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -193,6 +216,7 @@ export class ExplorerService {
|
||||||
|
|
||||||
const viewBySwimlaneData = this.processViewByResults(
|
const viewBySwimlaneData = this.processViewByResults(
|
||||||
response.results,
|
response.results,
|
||||||
|
response.cardinality,
|
||||||
fieldValues,
|
fieldValues,
|
||||||
bounds,
|
bounds,
|
||||||
viewBySwimlaneFieldName,
|
viewBySwimlaneFieldName,
|
||||||
|
@ -204,6 +228,55 @@ export class ExplorerService {
|
||||||
return viewBySwimlaneData;
|
return viewBySwimlaneData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async loadViewByTopFieldValuesForSelectedTime(
|
||||||
|
earliestMs: number,
|
||||||
|
latestMs: number,
|
||||||
|
selectedJobs: ExplorerJob[],
|
||||||
|
viewBySwimlaneFieldName: string,
|
||||||
|
swimlaneLimit: number,
|
||||||
|
perPage: number,
|
||||||
|
fromPage: number,
|
||||||
|
swimlaneContainerWidth: number
|
||||||
|
) {
|
||||||
|
const selectedJobIds = selectedJobs.map((d) => d.id);
|
||||||
|
|
||||||
|
// Find the top field values for the selected time, and then load the 'view by'
|
||||||
|
// swimlane over the full time range for those specific field values.
|
||||||
|
if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
|
||||||
|
const resp = await this.mlResultsService.getTopInfluencers(
|
||||||
|
selectedJobIds,
|
||||||
|
earliestMs,
|
||||||
|
latestMs,
|
||||||
|
swimlaneLimit,
|
||||||
|
perPage,
|
||||||
|
fromPage
|
||||||
|
);
|
||||||
|
if (resp.influencers[viewBySwimlaneFieldName] === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const topFieldValues: any[] = [];
|
||||||
|
const topInfluencers = resp.influencers[viewBySwimlaneFieldName];
|
||||||
|
if (Array.isArray(topInfluencers)) {
|
||||||
|
topInfluencers.forEach((influencerData) => {
|
||||||
|
if (influencerData.maxAnomalyScore > 0) {
|
||||||
|
topFieldValues.push(influencerData.influencerFieldValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return topFieldValues;
|
||||||
|
} else {
|
||||||
|
const resp = await this.mlResultsService.getScoresByBucket(
|
||||||
|
selectedJobIds,
|
||||||
|
earliestMs,
|
||||||
|
latestMs,
|
||||||
|
this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asSeconds() + 's',
|
||||||
|
swimlaneLimit
|
||||||
|
);
|
||||||
|
return Object.keys(resp.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getTimeBounds(): TimeRangeBounds {
|
private getTimeBounds(): TimeRangeBounds {
|
||||||
return this._customTimeRange !== undefined
|
return this._customTimeRange !== undefined
|
||||||
? this.timeFilter.calculateBounds(this._customTimeRange)
|
? this.timeFilter.calculateBounds(this._customTimeRange)
|
||||||
|
@ -245,6 +318,7 @@ export class ExplorerService {
|
||||||
|
|
||||||
private processViewByResults(
|
private processViewByResults(
|
||||||
scoresByInfluencerAndTime: Record<string, { [timeMs: number]: number }>,
|
scoresByInfluencerAndTime: Record<string, { [timeMs: number]: number }>,
|
||||||
|
cardinality: number,
|
||||||
sortedLaneValues: string[],
|
sortedLaneValues: string[],
|
||||||
bounds: any,
|
bounds: any,
|
||||||
viewBySwimlaneFieldName: string,
|
viewBySwimlaneFieldName: string,
|
||||||
|
@ -254,7 +328,7 @@ export class ExplorerService {
|
||||||
// Sorts the lanes according to the supplied array of lane
|
// Sorts the lanes according to the supplied array of lane
|
||||||
// values in the order in which they should be displayed,
|
// values in the order in which they should be displayed,
|
||||||
// or pass an empty array to sort lanes according to max score over all time.
|
// or pass an empty array to sort lanes according to max score over all time.
|
||||||
const dataset: OverallSwimlaneData = {
|
const dataset: ViewBySwimLaneData = {
|
||||||
fieldName: viewBySwimlaneFieldName,
|
fieldName: viewBySwimlaneFieldName,
|
||||||
points: [],
|
points: [],
|
||||||
laneLabels: [],
|
laneLabels: [],
|
||||||
|
@ -262,6 +336,7 @@ export class ExplorerService {
|
||||||
// Set the earliest and latest to be the same as the overall swim lane.
|
// Set the earliest and latest to be the same as the overall swim lane.
|
||||||
earliest: bounds.earliest,
|
earliest: bounds.earliest,
|
||||||
latest: bounds.latest,
|
latest: bounds.latest,
|
||||||
|
cardinality,
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxScoreByLaneLabel: Record<string, number> = {};
|
const maxScoreByLaneLabel: Record<string, number> = {};
|
|
@ -37,7 +37,7 @@ describe('DashboardService', () => {
|
||||||
// assert
|
// assert
|
||||||
expect(mockSavedObjectClient.find).toHaveBeenCalledWith({
|
expect(mockSavedObjectClient.find).toHaveBeenCalledWith({
|
||||||
type: 'dashboard',
|
type: 'dashboard',
|
||||||
perPage: 10,
|
perPage: 1000,
|
||||||
search: `test*`,
|
search: `test*`,
|
||||||
searchFields: ['title^3', 'description'],
|
searchFields: ['title^3', 'description'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function dashboardServiceProvider(
|
||||||
async fetchDashboards(query?: string) {
|
async fetchDashboards(query?: string) {
|
||||||
return await savedObjectClient.find<SavedObjectDashboard>({
|
return await savedObjectClient.find<SavedObjectDashboard>({
|
||||||
type: 'dashboard',
|
type: 'dashboard',
|
||||||
perPage: 10,
|
perPage: 1000,
|
||||||
search: query ? `${query}*` : '',
|
search: query ? `${query}*` : '',
|
||||||
searchFields: ['title^3', 'description'],
|
searchFields: ['title^3', 'description'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { annotations } from './annotations';
|
||||||
import { dataFrameAnalytics } from './data_frame_analytics';
|
import { dataFrameAnalytics } from './data_frame_analytics';
|
||||||
import { filters } from './filters';
|
import { filters } from './filters';
|
||||||
import { resultsApiProvider } from './results';
|
import { resultsApiProvider } from './results';
|
||||||
import { jobs } from './jobs';
|
import { jobsApiProvider } from './jobs';
|
||||||
import { fileDatavisualizer } from './datavisualizer';
|
import { fileDatavisualizer } from './datavisualizer';
|
||||||
import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info';
|
import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info';
|
||||||
|
|
||||||
|
@ -726,7 +726,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
|
||||||
dataFrameAnalytics,
|
dataFrameAnalytics,
|
||||||
filters,
|
filters,
|
||||||
results: resultsApiProvider(httpService),
|
results: resultsApiProvider(httpService),
|
||||||
jobs,
|
jobs: jobsApiProvider(httpService),
|
||||||
fileDatavisualizer,
|
fileDatavisualizer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { http } from '../http_service';
|
import { HttpService } from '../http_service';
|
||||||
|
|
||||||
import { basePath } from './index';
|
import { basePath } from './index';
|
||||||
import { Dictionary } from '../../../../common/types/common';
|
import { Dictionary } from '../../../../common/types/common';
|
||||||
|
@ -24,10 +24,10 @@ import {
|
||||||
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job';
|
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job';
|
||||||
import { Category } from '../../../../common/types/categories';
|
import { Category } from '../../../../common/types/categories';
|
||||||
|
|
||||||
export const jobs = {
|
export const jobsApiProvider = (httpService: HttpService) => ({
|
||||||
jobsSummary(jobIds: string[]) {
|
jobsSummary(jobIds: string[]) {
|
||||||
const body = JSON.stringify({ jobIds });
|
const body = JSON.stringify({ jobIds });
|
||||||
return http<MlSummaryJobs>({
|
return httpService.http<MlSummaryJobs>({
|
||||||
path: `${basePath()}/jobs/jobs_summary`,
|
path: `${basePath()}/jobs/jobs_summary`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -36,7 +36,10 @@ export const jobs = {
|
||||||
|
|
||||||
jobsWithTimerange(dateFormatTz: string) {
|
jobsWithTimerange(dateFormatTz: string) {
|
||||||
const body = JSON.stringify({ dateFormatTz });
|
const body = JSON.stringify({ dateFormatTz });
|
||||||
return http<{ jobs: MlJobWithTimeRange[]; jobsMap: Dictionary<MlJobWithTimeRange> }>({
|
return httpService.http<{
|
||||||
|
jobs: MlJobWithTimeRange[];
|
||||||
|
jobsMap: Dictionary<MlJobWithTimeRange>;
|
||||||
|
}>({
|
||||||
path: `${basePath()}/jobs/jobs_with_time_range`,
|
path: `${basePath()}/jobs/jobs_with_time_range`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -45,7 +48,7 @@ export const jobs = {
|
||||||
|
|
||||||
jobs(jobIds: string[]) {
|
jobs(jobIds: string[]) {
|
||||||
const body = JSON.stringify({ jobIds });
|
const body = JSON.stringify({ jobIds });
|
||||||
return http<CombinedJobWithStats[]>({
|
return httpService.http<CombinedJobWithStats[]>({
|
||||||
path: `${basePath()}/jobs/jobs`,
|
path: `${basePath()}/jobs/jobs`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -53,7 +56,7 @@ export const jobs = {
|
||||||
},
|
},
|
||||||
|
|
||||||
groups() {
|
groups() {
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/groups`,
|
path: `${basePath()}/jobs/groups`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
@ -61,7 +64,7 @@ export const jobs = {
|
||||||
|
|
||||||
updateGroups(updatedJobs: string[]) {
|
updateGroups(updatedJobs: string[]) {
|
||||||
const body = JSON.stringify({ jobs: updatedJobs });
|
const body = JSON.stringify({ jobs: updatedJobs });
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/update_groups`,
|
path: `${basePath()}/jobs/update_groups`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -75,7 +78,7 @@ export const jobs = {
|
||||||
end,
|
end,
|
||||||
});
|
});
|
||||||
|
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/force_start_datafeeds`,
|
path: `${basePath()}/jobs/force_start_datafeeds`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -84,7 +87,7 @@ export const jobs = {
|
||||||
|
|
||||||
stopDatafeeds(datafeedIds: string[]) {
|
stopDatafeeds(datafeedIds: string[]) {
|
||||||
const body = JSON.stringify({ datafeedIds });
|
const body = JSON.stringify({ datafeedIds });
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/stop_datafeeds`,
|
path: `${basePath()}/jobs/stop_datafeeds`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -93,7 +96,7 @@ export const jobs = {
|
||||||
|
|
||||||
deleteJobs(jobIds: string[]) {
|
deleteJobs(jobIds: string[]) {
|
||||||
const body = JSON.stringify({ jobIds });
|
const body = JSON.stringify({ jobIds });
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/delete_jobs`,
|
path: `${basePath()}/jobs/delete_jobs`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -102,7 +105,7 @@ export const jobs = {
|
||||||
|
|
||||||
closeJobs(jobIds: string[]) {
|
closeJobs(jobIds: string[]) {
|
||||||
const body = JSON.stringify({ jobIds });
|
const body = JSON.stringify({ jobIds });
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/close_jobs`,
|
path: `${basePath()}/jobs/close_jobs`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -111,7 +114,7 @@ export const jobs = {
|
||||||
|
|
||||||
forceStopAndCloseJob(jobId: string) {
|
forceStopAndCloseJob(jobId: string) {
|
||||||
const body = JSON.stringify({ jobId });
|
const body = JSON.stringify({ jobId });
|
||||||
return http<{ success: boolean }>({
|
return httpService.http<{ success: boolean }>({
|
||||||
path: `${basePath()}/jobs/force_stop_and_close_job`,
|
path: `${basePath()}/jobs/force_stop_and_close_job`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -121,7 +124,7 @@ export const jobs = {
|
||||||
jobAuditMessages(jobId: string, from?: number) {
|
jobAuditMessages(jobId: string, from?: number) {
|
||||||
const jobIdString = jobId !== undefined ? `/${jobId}` : '';
|
const jobIdString = jobId !== undefined ? `/${jobId}` : '';
|
||||||
const query = from !== undefined ? { from } : {};
|
const query = from !== undefined ? { from } : {};
|
||||||
return http<JobMessage[]>({
|
return httpService.http<JobMessage[]>({
|
||||||
path: `${basePath()}/job_audit_messages/messages${jobIdString}`,
|
path: `${basePath()}/job_audit_messages/messages${jobIdString}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
query,
|
query,
|
||||||
|
@ -129,7 +132,7 @@ export const jobs = {
|
||||||
},
|
},
|
||||||
|
|
||||||
deletingJobTasks() {
|
deletingJobTasks() {
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/deleting_jobs_tasks`,
|
path: `${basePath()}/jobs/deleting_jobs_tasks`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
@ -137,7 +140,7 @@ export const jobs = {
|
||||||
|
|
||||||
jobsExist(jobIds: string[]) {
|
jobsExist(jobIds: string[]) {
|
||||||
const body = JSON.stringify({ jobIds });
|
const body = JSON.stringify({ jobIds });
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/jobs_exist`,
|
path: `${basePath()}/jobs/jobs_exist`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -146,7 +149,7 @@ export const jobs = {
|
||||||
|
|
||||||
newJobCaps(indexPatternTitle: string, isRollup: boolean = false) {
|
newJobCaps(indexPatternTitle: string, isRollup: boolean = false) {
|
||||||
const query = isRollup === true ? { rollup: true } : {};
|
const query = isRollup === true ? { rollup: true } : {};
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}`,
|
path: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
query,
|
query,
|
||||||
|
@ -175,7 +178,7 @@ export const jobs = {
|
||||||
splitFieldName,
|
splitFieldName,
|
||||||
splitFieldValue,
|
splitFieldValue,
|
||||||
});
|
});
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/new_job_line_chart`,
|
path: `${basePath()}/jobs/new_job_line_chart`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -202,7 +205,7 @@ export const jobs = {
|
||||||
aggFieldNamePairs,
|
aggFieldNamePairs,
|
||||||
splitFieldName,
|
splitFieldName,
|
||||||
});
|
});
|
||||||
return http<any>({
|
return httpService.http<any>({
|
||||||
path: `${basePath()}/jobs/new_job_population_chart`,
|
path: `${basePath()}/jobs/new_job_population_chart`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -210,7 +213,7 @@ export const jobs = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getAllJobAndGroupIds() {
|
getAllJobAndGroupIds() {
|
||||||
return http<ExistingJobsAndGroups>({
|
return httpService.http<ExistingJobsAndGroups>({
|
||||||
path: `${basePath()}/jobs/all_jobs_and_group_ids`,
|
path: `${basePath()}/jobs/all_jobs_and_group_ids`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
@ -222,7 +225,7 @@ export const jobs = {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
});
|
});
|
||||||
return http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({
|
return httpService.http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({
|
||||||
path: `${basePath()}/jobs/look_back_progress`,
|
path: `${basePath()}/jobs/look_back_progress`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -249,7 +252,7 @@ export const jobs = {
|
||||||
end,
|
end,
|
||||||
analyzer,
|
analyzer,
|
||||||
});
|
});
|
||||||
return http<{
|
return httpService.http<{
|
||||||
examples: CategoryFieldExample[];
|
examples: CategoryFieldExample[];
|
||||||
sampleSize: number;
|
sampleSize: number;
|
||||||
overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS;
|
overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS;
|
||||||
|
@ -263,7 +266,10 @@ export const jobs = {
|
||||||
|
|
||||||
topCategories(jobId: string, count: number) {
|
topCategories(jobId: string, count: number) {
|
||||||
const body = JSON.stringify({ jobId, count });
|
const body = JSON.stringify({ jobId, count });
|
||||||
return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({
|
return httpService.http<{
|
||||||
|
total: number;
|
||||||
|
categories: Array<{ count?: number; category: Category }>;
|
||||||
|
}>({
|
||||||
path: `${basePath()}/jobs/top_categories`,
|
path: `${basePath()}/jobs/top_categories`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
|
@ -278,10 +284,13 @@ export const jobs = {
|
||||||
calendarEvents?: Array<{ start: number; end: number; description: string }>
|
calendarEvents?: Array<{ start: number; end: number; description: string }>
|
||||||
) {
|
) {
|
||||||
const body = JSON.stringify({ jobId, snapshotId, replay, end, calendarEvents });
|
const body = JSON.stringify({ jobId, snapshotId, replay, end, calendarEvents });
|
||||||
return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({
|
return httpService.http<{
|
||||||
|
total: number;
|
||||||
|
categories: Array<{ count?: number; category: Category }>;
|
||||||
|
}>({
|
||||||
path: `${basePath()}/jobs/revert_model_snapshot`,
|
path: `${basePath()}/jobs/revert_model_snapshot`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
|
@ -14,9 +14,19 @@ export function resultsServiceProvider(
|
||||||
earliestMs: number,
|
earliestMs: number,
|
||||||
latestMs: number,
|
latestMs: number,
|
||||||
interval: string | number,
|
interval: string | number,
|
||||||
maxResults: number
|
perPage?: number,
|
||||||
|
fromPage?: number
|
||||||
|
): Promise<any>;
|
||||||
|
getTopInfluencers(
|
||||||
|
selectedJobIds: string[],
|
||||||
|
earliestMs: number,
|
||||||
|
latestMs: number,
|
||||||
|
maxFieldValues: number,
|
||||||
|
perPage?: number,
|
||||||
|
fromPage?: number,
|
||||||
|
influencers?: any[],
|
||||||
|
influencersFilterQuery?: any
|
||||||
): Promise<any>;
|
): Promise<any>;
|
||||||
getTopInfluencers(): Promise<any>;
|
|
||||||
getTopInfluencerValues(): Promise<any>;
|
getTopInfluencerValues(): Promise<any>;
|
||||||
getOverallBucketScores(
|
getOverallBucketScores(
|
||||||
jobIds: any,
|
jobIds: any,
|
||||||
|
@ -33,6 +43,8 @@ export function resultsServiceProvider(
|
||||||
latestMs: number,
|
latestMs: number,
|
||||||
interval: string,
|
interval: string,
|
||||||
maxResults: number,
|
maxResults: number,
|
||||||
|
perPage: number,
|
||||||
|
fromPage: number,
|
||||||
influencersFilterQuery: any
|
influencersFilterQuery: any
|
||||||
): Promise<any>;
|
): Promise<any>;
|
||||||
getRecordInfluencers(): Promise<any>;
|
getRecordInfluencers(): Promise<any>;
|
||||||
|
|
|
@ -9,6 +9,10 @@ import _ from 'lodash';
|
||||||
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
|
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
|
||||||
import { escapeForElasticsearchQuery } from '../../util/string_utils';
|
import { escapeForElasticsearchQuery } from '../../util/string_utils';
|
||||||
import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns';
|
import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns';
|
||||||
|
import {
|
||||||
|
ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||||
|
SWIM_LANE_DEFAULT_PAGE_SIZE,
|
||||||
|
} from '../../explorer/explorer_constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards.
|
* Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards.
|
||||||
|
@ -24,7 +28,7 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
// Pass an empty array or ['*'] to search over all job IDs.
|
// Pass an empty array or ['*'] to search over all job IDs.
|
||||||
// Returned response contains a results property, with a key for job
|
// Returned response contains a results property, with a key for job
|
||||||
// which has results for the specified time range.
|
// which has results for the specified time range.
|
||||||
getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) {
|
getScoresByBucket(jobIds, earliestMs, latestMs, interval, perPage = 10, fromPage = 1) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const obj = {
|
const obj = {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -88,7 +92,7 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
jobId: {
|
jobId: {
|
||||||
terms: {
|
terms: {
|
||||||
field: 'job_id',
|
field: 'job_id',
|
||||||
size: maxResults !== undefined ? maxResults : 5,
|
size: jobIds?.length ?? 1,
|
||||||
order: {
|
order: {
|
||||||
anomalyScore: 'desc',
|
anomalyScore: 'desc',
|
||||||
},
|
},
|
||||||
|
@ -99,6 +103,12 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
field: 'anomaly_score',
|
field: 'anomaly_score',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bucketTruncate: {
|
||||||
|
bucket_sort: {
|
||||||
|
from: (fromPage - 1) * perPage,
|
||||||
|
size: perPage === 0 ? 1 : perPage,
|
||||||
|
},
|
||||||
|
},
|
||||||
byTime: {
|
byTime: {
|
||||||
date_histogram: {
|
date_histogram: {
|
||||||
field: 'timestamp',
|
field: 'timestamp',
|
||||||
|
@ -158,7 +168,9 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
jobIds,
|
jobIds,
|
||||||
earliestMs,
|
earliestMs,
|
||||||
latestMs,
|
latestMs,
|
||||||
maxFieldValues = 10,
|
maxFieldValues = ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||||
|
perPage = 10,
|
||||||
|
fromPage = 1,
|
||||||
influencers = [],
|
influencers = [],
|
||||||
influencersFilterQuery
|
influencersFilterQuery
|
||||||
) {
|
) {
|
||||||
|
@ -272,6 +284,12 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
aggs: {
|
aggs: {
|
||||||
|
bucketTruncate: {
|
||||||
|
bucket_sort: {
|
||||||
|
from: (fromPage - 1) * perPage,
|
||||||
|
size: perPage,
|
||||||
|
},
|
||||||
|
},
|
||||||
maxAnomalyScore: {
|
maxAnomalyScore: {
|
||||||
max: {
|
max: {
|
||||||
field: 'influencer_score',
|
field: 'influencer_score',
|
||||||
|
@ -472,7 +490,9 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
earliestMs,
|
earliestMs,
|
||||||
latestMs,
|
latestMs,
|
||||||
interval,
|
interval,
|
||||||
maxResults,
|
maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||||
|
perPage = SWIM_LANE_DEFAULT_PAGE_SIZE,
|
||||||
|
fromPage = 1,
|
||||||
influencersFilterQuery
|
influencersFilterQuery
|
||||||
) {
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -565,10 +585,15 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
aggs: {
|
aggs: {
|
||||||
|
influencerValuesCardinality: {
|
||||||
|
cardinality: {
|
||||||
|
field: 'influencer_field_value',
|
||||||
|
},
|
||||||
|
},
|
||||||
influencerFieldValues: {
|
influencerFieldValues: {
|
||||||
terms: {
|
terms: {
|
||||||
field: 'influencer_field_value',
|
field: 'influencer_field_value',
|
||||||
size: maxResults !== undefined ? maxResults : 10,
|
size: !!maxResults ? maxResults : ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||||
order: {
|
order: {
|
||||||
maxAnomalyScore: 'desc',
|
maxAnomalyScore: 'desc',
|
||||||
},
|
},
|
||||||
|
@ -579,6 +604,12 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
field: 'influencer_score',
|
field: 'influencer_score',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bucketTruncate: {
|
||||||
|
bucket_sort: {
|
||||||
|
from: (fromPage - 1) * perPage,
|
||||||
|
size: perPage,
|
||||||
|
},
|
||||||
|
},
|
||||||
byTime: {
|
byTime: {
|
||||||
date_histogram: {
|
date_histogram: {
|
||||||
field: 'timestamp',
|
field: 'timestamp',
|
||||||
|
@ -618,6 +649,8 @@ export function resultsServiceProvider(mlApiServices) {
|
||||||
obj.results[fieldValue] = fieldValues;
|
obj.results[fieldValue] = fieldValues;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
obj.cardinality = resp.aggregations?.influencerValuesCardinality?.value ?? 0;
|
||||||
|
|
||||||
resolve(obj);
|
resolve(obj);
|
||||||
})
|
})
|
||||||
.catch((resp) => {
|
.catch((resp) => {
|
||||||
|
|
|
@ -9,4 +9,3 @@ import { Subject } from 'rxjs';
|
||||||
import { Refresh } from '../routing/use_refresh';
|
import { Refresh } from '../routing/use_refresh';
|
||||||
|
|
||||||
export const mlTimefilterRefresh$ = new Subject<Required<Refresh>>();
|
export const mlTimefilterRefresh$ = new Subject<Required<Refresh>>();
|
||||||
export const mlTimefilterTimeChange$ = new Subject<Required<Refresh>>();
|
|
||||||
|
|
|
@ -16,10 +16,10 @@ import {
|
||||||
IContainer,
|
IContainer,
|
||||||
} from '../../../../../../src/plugins/embeddable/public';
|
} from '../../../../../../src/plugins/embeddable/public';
|
||||||
import { MlStartDependencies } from '../../plugin';
|
import { MlStartDependencies } from '../../plugin';
|
||||||
import { ExplorerSwimlaneContainer } from './explorer_swimlane_container';
|
import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container';
|
||||||
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
|
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
|
||||||
import { JobId } from '../../../common/types/anomaly_detection_jobs';
|
import { JobId } from '../../../common/types/anomaly_detection_jobs';
|
||||||
import { ExplorerService } from '../../application/services/explorer_service';
|
import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service';
|
||||||
import {
|
import {
|
||||||
Filter,
|
Filter,
|
||||||
Query,
|
Query,
|
||||||
|
@ -40,7 +40,7 @@ export interface AnomalySwimlaneEmbeddableCustomInput {
|
||||||
jobIds: JobId[];
|
jobIds: JobId[];
|
||||||
swimlaneType: SwimlaneType;
|
swimlaneType: SwimlaneType;
|
||||||
viewBy?: string;
|
viewBy?: string;
|
||||||
limit?: number;
|
perPage?: number;
|
||||||
|
|
||||||
// Embeddable inputs which are not included in the default interface
|
// Embeddable inputs which are not included in the default interface
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
|
@ -58,12 +58,12 @@ export interface AnomalySwimlaneEmbeddableCustomOutput {
|
||||||
jobIds: JobId[];
|
jobIds: JobId[];
|
||||||
swimlaneType: SwimlaneType;
|
swimlaneType: SwimlaneType;
|
||||||
viewBy?: string;
|
viewBy?: string;
|
||||||
limit?: number;
|
perPage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnomalySwimlaneServices {
|
export interface AnomalySwimlaneServices {
|
||||||
anomalyDetectorService: AnomalyDetectorService;
|
anomalyDetectorService: AnomalyDetectorService;
|
||||||
explorerService: ExplorerService;
|
anomalyTimelineService: AnomalyTimelineService;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnomalySwimlaneEmbeddableServices = [
|
export type AnomalySwimlaneEmbeddableServices = [
|
||||||
|
@ -101,14 +101,20 @@ export class AnomalySwimlaneEmbeddable extends Embeddable<
|
||||||
super.render(node);
|
super.render(node);
|
||||||
this.node = node;
|
this.node = node;
|
||||||
|
|
||||||
|
const I18nContext = this.services[0].i18n.Context;
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<ExplorerSwimlaneContainer
|
<I18nContext>
|
||||||
id={this.input.id}
|
<EmbeddableSwimLaneContainer
|
||||||
embeddableInput={this.getInput$()}
|
id={this.input.id}
|
||||||
services={this.services}
|
embeddableInput={this.getInput$()}
|
||||||
refresh={this.reload$.asObservable()}
|
services={this.services}
|
||||||
onOutputChange={(output) => this.updateOutput(output)}
|
refresh={this.reload$.asObservable()}
|
||||||
/>,
|
onInputChange={(input) => {
|
||||||
|
this.updateInput(input);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</I18nContext>,
|
||||||
node
|
node
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,9 @@ describe('AnomalySwimlaneEmbeddableFactory', () => {
|
||||||
});
|
});
|
||||||
expect(Object.keys(createServices[0])).toEqual(Object.keys(coreStart));
|
expect(Object.keys(createServices[0])).toEqual(Object.keys(coreStart));
|
||||||
expect(createServices[1]).toMatchObject(pluginsStart);
|
expect(createServices[1]).toMatchObject(pluginsStart);
|
||||||
expect(Object.keys(createServices[2])).toEqual(['anomalyDetectorService', 'explorerService']);
|
expect(Object.keys(createServices[2])).toEqual([
|
||||||
|
'anomalyDetectorService',
|
||||||
|
'anomalyTimelineService',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,7 +22,7 @@ import {
|
||||||
import { MlStartDependencies } from '../../plugin';
|
import { MlStartDependencies } from '../../plugin';
|
||||||
import { HttpService } from '../../application/services/http_service';
|
import { HttpService } from '../../application/services/http_service';
|
||||||
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
|
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
|
||||||
import { ExplorerService } from '../../application/services/explorer_service';
|
import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service';
|
||||||
import { mlResultsServiceProvider } from '../../application/services/results_service';
|
import { mlResultsServiceProvider } from '../../application/services/results_service';
|
||||||
import { resolveAnomalySwimlaneUserInput } from './anomaly_swimlane_setup_flyout';
|
import { resolveAnomalySwimlaneUserInput } from './anomaly_swimlane_setup_flyout';
|
||||||
import { mlApiServicesProvider } from '../../application/services/ml_api_service';
|
import { mlApiServicesProvider } from '../../application/services/ml_api_service';
|
||||||
|
@ -44,14 +44,10 @@ export class AnomalySwimlaneEmbeddableFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getExplicitInput(): Promise<Partial<AnomalySwimlaneEmbeddableInput>> {
|
public async getExplicitInput(): Promise<Partial<AnomalySwimlaneEmbeddableInput>> {
|
||||||
const [{ overlays, uiSettings }, , { anomalyDetectorService }] = await this.getServices();
|
const [coreStart] = await this.getServices();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await resolveAnomalySwimlaneUserInput({
|
return await resolveAnomalySwimlaneUserInput(coreStart);
|
||||||
anomalyDetectorService,
|
|
||||||
overlays,
|
|
||||||
uiSettings,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
@ -62,13 +58,13 @@ export class AnomalySwimlaneEmbeddableFactory
|
||||||
|
|
||||||
const httpService = new HttpService(coreStart.http);
|
const httpService = new HttpService(coreStart.http);
|
||||||
const anomalyDetectorService = new AnomalyDetectorService(httpService);
|
const anomalyDetectorService = new AnomalyDetectorService(httpService);
|
||||||
const explorerService = new ExplorerService(
|
const anomalyTimelineService = new AnomalyTimelineService(
|
||||||
pluginsStart.data.query.timefilter.timefilter,
|
pluginsStart.data.query.timefilter.timefilter,
|
||||||
coreStart.uiSettings,
|
coreStart.uiSettings,
|
||||||
mlResultsServiceProvider(mlApiServicesProvider(httpService))
|
mlResultsServiceProvider(mlApiServicesProvider(httpService))
|
||||||
);
|
);
|
||||||
|
|
||||||
return [coreStart, pluginsStart, { anomalyDetectorService, explorerService }];
|
return [coreStart, pluginsStart, { anomalyDetectorService, anomalyTimelineService }];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(
|
public async create(
|
||||||
|
|
|
@ -27,7 +27,7 @@ export interface AnomalySwimlaneInitializerProps {
|
||||||
defaultTitle: string;
|
defaultTitle: string;
|
||||||
influencers: string[];
|
influencers: string[];
|
||||||
initialInput?: Partial<
|
initialInput?: Partial<
|
||||||
Pick<AnomalySwimlaneEmbeddableInput, 'jobIds' | 'swimlaneType' | 'viewBy' | 'limit'>
|
Pick<AnomalySwimlaneEmbeddableInput, 'jobIds' | 'swimlaneType' | 'viewBy' | 'perPage'>
|
||||||
>;
|
>;
|
||||||
onCreate: (swimlaneProps: {
|
onCreate: (swimlaneProps: {
|
||||||
panelTitle: string;
|
panelTitle: string;
|
||||||
|
@ -38,11 +38,6 @@ export interface AnomalySwimlaneInitializerProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const limitOptions = [5, 10, 25, 50].map((limit) => ({
|
|
||||||
value: limit,
|
|
||||||
text: `${limit}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const AnomalySwimlaneInitializer: FC<AnomalySwimlaneInitializerProps> = ({
|
export const AnomalySwimlaneInitializer: FC<AnomalySwimlaneInitializerProps> = ({
|
||||||
defaultTitle,
|
defaultTitle,
|
||||||
influencers,
|
influencers,
|
||||||
|
@ -55,7 +50,6 @@ export const AnomalySwimlaneInitializer: FC<AnomalySwimlaneInitializerProps> = (
|
||||||
initialInput?.swimlaneType ?? SWIMLANE_TYPE.OVERALL
|
initialInput?.swimlaneType ?? SWIMLANE_TYPE.OVERALL
|
||||||
);
|
);
|
||||||
const [viewBySwimlaneFieldName, setViewBySwimlaneFieldName] = useState(initialInput?.viewBy);
|
const [viewBySwimlaneFieldName, setViewBySwimlaneFieldName] = useState(initialInput?.viewBy);
|
||||||
const [limit, setLimit] = useState(initialInput?.limit ?? 5);
|
|
||||||
|
|
||||||
const swimlaneTypeOptions = [
|
const swimlaneTypeOptions = [
|
||||||
{
|
{
|
||||||
|
@ -154,19 +148,6 @@ export const AnomalySwimlaneInitializer: FC<AnomalySwimlaneInitializerProps> = (
|
||||||
onChange={(e) => setViewBySwimlaneFieldName(e.target.value)}
|
onChange={(e) => setViewBySwimlaneFieldName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
<EuiFormRow
|
|
||||||
label={
|
|
||||||
<FormattedMessage id="xpack.ml.explorer.limitLabel" defaultMessage="Limit" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<EuiSelect
|
|
||||||
id="limit"
|
|
||||||
name="limit"
|
|
||||||
options={limitOptions}
|
|
||||||
value={limit}
|
|
||||||
onChange={(e) => setLimit(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</EuiForm>
|
</EuiForm>
|
||||||
|
@ -186,7 +167,6 @@ export const AnomalySwimlaneInitializer: FC<AnomalySwimlaneInitializerProps> = (
|
||||||
panelTitle,
|
panelTitle,
|
||||||
swimlaneType,
|
swimlaneType,
|
||||||
viewBy: viewBySwimlaneFieldName,
|
viewBy: viewBySwimlaneFieldName,
|
||||||
limit,
|
|
||||||
})}
|
})}
|
||||||
fill
|
fill
|
||||||
>
|
>
|
||||||
|
|
|
@ -5,10 +5,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IUiSettingsClient, OverlayStart } from 'kibana/public';
|
import { CoreStart } from 'kibana/public';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants';
|
import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants';
|
||||||
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
|
import {
|
||||||
|
KibanaContextProvider,
|
||||||
|
toMountPoint,
|
||||||
|
} from '../../../../../../src/plugins/kibana_react/public';
|
||||||
import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer';
|
import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer';
|
||||||
import { JobSelectorFlyout } from '../../application/components/job_selector/job_selector_flyout';
|
import { JobSelectorFlyout } from '../../application/components/job_selector/job_selector_flyout';
|
||||||
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
|
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
|
||||||
|
@ -17,19 +20,17 @@ import {
|
||||||
AnomalySwimlaneEmbeddableInput,
|
AnomalySwimlaneEmbeddableInput,
|
||||||
getDefaultPanelTitle,
|
getDefaultPanelTitle,
|
||||||
} from './anomaly_swimlane_embeddable';
|
} from './anomaly_swimlane_embeddable';
|
||||||
|
import { getMlGlobalServices } from '../../application/app';
|
||||||
|
import { HttpService } from '../../application/services/http_service';
|
||||||
|
|
||||||
export async function resolveAnomalySwimlaneUserInput(
|
export async function resolveAnomalySwimlaneUserInput(
|
||||||
{
|
coreStart: CoreStart,
|
||||||
overlays,
|
|
||||||
anomalyDetectorService,
|
|
||||||
uiSettings,
|
|
||||||
}: {
|
|
||||||
anomalyDetectorService: AnomalyDetectorService;
|
|
||||||
overlays: OverlayStart;
|
|
||||||
uiSettings: IUiSettingsClient;
|
|
||||||
},
|
|
||||||
input?: AnomalySwimlaneEmbeddableInput
|
input?: AnomalySwimlaneEmbeddableInput
|
||||||
): Promise<Partial<AnomalySwimlaneEmbeddableInput>> {
|
): Promise<Partial<AnomalySwimlaneEmbeddableInput>> {
|
||||||
|
const { http, uiSettings, overlays } = coreStart;
|
||||||
|
|
||||||
|
const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const maps = {
|
const maps = {
|
||||||
groupsMap: getInitialGroupsMap([]),
|
groupsMap: getInitialGroupsMap([]),
|
||||||
|
@ -41,48 +42,50 @@ export async function resolveAnomalySwimlaneUserInput(
|
||||||
|
|
||||||
const selectedIds = input?.jobIds;
|
const selectedIds = input?.jobIds;
|
||||||
|
|
||||||
const flyoutSession = overlays.openFlyout(
|
const flyoutSession = coreStart.overlays.openFlyout(
|
||||||
toMountPoint(
|
toMountPoint(
|
||||||
<JobSelectorFlyout
|
<KibanaContextProvider services={{ ...coreStart, mlServices: getMlGlobalServices(http) }}>
|
||||||
selectedIds={selectedIds}
|
<JobSelectorFlyout
|
||||||
withTimeRangeSelector={false}
|
selectedIds={selectedIds}
|
||||||
dateFormatTz={dateFormatTz}
|
withTimeRangeSelector={false}
|
||||||
singleSelection={false}
|
dateFormatTz={dateFormatTz}
|
||||||
timeseriesOnly={true}
|
singleSelection={false}
|
||||||
onFlyoutClose={() => {
|
timeseriesOnly={true}
|
||||||
flyoutSession.close();
|
onFlyoutClose={() => {
|
||||||
reject();
|
flyoutSession.close();
|
||||||
}}
|
reject();
|
||||||
onSelectionConfirmed={async ({ jobIds, groups }) => {
|
}}
|
||||||
const title = input?.title ?? getDefaultPanelTitle(jobIds);
|
onSelectionConfirmed={async ({ jobIds, groups }) => {
|
||||||
|
const title = input?.title ?? getDefaultPanelTitle(jobIds);
|
||||||
|
|
||||||
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
|
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
|
||||||
|
|
||||||
const influencers = anomalyDetectorService.extractInfluencers(jobs);
|
const influencers = anomalyDetectorService.extractInfluencers(jobs);
|
||||||
influencers.push(VIEW_BY_JOB_LABEL);
|
influencers.push(VIEW_BY_JOB_LABEL);
|
||||||
|
|
||||||
await flyoutSession.close();
|
await flyoutSession.close();
|
||||||
|
|
||||||
const modalSession = overlays.openModal(
|
const modalSession = overlays.openModal(
|
||||||
toMountPoint(
|
toMountPoint(
|
||||||
<AnomalySwimlaneInitializer
|
<AnomalySwimlaneInitializer
|
||||||
defaultTitle={title}
|
defaultTitle={title}
|
||||||
influencers={influencers}
|
influencers={influencers}
|
||||||
initialInput={input}
|
initialInput={input}
|
||||||
onCreate={({ panelTitle, viewBy, swimlaneType, limit }) => {
|
onCreate={({ panelTitle, viewBy, swimlaneType }) => {
|
||||||
modalSession.close();
|
modalSession.close();
|
||||||
resolve({ jobIds, title: panelTitle, swimlaneType, viewBy, limit });
|
resolve({ jobIds, title: panelTitle, swimlaneType, viewBy });
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
modalSession.close();
|
modalSession.close();
|
||||||
reject();
|
reject();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
maps={maps}
|
maps={maps}
|
||||||
/>
|
/>
|
||||||
|
</KibanaContextProvider>
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
'data-test-subj': 'mlAnomalySwimlaneEmbeddable',
|
'data-test-subj': 'mlAnomalySwimlaneEmbeddable',
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { ExplorerSwimlaneContainer } from './explorer_swimlane_container';
|
import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { I18nProvider } from '@kbn/i18n/react';
|
import { I18nProvider } from '@kbn/i18n/react';
|
||||||
import {
|
import {
|
||||||
|
@ -17,6 +17,7 @@ import { CoreStart } from 'kibana/public';
|
||||||
import { MlStartDependencies } from '../../plugin';
|
import { MlStartDependencies } from '../../plugin';
|
||||||
import { useSwimlaneInputResolver } from './swimlane_input_resolver';
|
import { useSwimlaneInputResolver } from './swimlane_input_resolver';
|
||||||
import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants';
|
import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants';
|
||||||
|
import { SwimlaneContainer } from '../../application/explorer/swimlane_container';
|
||||||
|
|
||||||
jest.mock('./swimlane_input_resolver', () => ({
|
jest.mock('./swimlane_input_resolver', () => ({
|
||||||
useSwimlaneInputResolver: jest.fn(() => {
|
useSwimlaneInputResolver: jest.fn(() => {
|
||||||
|
@ -24,12 +25,11 @@ jest.mock('./swimlane_input_resolver', () => ({
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../../application/explorer/explorer_swimlane', () => ({
|
jest.mock('../../application/explorer/swimlane_container', () => ({
|
||||||
ExplorerSwimlane: jest.fn(),
|
SwimlaneContainer: jest.fn(() => {
|
||||||
}));
|
return null;
|
||||||
|
}),
|
||||||
jest.mock('../../application/components/chart_tooltip', () => ({
|
isViewBySwimLaneData: jest.fn(),
|
||||||
MlTooltipComponent: jest.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const defaultOptions = { wrapper: I18nProvider };
|
const defaultOptions = { wrapper: I18nProvider };
|
||||||
|
@ -38,6 +38,7 @@ describe('ExplorerSwimlaneContainer', () => {
|
||||||
let embeddableInput: BehaviorSubject<Partial<AnomalySwimlaneEmbeddableInput>>;
|
let embeddableInput: BehaviorSubject<Partial<AnomalySwimlaneEmbeddableInput>>;
|
||||||
let refresh: BehaviorSubject<any>;
|
let refresh: BehaviorSubject<any>;
|
||||||
let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
|
let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
|
||||||
|
const onInputChange = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
embeddableInput = new BehaviorSubject({
|
embeddableInput = new BehaviorSubject({
|
||||||
|
@ -61,25 +62,39 @@ describe('ExplorerSwimlaneContainer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
(useSwimlaneInputResolver as jest.Mock).mockReturnValueOnce([
|
(useSwimlaneInputResolver as jest.Mock).mockReturnValueOnce([
|
||||||
mockOverallData,
|
|
||||||
SWIMLANE_TYPE.OVERALL,
|
SWIMLANE_TYPE.OVERALL,
|
||||||
undefined,
|
mockOverallData,
|
||||||
|
10,
|
||||||
|
jest.fn(),
|
||||||
|
{},
|
||||||
|
false,
|
||||||
|
null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { findByTestId } = render(
|
render(
|
||||||
<ExplorerSwimlaneContainer
|
<EmbeddableSwimLaneContainer
|
||||||
id={'test-swimlane-embeddable'}
|
id={'test-swimlane-embeddable'}
|
||||||
embeddableInput={
|
embeddableInput={
|
||||||
embeddableInput.asObservable() as Observable<AnomalySwimlaneEmbeddableInput>
|
embeddableInput.asObservable() as Observable<AnomalySwimlaneEmbeddableInput>
|
||||||
}
|
}
|
||||||
services={services}
|
services={services}
|
||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
|
onInputChange={onInputChange}
|
||||||
/>,
|
/>,
|
||||||
defaultOptions
|
defaultOptions
|
||||||
);
|
);
|
||||||
expect(
|
|
||||||
await findByTestId('mlMaxAnomalyScoreEmbeddable_test-swimlane-embeddable')
|
const calledWith = ((SwimlaneContainer as unknown) as jest.Mock<typeof SwimlaneContainer>).mock
|
||||||
).toBeDefined();
|
.calls[0][0];
|
||||||
|
|
||||||
|
expect(calledWith).toMatchObject({
|
||||||
|
perPage: 10,
|
||||||
|
swimlaneType: SWIMLANE_TYPE.OVERALL,
|
||||||
|
swimlaneData: mockOverallData,
|
||||||
|
isLoading: false,
|
||||||
|
swimlaneLimit: undefined,
|
||||||
|
fromPage: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render an error in case it could not fetch the ML swimlane data', async () => {
|
test('should render an error in case it could not fetch the ML swimlane data', async () => {
|
||||||
|
@ -87,38 +102,25 @@ describe('ExplorerSwimlaneContainer', () => {
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
{ message: 'Something went wrong' },
|
{ message: 'Something went wrong' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { findByText } = render(
|
const { findByText } = render(
|
||||||
<ExplorerSwimlaneContainer
|
<EmbeddableSwimLaneContainer
|
||||||
id={'test-swimlane-embeddable'}
|
id={'test-swimlane-embeddable'}
|
||||||
embeddableInput={
|
embeddableInput={
|
||||||
embeddableInput.asObservable() as Observable<AnomalySwimlaneEmbeddableInput>
|
embeddableInput.asObservable() as Observable<AnomalySwimlaneEmbeddableInput>
|
||||||
}
|
}
|
||||||
services={services}
|
services={services}
|
||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
|
onInputChange={onInputChange}
|
||||||
/>,
|
/>,
|
||||||
defaultOptions
|
defaultOptions
|
||||||
);
|
);
|
||||||
const errorMessage = await findByText('Something went wrong');
|
const errorMessage = await findByText('Something went wrong');
|
||||||
expect(errorMessage).toBeDefined();
|
expect(errorMessage).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render a loading indicator during the data fetching', async () => {
|
|
||||||
const { findByTestId } = render(
|
|
||||||
<ExplorerSwimlaneContainer
|
|
||||||
id={'test-swimlane-embeddable'}
|
|
||||||
embeddableInput={
|
|
||||||
embeddableInput.asObservable() as Observable<AnomalySwimlaneEmbeddableInput>
|
|
||||||
}
|
|
||||||
services={services}
|
|
||||||
refresh={refresh}
|
|
||||||
/>,
|
|
||||||
defaultOptions
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await findByTestId('loading_mlMaxAnomalyScoreEmbeddable_test-swimlane-embeddable')
|
|
||||||
).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
|
@ -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, { FC, useState } from 'react';
|
||||||
|
import { EuiCallOut } from '@elastic/eui';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { CoreStart } from 'kibana/public';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { MlStartDependencies } from '../../plugin';
|
||||||
|
import {
|
||||||
|
AnomalySwimlaneEmbeddableInput,
|
||||||
|
AnomalySwimlaneEmbeddableOutput,
|
||||||
|
AnomalySwimlaneServices,
|
||||||
|
} from './anomaly_swimlane_embeddable';
|
||||||
|
import { useSwimlaneInputResolver } from './swimlane_input_resolver';
|
||||||
|
import { SwimlaneType } from '../../application/explorer/explorer_constants';
|
||||||
|
import {
|
||||||
|
isViewBySwimLaneData,
|
||||||
|
SwimlaneContainer,
|
||||||
|
} from '../../application/explorer/swimlane_container';
|
||||||
|
|
||||||
|
export interface ExplorerSwimlaneContainerProps {
|
||||||
|
id: string;
|
||||||
|
embeddableInput: Observable<AnomalySwimlaneEmbeddableInput>;
|
||||||
|
services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
|
||||||
|
refresh: Observable<any>;
|
||||||
|
onInputChange: (output: Partial<AnomalySwimlaneEmbeddableOutput>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmbeddableSwimLaneContainer: FC<ExplorerSwimlaneContainerProps> = ({
|
||||||
|
id,
|
||||||
|
embeddableInput,
|
||||||
|
services,
|
||||||
|
refresh,
|
||||||
|
onInputChange,
|
||||||
|
}) => {
|
||||||
|
const [chartWidth, setChartWidth] = useState<number>(0);
|
||||||
|
const [fromPage, setFromPage] = useState<number>(1);
|
||||||
|
|
||||||
|
const [
|
||||||
|
swimlaneType,
|
||||||
|
swimlaneData,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
timeBuckets,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
] = useSwimlaneInputResolver(
|
||||||
|
embeddableInput,
|
||||||
|
onInputChange,
|
||||||
|
refresh,
|
||||||
|
services,
|
||||||
|
chartWidth,
|
||||||
|
fromPage
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<EuiCallOut
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.ml.swimlaneEmbeddable.errorMessage"
|
||||||
|
defaultMessage="Unable to load the ML swim lane data"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
color="danger"
|
||||||
|
iconType="alert"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</EuiCallOut>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ width: '100%', padding: '8px' }}
|
||||||
|
data-test-subj="mlAnomalySwimlaneEmbeddableWrapper"
|
||||||
|
>
|
||||||
|
<SwimlaneContainer
|
||||||
|
timeBuckets={timeBuckets}
|
||||||
|
swimlaneData={swimlaneData!}
|
||||||
|
swimlaneType={swimlaneType as SwimlaneType}
|
||||||
|
fromPage={fromPage}
|
||||||
|
perPage={perPage}
|
||||||
|
swimlaneLimit={isViewBySwimLaneData(swimlaneData) ? swimlaneData.cardinality : undefined}
|
||||||
|
onResize={(width) => {
|
||||||
|
setChartWidth(width);
|
||||||
|
}}
|
||||||
|
onPaginationChange={(update) => {
|
||||||
|
if (update.fromPage) {
|
||||||
|
setFromPage(update.fromPage);
|
||||||
|
}
|
||||||
|
if (update.perPage) {
|
||||||
|
setFromPage(1);
|
||||||
|
setPerPage(update.perPage);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={isLoading}
|
||||||
|
noDataWarning={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.ml.swimlaneEmbeddable.noDataFound"
|
||||||
|
defaultMessage="No anomalies found"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,126 +0,0 @@
|
||||||
/*
|
|
||||||
* 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, { FC, useCallback, useState } from 'react';
|
|
||||||
import {
|
|
||||||
EuiCallOut,
|
|
||||||
EuiFlexGroup,
|
|
||||||
EuiFlexItem,
|
|
||||||
EuiLoadingChart,
|
|
||||||
EuiResizeObserver,
|
|
||||||
EuiSpacer,
|
|
||||||
EuiText,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { throttle } from 'lodash';
|
|
||||||
import { CoreStart } from 'kibana/public';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
import { ExplorerSwimlane } from '../../application/explorer/explorer_swimlane';
|
|
||||||
import { MlStartDependencies } from '../../plugin';
|
|
||||||
import {
|
|
||||||
AnomalySwimlaneEmbeddableInput,
|
|
||||||
AnomalySwimlaneEmbeddableOutput,
|
|
||||||
AnomalySwimlaneServices,
|
|
||||||
} from './anomaly_swimlane_embeddable';
|
|
||||||
import { MlTooltipComponent } from '../../application/components/chart_tooltip';
|
|
||||||
import { useSwimlaneInputResolver } from './swimlane_input_resolver';
|
|
||||||
import { SwimlaneType } from '../../application/explorer/explorer_constants';
|
|
||||||
|
|
||||||
const RESIZE_THROTTLE_TIME_MS = 500;
|
|
||||||
|
|
||||||
export interface ExplorerSwimlaneContainerProps {
|
|
||||||
id: string;
|
|
||||||
embeddableInput: Observable<AnomalySwimlaneEmbeddableInput>;
|
|
||||||
services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
|
|
||||||
refresh: Observable<any>;
|
|
||||||
onOutputChange?: (output: Partial<AnomalySwimlaneEmbeddableOutput>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExplorerSwimlaneContainer: FC<ExplorerSwimlaneContainerProps> = ({
|
|
||||||
id,
|
|
||||||
embeddableInput,
|
|
||||||
services,
|
|
||||||
refresh,
|
|
||||||
}) => {
|
|
||||||
const [chartWidth, setChartWidth] = useState<number>(0);
|
|
||||||
|
|
||||||
const [swimlaneType, swimlaneData, timeBuckets, error] = useSwimlaneInputResolver(
|
|
||||||
embeddableInput,
|
|
||||||
refresh,
|
|
||||||
services,
|
|
||||||
chartWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
const onResize = useCallback(
|
|
||||||
throttle((e: { width: number; height: number }) => {
|
|
||||||
const labelWidth = 200;
|
|
||||||
setChartWidth(e.width - labelWidth);
|
|
||||||
}, RESIZE_THROTTLE_TIME_MS),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<EuiCallOut
|
|
||||||
title={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.ml.swimlaneEmbeddable.errorMessage"
|
|
||||||
defaultMessage="Unable to load the ML swim lane data"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
color="danger"
|
|
||||||
iconType="alert"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<p>{error.message}</p>
|
|
||||||
</EuiCallOut>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiResizeObserver onResize={onResize}>
|
|
||||||
{(resizeRef) => (
|
|
||||||
<div
|
|
||||||
style={{ width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden' }}
|
|
||||||
data-test-subj={`mlMaxAnomalyScoreEmbeddable_${id}`}
|
|
||||||
ref={(el) => {
|
|
||||||
resizeRef(el);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ width: '100%' }}>
|
|
||||||
<EuiSpacer size="m" />
|
|
||||||
|
|
||||||
{chartWidth > 0 && swimlaneData && swimlaneType ? (
|
|
||||||
<EuiText color="subdued" size="s" data-test-subj="mlAnomalySwimlaneEmbeddableWrapper">
|
|
||||||
<MlTooltipComponent>
|
|
||||||
{(tooltipService) => (
|
|
||||||
<ExplorerSwimlane
|
|
||||||
chartWidth={chartWidth}
|
|
||||||
timeBuckets={timeBuckets}
|
|
||||||
swimlaneData={swimlaneData}
|
|
||||||
swimlaneType={swimlaneType as SwimlaneType}
|
|
||||||
tooltipService={tooltipService}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</MlTooltipComponent>
|
|
||||||
</EuiText>
|
|
||||||
) : (
|
|
||||||
<EuiFlexGroup justifyContent="spaceAround">
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiLoadingChart
|
|
||||||
size="xl"
|
|
||||||
data-test-subj={`loading_mlMaxAnomalyScoreEmbeddable_${id}`}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</EuiResizeObserver>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -19,6 +19,7 @@ describe('useSwimlaneInputResolver', () => {
|
||||||
let embeddableInput: BehaviorSubject<Partial<AnomalySwimlaneEmbeddableInput>>;
|
let embeddableInput: BehaviorSubject<Partial<AnomalySwimlaneEmbeddableInput>>;
|
||||||
let refresh: Subject<any>;
|
let refresh: Subject<any>;
|
||||||
let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
|
let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
|
||||||
|
let onInputChange: jest.Mock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
@ -41,7 +42,7 @@ describe('useSwimlaneInputResolver', () => {
|
||||||
} as CoreStart,
|
} as CoreStart,
|
||||||
(null as unknown) as MlStartDependencies,
|
(null as unknown) as MlStartDependencies,
|
||||||
({
|
({
|
||||||
explorerService: {
|
anomalyTimelineService: {
|
||||||
setTimeRange: jest.fn(),
|
setTimeRange: jest.fn(),
|
||||||
loadOverallData: jest.fn(() =>
|
loadOverallData: jest.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
|
@ -69,6 +70,7 @@ describe('useSwimlaneInputResolver', () => {
|
||||||
},
|
},
|
||||||
} as unknown) as AnomalySwimlaneServices,
|
} as unknown) as AnomalySwimlaneServices,
|
||||||
];
|
];
|
||||||
|
onInputChange = jest.fn();
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
|
@ -79,9 +81,11 @@ describe('useSwimlaneInputResolver', () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
useSwimlaneInputResolver(
|
useSwimlaneInputResolver(
|
||||||
embeddableInput as Observable<AnomalySwimlaneEmbeddableInput>,
|
embeddableInput as Observable<AnomalySwimlaneEmbeddableInput>,
|
||||||
|
onInputChange,
|
||||||
refresh,
|
refresh,
|
||||||
services,
|
services,
|
||||||
1000
|
1000,
|
||||||
|
1
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -94,7 +98,7 @@ describe('useSwimlaneInputResolver', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(1);
|
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(1);
|
||||||
expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(1);
|
expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
embeddableInput.next({
|
embeddableInput.next({
|
||||||
|
@ -109,7 +113,7 @@ describe('useSwimlaneInputResolver', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2);
|
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2);
|
||||||
expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(2);
|
expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
embeddableInput.next({
|
embeddableInput.next({
|
||||||
|
@ -124,7 +128,7 @@ describe('useSwimlaneInputResolver', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2);
|
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2);
|
||||||
expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(3);
|
expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,23 +16,31 @@ import {
|
||||||
skipWhile,
|
skipWhile,
|
||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
tap,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { CoreStart } from 'kibana/public';
|
import { CoreStart } from 'kibana/public';
|
||||||
import { TimeBuckets } from '../../application/util/time_buckets';
|
import { TimeBuckets } from '../../application/util/time_buckets';
|
||||||
import {
|
import {
|
||||||
AnomalySwimlaneEmbeddableInput,
|
AnomalySwimlaneEmbeddableInput,
|
||||||
|
AnomalySwimlaneEmbeddableOutput,
|
||||||
AnomalySwimlaneServices,
|
AnomalySwimlaneServices,
|
||||||
} from './anomaly_swimlane_embeddable';
|
} from './anomaly_swimlane_embeddable';
|
||||||
import { MlStartDependencies } from '../../plugin';
|
import { MlStartDependencies } from '../../plugin';
|
||||||
import { SWIMLANE_TYPE, SwimlaneType } from '../../application/explorer/explorer_constants';
|
import {
|
||||||
|
ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||||
|
SWIM_LANE_DEFAULT_PAGE_SIZE,
|
||||||
|
SWIMLANE_TYPE,
|
||||||
|
SwimlaneType,
|
||||||
|
} from '../../application/explorer/explorer_constants';
|
||||||
import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters';
|
import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters';
|
||||||
import { Query } from '../../../../../../src/plugins/data/common/query';
|
import { Query } from '../../../../../../src/plugins/data/common/query';
|
||||||
import { esKuery, UI_SETTINGS } from '../../../../../../src/plugins/data/public';
|
import { esKuery, UI_SETTINGS } from '../../../../../../src/plugins/data/public';
|
||||||
import { ExplorerJob, OverallSwimlaneData } from '../../application/explorer/explorer_utils';
|
import { ExplorerJob, OverallSwimlaneData } from '../../application/explorer/explorer_utils';
|
||||||
import { parseInterval } from '../../../common/util/parse_interval';
|
import { parseInterval } from '../../../common/util/parse_interval';
|
||||||
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
|
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
|
||||||
|
import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container';
|
||||||
|
import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
|
||||||
|
|
||||||
const RESIZE_IGNORED_DIFF_PX = 20;
|
|
||||||
const FETCH_RESULTS_DEBOUNCE_MS = 500;
|
const FETCH_RESULTS_DEBOUNCE_MS = 500;
|
||||||
|
|
||||||
function getJobsObservable(
|
function getJobsObservable(
|
||||||
|
@ -48,17 +56,31 @@ function getJobsObservable(
|
||||||
|
|
||||||
export function useSwimlaneInputResolver(
|
export function useSwimlaneInputResolver(
|
||||||
embeddableInput: Observable<AnomalySwimlaneEmbeddableInput>,
|
embeddableInput: Observable<AnomalySwimlaneEmbeddableInput>,
|
||||||
|
onInputChange: (output: Partial<AnomalySwimlaneEmbeddableOutput>) => void,
|
||||||
refresh: Observable<any>,
|
refresh: Observable<any>,
|
||||||
services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices],
|
services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices],
|
||||||
chartWidth: number
|
chartWidth: number,
|
||||||
): [string | undefined, OverallSwimlaneData | undefined, TimeBuckets, Error | null | undefined] {
|
fromPage: number
|
||||||
const [{ uiSettings }, , { explorerService, anomalyDetectorService }] = services;
|
): [
|
||||||
|
string | undefined,
|
||||||
|
OverallSwimlaneData | undefined,
|
||||||
|
number,
|
||||||
|
(perPage: number) => void,
|
||||||
|
TimeBuckets,
|
||||||
|
boolean,
|
||||||
|
Error | null | undefined
|
||||||
|
] {
|
||||||
|
const [{ uiSettings }, , { anomalyTimelineService, anomalyDetectorService }] = services;
|
||||||
|
|
||||||
const [swimlaneData, setSwimlaneData] = useState<OverallSwimlaneData>();
|
const [swimlaneData, setSwimlaneData] = useState<OverallSwimlaneData>();
|
||||||
const [swimlaneType, setSwimlaneType] = useState<SwimlaneType>();
|
const [swimlaneType, setSwimlaneType] = useState<SwimlaneType>();
|
||||||
const [error, setError] = useState<Error | null>();
|
const [error, setError] = useState<Error | null>();
|
||||||
|
const [perPage, setPerPage] = useState<number | undefined>();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const chartWidth$ = useMemo(() => new Subject<number>(), []);
|
const chartWidth$ = useMemo(() => new Subject<number>(), []);
|
||||||
|
const fromPage$ = useMemo(() => new Subject<number>(), []);
|
||||||
|
const perPage$ = useMemo(() => new Subject<number>(), []);
|
||||||
|
|
||||||
const timeBuckets = useMemo(() => {
|
const timeBuckets = useMemo(() => {
|
||||||
return new TimeBuckets({
|
return new TimeBuckets({
|
||||||
|
@ -73,28 +95,32 @@ export function useSwimlaneInputResolver(
|
||||||
const subscription = combineLatest([
|
const subscription = combineLatest([
|
||||||
getJobsObservable(embeddableInput, anomalyDetectorService),
|
getJobsObservable(embeddableInput, anomalyDetectorService),
|
||||||
embeddableInput,
|
embeddableInput,
|
||||||
chartWidth$.pipe(
|
chartWidth$.pipe(skipWhile((v) => !v)),
|
||||||
skipWhile((v) => !v),
|
fromPage$,
|
||||||
distinctUntilChanged((prev, curr) => {
|
perPage$.pipe(
|
||||||
// emit only if the width has been changed significantly
|
startWith(undefined),
|
||||||
return Math.abs(curr - prev) < RESIZE_IGNORED_DIFF_PX;
|
// no need to emit when the initial value has been set
|
||||||
})
|
distinctUntilChanged(
|
||||||
|
(prev, curr) => prev === undefined && curr === SWIM_LANE_DEFAULT_PAGE_SIZE
|
||||||
|
)
|
||||||
),
|
),
|
||||||
refresh.pipe(startWith(null)),
|
refresh.pipe(startWith(null)),
|
||||||
])
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
|
tap(setIsLoading.bind(null, true)),
|
||||||
debounceTime(FETCH_RESULTS_DEBOUNCE_MS),
|
debounceTime(FETCH_RESULTS_DEBOUNCE_MS),
|
||||||
switchMap(([jobs, input, swimlaneContainerWidth]) => {
|
switchMap(([jobs, input, swimlaneContainerWidth, fromPageInput, perPageFromState]) => {
|
||||||
const {
|
const {
|
||||||
viewBy,
|
viewBy,
|
||||||
swimlaneType: swimlaneTypeInput,
|
swimlaneType: swimlaneTypeInput,
|
||||||
limit,
|
perPage: perPageInput,
|
||||||
timeRange,
|
timeRange,
|
||||||
filters,
|
filters,
|
||||||
query,
|
query,
|
||||||
|
viewMode,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
explorerService.setTimeRange(timeRange);
|
anomalyTimelineService.setTimeRange(timeRange);
|
||||||
|
|
||||||
if (!swimlaneType) {
|
if (!swimlaneType) {
|
||||||
setSwimlaneType(swimlaneTypeInput);
|
setSwimlaneType(swimlaneTypeInput);
|
||||||
|
@ -118,18 +144,34 @@ export function useSwimlaneInputResolver(
|
||||||
return of(undefined);
|
return of(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return from(explorerService.loadOverallData(explorerJobs, swimlaneContainerWidth)).pipe(
|
return from(
|
||||||
|
anomalyTimelineService.loadOverallData(explorerJobs, swimlaneContainerWidth)
|
||||||
|
).pipe(
|
||||||
switchMap((overallSwimlaneData) => {
|
switchMap((overallSwimlaneData) => {
|
||||||
const { earliest, latest } = overallSwimlaneData;
|
const { earliest, latest } = overallSwimlaneData;
|
||||||
|
|
||||||
if (overallSwimlaneData && swimlaneTypeInput === SWIMLANE_TYPE.VIEW_BY) {
|
if (overallSwimlaneData && swimlaneTypeInput === SWIMLANE_TYPE.VIEW_BY) {
|
||||||
|
if (perPageFromState === undefined) {
|
||||||
|
// set initial pagination from the input or default one
|
||||||
|
setPerPage(perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewMode === ViewMode.EDIT && perPageFromState !== perPageInput) {
|
||||||
|
// store per page value when the dashboard is in the edit mode
|
||||||
|
onInputChange({ perPage: perPageFromState });
|
||||||
|
}
|
||||||
|
|
||||||
return from(
|
return from(
|
||||||
explorerService.loadViewBySwimlane(
|
anomalyTimelineService.loadViewBySwimlane(
|
||||||
[],
|
[],
|
||||||
{ earliest, latest },
|
{ earliest, latest },
|
||||||
explorerJobs,
|
explorerJobs,
|
||||||
viewBy!,
|
viewBy!,
|
||||||
limit!,
|
isViewBySwimLaneData(swimlaneData)
|
||||||
|
? swimlaneData.cardinality
|
||||||
|
: ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||||
|
perPageFromState ?? perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE,
|
||||||
|
fromPageInput,
|
||||||
swimlaneContainerWidth,
|
swimlaneContainerWidth,
|
||||||
appliedFilters
|
appliedFilters
|
||||||
)
|
)
|
||||||
|
@ -156,6 +198,7 @@ export function useSwimlaneInputResolver(
|
||||||
if (data !== undefined) {
|
if (data !== undefined) {
|
||||||
setError(null);
|
setError(null);
|
||||||
setSwimlaneData(data);
|
setSwimlaneData(data);
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -164,11 +207,28 @@ export function useSwimlaneInputResolver(
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fromPage$.next(fromPage);
|
||||||
|
}, [fromPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (perPage === undefined) return;
|
||||||
|
perPage$.next(perPage);
|
||||||
|
}, [perPage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chartWidth$.next(chartWidth);
|
chartWidth$.next(chartWidth);
|
||||||
}, [chartWidth]);
|
}, [chartWidth]);
|
||||||
|
|
||||||
return [swimlaneType, swimlaneData, timeBuckets, error];
|
return [
|
||||||
|
swimlaneType,
|
||||||
|
swimlaneData,
|
||||||
|
perPage ?? SWIM_LANE_DEFAULT_PAGE_SIZE,
|
||||||
|
setPerPage,
|
||||||
|
timeBuckets,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processFilters(filters: Filter[], query: Query) {
|
export function processFilters(filters: Filter[], query: Query) {
|
||||||
|
|
|
@ -14,8 +14,6 @@ import {
|
||||||
AnomalySwimlaneEmbeddableOutput,
|
AnomalySwimlaneEmbeddableOutput,
|
||||||
} from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable';
|
} from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable';
|
||||||
import { resolveAnomalySwimlaneUserInput } from '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout';
|
import { resolveAnomalySwimlaneUserInput } from '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout';
|
||||||
import { HttpService } from '../application/services/http_service';
|
|
||||||
import { AnomalyDetectorService } from '../application/services/anomaly_detector_service';
|
|
||||||
|
|
||||||
export const EDIT_SWIMLANE_PANEL_ACTION = 'editSwimlanePanelAction';
|
export const EDIT_SWIMLANE_PANEL_ACTION = 'editSwimlanePanelAction';
|
||||||
|
|
||||||
|
@ -39,18 +37,10 @@ export function createEditSwimlanePanelAction(getStartServices: CoreSetup['getSt
|
||||||
throw new Error('Not possible to execute an action without the embeddable context');
|
throw new Error('Not possible to execute an action without the embeddable context');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [{ overlays, uiSettings, http }] = await getStartServices();
|
const [coreStart] = await getStartServices();
|
||||||
const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await resolveAnomalySwimlaneUserInput(
|
const result = await resolveAnomalySwimlaneUserInput(coreStart, embeddable.getInput());
|
||||||
{
|
|
||||||
anomalyDetectorService,
|
|
||||||
overlays,
|
|
||||||
uiSettings,
|
|
||||||
},
|
|
||||||
embeddable.getInput()
|
|
||||||
);
|
|
||||||
embeddable.updateInput(result);
|
embeddable.updateInput(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
|
|
|
@ -9790,8 +9790,6 @@
|
||||||
"xpack.ml.explorer.jobIdLabel": "ジョブ ID",
|
"xpack.ml.explorer.jobIdLabel": "ジョブ ID",
|
||||||
"xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(すべての影響因子のジョブスコア)",
|
"xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(すべての影響因子のジョブスコア)",
|
||||||
"xpack.ml.explorer.kueryBar.filterPlaceholder": "影響因子フィールドでフィルタリング… ({queryExample})",
|
"xpack.ml.explorer.kueryBar.filterPlaceholder": "影響因子フィールドでフィルタリング… ({queryExample})",
|
||||||
"xpack.ml.explorer.limitLabel": "制限",
|
|
||||||
"xpack.ml.explorer.loadingLabel": "読み込み中",
|
|
||||||
"xpack.ml.explorer.noConfiguredInfluencersTooltip": "選択されたジョブに影響因子が構成されていないため、トップインフルエンスリストは非表示になっています。",
|
"xpack.ml.explorer.noConfiguredInfluencersTooltip": "選択されたジョブに影響因子が構成されていないため、トップインフルエンスリストは非表示になっています。",
|
||||||
"xpack.ml.explorer.noInfluencersFoundTitle": "{viewBySwimlaneFieldName}影響因子が見つかりません",
|
"xpack.ml.explorer.noInfluencersFoundTitle": "{viewBySwimlaneFieldName}影響因子が見つかりません",
|
||||||
"xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "指定されたフィルターの{viewBySwimlaneFieldName} 影響因子が見つかりません",
|
"xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "指定されたフィルターの{viewBySwimlaneFieldName} 影響因子が見つかりません",
|
||||||
|
|
|
@ -9794,8 +9794,6 @@
|
||||||
"xpack.ml.explorer.jobIdLabel": "作业 ID",
|
"xpack.ml.explorer.jobIdLabel": "作业 ID",
|
||||||
"xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(所有影响因素的作业分数)",
|
"xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(所有影响因素的作业分数)",
|
||||||
"xpack.ml.explorer.kueryBar.filterPlaceholder": "按影响因素字段筛选……({queryExample})",
|
"xpack.ml.explorer.kueryBar.filterPlaceholder": "按影响因素字段筛选……({queryExample})",
|
||||||
"xpack.ml.explorer.limitLabel": "限制",
|
|
||||||
"xpack.ml.explorer.loadingLabel": "正在加载",
|
|
||||||
"xpack.ml.explorer.noConfiguredInfluencersTooltip": "“顶级影响因素”列表被隐藏,因为没有为所选作业配置影响因素。",
|
"xpack.ml.explorer.noConfiguredInfluencersTooltip": "“顶级影响因素”列表被隐藏,因为没有为所选作业配置影响因素。",
|
||||||
"xpack.ml.explorer.noInfluencersFoundTitle": "未找到任何 {viewBySwimlaneFieldName} 影响因素",
|
"xpack.ml.explorer.noInfluencersFoundTitle": "未找到任何 {viewBySwimlaneFieldName} 影响因素",
|
||||||
"xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "对于指定筛选找不到任何 {viewBySwimlaneFieldName} 影响因素",
|
"xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "对于指定筛选找不到任何 {viewBySwimlaneFieldName} 影响因素",
|
||||||
|
|
|
@ -76,7 +76,7 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid
|
||||||
async addAndEditSwimlaneInDashboard(dashboardTitle: string) {
|
async addAndEditSwimlaneInDashboard(dashboardTitle: string) {
|
||||||
await this.filterWithSearchString(dashboardTitle);
|
await this.filterWithSearchString(dashboardTitle);
|
||||||
await testSubjects.isDisplayed('mlDashboardSelectionTable > checkboxSelectAll');
|
await testSubjects.isDisplayed('mlDashboardSelectionTable > checkboxSelectAll');
|
||||||
await testSubjects.click('mlDashboardSelectionTable > checkboxSelectAll');
|
await testSubjects.clickWhenNotDisabled('mlDashboardSelectionTable > checkboxSelectAll');
|
||||||
expect(await testSubjects.isChecked('mlDashboardSelectionTable > checkboxSelectAll')).to.be(
|
expect(await testSubjects.isChecked('mlDashboardSelectionTable > checkboxSelectAll')).to.be(
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue