[ML] Remove dependency cache. (#189729)

## Summary

Fixes #153477.
Fixes #153476.
Part of #187772 (technical debt).
Part of #153288 (migrate enzyme tests to react-testing-lib).

Removes dependency cache. The major culprit making this PR large and not
easy to split is that `getHttp()` from the dependency cache was used
throughout the code base for services like `mlJobService` and
`ml/mlApiServices` which then themselves were directly imported and not
part of React component lifecycles.

- For functional components this means mostly migrating to hooks that
allow accessing services.
- We still have a bit of a mix of usage of `withKibana` and `context`
for class based React components. This was not consolidated in this PR,
I took what's there and adjusted how services get used. These components
access services via `this.props.kibana.services.*` or
`this.context.services.*`.
- Functions no longer access the global services provided via dependency
cache but were updated to receive services via arguments.
- Stateful services like `mlJobService` are exposed now via a factory
that makes sure the service gets instantiated only once.
- Some tests where the mocks needed quite some refactoring were ported
to `react-testing-lib`. They no longer make use of snapshots or call
component methods which should be considered implementation details.
- We have a mix of usage of the plain `toasts` via `useMlKibana` and our
own `toastNotificationServices` that wraps `toasts`. I didn't
consolidate this in this PR but used what's available for the given
code.
- For class based components, service initializations were moved from
`componentDidMount()` to `constructor()` where I spotted it.
- We have a bit of a mix of naming: `ml`, `mlApiServices`,
`useMlApiContext()` for the same thing. I didn't consolidate the naming
in this PR, to avoid making this PR even larger. This can be done in a
follow up, once this PR is in this should be more straightforward and
less risky.
- Turns out `explorer_chart_config_builder.js` is no longer used
anywhere so I deleted it.
- `jobs/jobs_list/components/utils.d.ts` was missing some definitions,
tried to fix them.
- Moved `stashJobForCloning` to be a method of `mlJobService`.
- The `MetricSelector` component was an exact copy besides the i18n
label, consolidated that so anomaly detection wizards use the same
component.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Walter Rafelsberger 2024-08-29 11:35:37 +02:00 committed by GitHub
parent b79f5abed3
commit 38f4aa0776
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
247 changed files with 2579 additions and 3150 deletions

View file

@ -22,7 +22,6 @@ import useObservable from 'react-use/lib/useObservable';
import type { ExperimentalFeatures, MlFeatures } from '../../common/constants/app';
import { ML_STORAGE_KEYS } from '../../common/types/storage';
import type { MlSetupDependencies, MlStartDependencies } from '../plugin';
import { clearCache, setDependencyCache } from './util/dependency_cache';
import { setLicenseCache } from './license';
import { MlRouter } from './routing';
import type { PageDependencies } from './routing/router';
@ -97,7 +96,7 @@ const App: FC<AppProps> = ({
uiActions: deps.uiActions,
unifiedSearch: deps.unifiedSearch,
usageCollection: deps.usageCollection,
mlServices: getMlGlobalServices(coreStart.http, deps.data.dataViews, deps.usageCollection),
mlServices: getMlGlobalServices(coreStart, deps.data.dataViews, deps.usageCollection),
};
}, [deps, coreStart]);
@ -160,18 +159,6 @@ export const renderApp = (
mlFeatures: MlFeatures,
experimentalFeatures: ExperimentalFeatures
) => {
setDependencyCache({
timefilter: deps.data.query.timefilter,
fieldFormats: deps.fieldFormats,
config: coreStart.uiSettings!,
docLinks: coreStart.docLinks!,
toastNotifications: coreStart.notifications.toasts,
recentlyAccessed: coreStart.chrome!.recentlyAccessed,
application: coreStart.application,
http: coreStart.http,
maps: deps.maps,
});
appMountParams.onAppLeave((actions) => actions.default());
ReactDOM.render(
@ -187,7 +174,6 @@ export const renderApp = (
);
return () => {
clearCache();
ReactDOM.unmountComponentAtNode(appMountParams.element);
deps.data.search.session.clear();
};

View file

@ -197,10 +197,11 @@ export function checkGetManagementMlJobsResolver({ mlCapabilities }: MlGlobalSer
}
export function checkCreateJobsCapabilitiesResolver(
mlApiServices: MlApiServices,
redirectToJobsManagementPage: () => Promise<void>
): Promise<MlCapabilities> {
return new Promise((resolve, reject) => {
getCapabilities()
getCapabilities(mlApiServices)
.then(async ({ capabilities, isPlatinumOrTrialLicense }) => {
_capabilities = capabilities;
// if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect,

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import { ml } from '../services/ml_api_service';
import type { MlApiServices } from '../services/ml_api_service';
import type { MlCapabilitiesResponse } from '../../../common/types/capabilities';
export function getCapabilities(): Promise<MlCapabilitiesResponse> {
export function getCapabilities(ml: MlApiServices): Promise<MlCapabilitiesResponse> {
return ml.checkMlCapabilities();
}

View file

@ -9,7 +9,9 @@ import useObservable from 'react-use/lib/useObservable';
import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json';
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { CoreStart } from '@kbn/core/public';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import type { Annotation } from '../../../../../common/types/annotations';
import { AnnotationUpdatesService } from '../../../services/annotations_service';
@ -17,9 +19,17 @@ import { AnnotationUpdatesService } from '../../../services/annotations_service'
import { AnnotationFlyout } from '.';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
jest.mock('../../../util/dependency_cache', () => ({
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
}));
const kibanaReactContextMock = createKibanaReactContext({
mlServices: {
mlApiServices: {
annotations: {
indexAnnotation: jest.fn().mockResolvedValue({}),
deleteAnnotation: jest.fn().mockResolvedValue({}),
},
},
},
notifications: { toasts: { addDanger: jest.fn(), addSuccess: jest.fn() } },
} as unknown as Partial<CoreStart>);
const MlAnnotationUpdatesContextProvider = ({
annotationUpdatesService,
@ -30,7 +40,9 @@ const MlAnnotationUpdatesContextProvider = ({
}) => {
return (
<MlAnnotationUpdatesContext.Provider value={annotationUpdatesService}>
<IntlProvider>{children}</IntlProvider>
<IntlProvider>
<kibanaReactContextMock.Provider>{children}</kibanaReactContextMock.Provider>
</IntlProvider>
</MlAnnotationUpdatesContext.Provider>
);
};

View file

@ -30,6 +30,7 @@ import {
import type { CommonProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { context } from '@kbn/kibana-react-plugin/public';
import { type MlPartitionFieldsType, ML_PARTITION_FIELDS } from '@kbn/ml-anomaly-utils';
import {
ANNOTATION_MAX_LENGTH_CHARS,
@ -42,13 +43,12 @@ import type {
import { annotationsRefreshed } from '../../../services/annotations_service';
import { AnnotationDescriptionList } from '../annotation_description_list';
import { DeleteAnnotationModal } from '../delete_annotation_modal';
import { ml } from '../../../services/ml_api_service';
import { getToastNotifications } from '../../../util/dependency_cache';
import {
getAnnotationFieldName,
getAnnotationFieldValue,
} from '../../../../../common/types/annotations';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
import type { MlKibanaReactContextValue } from '../../../contexts/kibana';
interface ViewableDetector {
index: number;
@ -78,6 +78,9 @@ interface State {
}
export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
static contextType = context;
declare context: MlKibanaReactContextValue;
private deletionInProgress = false;
public state: State = {
@ -126,7 +129,6 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
if (this.deletionInProgress) return;
const { annotationState } = this.state;
const toastNotifications = getToastNotifications();
if (annotationState === null || annotationState._id === undefined) {
return;
@ -134,6 +136,8 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
this.deletionInProgress = true;
const ml = this.context.services.mlServices.mlApiServices;
const toastNotifications = this.context.services.notifications.toasts;
try {
await ml.annotations.deleteAnnotation(annotationState._id);
toastNotifications.addSuccess(
@ -237,11 +241,12 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
annotation.event = annotation.event ?? ANNOTATION_EVENT_USER;
annotationUpdatesService.setValue(null);
const ml = this.context.services.mlServices.mlApiServices;
const toastNotifications = this.context.services.notifications.toasts;
ml.annotations
.indexAnnotation(annotation)
.then(() => {
annotationsRefreshed();
const toastNotifications = getToastNotifications();
if (typeof annotation._id === 'undefined') {
toastNotifications.addSuccess(
i18n.translate(
@ -265,7 +270,6 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
}
})
.catch((resp) => {
const toastNotifications = getToastNotifications();
if (typeof annotation._id === 'undefined') {
toastNotifications.addDanger(
i18n.translate(

View file

@ -29,10 +29,11 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { addItemToRecentlyAccessed } from '../../../util/recently_accessed';
import { ml } from '../../../services/ml_api_service';
import { mlJobService } from '../../../services/job_service';
import { mlJobServiceFactory } from '../../../services/job_service';
import { toastNotificationServiceProvider } from '../../../services/toast_notification_service';
import { mlTableService } from '../../../services/table_service';
import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search';
import {
@ -45,7 +46,6 @@ import {
ANNOTATION_EVENT_USER,
ANNOTATION_EVENT_DELAYED_DATA,
} from '../../../../../common/constants/annotations';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { ML_APP_LOCATOR, ML_PAGES } from '../../../../../common/constants/locator';
import { timeFormatter } from '@kbn/ml-date-utils';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
@ -90,10 +90,8 @@ class AnnotationsTableUI extends Component {
queryText: `event:(${ANNOTATION_EVENT_USER} or ${ANNOTATION_EVENT_DELAYED_DATA})`,
searchError: undefined,
jobId:
Array.isArray(this.props.jobs) &&
this.props.jobs.length > 0 &&
this.props.jobs[0] !== undefined
? this.props.jobs[0].job_id
Array.isArray(props.jobs) && props.jobs.length > 0 && props.jobs[0] !== undefined
? props.jobs[0].job_id
: undefined,
datafeedFlyoutVisible: false,
modelSnapshot: null,
@ -103,6 +101,10 @@ class AnnotationsTableUI extends Component {
this.sorting = {
sort: { field: 'timestamp', direction: 'asc' },
};
this.mlJobService = mlJobServiceFactory(
toastNotificationServiceProvider(props.kibana.services.notifications.toasts),
props.kibana.services.mlServices.mlApiServices
);
}
getAnnotations() {
@ -113,6 +115,8 @@ class AnnotationsTableUI extends Component {
isLoading: true,
});
const ml = this.props.kibana.services.mlServices.mlApiServices;
if (dataCounts.processed_record_count > 0) {
// Load annotations for the selected job.
ml.annotations
@ -177,7 +181,7 @@ class AnnotationsTableUI extends Component {
}
}
return mlJobService.getJob(jobId);
return this.mlJobService.getJob(jobId);
}
annotationsRefreshSubscription = null;

View file

@ -26,7 +26,6 @@ import { AnomalyDetails } from './anomaly_details';
import { mlTableService } from '../../services/table_service';
import { RuleEditorFlyout } from '../rule_editor';
import { ml } from '../../services/ml_api_service';
import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS, MAX_CHARS } from './anomalies_table_constants';
export class AnomaliesTableInternal extends Component {
@ -69,6 +68,7 @@ export class AnomaliesTableInternal extends Component {
}
toggleRow = async (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => {
const ml = this.context.services.mlServices.mlApiServices;
const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap };
if (itemIdToExpandedRowMap[item.rowId]) {
delete itemIdToExpandedRowMap[item.rowId];

View file

@ -52,14 +52,13 @@ import { parseInterval } from '../../../../common/util/parse_interval';
import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator';
import { getFiltersForDSLQuery } from '../../../../common/util/job_utils';
import { mlJobService } from '../../services/job_service';
import { ml } from '../../services/ml_api_service';
import { useMlJobService } from '../../services/job_service';
import { escapeKueryForFieldValuePair, replaceStringTokens } from '../../util/string_utils';
import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_utils';
import type { SourceIndicesWithGeoFields } from '../../explorer/explorer_utils';
import { escapeDoubleQuotes, getDateFormatTz } from '../../explorer/explorer_utils';
import { usePermissionCheck } from '../../capabilities/check_capabilities';
import { useMlKibana } from '../../contexts/kibana';
import { useMlApiContext, useMlKibana } from '../../contexts/kibana';
import { useMlIndexUtils } from '../../util/index_service';
import { getQueryStringForInfluencers } from './get_query_string_for_influencers';
@ -101,13 +100,24 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
const kibana = useMlKibana();
const {
services: { data, share, application, uiActions },
services: {
data,
share,
application,
uiActions,
uiSettings,
notifications: { toasts },
},
} = kibana;
const { getDataViewById, getDataViewIdFromName } = useMlIndexUtils();
const ml = useMlApiContext();
const mlJobService = useMlJobService();
const job = useMemo(() => {
if (props.selectedJob !== undefined) return props.selectedJob;
return mlJobService.getJob(props.anomaly.jobId);
// skip mlJobService from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.anomaly.jobId, props.selectedJob]);
const categorizationFieldName = job.analysis_config.categorization_field_name;
@ -145,7 +155,9 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
const getAnomaliesMapsLink = async (anomaly: MlAnomaliesTableRecord) => {
const initialLayers = getInitialAnomaliesLayers(anomaly.jobId);
const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz(getDateFormatTz());
const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz(
getDateFormatTz(uiSettings)
);
const anomalyBucketStart = anomalyBucketStartMoment.toISOString();
const anomalyBucketEnd = anomalyBucketStartMoment
.add(anomaly.source.bucket_span, 'seconds')
@ -186,7 +198,9 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
sourceIndicesWithGeoFields[anomaly.jobId]
);
// Widen the timerange by one bucket span on start/end to increase chances of always having data on the map
const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz(getDateFormatTz());
const anomalyBucketStartMoment = moment(anomaly.source.timestamp).tz(
getDateFormatTz(uiSettings)
);
const anomalyBucketStart = anomalyBucketStartMoment
.subtract(anomaly.source.bucket_span, 'seconds')
.toISOString();
@ -513,7 +527,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
.catch((resp) => {
// eslint-disable-next-line no-console
console.log('openCustomUrl(): error loading categoryDefinition:', resp);
const { toasts } = kibana.services.notifications;
toasts.addDanger(
i18n.translate('xpack.ml.anomaliesTable.linksMenu.unableToOpenLinkErrorMessage', {
defaultMessage:
@ -615,7 +628,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
if (job === undefined) {
// eslint-disable-next-line no-console
console.log(`viewExamples(): no job found with ID: ${props.anomaly.jobId}`);
const { toasts } = kibana.services.notifications;
toasts.addDanger(
i18n.translate('xpack.ml.anomaliesTable.linksMenu.unableToViewExamplesErrorMessage', {
defaultMessage: 'Unable to view examples as no details could be found for job ID {jobId}',
@ -702,7 +714,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
.catch((resp) => {
// eslint-disable-next-line no-console
console.log('viewExamples(): error loading categoryDefinition:', resp);
const { toasts } = kibana.services.notifications;
toasts.addDanger(
i18n.translate('xpack.ml.anomaliesTable.linksMenu.loadingDetailsErrorMessage', {
defaultMessage:
@ -736,7 +747,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
`viewExamples(): error finding type of field ${categorizationFieldName} in indices:`,
datafeedIndices
);
const { toasts } = kibana.services.notifications;
toasts.addDanger(
i18n.translate('xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage', {
defaultMessage:

View file

@ -11,7 +11,7 @@ import {
ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
ANOMALY_DETECTION_ENABLE_TIME_RANGE,
} from '../../../../common/constants/settings';
import { mlJobService } from '../../services/job_service';
import { useMlJobService } from '../../services/job_service';
export const useCreateADLinks = () => {
const {
@ -19,6 +19,7 @@ export const useCreateADLinks = () => {
http: { basePath },
},
} = useMlKibana();
const mlJobService = useMlJobService();
const useUserTimeSettings = useUiSettings().get(ANOMALY_DETECTION_ENABLE_TIME_RANGE);
const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE);

View file

@ -29,7 +29,7 @@ import {
} from '@kbn/ml-data-frame-analytics-utils';
import { parseUrlState } from '@kbn/ml-url-state';
import { useMlKibana } from '../../../contexts/kibana';
import { useMlApiContext, useMlKibana } from '../../../contexts/kibana';
import { useToastNotificationService } from '../../../services/toast_notification_service';
import { isValidLabel, openCustomUrlWindow } from '../../../util/custom_url_utils';
import { getTestUrl } from './utils';
@ -73,6 +73,7 @@ export const CustomUrlList: FC<CustomUrlListProps> = ({
data: { dataViews },
},
} = useMlKibana();
const ml = useMlApiContext();
const { displayErrorToast } = useToastNotificationService();
const [expandedUrlIndex, setExpandedUrlIndex] = useState<number | null>(null);
@ -160,7 +161,14 @@ export const CustomUrlList: FC<CustomUrlListProps> = ({
if (index < customUrls.length) {
try {
const testUrl = await getTestUrl(job, customUrl, timefieldName, undefined, isPartialDFAJob);
const testUrl = await getTestUrl(
ml,
job,
customUrl,
timefieldName,
undefined,
isPartialDFAJob
);
openCustomUrlWindow(testUrl, customUrl, http.basePath.get());
} catch (error) {
displayErrorToast(

View file

@ -42,12 +42,12 @@ import {
replaceTokensInDFAUrlValue,
isValidLabel,
} from '../../../util/custom_url_utils';
import { ml } from '../../../services/ml_api_service';
import { escapeForElasticsearchQuery } from '../../../util/string_utils';
import type { CombinedJob, Job } from '../../../../../common/types/anomaly_detection_jobs';
import { isAnomalyDetectionJob } from '../../../../../common/types/anomaly_detection_jobs';
import type { TimeRangeType } from './constants';
import type { MlApiServices } from '../../../services/ml_api_service';
export interface TimeRange {
type: TimeRangeType;
@ -426,7 +426,11 @@ function buildAppStateQueryParam(queryFieldNames: string[]) {
// Builds the full URL for testing out a custom URL configuration, which
// may contain dollar delimited partition / influencer entity tokens and
// drilldown time range settings.
async function getAnomalyDetectionJobTestUrl(job: Job, customUrl: MlUrlConfig): Promise<string> {
async function getAnomalyDetectionJobTestUrl(
ml: MlApiServices,
job: Job,
customUrl: MlUrlConfig
): Promise<string> {
const interval = parseInterval(job.analysis_config.bucket_span!);
const bucketSpanSecs = interval !== null ? interval.asSeconds() : 0;
@ -516,6 +520,7 @@ async function getAnomalyDetectionJobTestUrl(job: Job, customUrl: MlUrlConfig):
}
async function getDataFrameAnalyticsTestUrl(
ml: MlApiServices,
job: DataFrameAnalyticsConfig,
customUrl: MlKibanaUrlConfig,
timeFieldName: string | null,
@ -589,6 +594,7 @@ async function getDataFrameAnalyticsTestUrl(
}
export function getTestUrl(
ml: MlApiServices,
job: Job | DataFrameAnalyticsConfig,
customUrl: MlUrlConfig,
timeFieldName: string | null,
@ -597,6 +603,7 @@ export function getTestUrl(
) {
if (isDataFrameAnalyticsConfigs(job) || isPartialDFAJob) {
return getDataFrameAnalyticsTestUrl(
ml,
job as DataFrameAnalyticsConfig,
customUrl,
timeFieldName,
@ -605,5 +612,5 @@ export function getTestUrl(
);
}
return getAnomalyDetectionJobTestUrl(job, customUrl);
return getAnomalyDetectionJobTestUrl(ml, job, customUrl);
}

View file

@ -42,7 +42,7 @@ import {
} from './custom_url_editor/utils';
import { openCustomUrlWindow } from '../../util/custom_url_utils';
import type { CustomUrlsWrapperProps } from './custom_urls_wrapper';
import { indexServiceFactory } from '../../util/index_service';
import { indexServiceFactory, type MlIndexUtils } from '../../util/index_service';
interface CustomUrlsState {
customUrls: MlUrlConfig[];
@ -62,9 +62,10 @@ export class CustomUrls extends Component<CustomUrlsProps, CustomUrlsState> {
static contextType = context;
declare context: MlKibanaReactContextValue;
private toastNotificationService: ToastNotificationService | undefined;
private toastNotificationService: ToastNotificationService;
private mlIndexUtils: MlIndexUtils;
constructor(props: CustomUrlsProps) {
constructor(props: CustomUrlsProps, constructorContext: MlKibanaReactContextValue) {
super(props);
this.state = {
@ -74,6 +75,11 @@ export class CustomUrls extends Component<CustomUrlsProps, CustomUrlsState> {
editorOpen: false,
supportedFilterFields: [],
};
this.toastNotificationService = toastNotificationServiceProvider(
constructorContext.services.notifications.toasts
);
this.mlIndexUtils = indexServiceFactory(constructorContext.services.data.dataViews);
}
static getDerivedStateFromProps(props: CustomUrlsProps) {
@ -84,10 +90,7 @@ export class CustomUrls extends Component<CustomUrlsProps, CustomUrlsState> {
}
componentDidMount() {
const { toasts } = this.context.services.notifications;
this.toastNotificationService = toastNotificationServiceProvider(toasts);
const { dashboardService } = this.props;
const mlIndexUtils = indexServiceFactory(this.context.services.data.dataViews);
dashboardService
.fetchDashboards()
@ -106,7 +109,7 @@ export class CustomUrls extends Component<CustomUrlsProps, CustomUrlsState> {
);
});
mlIndexUtils
this.mlIndexUtils
.loadDataViewListItems()
.then((dataViewListItems) => {
this.setState({ dataViewListItems });
@ -175,6 +178,7 @@ export class CustomUrls extends Component<CustomUrlsProps, CustomUrlsState> {
http: { basePath },
data: { dataViews },
dashboard,
mlServices: { mlApiServices: ml },
} = this.context.services;
const dataViewId = this.state?.editorSettings?.kibanaSettings?.discoverIndexPatternId;
const job = this.props.job;
@ -190,6 +194,7 @@ export class CustomUrls extends Component<CustomUrlsProps, CustomUrlsState> {
buildCustomUrlFromSettings(dashboard, this.state.editorSettings as CustomUrlSettings).then(
(customUrl) => {
getTestUrl(
ml,
job,
customUrl,
timefieldName,

View file

@ -6,13 +6,15 @@
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { context } from '@kbn/kibana-react-plugin/public';
import { RecognizedResult } from './recognized_result';
import { ml } from '../../services/ml_api_service';
export class DataRecognizer extends Component {
static contextType = context;
constructor(props) {
super(props);
@ -27,6 +29,7 @@ export class DataRecognizer extends Component {
}
componentDidMount() {
const ml = this.context.services.mlServices.mlApiServices;
// once the mount is complete, call the recognize endpoint to see if the index format is known to us,
ml.recognizeIndex({ indexPatternTitle: this.indexPattern.title })
.then((resp) => {

View file

@ -101,13 +101,18 @@ export class ItemsGridPagination extends Component {
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiPopover
data-test-subj="mlItemsGridPaginationPopover"
id="customizablePagination"
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="none"
>
<EuiContextMenuPanel items={items} className="ml-items-grid-page-size-menu" />
<EuiContextMenuPanel
data-test-subj="mlItemsGridPaginationMenuPanel"
items={items}
className="ml-items-grid-page-size-menu"
/>
</EuiPopover>
</EuiFlexItem>

View file

@ -29,7 +29,7 @@ import { extractErrorMessage } from '@kbn/ml-error-utils';
import { i18n } from '@kbn/i18n';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { debounce } from 'lodash';
import { useMlKibana } from '../../../contexts/kibana';
import { useMlApiContext, useMlKibana } from '../../../contexts/kibana';
import { isValidIndexName } from '../../../../../common/util/es_utils';
import { createKibanaDataView, checkIndexExists } from '../retry_create_data_view';
import { useToastNotificationService } from '../../../services/toast_notification_service';
@ -82,12 +82,11 @@ export const ReindexWithPipeline: FC<Props> = ({ pipelineName, sourceIndex }) =>
application: { capabilities },
share,
data,
mlServices: {
mlApiServices: { getIndices, reindexWithPipeline, hasPrivileges },
},
docLinks: { links },
},
} = useMlKibana();
const ml = useMlApiContext();
const { getIndices, reindexWithPipeline, hasPrivileges } = ml;
const { displayErrorToast } = useToastNotificationService();
@ -124,7 +123,7 @@ export const ReindexWithPipeline: FC<Props> = ({ pipelineName, sourceIndex }) =>
);
const debouncedIndexCheck = debounce(async () => {
const checkResp = await checkIndexExists(destinationIndex);
const checkResp = await checkIndexExists(destinationIndex, ml);
if (checkResp.errorMessage !== undefined) {
displayErrorToast(
checkResp.errorMessage,
@ -237,7 +236,11 @@ export const ReindexWithPipeline: FC<Props> = ({ pipelineName, sourceIndex }) =>
useEffect(
function createDiscoverLink() {
async function createDataView() {
const dataViewCreationResult = await createKibanaDataView(destinationIndex, data.dataViews);
const dataViewCreationResult = await createKibanaDataView(
destinationIndex,
data.dataViews,
ml
);
if (
dataViewCreationResult?.success === true &&
dataViewCreationResult?.dataViewId &&
@ -251,6 +254,8 @@ export const ReindexWithPipeline: FC<Props> = ({ pipelineName, sourceIndex }) =>
createDataView();
}
},
// Skip ml API services from deps check
// eslint-disable-next-line react-hooks/exhaustive-deps
[
reindexingTaskId,
destinationIndex,

View file

@ -67,10 +67,7 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex, mode }) => {
const [lastFetchedSampleDocsString, setLastFetchedSampleDocsString] = useState<string>('');
const [isValid, setIsValid] = useState<boolean>(true);
const [showCallOut, setShowCallOut] = useState<boolean>(true);
const {
esSearch,
trainedModels: { trainedModelPipelineSimulate },
} = useMlApiContext();
const ml = useMlApiContext();
const {
notifications: { toasts },
services: {
@ -91,7 +88,7 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex, mode }) => {
const simulatePipeline = async () => {
try {
const result = await trainedModelPipelineSimulate(
const result = await ml.trainedModels.trainedModelPipelineSimulate(
pipelineConfig,
JSON.parse(sampleDocsString) as IngestSimulateDocument[]
);
@ -130,7 +127,7 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex, mode }) => {
let records: IngestSimulateDocument[] = [];
let resp;
try {
resp = await esSearch(body);
resp = await ml.esSearch(body);
if (resp && resp.hits.total.value > 0) {
records = resp.hits.hits;
@ -144,7 +141,9 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex, mode }) => {
setLastFetchedSampleDocsString(JSON.stringify(records, null, 2));
setIsValid(true);
},
[esSearch]
// skip ml API service from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const { getSampleDoc, getRandomSampleDoc } = useMemo(
@ -178,7 +177,7 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex, mode }) => {
useEffect(
function checkSourceIndexExists() {
async function ensureSourceIndexExists() {
const resp = await checkIndexExists(sourceIndex!);
const resp = await checkIndexExists(sourceIndex!, ml);
const indexExists = resp.resp && resp.resp[sourceIndex!] && resp.resp[sourceIndex!].exists;
if (indexExists === false) {
setSourceIndexMissingError(sourceIndexMissingMessage);
@ -188,6 +187,8 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex, mode }) => {
ensureSourceIndexExists();
}
},
// skip ml API service from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[sourceIndex, sourceIndexMissingError]
);

View file

@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
import { extractErrorMessage } from '@kbn/ml-error-utils';
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
import { DuplicateDataViewError } from '@kbn/data-plugin/public';
import { ml } from '../../services/ml_api_service';
import type { MlApiServices } from '../../services/ml_api_service';
import type { FormMessage } from '../../data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state';
interface CreateKibanaDataViewResponse {
@ -25,7 +25,7 @@ function delay(ms = 1000) {
});
}
export async function checkIndexExists(destIndex: string) {
export async function checkIndexExists(destIndex: string, ml: MlApiServices) {
let resp;
let errorMessage;
try {
@ -36,20 +36,23 @@ export async function checkIndexExists(destIndex: string) {
return { resp, errorMessage };
}
export async function retryIndexExistsCheck(destIndex: string): Promise<{
export async function retryIndexExistsCheck(
destIndex: string,
ml: MlApiServices
): Promise<{
success: boolean;
indexExists: boolean;
errorMessage?: string;
}> {
let retryCount = 15;
let resp = await checkIndexExists(destIndex);
let resp = await checkIndexExists(destIndex, ml);
let indexExists = resp.resp && resp.resp[destIndex] && resp.resp[destIndex].exists;
while (retryCount > 1 && !indexExists) {
retryCount--;
await delay(1000);
resp = await checkIndexExists(destIndex);
resp = await checkIndexExists(destIndex, ml);
indexExists = resp.resp && resp.resp[destIndex] && resp.resp[destIndex].exists;
}
@ -67,12 +70,13 @@ export async function retryIndexExistsCheck(destIndex: string): Promise<{
export const createKibanaDataView = async (
destinationIndex: string,
dataViewsService: DataViewsContract,
ml: MlApiServices,
timeFieldName?: string,
callback?: (response: FormMessage) => void
) => {
const response: CreateKibanaDataViewResponse = { success: false, message: '' };
const dataViewName = destinationIndex;
const exists = await retryIndexExistsCheck(destinationIndex);
const exists = await retryIndexExistsCheck(destinationIndex, ml);
if (exists?.success === true) {
// index exists - create data view
if (exists?.indexExists === true) {

View file

@ -30,8 +30,7 @@ import type {
ModelSnapshot,
CombinedJobWithStats,
} from '../../../../../common/types/anomaly_detection_jobs';
import { ml } from '../../../services/ml_api_service';
import { useNotifications } from '../../../contexts/kibana';
import { useMlApiContext, useNotifications } from '../../../contexts/kibana';
interface Props {
snapshot: ModelSnapshot;
@ -40,6 +39,7 @@ interface Props {
}
export const EditModelSnapshotFlyout: FC<Props> = ({ snapshot, job, closeFlyout }) => {
const ml = useMlApiContext();
const { toasts } = useNotifications();
const [description, setDescription] = useState(snapshot.description);
const [retain, setRetain] = useState(snapshot.retain);

View file

@ -12,10 +12,10 @@ import { i18n } from '@kbn/i18n';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiLoadingSpinner } from '@elastic/eui';
import { timeFormatter } from '@kbn/ml-date-utils';
import { useMlApiContext } from '../../contexts/kibana';
import { usePermissionCheck } from '../../capabilities/check_capabilities';
import { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout';
import { RevertModelSnapshotFlyout } from './revert_model_snapshot_flyout';
import { ml } from '../../services/ml_api_service';
import { DATAFEED_STATE, JOB_STATE } from '../../../../common/constants/states';
import { CloseJobConfirm } from './close_job_confirm';
import type {
@ -36,6 +36,8 @@ export enum COMBINED_JOB_STATE {
}
export const ModelSnapshotTable: FC<Props> = ({ job, refreshJobList }) => {
const ml = useMlApiContext();
const [canCreateJob, canStartStopDatafeed] = usePermissionCheck([
'canCreateJob',
'canStartStopDatafeed',
@ -71,7 +73,8 @@ export const ModelSnapshotTable: FC<Props> = ({ job, refreshJobList }) => {
const checkJobIsClosed = useCallback(
async (snapshot: ModelSnapshot) => {
const state = await getCombinedJobState(job.job_id);
const jobs = await ml.jobs.jobs([job.job_id]);
const state = getCombinedJobState(jobs);
if (state === COMBINED_JOB_STATE.UNKNOWN) {
// this will only happen if the job has been deleted by another user
// between the time the row has been expended and now
@ -90,6 +93,8 @@ export const ModelSnapshotTable: FC<Props> = ({ job, refreshJobList }) => {
setCloseJobModalVisible(snapshot);
}
},
// skip mlApiServices from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[job]
);
@ -101,12 +106,15 @@ export const ModelSnapshotTable: FC<Props> = ({ job, refreshJobList }) => {
const forceCloseJob = useCallback(async () => {
await ml.jobs.forceStopAndCloseJob(job.job_id);
if (closeJobModalVisible !== null) {
const state = await getCombinedJobState(job.job_id);
const jobs = await ml.jobs.jobs([job.job_id]);
const state = getCombinedJobState(jobs);
if (state === COMBINED_JOB_STATE.CLOSED) {
setRevertSnapshot(closeJobModalVisible);
}
}
hideCloseJobModalVisible();
// skip mlApiServices from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [job, closeJobModalVisible]);
const closeEditFlyout = useCallback((reload: boolean) => {
@ -260,9 +268,7 @@ export const ModelSnapshotTable: FC<Props> = ({ job, refreshJobList }) => {
);
};
async function getCombinedJobState(jobId: string) {
const jobs = await ml.jobs.jobs([jobId]);
function getCombinedJobState(jobs: CombinedJobWithStats[]) {
if (jobs.length !== 1) {
return COMBINED_JOB_STATE.UNKNOWN;
}

View file

@ -36,10 +36,9 @@ import type {
ModelSnapshot,
CombinedJobWithStats,
} from '../../../../../common/types/anomaly_detection_jobs';
import { ml } from '../../../services/ml_api_service';
import { useNotifications } from '../../../contexts/kibana';
import { useMlApiContext, useNotifications } from '../../../contexts/kibana';
import { chartLoaderProvider } from './chart_loader';
import { mlResultsService } from '../../../services/results_service';
import { mlResultsServiceProvider } from '../../../services/results_service';
import type { LineChartPoint } from '../../../jobs/new_job/common/chart_loader';
import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart';
import type { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader';
@ -64,9 +63,11 @@ export const RevertModelSnapshotFlyout: FC<Props> = ({
closeFlyout,
refresh,
}) => {
const ml = useMlApiContext();
const { toasts } = useNotifications();
const { loadAnomalyDataForJob, loadEventRateForJob } = useMemo(
() => chartLoaderProvider(mlResultsService),
() => chartLoaderProvider(mlResultsServiceProvider(ml)),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const [currentSnapshot, setCurrentSnapshot] = useState(snapshot);

View file

@ -53,7 +53,7 @@ import {
} from './utils';
import { getPartitioningFieldNames } from '../../../../common/util/job_utils';
import { mlJobService } from '../../services/job_service';
import { mlJobServiceFactory } from '../../services/job_service';
import { toastNotificationServiceProvider } from '../../services/toast_notification_service';
class RuleEditorFlyoutUI extends Component {
@ -80,6 +80,11 @@ class RuleEditorFlyoutUI extends Component {
this.partitioningFieldNames = [];
this.canGetFilters = checkPermission('canGetFilters');
this.mlJobService = mlJobServiceFactory(
toastNotificationServiceProvider(props.kibana.services.notifications.toasts),
props.kibana.services.mlServices.mlApiServices
);
}
componentDidMount() {
@ -101,7 +106,7 @@ class RuleEditorFlyoutUI extends Component {
showFlyout = (anomaly) => {
let ruleIndex = -1;
const job = this.props.selectedJob ?? mlJobService.getJob(anomaly.jobId);
const job = this.props.selectedJob ?? this.mlJobService.getJob(anomaly.jobId);
if (job === undefined) {
// No details found for this job, display an error and
// don't open the Flyout as no edits can be made without the job.
@ -337,6 +342,7 @@ class RuleEditorFlyoutUI extends Component {
};
updateRuleAtIndex = (ruleIndex, editedRule) => {
const mlJobService = this.mlJobService;
const { toasts } = this.props.kibana.services.notifications;
const { mlApiServices } = this.props.kibana.services.mlServices;
const { job, anomaly } = this.state;
@ -344,7 +350,7 @@ class RuleEditorFlyoutUI extends Component {
const jobId = job.job_id;
const detectorIndex = anomaly.detectorIndex;
saveJobRule(job, detectorIndex, ruleIndex, editedRule, mlApiServices)
saveJobRule(mlJobService, job, detectorIndex, ruleIndex, editedRule, mlApiServices)
.then((resp) => {
if (resp.success) {
toasts.add({
@ -392,13 +398,14 @@ class RuleEditorFlyoutUI extends Component {
};
deleteRuleAtIndex = (index) => {
const mlJobService = this.mlJobService;
const { toasts } = this.props.kibana.services.notifications;
const { mlApiServices } = this.props.kibana.services.mlServices;
const { job, anomaly } = this.state;
const jobId = job.job_id;
const detectorIndex = anomaly.detectorIndex;
deleteJobRule(job, detectorIndex, index, mlApiServices)
deleteJobRule(mlJobService, job, detectorIndex, index, mlApiServices)
.then((resp) => {
if (resp.success) {
toasts.addSuccess(

View file

@ -7,7 +7,7 @@
// Mock the services required for reading and writing job data.
jest.mock('../../services/job_service', () => ({
mlJobService: {
mlJobServiceFactory: () => ({
getJob: () => {
return {
job_id: 'farequote_no_by',
@ -43,9 +43,8 @@ jest.mock('../../services/job_service', () => ({
},
};
},
},
}),
}));
jest.mock('../../services/ml_api_service', () => 'ml');
jest.mock('../../capabilities/check_capabilities', () => ({
checkPermission: () => true,
}));
@ -93,6 +92,7 @@ function prepareTest() {
},
},
},
mlServices: { mlApiServices: {} },
notifications: {
toasts: {
addDanger: () => {},

View file

@ -1,182 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RuleActionPanel renders panel for rule with a condition 1`] = `
<EuiPanel
className="select-rule-action-panel"
paddingSize="m"
>
<EuiDescriptionList
columnWidths={
Array [
15,
85,
]
}
listItems={
Array [
Object {
"description": "skip result when actual is less than 1",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Rule"
id="xpack.ml.ruleEditor.ruleActionPanel.ruleTitle"
/>,
},
Object {
"description": <EditConditionLink
anomaly={
Object {
"actual": Array [
50,
],
"detectorIndex": 0,
"source": Object {
"airline": Array [
"AAL",
],
"function": "mean",
},
"typical": Array [
1.23,
],
}
}
appliesTo="actual"
conditionIndex={0}
conditionValue={1}
updateConditionValue={[Function]}
/>,
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="xpack.ml.ruleEditor.ruleActionPanel.actionsTitle"
/>,
},
Object {
"description": <EuiLink
onClick={[Function]}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit rule"
id="xpack.ml.ruleEditor.ruleActionPanel.editRuleLinkText"
/>
</EuiLink>,
"title": "",
},
Object {
"description": <DeleteRuleModal
deleteRuleAtIndex={[MockFunction]}
ruleIndex={0}
/>,
"title": "",
},
]
}
type="column"
/>
</EuiPanel>
`;
exports[`RuleActionPanel renders panel for rule with a condition and scope, value not in filter list 1`] = `
<EuiPanel
className="select-rule-action-panel"
paddingSize="m"
>
<EuiDescriptionList
columnWidths={
Array [
15,
85,
]
}
listItems={
Array [
Object {
"description": "skip model update when airline is not in eu-airlines",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Rule"
id="xpack.ml.ruleEditor.ruleActionPanel.ruleTitle"
/>,
},
Object {
"description": <AddToFilterListLink
addItemToFilterList={[MockFunction]}
fieldValue="AAL"
filterId="eu-airlines"
/>,
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="xpack.ml.ruleEditor.ruleActionPanel.actionsTitle"
/>,
},
Object {
"description": <EuiLink
onClick={[Function]}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit rule"
id="xpack.ml.ruleEditor.ruleActionPanel.editRuleLinkText"
/>
</EuiLink>,
"title": "",
},
Object {
"description": <DeleteRuleModal
deleteRuleAtIndex={[MockFunction]}
ruleIndex={1}
/>,
"title": "",
},
]
}
type="column"
/>
</EuiPanel>
`;
exports[`RuleActionPanel renders panel for rule with scope, value in filter list 1`] = `
<EuiPanel
className="select-rule-action-panel"
paddingSize="m"
>
<EuiDescriptionList
columnWidths={
Array [
15,
85,
]
}
listItems={
Array [
Object {
"description": "skip model update when airline is not in eu-airlines",
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Rule"
id="xpack.ml.ruleEditor.ruleActionPanel.ruleTitle"
/>,
},
Object {
"description": <EuiLink
onClick={[Function]}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit rule"
id="xpack.ml.ruleEditor.ruleActionPanel.editRuleLinkText"
/>
</EuiLink>,
"title": <Memo(MemoizedFormattedMessage)
defaultMessage="Actions"
id="xpack.ml.ruleEditor.ruleActionPanel.actionsTitle"
/>,
},
Object {
"description": <DeleteRuleModal
deleteRuleAtIndex={[MockFunction]}
ruleIndex={1}
/>,
"title": "",
},
]
}
type="column"
/>
</EuiPanel>
`;

View file

@ -11,19 +11,21 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { cloneDeep } from 'lodash';
import { EuiDescriptionList, EuiLink, EuiPanel } from '@elastic/eui';
import { cloneDeep } from 'lodash';
import { context } from '@kbn/kibana-react-plugin/public';
import { AddToFilterListLink } from './add_to_filter_list_link';
import { DeleteRuleModal } from './delete_rule_modal';
import { EditConditionLink } from './edit_condition_link';
import { buildRuleDescription } from '../utils';
import { ml } from '../../../services/ml_api_service';
import { FormattedMessage } from '@kbn/i18n-react';
export class RuleActionPanel extends Component {
static contextType = context;
constructor(props) {
super(props);
@ -41,6 +43,7 @@ export class RuleActionPanel extends Component {
}
componentDidMount() {
const ml = this.context.services.mlServices.mlApiServices;
// If the rule has a scope section with a single partitioning field key,
// load the filter list to check whether to add a link to add the
// anomaly partitioning field value to the filter list.

View file

@ -5,6 +5,15 @@
* 2.0.
*/
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { ML_DETECTOR_RULE_ACTION } from '@kbn/ml-anomaly-utils';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { RuleActionPanel } from './rule_action_panel';
jest.mock('../../../services/job_service', () => 'mlJobService');
// Mock the call for loading a filter.
@ -19,22 +28,18 @@ const mockTestFilter = {
jobs: ['farequote'],
},
};
jest.mock('../../../services/ml_api_service', () => ({
ml: {
filters: {
filters: () => {
return Promise.resolve(mockTestFilter);
const kibanaReactContextMock = createKibanaReactContext({
mlServices: {
mlApiServices: {
filters: {
filters: () => {
return Promise.resolve(mockTestFilter);
},
},
},
},
}));
import React from 'react';
import { shallowWithIntl } from '@kbn/test-jest-helpers';
import { ML_DETECTOR_RULE_ACTION } from '@kbn/ml-anomaly-utils';
import { RuleActionPanel } from './rule_action_panel';
});
describe('RuleActionPanel', () => {
const job = {
@ -117,9 +122,21 @@ describe('RuleActionPanel', () => {
ruleIndex: 0,
};
const component = shallowWithIntl(<RuleActionPanel {...props} />);
render(
<IntlProvider>
<kibanaReactContextMock.Provider>
<RuleActionPanel {...props} />
</kibanaReactContextMock.Provider>
</IntlProvider>
);
expect(component).toMatchSnapshot();
expect(screen.getByText('Rule')).toBeInTheDocument();
expect(screen.getByText('skip result when actual is less than 1')).toBeInTheDocument();
expect(screen.getByText('Actions')).toBeInTheDocument();
expect(screen.getByText('Update rule condition from 1 to')).toBeInTheDocument();
expect(screen.getByText('Update')).toBeInTheDocument();
expect(screen.getByText('Edit rule')).toBeInTheDocument();
expect(screen.getByText('Delete rule')).toBeInTheDocument();
});
test('renders panel for rule with scope, value in filter list', () => {
@ -128,19 +145,46 @@ describe('RuleActionPanel', () => {
ruleIndex: 1,
};
const component = shallowWithIntl(<RuleActionPanel {...props} />);
render(
<IntlProvider>
<kibanaReactContextMock.Provider>
<RuleActionPanel {...props} />
</kibanaReactContextMock.Provider>
</IntlProvider>
);
expect(component).toMatchSnapshot();
expect(screen.getByText('Rule')).toBeInTheDocument();
expect(
screen.getByText('skip model update when airline is not in eu-airlines')
).toBeInTheDocument();
expect(screen.getByText('Actions')).toBeInTheDocument();
expect(screen.getByText('Edit rule')).toBeInTheDocument();
expect(screen.getByText('Delete rule')).toBeInTheDocument();
});
test('renders panel for rule with a condition and scope, value not in filter list', () => {
test('renders panel for rule with a condition and scope, value not in filter list', async () => {
const props = {
...requiredProps,
ruleIndex: 1,
};
const wrapper = shallowWithIntl(<RuleActionPanel {...props} />);
wrapper.setState({ showAddToFilterListLink: true });
expect(wrapper).toMatchSnapshot();
await waitFor(() => {
render(
<IntlProvider>
<kibanaReactContextMock.Provider>
<RuleActionPanel {...props} />
</kibanaReactContextMock.Provider>
</IntlProvider>
);
});
expect(screen.getByText('Rule')).toBeInTheDocument();
expect(
screen.getByText('skip model update when airline is not in eu-airlines')
).toBeInTheDocument();
expect(screen.getByText('Actions')).toBeInTheDocument();
expect(screen.getByText('Add AAL to eu-airlines')).toBeInTheDocument();
expect(screen.getByText('Edit rule')).toBeInTheDocument();
expect(screen.getByText('Delete rule')).toBeInTheDocument();
});
});

View file

@ -15,7 +15,6 @@ import {
ML_DETECTOR_RULE_OPERATOR,
} from '@kbn/ml-anomaly-utils';
import { mlJobService } from '../../services/job_service';
import { processCreatedBy } from '../../../../common/util/job_utils';
export function getNewConditionDefaults() {
@ -69,7 +68,14 @@ export function isValidRule(rule) {
return isValid;
}
export function saveJobRule(job, detectorIndex, ruleIndex, editedRule, mlApiServices) {
export function saveJobRule(
mlJobService,
job,
detectorIndex,
ruleIndex,
editedRule,
mlApiServices
) {
const detector = job.analysis_config.detectors[detectorIndex];
// Filter out any scope expression where the UI=specific 'enabled'
@ -102,16 +108,16 @@ export function saveJobRule(job, detectorIndex, ruleIndex, editedRule, mlApiServ
}
}
return updateJobRules(job, detectorIndex, rules, mlApiServices);
return updateJobRules(mlJobService, job, detectorIndex, rules, mlApiServices);
}
export function deleteJobRule(job, detectorIndex, ruleIndex, mlApiServices) {
export function deleteJobRule(mlJobService, job, detectorIndex, ruleIndex, mlApiServices) {
const detector = job.analysis_config.detectors[detectorIndex];
let customRules = [];
if (detector.custom_rules !== undefined && ruleIndex < detector.custom_rules.length) {
customRules = cloneDeep(detector.custom_rules);
customRules.splice(ruleIndex, 1);
return updateJobRules(job, detectorIndex, customRules, mlApiServices);
return updateJobRules(mlJobService, job, detectorIndex, customRules, mlApiServices);
} else {
return Promise.reject(
new Error(
@ -127,7 +133,7 @@ export function deleteJobRule(job, detectorIndex, ruleIndex, mlApiServices) {
}
}
export function updateJobRules(job, detectorIndex, rules, mlApiServices) {
export function updateJobRules(mlJobService, job, detectorIndex, rules, mlApiServices) {
// Pass just the detector with the edited rule to the updateJob endpoint.
const jobId = job.job_id;
const jobData = {
@ -149,17 +155,14 @@ export function updateJobRules(job, detectorIndex, rules, mlApiServices) {
mlApiServices
.updateJob({ jobId: jobId, job: jobData })
.then(() => {
// If using mlJobService, refresh the job data in the job service before resolving.
if (mlJobService) {
mlJobService
.refreshJob(jobId)
.then(() => {
resolve({ success: true });
})
.catch((refreshResp) => {
reject(refreshResp);
});
}
mlJobService
.refreshJob(jobId)
.then(() => {
resolve({ success: true });
})
.catch((refreshResp) => {
reject(refreshResp);
});
})
.catch((resp) => {
reject(resp);

View file

@ -1,64 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ValidateJob renders button and modal with a message 1`] = `
<Fragment>
<div>
<EuiButton
fill={true}
iconSide="right"
iconType="questionInCircle"
isDisabled={false}
isLoading={true}
onClick={[Function]}
size="s"
>
<MemoizedFormattedMessage
defaultMessage="Validate Job"
id="xpack.ml.validateJob.validateJobButtonLabel"
/>
</EuiButton>
</div>
</Fragment>
`;
exports[`ValidateJob renders the button 1`] = `
<Fragment>
<div>
<EuiButton
fill={true}
iconSide="right"
iconType="questionInCircle"
isDisabled={false}
isLoading={true}
onClick={[Function]}
size="s"
>
<MemoizedFormattedMessage
defaultMessage="Validate Job"
id="xpack.ml.validateJob.validateJobButtonLabel"
/>
</EuiButton>
</div>
</Fragment>
`;
exports[`ValidateJob renders the button and modal with a success message 1`] = `
<Fragment>
<div>
<EuiButton
fill={true}
iconSide="right"
iconType="questionInCircle"
isDisabled={false}
isLoading={true}
onClick={[Function]}
size="s"
>
<MemoizedFormattedMessage
defaultMessage="Validate Job"
id="xpack.ml.validateJob.validateJobButtonLabel"
/>
</EuiButton>
</div>
</Fragment>
`;

View file

@ -9,7 +9,6 @@ import type { FC } from 'react';
declare const ValidateJob: FC<{
getJobConfig: any;
getDuration: any;
ml: any;
embedded?: boolean;
setIsValid?: (valid: boolean) => void;
idFilterList?: string[];

View file

@ -26,8 +26,6 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { getDocLinks } from '../../util/dependency_cache';
import { parseMessages } from '../../../../common/constants/messages';
import { VALIDATION_STATUS } from '../../../../common/constants/validation';
import { Callout, statusToEuiIconType } from '../callout';
@ -76,7 +74,7 @@ MessageList.propTypes = {
const LoadingSpinner = () => (
<EuiFlexGroup justifyContent="spaceAround" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
<EuiLoadingSpinner size="xl" data-test-subj="mlValidateJobLoadingSpinner" />
</EuiFlexItem>
</EuiFlexGroup>
);
@ -120,6 +118,7 @@ export class ValidateJobUI extends Component {
};
validate = () => {
const docLinks = this.props.kibana.services.docLinks;
const job = this.props.getJobConfig();
const getDuration = this.props.getDuration;
const duration = typeof getDuration === 'function' ? getDuration() : undefined;
@ -131,10 +130,10 @@ export class ValidateJobUI extends Component {
if (typeof duration === 'object' && duration.start !== null && duration.end !== null) {
let shouldShowLoadingIndicator = true;
this.props.ml
this.props.kibana.services.mlServices.mlApiServices
.validateJob({ duration, fields, job })
.then((validationMessages) => {
const messages = parseMessages(validationMessages, getDocLinks());
const messages = parseMessages(validationMessages, docLinks);
shouldShowLoadingIndicator = false;
const messagesContainError = messages.some((m) => m.status === VALIDATION_STATUS.ERROR);
@ -229,7 +228,7 @@ export class ValidateJobUI extends Component {
};
render() {
const jobTipsUrl = getDocLinks().links.ml.anomalyDetectionJobTips;
const jobTipsUrl = this.props.kibana.services.docLinks.links.ml.anomalyDetectionJobTips;
// only set to false if really false and not another falsy value, so it defaults to true.
const fill = this.props.fill === false ? false : true;
// default to false if not explicitly set to true
@ -244,7 +243,8 @@ export class ValidateJobUI extends Component {
{embedded === false ? (
<div>
<EuiButton
onClick={this.validate}
data-test-subj="mlValidateJobButton"
onClick={(e) => this.validate(e)}
size="s"
fill={fill}
iconType={isCurrentJobConfig ? this.state.ui.iconType : defaultIconType}
@ -260,6 +260,7 @@ export class ValidateJobUI extends Component {
{!isDisabled && this.state.ui.isModalVisible && (
<Modal
data-test-subj="mlValidateJobModal"
close={this.closeModal}
title={
<FormattedMessage
@ -321,7 +322,6 @@ ValidateJobUI.propTypes = {
getJobConfig: PropTypes.func.isRequired,
isCurrentJobConfig: PropTypes.bool,
isDisabled: PropTypes.bool,
ml: PropTypes.object.isRequired,
embedded: PropTypes.bool,
setIsValid: PropTypes.func,
idFilterList: PropTypes.array,

View file

@ -5,76 +5,19 @@
* 2.0.
*/
import { shallowWithIntl } from '@kbn/test-jest-helpers';
import { render, waitFor } from '@testing-library/react';
import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { ValidateJob } from './validate_job_view';
jest.mock('../../util/dependency_cache', () => ({
getDocLinks: () => ({
links: {
ml: {
anomalyDetectionJobTips: 'jest-metadata-mock-url',
},
},
}),
}));
jest.mock('@kbn/kibana-react-plugin/public', () => ({
withKibana: (comp) => {
return comp;
},
}));
const job = {
job_id: 'test-id',
};
const getJobConfig = () => job;
const getDuration = () => ({ start: 0, end: 1 });
function prepareTest(messages) {
const p = Promise.resolve(messages);
const ml = {
validateJob: () => Promise.resolve(messages),
};
const kibana = {
services: {
notifications: { toasts: { addDanger: jest.fn(), addError: jest.fn() } },
},
};
const component = (
<ValidateJob getDuration={getDuration} getJobConfig={getJobConfig} ml={ml} kibana={kibana} />
);
const wrapper = shallowWithIntl(component);
return { wrapper, p };
}
describe('ValidateJob', () => {
const test1 = prepareTest({
success: true,
messages: [],
});
test('renders the button', () => {
expect(test1.wrapper).toMatchSnapshot();
});
test('renders the button and modal with a success message', () => {
test1.wrapper.instance().validate();
test1.p.then(() => {
test1.wrapper.update();
expect(test1.wrapper).toMatchSnapshot();
});
});
const test2 = prepareTest({
success: true,
messages: [
const mockValidateJob = jest.fn().mockImplementation(({ job }) => {
console.log('job', job);
if (job.job_id === 'job1') {
return Promise.resolve([]);
} else if (job.job_id === 'job2') {
return Promise.resolve([
{
fieldName: 'airline',
id: 'over_field_low_cardinality',
@ -82,14 +25,84 @@ describe('ValidateJob', () => {
text: 'Cardinality of over_field "airline" is low and therefore less suitable for population analysis.',
url: 'https://www.elastic.co/blog/sizing-machine-learning-with-elasticsearch',
},
],
]);
} else {
return Promise.reject(new Error('Unknown job'));
}
});
const mockKibanaContext = {
services: {
docLinks: { links: { ml: { anomalyDetectionJobTips: 'https://anomalyDetectionJobTips' } } },
notifications: { toasts: { addDanger: jest.fn(), addError: jest.fn() } },
mlServices: { mlApiServices: { validateJob: mockValidateJob } },
},
};
const mockReact = React;
jest.mock('@kbn/kibana-react-plugin/public', () => ({
withKibana: (type) => {
const EnhancedType = (props) => {
return mockReact.createElement(type, {
...props,
kibana: mockKibanaContext,
});
};
return EnhancedType;
},
}));
const job = {
job_id: 'job2',
};
const getJobConfig = () => job;
const getDuration = () => ({ start: 0, end: 1 });
describe('ValidateJob', () => {
test('renders the button when not in embedded mode', () => {
const { getByTestId, queryByTestId } = render(
<IntlProvider>
<ValidateJob
isDisabled={false}
embedded={false}
getDuration={getDuration}
getJobConfig={getJobConfig}
/>
</IntlProvider>
);
const button = getByTestId('mlValidateJobButton');
expect(button).toBeInTheDocument();
const loadingSpinner = queryByTestId('mlValidateJobLoadingSpinner');
expect(loadingSpinner).not.toBeInTheDocument();
const modal = queryByTestId('mlValidateJobModal');
expect(modal).not.toBeInTheDocument();
});
test('renders button and modal with a message', () => {
test2.wrapper.instance().validate();
test2.p.then(() => {
test2.wrapper.update();
expect(test2.wrapper).toMatchSnapshot();
test('renders no button when in embedded mode', async () => {
const { queryByTestId, getByTestId } = render(
<IntlProvider>
<ValidateJob
isDisabled={false}
embedded={true}
getDuration={getDuration}
getJobConfig={getJobConfig}
/>
</IntlProvider>
);
expect(queryByTestId('mlValidateJobButton')).not.toBeInTheDocument();
expect(getByTestId('mlValidateJobLoadingSpinner')).toBeInTheDocument();
expect(queryByTestId('mlValidateJobModal')).not.toBeInTheDocument();
await waitFor(() => expect(mockValidateJob).toHaveBeenCalledTimes(1));
// wait for the loading spinner to disappear and show a callout instead
await waitFor(() => {
expect(queryByTestId('mlValidateJobLoadingSpinner')).not.toBeInTheDocument();
expect(queryByTestId('mlValidationCallout warning')).toBeInTheDocument();
});
});
});

View file

@ -19,8 +19,8 @@ import {
ANALYSIS_CONFIG_TYPE,
} from '@kbn/ml-data-frame-analytics-utils';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ml } from '../../services/ml_api_service';
import type { Dictionary } from '../../../../common/types/common';
import type { MlApiServices } from '../../services/ml_api_service';
export type IndexPattern = string;
@ -289,6 +289,7 @@ export enum REGRESSION_STATS {
}
interface LoadEvalDataConfig {
mlApiServices: MlApiServices;
isTraining?: boolean;
index: string;
dependentVariable: string;
@ -303,6 +304,7 @@ interface LoadEvalDataConfig {
}
export const loadEvalData = async ({
mlApiServices,
isTraining,
index,
dependentVariable,
@ -360,7 +362,7 @@ export const loadEvalData = async ({
};
try {
const evalResult = await ml.dataFrameAnalytics.evaluateDataFrameAnalytics(config);
const evalResult = await mlApiServices.dataFrameAnalytics.evaluateDataFrameAnalytics(config);
results.success = true;
results.eval = evalResult;
return results;
@ -371,6 +373,7 @@ export const loadEvalData = async ({
};
interface LoadDocsCountConfig {
mlApiServices: MlApiServices;
ignoreDefaultQuery?: boolean;
isTraining?: boolean;
searchQuery: estypes.QueryDslQueryContainer;
@ -384,6 +387,7 @@ interface LoadDocsCountResponse {
}
export const loadDocsCount = async ({
mlApiServices,
ignoreDefaultQuery = true,
isTraining,
searchQuery,
@ -398,7 +402,7 @@ export const loadDocsCount = async ({
query,
};
const resp: TrackTotalHitsSearchResponse = await ml.esSearch({
const resp: TrackTotalHitsSearchResponse = await mlApiServices.esSearch({
index: destIndex,
size: 0,
body,

View file

@ -11,16 +11,19 @@ import { type DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-util
import type { EsSorting, UseDataGridReturnType } from '@kbn/ml-data-grid';
import { getProcessedFields, INDEX_STATUS } from '@kbn/ml-data-grid';
import { ml } from '../../services/ml_api_service';
import { newJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { mlJobCapsServiceAnalyticsFactory } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import type { MlApiServices } from '../../services/ml_api_service';
export const getIndexData = async (
mlApiServices: MlApiServices,
jobConfig: DataFrameAnalyticsConfig | undefined,
dataGrid: UseDataGridReturnType,
searchQuery: estypes.QueryDslQueryContainer,
options: { didCancel: boolean }
) => {
if (jobConfig !== undefined) {
const newJobCapsServiceAnalytics = mlJobCapsServiceAnalyticsFactory(mlApiServices);
const {
pagination,
setErrorMessage,
@ -47,7 +50,7 @@ export const getIndexData = async (
const { pageIndex, pageSize } = pagination;
// TODO: remove results_field from `fields` when possible
const resp: estypes.SearchResponse = await ml.esSearch({
const resp: estypes.SearchResponse = await mlApiServices.esSearch({
index: jobConfig.dest.index,
body: {
fields: ['*'],

View file

@ -8,19 +8,22 @@
import type { ES_FIELD_TYPES } from '@kbn/field-types';
import type { DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-utils';
import { newJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { mlJobCapsServiceAnalyticsFactory } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import type { MlApiServices } from '../../services/ml_api_service';
export interface FieldTypes {
[key: string]: ES_FIELD_TYPES;
}
export const getIndexFields = (
mlApiServices: MlApiServices,
jobConfig: DataFrameAnalyticsConfig | undefined,
needsDestIndexFields: boolean
) => {
if (jobConfig !== undefined) {
const { selectedFields: defaultSelected, docFields } =
newJobCapsServiceAnalytics.getDefaultFields(jobConfig, needsDestIndexFields);
const { selectedFields: defaultSelected, docFields } = mlJobCapsServiceAnalyticsFactory(
mlApiServices
).getDefaultFields(jobConfig, needsDestIndexFields);
const types: FieldTypes = {};
const allFields: string[] = [];

View file

@ -19,14 +19,13 @@ import {
type TotalFeatureImportance,
} from '@kbn/ml-data-frame-analytics-utils';
import { useMlKibana } from '../../contexts/kibana';
import { ml } from '../../services/ml_api_service';
import { newJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useMlApiContext, useMlKibana } from '../../contexts/kibana';
import { useNewJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useMlIndexUtils } from '../../util/index_service';
import { isGetDataFrameAnalyticsStatsResponseOk } from '../pages/analytics_management/services/analytics_service/get_analytics';
import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models';
import { getToastNotificationService } from '../../services/toast_notification_service';
import { useToastNotificationService } from '../../services/toast_notification_service';
import { getDestinationIndex } from './get_destination_index';
export const useResultsViewConfig = (jobId: string) => {
@ -35,8 +34,11 @@ export const useResultsViewConfig = (jobId: string) => {
data: { dataViews },
},
} = useMlKibana();
const toastNotificationService = useToastNotificationService();
const ml = useMlApiContext();
const { getDataViewIdFromName } = useMlIndexUtils();
const trainedModelsApiService = useTrainedModelsApiService();
const newJobCapsServiceAnalytics = useNewJobCapsServiceAnalytics();
const [dataView, setDataView] = useState<DataView | undefined>(undefined);
const [dataViewErrorMessage, setDataViewErrorMessage] = useState<undefined | string>(undefined);
@ -92,7 +94,7 @@ export const useResultsViewConfig = (jobId: string) => {
setTotalFeatureImportance(inferenceModel?.metadata?.total_feature_importance);
}
} catch (e) {
getToastNotificationService().displayErrorToast(e);
toastNotificationService.displayErrorToast(e);
}
}

View file

@ -32,7 +32,7 @@ import {
import { HyperParameters } from './hyper_parameters';
import type { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { getModelMemoryLimitErrors } from '../../../analytics_management/hooks/use_create_analytics_form/reducer';
import { useMlKibana } from '../../../../../contexts/kibana';
import { useMlKibana, useMlApiContext } from '../../../../../contexts/kibana';
import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { ANALYTICS_STEPS } from '../../page';
import { fetchExplainData } from '../shared';
@ -134,6 +134,7 @@ export const AdvancedStepForm: FC<CreateAnalyticsStepProps> = ({
const {
services: { docLinks },
} = useMlKibana();
const mlApiServices = useMlApiContext();
const classAucRocDocLink = docLinks.links.ml.classificationAucRoc;
const { setEstimatedModelMemoryLimit, setFormState } = actions;
@ -205,7 +206,10 @@ export const AdvancedStepForm: FC<CreateAnalyticsStepProps> = ({
useEffect(() => {
setFetchingAdvancedParamErrors(true);
(async function () {
const { success, errorMessage, errorReason, expectedMemory } = await fetchExplainData(form);
const { success, errorMessage, errorReason, expectedMemory } = await fetchExplainData(
mlApiServices,
form
);
const paramErrors: AdvancedParamErrors = {};
if (success) {

View file

@ -29,13 +29,13 @@ import {
} from '@kbn/ml-data-frame-analytics-utils';
import { DataGrid } from '@kbn/ml-data-grid';
import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils';
import { useMlKibana } from '../../../../../contexts/kibana';
import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana';
import {
EuiComboBoxWithFieldStats,
FieldStatsFlyoutProvider,
} from '../../../../../components/field_stats_flyout';
import type { FieldForStats } from '../../../../../components/field_stats_flyout/field_stats_info_button';
import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useDataSource } from '../../../../../contexts/ml';
import { getScatterplotMatrixLegendType } from '../../../../common/get_scatterplot_matrix_legend_type';
@ -44,7 +44,6 @@ import { Messages } from '../shared';
import type { State } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { handleExplainErrorMessage, shouldAddAsDepVarOption } from './form_options_validation';
import { getToastNotifications } from '../../../../../util/dependency_cache';
import { ANALYTICS_STEPS } from '../../page';
import { ContinueButton } from '../continue_button';
@ -115,6 +114,10 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
setCurrentStep,
sourceDataViewTitle,
}) => {
const { services } = useMlKibana();
const toastNotifications = services.notifications.toasts;
const mlApiServices = useMlApiContext();
const newJobCapsServiceAnalytics = useNewJobCapsServiceAnalytics();
const { selectedDataView, selectedSavedSearch } = useDataSource();
const { savedSearchQuery, savedSearchQueryStr } = useSavedSearch();
@ -167,8 +170,6 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
language: jobConfigQueryLanguage ?? SEARCH_QUERY_LANGUAGE.KUERY,
});
const toastNotifications = getToastNotifications();
const setJobConfigQuery: ExplorationQueryBarProps['setSearchQuery'] = (update) => {
if (update.query) {
setFormState({
@ -287,7 +288,7 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
fieldSelection,
errorMessage,
noDocsContainMappedFields: noDocsWithFields,
} = await fetchExplainData(formToUse);
} = await fetchExplainData(mlApiServices, formToUse);
if (success) {
if (shouldUpdateEstimatedMml) {
@ -443,7 +444,7 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
fieldSelection,
errorMessage,
noDocsContainMappedFields: noDocsWithFields,
} = await fetchExplainData(formCopy);
} = await fetchExplainData(mlApiServices, formCopy);
if (success) {
// update the field selection table
const hasRequiredFields = fieldSelection.some(
@ -547,7 +548,6 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
[dependentVariableEmpty, jobType, scatterplotMatrixProps.fields.length]
);
const { services } = useMlKibana();
const fieldStatsServices: FieldStatsServices = useMemo(() => {
const { uiSettings, data, fieldFormats, charts } = services;
return {

View file

@ -19,7 +19,7 @@ import {
} from '@kbn/ml-data-frame-analytics-utils';
import type { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { CATEGORICAL_TYPES } from './form_options_validation';
import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
const containsClassificationFieldsCb = ({ name, type }: Field) =>
!OMIT_FIELDS.includes(name) &&
@ -73,7 +73,7 @@ export const SupportedFieldsMessage: FC<Props> = ({ jobType }) => {
const [sourceIndexContainsSupportedFields, setSourceIndexContainsSupportedFields] =
useState<boolean>(true);
const [sourceIndexFieldsCheckFailed, setSourceIndexFieldsCheckFailed] = useState<boolean>(false);
const { fields } = newJobCapsServiceAnalytics;
const { fields } = useNewJobCapsServiceAnalytics();
// Find out if data view contains supported fields for job type. Provides a hint in the form
// that job may not run correctly if no supported fields are found.

View file

@ -14,8 +14,7 @@ import { i18n } from '@kbn/i18n';
import { extractErrorMessage } from '@kbn/ml-error-utils';
import { dynamic } from '@kbn/shared-ux-utility';
import { useNotifications } from '../../../../../contexts/kibana';
import { ml } from '../../../../../services/ml_api_service';
import { useMlApiContext, useNotifications } from '../../../../../contexts/kibana';
import type { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { CreateStep } from '../create_step';
import { ANALYTICS_STEPS } from '../../page';
@ -25,6 +24,7 @@ const EditorComponent = dynamic(async () => ({
}));
export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = (props) => {
const ml = useMlApiContext();
const { actions, state } = props;
const { setAdvancedEditorRawString, setFormState } = actions;

View file

@ -16,8 +16,7 @@ import {
} from '@kbn/ml-data-frame-analytics-utils';
import { getDataFrameAnalyticsProgressPhase } from '../../../analytics_management/components/analytics_list/common';
import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics';
import { useMlKibana } from '../../../../../contexts/kibana';
import { ml } from '../../../../../services/ml_api_service';
import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana';
import { BackToListPanel } from '../back_to_list_panel';
import { ViewResultsPanel } from '../view_results_panel';
import { ProgressStats } from './progress_stats';
@ -49,6 +48,7 @@ export const CreateStepFooter: FC<Props> = ({ jobId, jobType, showProgress }) =>
const {
services: { notifications },
} = useMlKibana();
const ml = useMlApiContext();
useEffect(() => {
setInitialized(true);

View file

@ -14,12 +14,11 @@ import { extractErrorMessage } from '@kbn/ml-error-utils';
import { CreateDataViewForm } from '@kbn/ml-data-view-utils/components/create_data_view_form_row';
import { DestinationIndexForm } from '@kbn/ml-creation-wizard-utils/components/destination_index_form';
import { useMlKibana } from '../../../../../contexts/kibana';
import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana';
import type { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form';
import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation';
import { ContinueButton } from '../continue_button';
import { ANALYTICS_STEPS } from '../../page';
import { ml } from '../../../../../services/ml_api_service';
import { useCanCreateDataView } from '../../hooks/use_can_create_data_view';
import { useDataViewTimeFields } from '../../hooks/use_data_view_time_fields';
import { AdditionalSection } from './additional_section';
@ -43,6 +42,7 @@ export const DetailsStepForm: FC<CreateAnalyticsStepProps> = ({
const {
services: { docLinks, notifications },
} = useMlKibana();
const ml = useMlApiContext();
const canCreateDataView = useCanCreateDataView();
const { dataViewAvailableTimeFields, onTimeFieldChanged } = useDataViewTimeFields({

View file

@ -11,11 +11,11 @@ import type {
DfAnalyticsExplainResponse,
FieldSelectionItem,
} from '@kbn/ml-data-frame-analytics-utils';
import { ml } from '../../../../../services/ml_api_service';
import type { State } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { getJobConfigFromFormState } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import type { MlApiServices } from '../../../../../services/ml_api_service';
export const fetchExplainData = async (formState: State['form']) => {
export const fetchExplainData = async (mlApiServices: MlApiServices, formState: State['form']) => {
const jobConfig = getJobConfigFromFormState(formState);
let errorMessage = '';
let errorReason = '';
@ -28,9 +28,8 @@ export const fetchExplainData = async (formState: State['form']) => {
delete jobConfig.dest;
delete jobConfig.model_memory_limit;
delete jobConfig.analyzed_fields;
const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics(
jobConfig
);
const resp: DfAnalyticsExplainResponse =
await mlApiServices.dataFrameAnalytics.explainDataFrameAnalytics(jobConfig);
expectedMemory = resp.memory_estimation?.expected_memory_without_disk;
fieldSelection = resp.field_selection || [];
} catch (error) {

View file

@ -34,10 +34,9 @@ import {
INDEX_STATUS,
} from '@kbn/ml-data-grid';
import { useMlApiContext } from '../../../../contexts/kibana';
import { DataLoader } from '../../../../datavisualizer/index_based/data_loader';
import { ml } from '../../../../services/ml_api_service';
type IndexSearchResponse = estypes.SearchResponse;
interface MLEuiDataGridColumn extends EuiDataGridColumn {
@ -82,6 +81,7 @@ export const useIndexData = (
toastNotifications: CoreSetup['notifications']['toasts'],
runtimeMappings?: RuntimeMappings
): UseIndexDataReturnType => {
const ml = useMlApiContext();
// Fetch 500 random documents to determine populated fields.
// This is a workaround to avoid passing potentially thousands of unpopulated fields
// (for example, as part of filebeat/metricbeat/ECS based indices)
@ -258,7 +258,7 @@ export const useIndexData = (
]);
const dataLoader = useMemo(
() => new DataLoader(dataView, toastNotifications),
() => new DataLoader(dataView, ml),
// eslint-disable-next-line react-hooks/exhaustive-deps
[dataView]
);

View file

@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DataFrameAnalyticsId } from '@kbn/ml-data-frame-analytics-utils';
import { useDataSource } from '../../../contexts/ml/data_source_context';
import { ml } from '../../../services/ml_api_service';
import { useMlApiContext } from '../../../contexts/kibana';
import { useCreateAnalyticsForm } from '../analytics_management/hooks/use_create_analytics_form';
import { CreateAnalyticsAdvancedEditor } from './components/create_analytics_advanced_editor';
import {
@ -46,6 +46,7 @@ interface Props {
}
export const Page: FC<Props> = ({ jobId }) => {
const ml = useMlApiContext();
const [currentStep, setCurrentStep] = useState<ANALYTICS_STEPS>(ANALYTICS_STEPS.CONFIGURATION);
const [activatedSteps, setActivatedSteps] = useState<boolean[]>([
true,

View file

@ -16,11 +16,11 @@ import {
type DataFrameAnalyticsConfig,
} from '@kbn/ml-data-frame-analytics-utils';
import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useMlApiContext } from '../../../../../contexts/kibana';
import type { ResultsSearchQuery, ClassificationMetricItem } from '../../../../common/analytics';
import { isClassificationEvaluateResponse } from '../../../../common/analytics';
import { loadEvalData, loadDocsCount } from '../../../../common';
import { isTrainingFilter } from './is_training_filter';
@ -60,6 +60,8 @@ export const useConfusionMatrix = (
jobConfig: DataFrameAnalyticsConfig,
searchQuery: ResultsSearchQuery
) => {
const mlApiServices = useMlApiContext();
const newJobCapsServiceAnalytics = useNewJobCapsServiceAnalytics();
const [confusionMatrixData, setConfusionMatrixData] = useState<ConfusionMatrix[]>([]);
const [overallAccuracy, setOverallAccuracy] = useState<null | number>(null);
const [avgRecall, setAvgRecall] = useState<null | number>(null);
@ -87,6 +89,7 @@ export const useConfusionMatrix = (
}
const evalData = await loadEvalData({
mlApiServices,
isTraining,
index: jobConfig.dest.index,
dependentVariable,
@ -98,6 +101,7 @@ export const useConfusionMatrix = (
});
const docsCountResp = await loadDocsCount({
mlApiServices,
isTraining,
searchQuery,
resultsField,

View file

@ -15,7 +15,8 @@ import {
type RocCurveItem,
} from '@kbn/ml-data-frame-analytics-utils';
import { newJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics';
import { useMlApiContext } from '../../../../../contexts/kibana';
import type { ResultsSearchQuery } from '../../../../common/analytics';
import { isClassificationEvaluateResponse } from '../../../../common/analytics';
@ -39,6 +40,8 @@ export const useRocCurve = (
searchQuery: ResultsSearchQuery,
columns: string[]
) => {
const mlApiServices = useMlApiContext();
const newJobCapsServiceAnalytics = useNewJobCapsServiceAnalytics();
const classificationClasses = columns.filter(
(d) => d !== ACTUAL_CLASS_ID && d !== OTHER_CLASS_ID
);
@ -74,6 +77,7 @@ export const useRocCurve = (
for (let i = 0; i < classificationClasses.length; i++) {
const rocCurveClassName = classificationClasses[i];
const evalData = await loadEvalData({
mlApiServices,
isTraining: isTrainingFilter(searchQuery, resultsField),
index: jobConfig.dest.index,
dependentVariable,

View file

@ -17,13 +17,11 @@ import {
type DataFrameAnalysisConfigType,
} from '@kbn/ml-data-frame-analytics-utils';
import { ml } from '../../../../../services/ml_api_service';
import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics';
import type { DataFrameAnalyticsListRow } from '../../../analytics_management/components/analytics_list/common';
import { DATA_FRAME_MODE } from '../../../analytics_management/components/analytics_list/common';
import { ExpandedRow } from '../../../analytics_management/components/analytics_list/expanded_row';
import { useMlApiContext } from '../../../../../contexts/kibana';
import type { ExpandableSectionProps } from './expandable_section';
import { ExpandableSection, HEADER_ITEMS_LOADING } from './expandable_section';
@ -77,6 +75,8 @@ interface ExpandableSectionAnalyticsProps {
}
export const ExpandableSectionAnalytics: FC<ExpandableSectionAnalyticsProps> = ({ jobId }) => {
const ml = useMlApiContext();
const [expandedRowItem, setExpandedRowItem] = useState<DataFrameAnalyticsListRow | undefined>();
const fetchStats = async () => {

View file

@ -40,7 +40,6 @@ import {
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils';
import { getToastNotifications } from '../../../../../util/dependency_cache';
import type { useColorRange } from '../../../../../components/color_range_legend';
import { ColorRangeLegend } from '../../../../../components/color_range_legend';
import { useMlKibana } from '../../../../../contexts/kibana';
@ -140,6 +139,7 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
share,
data,
http: { basePath },
notifications: { toasts },
},
} = useMlKibana();
@ -394,7 +394,7 @@ export const ExpandableSectionResults: FC<ExpandableSectionResultsProps> = ({
}
dataTestSubj="mlExplorationDataGrid"
renderCellPopover={renderCellPopover}
toastNotifications={getToastNotifications()}
toastNotifications={toasts}
/>
)}
</>

View file

@ -14,9 +14,6 @@ import type {
DataFrameTaskStateType,
} from '@kbn/ml-data-frame-analytics-utils';
import { getToastNotifications } from '../../../../../util/dependency_cache';
import { useMlKibana } from '../../../../../contexts/kibana';
import type { ResultsSearchQuery } from '../../../../common/analytics';
import { ExpandableSectionResults } from '../expandable_section';
@ -33,19 +30,7 @@ interface Props {
export const ExplorationResultsTable: FC<Props> = React.memo(
({ dataView, jobConfig, needsDestDataView, searchQuery }) => {
const {
services: {
mlServices: { mlApiServices },
},
} = useMlKibana();
const classificationData = useExplorationResults(
dataView,
jobConfig,
searchQuery,
getToastNotifications(),
mlApiServices
);
const classificationData = useExplorationResults(dataView, jobConfig, searchQuery);
if (jobConfig === undefined || classificationData === undefined) {
return null;

View file

@ -9,7 +9,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import type { EuiDataGridColumn } from '@elastic/eui';
import type { CoreSetup } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import { extractErrorMessage } from '@kbn/ml-error-utils';
@ -35,7 +34,7 @@ import {
} from '@kbn/ml-data-grid';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { MlApiServices } from '../../../../../services/ml_api_service';
import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana';
import { DataLoader } from '../../../../../datavisualizer/index_based/data_loader';
import { getIndexData, getIndexFields } from '../../../../common';
@ -45,10 +44,14 @@ import { useExplorationDataGrid } from './use_exploration_data_grid';
export const useExplorationResults = (
dataView: DataView | undefined,
jobConfig: DataFrameAnalyticsConfig | undefined,
searchQuery: estypes.QueryDslQueryContainer,
toastNotifications: CoreSetup['notifications']['toasts'],
mlApiServices: MlApiServices
searchQuery: estypes.QueryDslQueryContainer
): UseIndexDataReturnType => {
const {
services: {
notifications: { toasts },
},
} = useMlKibana();
const ml = useMlApiContext();
const [baseline, setBaseLine] = useState<FeatureImportanceBaseline | undefined>();
const trainedModelsApiService = useTrainedModelsApiService();
@ -60,7 +63,7 @@ export const useExplorationResults = (
if (jobConfig !== undefined) {
const resultsField = jobConfig.dest.results_field!;
const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields);
const { fieldTypes } = getIndexFields(ml, jobConfig, needsDestIndexFields);
columns.push(
...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) =>
sortExplorationResultsFields(a.id, b.id, jobConfig)
@ -81,7 +84,7 @@ export const useExplorationResults = (
// passed on to `getIndexData`.
useEffect(() => {
const options = { didCancel: false };
getIndexData(jobConfig, dataGrid, searchQuery, options);
getIndexData(ml, jobConfig, dataGrid, searchQuery, options);
return () => {
options.didCancel = true;
};
@ -90,7 +93,7 @@ export const useExplorationResults = (
}, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]);
const dataLoader = useMemo(
() => (dataView !== undefined ? new DataLoader(dataView, toastNotifications) : undefined),
() => (dataView !== undefined ? new DataLoader(dataView, ml) : undefined),
// eslint-disable-next-line react-hooks/exhaustive-deps
[dataView]
);
@ -110,7 +113,7 @@ export const useExplorationResults = (
dataGrid.setColumnCharts(columnChartsData);
}
} catch (e) {
showDataGridColumnChartErrorMessageToast(e, toastNotifications);
showDataGridColumnChartErrorMessageToast(e, toasts);
}
};
@ -158,7 +161,7 @@ export const useExplorationResults = (
} catch (e) {
const error = extractErrorMessage(e);
toastNotifications.addDanger({
toasts.addDanger({
title: i18n.translate(
'xpack.ml.dataframe.analytics.explorationResults.baselineErrorMessageToast',
{
@ -169,7 +172,7 @@ export const useExplorationResults = (
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mlApiServices, jobConfig]);
}, [jobConfig]);
useEffect(() => {
getAnalyticsBaseline();

View file

@ -33,7 +33,7 @@ import {
COLOR_RANGE,
COLOR_RANGE_SCALE,
} from '../../../../../components/color_range_legend';
import { getToastNotifications } from '../../../../../util/dependency_cache';
import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana';
import { getIndexData, getIndexFields } from '../../../../common';
@ -45,6 +45,12 @@ export const useOutlierData = (
jobConfig: DataFrameAnalyticsConfig | undefined,
searchQuery: estypes.QueryDslQueryContainer
): UseIndexDataReturnType => {
const {
services: {
notifications: { toasts },
},
} = useMlKibana();
const ml = useMlApiContext();
const needsDestIndexFields =
dataView !== undefined && dataView.title === jobConfig?.source.index[0];
@ -53,7 +59,7 @@ export const useOutlierData = (
if (jobConfig !== undefined && dataView !== undefined) {
const resultsField = jobConfig.dest.results_field;
const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields);
const { fieldTypes } = getIndexFields(ml, jobConfig, needsDestIndexFields);
newColumns.push(
...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField!).sort((a: any, b: any) =>
sortExplorationResultsFields(a.id, b.id, jobConfig)
@ -86,7 +92,7 @@ export const useOutlierData = (
// passed on to `getIndexData`.
useEffect(() => {
const options = { didCancel: false };
getIndexData(jobConfig, dataGrid, searchQuery, options);
getIndexData(ml, jobConfig, dataGrid, searchQuery, options);
return () => {
options.didCancel = true;
};
@ -95,7 +101,9 @@ export const useOutlierData = (
}, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]);
const dataLoader = useMemo(
() => (dataView !== undefined ? new DataLoader(dataView, getToastNotifications()) : undefined),
() => (dataView !== undefined ? new DataLoader(dataView, ml) : undefined),
// skip ml API services from deps check
// eslint-disable-next-line react-hooks/exhaustive-deps
[dataView]
);
@ -114,7 +122,7 @@ export const useOutlierData = (
dataGrid.setColumnCharts(columnChartsData);
}
} catch (e) {
showDataGridColumnChartErrorMessageToast(e, getToastNotifications());
showDataGridColumnChartErrorMessageToast(e, toasts);
}
};

View file

@ -28,7 +28,7 @@ import {
} from '@kbn/ml-data-frame-analytics-utils';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { useMlKibana } from '../../../../../contexts/kibana';
import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana';
import type { Eval } from '../../../../common';
import { getValuesFromResponse, loadEvalData, loadDocsCount } from '../../../../common';
@ -65,6 +65,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
const {
services: { docLinks },
} = useMlKibana();
const mlApiServices = useMlApiContext();
const docLink = docLinks.links.ml.regressionEvaluation;
const [trainingEval, setTrainingEval] = useState<Eval>(defaultEval);
const [generalizationEval, setGeneralizationEval] = useState<Eval>(defaultEval);
@ -84,6 +85,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
setIsLoadingGeneralization(true);
const genErrorEval = await loadEvalData({
mlApiServices,
isTraining: false,
index,
dependentVariable,
@ -122,6 +124,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
setIsLoadingTraining(true);
const trainingErrorEval = await loadEvalData({
mlApiServices,
isTraining: true,
index,
dependentVariable,
@ -159,6 +162,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
const loadData = async () => {
loadGeneralizationData(false);
const genDocsCountResp = await loadDocsCount({
mlApiServices,
ignoreDefaultQuery: false,
isTraining: false,
searchQuery,
@ -173,6 +177,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
loadTrainingData(false);
const trainDocsCountResp = await loadDocsCount({
mlApiServices,
ignoreDefaultQuery: false,
isTraining: true,
searchQuery,

View file

@ -21,11 +21,8 @@ jest.mock('../../../../../capabilities/check_capabilities', () => ({
createPermissionFailureMessage: jest.fn(),
}));
jest.mock('../../../../../util/dependency_cache', () => ({
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
}));
jest.mock('../../../../../contexts/kibana', () => ({
useMlApiContext: jest.fn(),
useMlKibana: () => ({
services: { ...mockCoreServices.createStart(), data: { data_view: { find: jest.fn() } } },
}),

View file

@ -14,9 +14,9 @@ import { useMlKibana } from '../../../../../contexts/kibana';
import { useToastNotificationService } from '../../../../../services/toast_notification_service';
import {
deleteAnalytics,
deleteAnalyticsAndDestIndex,
canDeleteIndex,
useDeleteAnalytics,
useDeleteAnalyticsAndDestIndex,
useCanDeleteIndex,
} from '../../services/analytics_service';
import type {
@ -56,6 +56,9 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
const indexName = getDestinationIndex(item?.config);
const toastNotificationService = useToastNotificationService();
const deleteAnalytics = useDeleteAnalytics();
const deleteAnalyticsAndDestIndex = useDeleteAnalyticsAndDestIndex();
const canDeleteIndex = useCanDeleteIndex();
const checkDataViewExists = async () => {
try {
@ -83,7 +86,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
};
const checkUserIndexPermission = async () => {
try {
const userCanDelete = await canDeleteIndex(indexName, toastNotificationService);
const userCanDelete = await canDeleteIndex(indexName);
if (userCanDelete) {
setUserCanDeleteIndex(true);
}
@ -133,11 +136,10 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
deleteAnalyticsAndDestIndex(
item.config,
deleteTargetIndex,
dataViewExists && deleteDataView,
toastNotificationService
dataViewExists && deleteDataView
);
} else {
deleteAnalytics(item.config, toastNotificationService);
deleteAnalytics(item.config);
}
}
};

View file

@ -37,7 +37,6 @@ import {
} from '@kbn/ml-data-frame-analytics-utils';
import { useMlKibana, useMlApiContext } from '../../../../../contexts/kibana';
import { ml } from '../../../../../services/ml_api_service';
import { useToastNotificationService } from '../../../../../services/toast_notification_service';
import type { MemoryInputValidatorResult } from '../../../../../../../common/util/validators';
import { memoryInputValidator } from '../../../../../../../common/util/validators';
@ -70,10 +69,10 @@ export const EditActionFlyout: FC<Required<EditAction>> = ({ closeFlyout, item }
} = useMlKibana();
const { refresh } = useRefreshAnalyticsList();
const mlApiServices = useMlApiContext();
const ml = useMlApiContext();
const {
dataFrameAnalytics: { getDataFrameAnalytics },
} = mlApiServices;
} = ml;
const toastNotificationService = useToastNotificationService();

View file

@ -16,8 +16,7 @@ import {
isDataFrameAnalyticsFailed,
isDataFrameAnalyticsRunning,
} from '../analytics_list/common';
import { startAnalytics } from '../../services/analytics_service';
import { useToastNotificationService } from '../../../../../services/toast_notification_service';
import { useStartAnalytics } from '../../services/analytics_service';
import { startActionNameText, StartActionName } from './start_action_name';
@ -27,13 +26,13 @@ export const useStartAction = (canStartStopDataFrameAnalytics: boolean) => {
const [item, setItem] = useState<DataFrameAnalyticsListRow>();
const toastNotificationService = useToastNotificationService();
const startAnalytics = useStartAnalytics();
const closeModal = () => setModalVisible(false);
const startAndCloseModal = () => {
if (item !== undefined) {
setModalVisible(false);
startAnalytics(item, toastNotificationService);
startAnalytics(item);
}
};

View file

@ -12,14 +12,15 @@ import type {
DataFrameAnalyticsListRow,
} from '../analytics_list/common';
import { isDataFrameAnalyticsFailed, isDataFrameAnalyticsRunning } from '../analytics_list/common';
import { stopAnalytics } from '../../services/analytics_service';
import { useStopAnalytics } from '../../services/analytics_service';
import { stopActionNameText, StopActionName } from './stop_action_name';
export type StopAction = ReturnType<typeof useStopAction>;
export const useStopAction = (canStartStopDataFrameAnalytics: boolean) => {
const [isModalVisible, setModalVisible] = useState(false);
const stopAnalytics = useStopAnalytics();
const [isModalVisible, setModalVisible] = useState(false);
const [item, setItem] = useState<DataFrameAnalyticsListRow>();
const closeModal = () => setModalVisible(false);

View file

@ -30,7 +30,7 @@ import { ML_PAGES } from '../../../../../../../common/constants/locator';
import type { DataFrameAnalyticsListRow, ItemIdToExpandedRowMap } from './common';
import { DataFrameAnalyticsListColumn } from './common';
import { getAnalyticsFactory } from '../../services/analytics_service';
import { useGetAnalytics } from '../../services/analytics_service';
import { getJobTypeBadge, getTaskStateBadge, useColumns } from './use_columns';
import { ExpandedRow } from './expanded_row';
import type { AnalyticStatsBarStats } from '../../../../../components/stats_bar';
@ -127,7 +127,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
const disabled = !canCreateDataFrameAnalytics || !canStartStopDataFrameAnalytics;
const getAnalytics = getAnalyticsFactory(
const getAnalytics = useGetAnalytics(
setAnalytics,
setAnalyticsStats,
setErrorMessage,

View file

@ -10,7 +10,7 @@ import './expanded_row_messages_pane.scss';
import type { FC } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { ml } from '../../../../../services/ml_api_service';
import { useMlApiContext } from '../../../../../contexts/kibana';
import { useRefreshAnalyticsList } from '../../../../common';
import { JobMessages } from '../../../../../components/job_messages';
import type { JobMessage } from '../../../../../../../common/types/audit_message';
@ -22,6 +22,7 @@ interface Props {
}
export const ExpandedRowMessagesPane: FC<Props> = ({ analyticsId, dataTestSubj }) => {
const ml = useMlApiContext();
const [messages, setMessages] = useState<JobMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');

View file

@ -13,9 +13,8 @@ import { extractErrorMessage } from '@kbn/ml-error-utils';
import { extractErrorProperties } from '@kbn/ml-error-utils';
import type { DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-utils';
import { useMlKibana } from '../../../../../contexts/kibana';
import { useMlApiContext, useMlKibana } from '../../../../../contexts/kibana';
import type { DeepReadonly } from '../../../../../../../common/types/common';
import { ml } from '../../../../../services/ml_api_service';
import { useRefreshAnalyticsList } from '../../../../common';
import { extractCloningConfig, isAdvancedConfig } from '../../components/action_clone';
@ -50,6 +49,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
data: { dataViews },
},
} = useMlKibana();
const ml = useMlApiContext();
const [state, dispatch] = useReducer(reducer, getInitialState());
const { refresh } = useRefreshAnalyticsList();

View file

@ -8,143 +8,159 @@
import { i18n } from '@kbn/i18n';
import { extractErrorMessage } from '@kbn/ml-error-utils';
import { ml } from '../../../../../services/ml_api_service';
import type { ToastNotificationService } from '../../../../../services/toast_notification_service';
import { useMlApiContext } from '../../../../../contexts/kibana';
import { useToastNotificationService } from '../../../../../services/toast_notification_service';
import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common';
import type { DataFrameAnalyticsListRow } from '../../components/analytics_list/common';
export const deleteAnalytics = async (
analyticsConfig: DataFrameAnalyticsListRow['config'],
toastNotificationService: ToastNotificationService
) => {
try {
await ml.dataFrameAnalytics.deleteDataFrameAnalytics(analyticsConfig.id);
toastNotificationService.displaySuccessToast(
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', {
defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.',
values: { analyticsId: analyticsConfig.id },
})
);
} catch (e) {
toastNotificationService.displayErrorToast(
e,
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}',
values: { analyticsId: analyticsConfig.id },
})
);
}
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
};
export const useDeleteAnalytics = () => {
const toastNotificationService = useToastNotificationService();
const ml = useMlApiContext();
export const deleteAnalyticsAndDestIndex = async (
analyticsConfig: DataFrameAnalyticsListRow['config'],
deleteDestIndex: boolean,
deleteDestDataView: boolean,
toastNotificationService: ToastNotificationService
) => {
const destinationIndex = analyticsConfig.dest.index;
try {
const status = await ml.dataFrameAnalytics.deleteDataFrameAnalyticsAndDestIndex(
analyticsConfig.id,
deleteDestIndex,
deleteDestDataView
);
if (status.analyticsJobDeleted?.success) {
return async (analyticsConfig: DataFrameAnalyticsListRow['config']) => {
try {
await ml.dataFrameAnalytics.deleteDataFrameAnalytics(analyticsConfig.id);
toastNotificationService.displaySuccessToast(
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', {
defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.',
values: { analyticsId: analyticsConfig.id },
})
);
}
if (status.analyticsJobDeleted?.error) {
} catch (e) {
toastNotificationService.displayErrorToast(
status.analyticsJobDeleted.error,
e,
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}',
values: { analyticsId: analyticsConfig.id },
})
);
}
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
};
};
if (status.destIndexDeleted?.success) {
toastNotificationService.displaySuccessToast(
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage', {
defaultMessage: 'Request to delete destination index {destinationIndex} acknowledged.',
values: { destinationIndex },
})
);
}
if (status.destIndexDeleted?.error) {
toastNotificationService.displayErrorToast(
status.destIndexDeleted.error,
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage', {
defaultMessage: 'An error occurred deleting destination index {destinationIndex}',
values: { destinationIndex },
})
);
}
export const useDeleteAnalyticsAndDestIndex = () => {
const toastNotificationService = useToastNotificationService();
const ml = useMlApiContext();
if (status.destDataViewDeleted?.success) {
toastNotificationService.displaySuccessToast(
i18n.translate(
'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewSuccessMessage',
{
defaultMessage: 'Request to delete data view {destinationIndex} acknowledged.',
return async (
analyticsConfig: DataFrameAnalyticsListRow['config'],
deleteDestIndex: boolean,
deleteDestDataView: boolean
) => {
const destinationIndex = analyticsConfig.dest.index;
try {
const status = await ml.dataFrameAnalytics.deleteDataFrameAnalyticsAndDestIndex(
analyticsConfig.id,
deleteDestIndex,
deleteDestDataView
);
if (status.analyticsJobDeleted?.success) {
toastNotificationService.displaySuccessToast(
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', {
defaultMessage:
'Request to delete data frame analytics job {analyticsId} acknowledged.',
values: { analyticsId: analyticsConfig.id },
})
);
}
if (status.analyticsJobDeleted?.error) {
toastNotificationService.displayErrorToast(
status.analyticsJobDeleted.error,
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}',
values: { analyticsId: analyticsConfig.id },
})
);
}
if (status.destIndexDeleted?.success) {
toastNotificationService.displaySuccessToast(
i18n.translate(
'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage',
{
defaultMessage:
'Request to delete destination index {destinationIndex} acknowledged.',
values: { destinationIndex },
}
)
);
}
if (status.destIndexDeleted?.error) {
toastNotificationService.displayErrorToast(
status.destIndexDeleted.error,
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage', {
defaultMessage: 'An error occurred deleting destination index {destinationIndex}',
values: { destinationIndex },
}
)
);
}
if (status.destDataViewDeleted?.error) {
const error = extractErrorMessage(status.destDataViewDeleted.error);
toastNotificationService.displayDangerToast(
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewErrorMessage', {
defaultMessage: 'An error occurred deleting data view {destinationIndex}: {error}',
values: { destinationIndex, error },
})
);
}
if (status.destDataViewDeleted?.success) {
toastNotificationService.displaySuccessToast(
i18n.translate(
'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewSuccessMessage',
{
defaultMessage: 'Request to delete data view {destinationIndex} acknowledged.',
values: { destinationIndex },
}
)
);
}
if (status.destDataViewDeleted?.error) {
const error = extractErrorMessage(status.destDataViewDeleted.error);
toastNotificationService.displayDangerToast(
i18n.translate(
'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithDataViewErrorMessage',
{
defaultMessage: 'An error occurred deleting data view {destinationIndex}: {error}',
values: { destinationIndex, error },
}
)
);
}
} catch (e) {
toastNotificationService.displayErrorToast(
e,
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}',
values: { analyticsId: analyticsConfig.id },
})
);
}
} catch (e) {
toastNotificationService.displayErrorToast(
e,
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}',
values: { analyticsId: analyticsConfig.id },
})
);
}
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
};
};
export const canDeleteIndex = async (
indexName: string,
toastNotificationService: ToastNotificationService
) => {
try {
const privilege = await ml.hasPrivileges({
index: [
{
names: [indexName], // uses wildcard
privileges: ['delete_index'],
},
],
});
if (!privilege) {
return false;
export const useCanDeleteIndex = () => {
const toastNotificationService = useToastNotificationService();
const ml = useMlApiContext();
return async (indexName: string) => {
try {
const privilege = await ml.hasPrivileges({
index: [
{
names: [indexName], // uses wildcard
privileges: ['delete_index'],
},
],
});
if (!privilege) {
return false;
}
return (
privilege.hasPrivileges === undefined || privilege.hasPrivileges.has_all_requested === true
);
} catch (e) {
const error = extractErrorMessage(e);
toastNotificationService.displayDangerToast(
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage', {
defaultMessage: 'User does not have permission to delete index {indexName}: {error}',
values: { indexName, error },
})
);
}
return (
privilege.hasPrivileges === undefined || privilege.hasPrivileges.has_all_requested === true
);
} catch (e) {
const error = extractErrorMessage(e);
toastNotificationService.displayDangerToast(
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage', {
defaultMessage: 'User does not have permission to delete index {indexName}: {error}',
values: { indexName, error },
})
);
}
};
};

View file

@ -11,7 +11,7 @@ import {
type DataFrameAnalysisConfigType,
DATA_FRAME_TASK_STATE,
} from '@kbn/ml-data-frame-analytics-utils';
import { ml } from '../../../../../services/ml_api_service';
import { useMlApiContext } from '../../../../../contexts/kibana';
import type {
GetDataFrameAnalyticsStatsResponseError,
GetDataFrameAnalyticsStatsResponseOk,
@ -106,7 +106,7 @@ export function getAnalyticsJobsStats(
return resultStats;
}
export const getAnalyticsFactory = (
export const useGetAnalytics = (
setAnalytics: React.Dispatch<React.SetStateAction<DataFrameAnalyticsListRow[]>>,
setAnalyticsStats: (update: AnalyticStatsBarStats | undefined) => void,
setErrorMessage: React.Dispatch<
@ -116,6 +116,8 @@ export const getAnalyticsFactory = (
setJobsAwaitingNodeCount: React.Dispatch<React.SetStateAction<number>>,
blockRefresh: boolean
): GetAnalytics => {
const ml = useMlApiContext();
let concurrentLoads = 0;
const getAnalytics = async (forceRefresh = false) => {

View file

@ -5,7 +5,11 @@
* 2.0.
*/
export { getAnalyticsFactory } from './get_analytics';
export { deleteAnalytics, deleteAnalyticsAndDestIndex, canDeleteIndex } from './delete_analytics';
export { startAnalytics } from './start_analytics';
export { stopAnalytics } from './stop_analytics';
export { useGetAnalytics } from './get_analytics';
export {
useDeleteAnalytics,
useDeleteAnalyticsAndDestIndex,
useCanDeleteIndex,
} from './delete_analytics';
export { useStartAnalytics } from './start_analytics';
export { useStopAnalytics } from './stop_analytics';

View file

@ -6,32 +6,35 @@
*/
import { i18n } from '@kbn/i18n';
import { ml } from '../../../../../services/ml_api_service';
import type { ToastNotificationService } from '../../../../../services/toast_notification_service';
import { useMlApiContext } from '../../../../../contexts/kibana';
import { useToastNotificationService } from '../../../../../services/toast_notification_service';
import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common';
import type { DataFrameAnalyticsListRow } from '../../components/analytics_list/common';
export const startAnalytics = async (
d: DataFrameAnalyticsListRow,
toastNotificationService: ToastNotificationService
) => {
try {
await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id);
toastNotificationService.displaySuccessToast(
i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage', {
defaultMessage: 'Request to start data frame analytics {analyticsId} acknowledged.',
values: { analyticsId: d.config.id },
})
);
} catch (e) {
toastNotificationService.displayErrorToast(
e,
i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsErrorTitle', {
defaultMessage: 'Error starting job',
})
);
}
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
export const useStartAnalytics = () => {
const toastNotificationService = useToastNotificationService();
const ml = useMlApiContext();
return async (d: DataFrameAnalyticsListRow) => {
try {
await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id);
toastNotificationService.displaySuccessToast(
i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage', {
defaultMessage: 'Request to start data frame analytics {analyticsId} acknowledged.',
values: { analyticsId: d.config.id },
})
);
} catch (e) {
toastNotificationService.displayErrorToast(
e,
i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsErrorTitle', {
defaultMessage: 'Error starting job',
})
);
}
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
};
};

View file

@ -6,35 +6,41 @@
*/
import { i18n } from '@kbn/i18n';
import { getToastNotifications } from '../../../../../util/dependency_cache';
import { ml } from '../../../../../services/ml_api_service';
import { useMlApiContext } from '../../../../../contexts/kibana';
import { useToastNotificationService } from '../../../../../services/toast_notification_service';
import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common';
import type { DataFrameAnalyticsListRow } from '../../components/analytics_list/common';
import { isDataFrameAnalyticsFailed } from '../../components/analytics_list/common';
export const stopAnalytics = async (d: DataFrameAnalyticsListRow) => {
const toastNotifications = getToastNotifications();
try {
await ml.dataFrameAnalytics.stopDataFrameAnalytics(
d.config.id,
isDataFrameAnalyticsFailed(d.stats.state)
);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage', {
defaultMessage: 'Request to stop data frame analytics {analyticsId} acknowledged.',
values: { analyticsId: d.config.id },
})
);
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsErrorMessage', {
defaultMessage:
'An error occurred stopping the data frame analytics {analyticsId}: {error}',
values: { analyticsId: d.config.id, error: JSON.stringify(e) },
})
);
}
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
export const useStopAnalytics = () => {
const toastNotificationService = useToastNotificationService();
const ml = useMlApiContext();
return async (d: DataFrameAnalyticsListRow) => {
try {
await ml.dataFrameAnalytics.stopDataFrameAnalytics(
d.config.id,
isDataFrameAnalyticsFailed(d.stats.state)
);
toastNotificationService.displaySuccessToast(
i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage', {
defaultMessage: 'Request to stop data frame analytics {analyticsId} acknowledged.',
values: { analyticsId: d.config.id },
})
);
} catch (e) {
toastNotificationService.displayErrorToast(
e,
i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsErrorMessage', {
defaultMessage:
'An error occurred stopping the data frame analytics {analyticsId}: {error}',
values: { analyticsId: d.config.id, error: JSON.stringify(e) },
})
);
}
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH);
};
};

View file

@ -14,8 +14,7 @@ import {
JOB_MAP_NODE_TYPES,
type AnalyticsMapReturnType,
} from '@kbn/ml-data-frame-analytics-utils';
import { ml } from '../../../services/ml_api_service';
import { useMlApiContext } from '../../../contexts/kibana';
interface GetDataObjectParameter {
analyticsId?: string;
id?: string;
@ -24,6 +23,7 @@ interface GetDataObjectParameter {
}
export const useFetchAnalyticsMapData = () => {
const ml = useMlApiContext();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [elements, setElements] = useState<cytoscape.ElementDefinition[]>([]);
const [error, setError] = useState<any>();

View file

@ -17,7 +17,7 @@ import type {
import { useTimefilter } from '@kbn/ml-date-picker';
import type { ResultLinks } from '@kbn/data-visualizer-plugin/common/app';
import { HelpMenu } from '../../components/help_menu';
import { useMlKibana, useMlLocator } from '../../contexts/kibana';
import { useMlApiContext, useMlKibana, useMlLocator } from '../../contexts/kibana';
import { ML_PAGES } from '../../../../common/constants/locator';
import { isFullLicense } from '../../license';
@ -36,8 +36,9 @@ export const FileDataVisualizerPage: FC = () => {
},
},
} = useMlKibana();
const mlApiServices = useMlApiContext();
const mlLocator = useMlLocator()!;
getMlNodeCount();
getMlNodeCount(mlApiServices);
const [FileDataVisualizer, setFileDataVisualizer] = useState<FileDataVisualizerSpec | null>(null);
const [resultLinks, setResultLinks] = useState<ResultLinks | null>(null);
@ -104,7 +105,7 @@ export const FileDataVisualizerPage: FC = () => {
useEffect(() => {
// ML uses this function
if (dataVisualizer !== undefined) {
getMlNodeCount();
getMlNodeCount(mlApiServices);
const { getFileDataVisualizerComponent } = dataVisualizer;
getFileDataVisualizerComponent().then((resp) => {
const items = resp();

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import type { CoreSetup } from '@kbn/core/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { DEFAULT_SAMPLER_SHARD_SIZE } from '@kbn/ml-agg-utils';
import { OMIT_FIELDS } from '@kbn/ml-anomaly-utils';
@ -14,23 +12,21 @@ import { type RuntimeMappings } from '@kbn/ml-runtime-field-utils';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { IndexPatternTitle } from '../../../../../common/types/kibana';
import type { MlApiServices } from '../../../services/ml_api_service';
import { ml } from '../../../services/ml_api_service';
import type { FieldHistogramRequestConfig } from '../common/request';
// Maximum number of examples to obtain for text type fields.
const MAX_EXAMPLES_DEFAULT: number = 10;
export class DataLoader {
private _indexPattern: DataView;
private _runtimeMappings: RuntimeMappings;
private _indexPatternTitle: IndexPatternTitle = '';
private _maxExamples: number = MAX_EXAMPLES_DEFAULT;
constructor(indexPattern: DataView, toastNotifications?: CoreSetup['notifications']['toasts']) {
this._indexPattern = indexPattern;
constructor(private _indexPattern: DataView, private _mlApiServices: MlApiServices) {
this._runtimeMappings = this._indexPattern.getComputedFields().runtimeFields as RuntimeMappings;
this._indexPatternTitle = indexPattern.title;
this._indexPatternTitle = _indexPattern.title;
}
async loadFieldHistograms(
@ -39,7 +35,7 @@ export class DataLoader {
samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE,
editorRuntimeMappings?: RuntimeMappings
): Promise<any[]> {
const stats = await ml.getVisualizerFieldHistograms({
const stats = await this._mlApiServices.getVisualizerFieldHistograms({
indexPattern: this._indexPatternTitle,
query,
fields,

View file

@ -18,7 +18,7 @@ import type {
import { useTimefilter } from '@kbn/ml-date-picker';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import useMountedState from 'react-use/lib/useMountedState';
import { useMlKibana, useMlLocator } from '../../contexts/kibana';
import { useMlApiContext, useMlKibana, useMlLocator } from '../../contexts/kibana';
import { HelpMenu } from '../../components/help_menu';
import { ML_PAGES } from '../../../../common/constants/locator';
import { isFullLicense } from '../../license';
@ -40,10 +40,11 @@ export const IndexDataVisualizerPage: FC<{ esql: boolean }> = ({ esql = false })
},
},
} = useMlKibana();
const mlApiServices = useMlApiContext();
const { showNodeInfo } = useEnabledFeatures();
const mlLocator = useMlLocator()!;
const mlFeaturesDisabled = !isFullLicense();
getMlNodeCount();
getMlNodeCount(mlApiServices);
const [IndexDataVisualizer, setIndexDataVisualizer] = useState<IndexDataVisualizerSpec | null>(
null

View file

@ -9,12 +9,13 @@ import { from } from 'rxjs';
import { map } from 'rxjs';
import type { MlFieldFormatService } from '../../services/field_format_service';
import { mlJobService } from '../../services/job_service';
import type { MlJobService } from '../../services/job_service';
import { EXPLORER_ACTION } from '../explorer_constants';
import { createJobs } from '../explorer_utils';
export function jobSelectionActionCreator(
mlJobService: MlJobService,
mlFieldFormatService: MlFieldFormatService,
selectedJobIds: string[]
) {

View file

@ -18,6 +18,7 @@ import type { TimefilterContract } from '@kbn/data-plugin/public';
import { useTimefilter } from '@kbn/ml-date-picker';
import type { InfluencersFilterQuery } from '@kbn/ml-anomaly-utils';
import type { TimeBucketsInterval, TimeRangeBounds } from '@kbn/ml-time-buckets';
import type { IUiSettingsClient } from '@kbn/core/public';
import type { AppStateSelectedCells, ExplorerJob } from '../explorer_utils';
import {
getDateFormatTz,
@ -31,11 +32,13 @@ import {
loadOverallAnnotations,
} from '../explorer_utils';
import type { ExplorerState } from '../reducers';
import { useMlKibana } from '../../contexts/kibana';
import { useMlApiContext, useUiSettings } from '../../contexts/kibana';
import type { MlResultsService } from '../../services/results_service';
import { mlResultsServiceProvider } from '../../services/results_service';
import type { AnomalyExplorerChartsService } from '../../services/anomaly_explorer_charts_service';
import { useAnomalyExplorerContext } from '../anomaly_explorer_context';
import type { MlApiServices } from '../../services/ml_api_service';
import { useMlJobService, type MlJobService } from '../../services/job_service';
// Memoize the data fetching methods.
// wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument
@ -93,6 +96,9 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi
* Fetches the data necessary for the Anomaly Explorer using observables.
*/
const loadExplorerDataProvider = (
uiSettings: IUiSettingsClient,
mlApiServices: MlApiServices,
mlJobService: MlJobService,
mlResultsService: MlResultsService,
anomalyExplorerChartsService: AnomalyExplorerChartsService,
timefilter: TimefilterContract
@ -120,14 +126,20 @@ const loadExplorerDataProvider = (
const timerange = getSelectionTimeRange(selectedCells, bounds);
const dateFormatTz = getDateFormatTz();
const dateFormatTz = getDateFormatTz(uiSettings);
// First get the data where we have all necessary args at hand using forkJoin:
// annotationsData, anomalyChartRecords, influencers, overallState, tableData
return forkJoin({
overallAnnotations: memoizedLoadOverallAnnotations(lastRefresh, selectedJobs, bounds),
overallAnnotations: memoizedLoadOverallAnnotations(
lastRefresh,
mlApiServices,
selectedJobs,
bounds
),
annotationsData: memoizedLoadAnnotationsTableData(
lastRefresh,
mlApiServices,
selectedCells,
selectedJobs,
bounds
@ -155,6 +167,8 @@ const loadExplorerDataProvider = (
: Promise.resolve({}),
tableData: memoizedLoadAnomaliesTableData(
lastRefresh,
mlApiServices,
mlJobService,
selectedCells,
selectedJobs,
dateFormatTz,
@ -202,20 +216,23 @@ const loadExplorerDataProvider = (
};
export const useExplorerData = (): [Partial<ExplorerState> | undefined, (d: any) => void] => {
const uiSettings = useUiSettings();
const timefilter = useTimefilter();
const {
services: {
mlServices: { mlApiServices },
},
} = useMlKibana();
const mlApiServices = useMlApiContext();
const mlJobService = useMlJobService();
const { anomalyExplorerChartsService } = useAnomalyExplorerContext();
const loadExplorerData = useMemo(() => {
const mlResultsService = mlResultsServiceProvider(mlApiServices);
return loadExplorerDataProvider(mlResultsService, anomalyExplorerChartsService, timefilter);
return loadExplorerDataProvider(
uiSettings,
mlApiServices,
mlJobService,
mlResultsService,
anomalyExplorerChartsService,
timefilter
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View file

@ -19,6 +19,7 @@ import { AnomalyExplorerChartsService } from '../services/anomaly_explorer_chart
import { useTableSeverity } from '../components/controls/select_severity';
import { AnomalyDetectionAlertsStateService } from './alerts';
import { explorerServiceFactory, type ExplorerService } from './explorer_dashboard_service';
import { useMlJobService } from '../services/job_service';
export interface AnomalyExplorerContextValue {
anomalyExplorerChartsService: AnomalyExplorerChartsService;
@ -65,6 +66,7 @@ export const AnomalyExplorerContextProvider: FC<PropsWithChildren<unknown>> = ({
data,
},
} = useMlKibana();
const mlJobService = useMlJobService();
const [, , tableSeverityState] = useTableSeverity();
@ -80,7 +82,7 @@ export const AnomalyExplorerContextProvider: FC<PropsWithChildren<unknown>> = ({
// updates so using `useEffect` is the right thing to do here to not get errors
// related to React lifecycle methods.
useEffect(() => {
const explorerService = explorerServiceFactory(mlFieldFormatService);
const explorerService = explorerServiceFactory(mlJobService, mlFieldFormatService);
const anomalyTimelineService = new AnomalyTimelineService(
timefilter,
@ -93,6 +95,7 @@ export const AnomalyExplorerContextProvider: FC<PropsWithChildren<unknown>> = ({
);
const anomalyTimelineStateService = new AnomalyTimelineStateService(
mlJobService,
anomalyExplorerUrlStateService,
anomalyExplorerCommonStateService,
anomalyTimelineService,

View file

@ -38,8 +38,7 @@ import {
SWIMLANE_TYPE,
VIEW_BY_JOB_LABEL,
} from './explorer_constants';
// FIXME get rid of the static import
import { mlJobService } from '../services/job_service';
import type { MlJobService } from '../services/job_service';
import { getSelectionInfluencers, getSelectionTimeRange } from './explorer_utils';
import type { Refresh } from '../routing/use_refresh';
import { StateService } from '../services/state_service';
@ -107,6 +106,7 @@ export class AnomalyTimelineStateService extends StateService {
);
constructor(
private mlJobService: MlJobService,
private anomalyExplorerUrlStateService: AnomalyExplorerUrlStateService,
private anomalyExplorerCommonStateService: AnomalyExplorerCommonStateService,
private anomalyTimelineService: AnomalyTimelineService,
@ -482,6 +482,7 @@ export class AnomalyTimelineStateService extends StateService {
selectedCells: AppStateSelectedCells | undefined | null,
selectedJobs: ExplorerJob[] | undefined
) {
const mlJobService = this.mlJobService;
const selectedJobIds = selectedJobs?.map((d) => d.id) ?? [];
// Unique influencers for the selected job(s).

View file

@ -380,6 +380,7 @@ export const Explorer: FC<ExplorerUIProps> = ({
services: {
charts: chartsService,
data: { dataViews: dataViewsService },
uiSettings,
},
} = useMlKibana();
const { euiTheme } = useEuiTheme();
@ -442,7 +443,7 @@ export const Explorer: FC<ExplorerUIProps> = ({
);
const jobSelectorProps = {
dateFormatTz: getDateFormatTz(),
dateFormatTz: getDateFormatTz(uiSettings),
} as JobSelectorProps;
const noJobsSelected = !selectedJobs || selectedJobs.length === 0;

View file

@ -1,53 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`buildConfig get dataConfig for anomaly record 1`] = `
Object {
"bucketSpanSeconds": 900,
"datafeedConfig": Object {
"chunking_config": Object {
"mode": "auto",
},
"datafeed_id": "datafeed-mock-job-id",
"indices": Array [
"farequote-2017",
],
"job_id": "mock-job-id",
"query": Object {
"match_all": Object {
"boost": 1,
},
},
"query_delay": "86658ms",
"scroll_size": 1000,
"state": "stopped",
},
"detectorIndex": 0,
"detectorLabel": "mean(responsetime)",
"entityFields": Array [
Object {
"fieldName": "airline",
"fieldType": "partition",
"fieldValue": "JAL",
},
],
"fieldName": "responsetime",
"functionDescription": "mean",
"infoTooltip": Object {
"aggregationInterval": "15m",
"chartFunction": "avg responsetime",
"entityFields": Array [
Object {
"fieldName": "airline",
"fieldValue": "JAL",
},
],
"jobId": "mock-job-id",
},
"interval": "15m",
"jobId": "mock-job-id",
"metricFieldName": "responsetime",
"metricFunction": "avg",
"summaryCountFieldName": undefined,
"timeField": "@timestamp",
}
`;

View file

@ -1,74 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/*
* Builds the configuration object used to plot a chart showing where the anomalies occur in
* the raw data in the Explorer dashboard.
*/
import { parseInterval } from '../../../../common/util/parse_interval';
import { getEntityFieldList, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils';
import { buildConfigFromDetector } from '../../util/chart_config_builder';
import { mlJobService } from '../../services/job_service';
import { mlFunctionToESAggregation } from '../../../../common/util/job_utils';
// Builds the chart configuration for the provided anomaly record, returning
// an object with properties used for the display (series function and field, aggregation interval etc),
// and properties for the datafeed used for the job (indices, time field etc).
export function buildConfig(record) {
const job = mlJobService.getJob(record.job_id);
const detectorIndex = record.detector_index;
const config = buildConfigFromDetector(job, detectorIndex);
// Add extra properties used by the explorer dashboard charts.
config.functionDescription = record.function_description;
config.bucketSpanSeconds = parseInterval(job.analysis_config.bucket_span).asSeconds();
config.detectorLabel = record.function;
if (
mlJobService.detectorsByJob[record.job_id] !== undefined &&
detectorIndex < mlJobService.detectorsByJob[record.job_id].length
) {
config.detectorLabel =
mlJobService.detectorsByJob[record.job_id][detectorIndex].detector_description;
} else {
if (record.field_name !== undefined) {
config.detectorLabel += ` ${config.fieldName}`;
}
}
if (record.field_name !== undefined) {
config.fieldName = record.field_name;
config.metricFieldName = record.field_name;
}
// Add the 'entity_fields' i.e. the partition, by, over fields which
// define the metric series to be plotted.
config.entityFields = getEntityFieldList(record);
if (record.function === ML_JOB_AGGREGATION.METRIC) {
config.metricFunction = mlFunctionToESAggregation(record.function_description);
}
// Build the tooltip data for the chart info icon, showing further details on what is being plotted.
let functionLabel = config.metricFunction;
if (config.metricFieldName !== undefined) {
functionLabel += ` ${config.metricFieldName}`;
}
config.infoTooltip = {
jobId: record.job_id,
aggregationInterval: config.interval,
chartFunction: functionLabel,
entityFields: config.entityFields.map((f) => ({
fieldName: f.fieldName,
fieldValue: f.fieldValue,
})),
};
return config;
}

View file

@ -1,28 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import mockAnomalyRecord from './__mocks__/mock_anomaly_record.json';
import mockDetectorsByJob from './__mocks__/mock_detectors_by_job.json';
import mockJobConfig from './__mocks__/mock_job_config.json';
jest.mock('../../services/job_service', () => ({
mlJobService: {
getJob() {
return mockJobConfig;
},
detectorsByJob: mockDetectorsByJob,
},
}));
import { buildConfig } from './explorer_chart_config_builder';
describe('buildConfig', () => {
test('get dataConfig for anomaly record', () => {
const dataConfig = buildConfig(mockAnomalyRecord);
expect(dataConfig).toMatchSnapshot();
});
});

View file

@ -95,7 +95,6 @@ function ExplorerChartContainer({
timefilter,
timeRange,
onSelectEntity,
recentlyAccessed,
tooManyBucketsCalloutMsg,
showSelectedInterval,
chartsService,
@ -105,6 +104,7 @@ function ExplorerChartContainer({
const {
services: {
chrome: { recentlyAccessed },
share,
application: { navigateToApp },
},
@ -389,11 +389,7 @@ export const ExplorerChartsContainerUI = ({
chartsService,
}) => {
const {
services: {
chrome: { recentlyAccessed },
embeddable: embeddablePlugin,
maps: mapsPlugin,
},
services: { embeddable: embeddablePlugin, maps: mapsPlugin },
} = kibana;
let seriesToPlotFiltered;
@ -452,7 +448,6 @@ export const ExplorerChartsContainerUI = ({
timefilter={timefilter}
timeRange={timeRange}
onSelectEntity={onSelectEntity}
recentlyAccessed={recentlyAccessed}
tooManyBucketsCalloutMsg={tooManyBucketsCalloutMsg}
showSelectedInterval={showSelectedInterval}
chartsService={chartsService}

View file

@ -21,16 +21,11 @@ import { kibanaContextMock } from '../../contexts/kibana/__mocks__/kibana_contex
import { timeBucketsMock } from '../../util/__mocks__/time_buckets';
import { timefilterMock } from '../../contexts/kibana/__mocks__/use_timefilter';
jest.mock('../../services/job_service', () => ({
mlJobService: {
getJob: jest.fn(),
},
}));
jest.mock('../../contexts/kibana', () => ({
useMlKibana: () => {
return {
services: {
chrome: { recentlyAccessed: { add: jest.fn() } },
share: {
url: {
locators: {

View file

@ -20,6 +20,7 @@ import { EXPLORER_ACTION } from './explorer_constants';
import type { ExplorerState } from './reducers';
import { explorerReducer, getExplorerDefaultState } from './reducers';
import type { MlFieldFormatService } from '../services/field_format_service';
import type { MlJobService } from '../services/job_service';
type ExplorerAction = Action | Observable<ActionPayload>;
export const explorerAction$ = new Subject<ExplorerAction>();
@ -52,7 +53,10 @@ const setExplorerDataActionCreator = (payload: DeepPartial<ExplorerState>) => ({
});
// Export observable state and action dispatchers as service
export const explorerServiceFactory = (mlFieldFormatService: MlFieldFormatService) => ({
export const explorerServiceFactory = (
mlJobService: MlJobService,
mlFieldFormatService: MlFieldFormatService
) => ({
state$: explorerState$,
clearExplorerData: () => {
explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_EXPLORER_DATA });
@ -64,7 +68,9 @@ export const explorerServiceFactory = (mlFieldFormatService: MlFieldFormatServic
explorerAction$.next({ type: EXPLORER_ACTION.CLEAR_JOBS });
},
updateJobSelection: (selectedJobIds: string[]) => {
explorerAction$.next(jobSelectionActionCreator(mlFieldFormatService, selectedJobIds));
explorerAction$.next(
jobSelectionActionCreator(mlJobService, mlFieldFormatService, selectedJobIds)
);
},
setExplorerData: (payload: DeepPartial<ExplorerState>) => {
explorerAction$.next(setExplorerDataActionCreator(payload));

View file

@ -24,9 +24,10 @@ import {
type MlRecordForInfluencer,
ML_JOB_AGGREGATION,
} from '@kbn/ml-anomaly-utils';
import type { InfluencersFilterQuery } from '@kbn/ml-anomaly-utils';
import type { TimeRangeBounds } from '@kbn/ml-time-buckets';
import type { IUiSettingsClient } from '@kbn/core/public';
import {
ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE,
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
@ -39,9 +40,7 @@ import {
isTimeSeriesViewJob,
} from '../../../common/util/job_utils';
import { parseInterval } from '../../../common/util/parse_interval';
import { ml } from '../services/ml_api_service';
import { mlJobService } from '../services/job_service';
import { getUiSettings } from '../util/dependency_cache';
import type { MlJobService } from '../services/job_service';
import type { SwimlaneType } from './explorer_constants';
import {
@ -53,6 +52,7 @@ import {
import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import type { MlResultsService } from '../services/results_service';
import type { Annotations, AnnotationsTable } from '../../../common/types/annotations';
import type { MlApiServices } from '../services/ml_api_service';
export interface ExplorerJob {
id: string;
@ -239,7 +239,7 @@ export async function loadFilteredTopInfluencers(
)) as any[];
}
export function getInfluencers(selectedJobs: any[]): string[] {
export function getInfluencers(mlJobService: MlJobService, selectedJobs: any[]): string[] {
const influencers: string[] = [];
selectedJobs.forEach((selectedJob) => {
const job = mlJobService.getJob(selectedJob.id);
@ -250,15 +250,14 @@ export function getInfluencers(selectedJobs: any[]): string[] {
return influencers;
}
export function getDateFormatTz(): string {
const uiSettings = getUiSettings();
export function getDateFormatTz(uiSettings: IUiSettingsClient): string {
// Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table.
const tzConfig = uiSettings.get('dateFormat:tz');
const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess();
return dateFormatTz;
}
export function getFieldsByJob() {
export function getFieldsByJob(mlJobService: MlJobService) {
return mlJobService.jobs.reduce(
(reducedFieldsByJob, job) => {
// Add the list of distinct by, over, partition and influencer fields for each job.
@ -353,6 +352,7 @@ export function getSelectionJobIds(
}
export function loadOverallAnnotations(
mlApiServices: MlApiServices,
selectedJobs: ExplorerJob[],
bounds: TimeRangeBounds
): Promise<AnnotationsTable> {
@ -361,7 +361,7 @@ export function loadOverallAnnotations(
return new Promise((resolve) => {
lastValueFrom(
ml.annotations.getAnnotations$({
mlApiServices.annotations.getAnnotations$({
jobIds,
earliestMs: timeRange.earliestMs,
latestMs: timeRange.latestMs,
@ -407,6 +407,7 @@ export function loadOverallAnnotations(
}
export function loadAnnotationsTableData(
mlApiServices: MlApiServices,
selectedCells: AppStateSelectedCells | undefined | null,
selectedJobs: ExplorerJob[],
bounds: Required<TimeRangeBounds>
@ -416,7 +417,7 @@ export function loadAnnotationsTableData(
return new Promise((resolve) => {
lastValueFrom(
ml.annotations.getAnnotations$({
mlApiServices.annotations.getAnnotations$({
jobIds,
earliestMs: timeRange.earliestMs,
latestMs: timeRange.latestMs,
@ -465,6 +466,8 @@ export function loadAnnotationsTableData(
}
export async function loadAnomaliesTableData(
mlApiServices: MlApiServices,
mlJobService: MlJobService,
selectedCells: AppStateSelectedCells | undefined | null,
selectedJobs: ExplorerJob[],
dateFormatTz: string,
@ -479,7 +482,7 @@ export async function loadAnomaliesTableData(
const timeRange = getSelectionTimeRange(selectedCells, bounds);
return new Promise((resolve, reject) => {
ml.results
mlApiServices.results
.getAnomaliesTableData(
jobIds,
[],

View file

@ -7,15 +7,12 @@
import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns';
import type { ExplorerJob } from '../../explorer_utils';
import { getInfluencers } from '../../explorer_utils';
// Creates index pattern in the format expected by the kuery bar/kuery autocomplete provider
// Field objects required fields: name, type, aggregatable, searchable
export function getIndexPattern(selectedJobs: ExplorerJob[]) {
export function getIndexPattern(influencers: string[]) {
return {
title: ML_RESULTS_INDEX_PATTERN,
fields: getInfluencers(selectedJobs).map((influencer) => ({
fields: influencers.map((influencer) => ({
name: influencer,
type: 'string',
aggregatable: true,

View file

@ -6,16 +6,15 @@
*/
import type { ActionPayload } from '../../explorer_dashboard_service';
import { getInfluencers } from '../../explorer_utils';
import { getIndexPattern } from './get_index_pattern';
import type { ExplorerState } from './state';
export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload): ExplorerState => {
const { selectedJobs } = payload;
const { selectedJobs, noInfluencersConfigured } = payload;
const stateUpdate: ExplorerState = {
...state,
noInfluencersConfigured: getInfluencers(selectedJobs).length === 0,
noInfluencersConfigured,
selectedJobs,
};

View file

@ -19,8 +19,10 @@ import {
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useMlKibana } from '../../../../contexts/kibana';
import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs';
import { isManagedJob } from '../../../jobs_utils';
import { useMlJobService } from '../../../../services/job_service';
import { closeJobs } from '../utils';
import { ManagedJobsWarningCallout } from './managed_jobs_warning_callout';
@ -37,6 +39,12 @@ export const CloseJobsConfirmModal: FC<Props> = ({
unsetShowFunction,
refreshJobs,
}) => {
const {
services: {
notifications: { toasts },
},
} = useMlKibana();
const mlJobService = useMlJobService();
const [modalVisible, setModalVisible] = useState(false);
const [hasManagedJob, setHasManaged] = useState(true);
const [jobsToReset, setJobsToReset] = useState<MlSummaryJob[]>([]);
@ -113,7 +121,7 @@ export const CloseJobsConfirmModal: FC<Props> = ({
<EuiButton
onClick={() => {
closeJobs(jobsToReset, refreshJobs);
closeJobs(toasts, mlJobService, jobsToReset, refreshJobs);
closeModal();
}}
fill

View file

@ -19,8 +19,10 @@ import {
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useMlKibana } from '../../../../contexts/kibana';
import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs';
import { isManagedJob } from '../../../jobs_utils';
import { useMlJobService } from '../../../../services/job_service';
import { stopDatafeeds } from '../utils';
import { ManagedJobsWarningCallout } from './managed_jobs_warning_callout';
@ -38,6 +40,12 @@ export const StopDatafeedsConfirmModal: FC<Props> = ({
unsetShowFunction,
refreshJobs,
}) => {
const {
services: {
notifications: { toasts },
},
} = useMlKibana();
const mlJobService = useMlJobService();
const [modalVisible, setModalVisible] = useState(false);
const [hasManagedJob, setHasManaged] = useState(true);
const [jobsToStop, setJobsToStop] = useState<MlSummaryJob[]>([]);
@ -114,7 +122,7 @@ export const StopDatafeedsConfirmModal: FC<Props> = ({
<EuiButton
onClick={() => {
stopDatafeeds(jobsToStop, refreshJobs);
stopDatafeeds(toasts, mlJobService, jobsToStop, refreshJobs);
closeModal();
}}
fill

View file

@ -110,6 +110,7 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({
onClose,
onModelSnapshotAnnotationClick,
}) => {
const mlApiServices = useMlApiContext();
const [data, setData] = useState<{
datafeedConfig: CombinedJobWithStats['datafeed_config'] | undefined;
bucketSpan: string | undefined;
@ -212,7 +213,7 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({
const getJobAndSnapshotData = useCallback(async () => {
try {
const job: CombinedJobWithStats = await loadFullJob(jobId);
const job: CombinedJobWithStats = await loadFullJob(mlApiServices, jobId);
const modelSnapshotResultsLine: LineAnnotationDatumWithModelSnapshot[] = [];
const modelSnapshotsResp = await getModelSnapshots(jobId);
const modelSnapshots = modelSnapshotsResp.model_snapshots ?? [];
@ -659,6 +660,7 @@ export const JobListDatafeedChartFlyout: FC<JobListDatafeedChartFlyoutProps> = (
unsetShowFunction,
refreshJobs,
}) => {
const mlApiServices = useMlApiContext();
const [isVisible, setIsVisible] = useState(false);
const [job, setJob] = useState<MlSummaryJob | undefined>();
const [jobWithStats, setJobWithStats] = useState<CombinedJobWithStats | undefined>();
@ -675,9 +677,11 @@ export const JobListDatafeedChartFlyout: FC<JobListDatafeedChartFlyoutProps> = (
const showRevertModelSnapshot = useCallback(async () => {
// Need to load the full job with stats, as the model snapshot
// flyout needs the timestamp of the last result.
const fullJob: CombinedJobWithStats = await loadFullJob(job!.id);
const fullJob: CombinedJobWithStats = await loadFullJob(mlApiServices, job!.id);
setJobWithStats(fullJob);
setIsRevertModelSnapshotFlyoutVisible(true);
// exclude mlApiServices from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [job]);
useEffect(() => {

View file

@ -23,9 +23,11 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useMlKibana } from '../../../../contexts/kibana';
import { deleteJobs } from '../utils';
import { BLOCKED_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list';
import { DeleteSpaceAwareItemCheckModal } from '../../../../components/delete_space_aware_item_check_modal';
import { useMlJobService } from '../../../../services/job_service';
import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs';
import { isManagedJob } from '../../../jobs_utils';
import { ManagedJobsWarningCallout } from '../confirm_modals/managed_jobs_warning_callout';
@ -39,6 +41,12 @@ interface Props {
}
export const DeleteJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction, refreshJobs }) => {
const {
services: {
notifications: { toasts },
},
} = useMlKibana();
const mlJobService = useMlJobService();
const [deleting, setDeleting] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [adJobs, setAdJobs] = useState<MlSummaryJob[]>([]);
@ -83,6 +91,8 @@ export const DeleteJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction,
const deleteJob = useCallback(() => {
setDeleting(true);
deleteJobs(
toasts,
mlJobService,
jobIds.map((id) => ({ id })),
deleteUserAnnotations,
deleteAlertingRules
@ -92,6 +102,8 @@ export const DeleteJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction,
closeModal();
refreshJobs();
}, BLOCKED_JOBS_REFRESH_INTERVAL_MS);
// exclude mlJobservice from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [jobIds, deleteUserAnnotations, deleteAlertingRules, closeModal, refreshJobs]);
if (modalVisible === false || jobIds.length === 0) {

View file

@ -10,6 +10,7 @@ import React, { Component } from 'react';
import { cloneDeep, isEqual, pick } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import {
EuiButton,
EuiButtonEmpty,
@ -30,8 +31,6 @@ import { saveJob } from './edit_utils';
import { loadFullJob } from '../utils';
import { validateModelMemoryLimit, validateGroupNames } from '../validate_job';
import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service';
import { ml } from '../../../../services/ml_api_service';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { XJson } from '@kbn/es-ui-shared-plugin/public';
import { DATAFEED_STATE, JOB_STATE } from '../../../../../../common/constants/states';
import { CustomUrlsWrapper, isValidCustomUrls } from '../../../../components/custom_urls';
@ -43,8 +42,8 @@ const { collapseLiteralStrings } = XJson;
export class EditJobFlyoutUI extends Component {
_initialJobFormState = null;
constructor(props) {
super(props);
constructor(props, constructorContext) {
super(props, constructorContext);
this.state = {
job: {},
@ -121,7 +120,7 @@ export class EditJobFlyoutUI extends Component {
showFlyout = (jobLite) => {
const hasDatafeed = jobLite.hasDatafeed;
loadFullJob(jobLite.id)
loadFullJob(this.props.kibana.services.mlServices.mlApiServices, jobLite.id)
.then((job) => {
this.extractJob(job, hasDatafeed);
this.setState({
@ -204,6 +203,8 @@ export class EditJobFlyoutUI extends Component {
).message;
}
const ml = this.props.kibana.services.mlServices.mlApiServices;
if (jobDetails.jobGroups !== undefined) {
jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message;
if (jobGroupsValidationError === '') {
@ -272,10 +273,11 @@ export class EditJobFlyoutUI extends Component {
customUrls: this.state.jobCustomUrls,
};
const mlApiServices = this.props.kibana.services.mlServices.mlApiServices;
const { toasts } = this.props.kibana.services.notifications;
const toastNotificationService = toastNotificationServiceProvider(toasts);
saveJob(this.state.job, newJobData)
saveJob(mlApiServices, this.state.job, newJobData)
.then(() => {
toasts.addSuccess(
i18n.translate('xpack.ml.jobsList.editJobFlyout.changesSavedNotificationMessage', {

View file

@ -8,9 +8,8 @@
import { difference } from 'lodash';
import { getNewJobLimits } from '../../../../services/ml_server_info';
import { processCreatedBy } from '../../../../../../common/util/job_utils';
import { ml } from '../../../../services/ml_api_service';
export function saveJob(job, newJobData, finish) {
export function saveJob(mlApiServices, job, newJobData, finish) {
return new Promise((resolve, reject) => {
const jobData = {
...extractDescription(job, newJobData),
@ -30,7 +29,7 @@ export function saveJob(job, newJobData, finish) {
}
const saveDatafeedWrapper = () => {
saveDatafeed(datafeedData, job, finish)
saveDatafeed(mlApiServices, datafeedData, job, finish)
.then(() => {
resolve();
})
@ -41,7 +40,8 @@ export function saveJob(job, newJobData, finish) {
// if anything has changed, post the changes
if (Object.keys(jobData).length) {
ml.updateJob({ jobId: job.job_id, job: jobData })
mlApiServices
.updateJob({ jobId: job.job_id, job: jobData })
.then(() => {
saveDatafeedWrapper();
})
@ -54,11 +54,12 @@ export function saveJob(job, newJobData, finish) {
});
}
function saveDatafeed(datafeedConfig, job) {
function saveDatafeed(mlApiServices, datafeedConfig, job) {
return new Promise((resolve, reject) => {
if (Object.keys(datafeedConfig).length) {
const datafeedId = job.datafeed_config.datafeed_id;
ml.updateDatafeed({ datafeedId, datafeedConfig })
mlApiServices
.updateDatafeed({ datafeedId, datafeedConfig })
.then(() => {
resolve();
})

View file

@ -7,16 +7,26 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui';
import { mlJobService } from '../../../../../services/job_service';
import { FormattedMessage } from '@kbn/i18n-react';
import { context } from '@kbn/kibana-react-plugin/public';
import { mlJobServiceFactory } from '../../../../../services/job_service';
import { toastNotificationServiceProvider } from '../../../../../services/toast_notification_service';
import { detectorToString } from '../../../../../util/string_utils';
export class Detectors extends Component {
constructor(props) {
super(props);
static contextType = context;
constructor(props, constructorContext) {
super(props, constructorContext);
const mlJobService = mlJobServiceFactory(
toastNotificationServiceProvider(constructorContext.services.notifications.toasts),
constructorContext.services.mlServices.mlApiServices
);
this.detectors = mlJobService.getJobGroups().map((g) => ({ label: g.id }));

View file

@ -17,12 +17,13 @@ import {
EuiFieldNumber,
} from '@elastic/eui';
import { ml } from '../../../../../services/ml_api_service';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { tabColor } from '../../../../../../../common/util/group_color_utils';
export class JobDetails extends Component {
export class JobDetailsUI extends Component {
constructor(props) {
super(props);
@ -41,6 +42,7 @@ export class JobDetails extends Component {
}
componentDidMount() {
const ml = this.props.kibana.services.mlServices.mlApiServices;
// load groups to populate the select options
ml.jobs
.groups()
@ -259,10 +261,12 @@ export class JobDetails extends Component {
);
}
}
JobDetails.propTypes = {
JobDetailsUI.propTypes = {
datafeedRunning: PropTypes.bool.isRequired,
jobDescription: PropTypes.string.isRequired,
jobGroups: PropTypes.array.isRequired,
jobModelMemoryLimit: PropTypes.string.isRequired,
setJobDetails: PropTypes.func.isRequired,
};
export const JobDetails = withKibana(JobDetailsUI);

View file

@ -22,6 +22,10 @@ import { i18n } from '@kbn/i18n';
import { isManagedJob } from '../../../jobs_utils';
export function actionsMenuContent(
toastNotifications,
application,
mlApiServices,
mlJobService,
showEditJobFlyout,
showDatafeedChartFlyout,
showDeleteJobModal,
@ -73,7 +77,7 @@ export function actionsMenuContent(
if (isManagedJob(item)) {
showStopDatafeedsConfirmModal([item]);
} else {
stopDatafeeds([item], refreshJobs);
stopDatafeeds(toastNotifications, mlJobService, [item], refreshJobs);
}
closeMenu(true);
@ -110,7 +114,7 @@ export function actionsMenuContent(
if (isManagedJob(item)) {
showCloseJobsConfirmModal([item]);
} else {
closeJobs([item], refreshJobs);
closeJobs(toastNotifications, mlJobService, [item], refreshJobs);
}
closeMenu(true);
@ -149,7 +153,7 @@ export function actionsMenuContent(
return isJobBlocked(item) === false && canCreateJob;
},
onClick: (item) => {
cloneJob(item.id);
cloneJob(toastNotifications, application, mlApiServices, mlJobService, item.id);
closeMenu(true);
},
'data-test-subj': 'mlActionButtonCloneJob',

View file

@ -39,13 +39,15 @@ const MAX_FORECASTS = 500;
* Table component for rendering the lists of forecasts run on an ML job.
*/
export class ForecastsTable extends Component {
constructor(props) {
super(props);
constructor(props, constructorContext) {
super(props, constructorContext);
this.state = {
isLoading: props.job.data_counts.processed_record_count !== 0,
forecasts: [],
};
this.mlForecastService;
this.mlForecastService = forecastServiceFactory(
constructorContext.services.mlServices.mlApiServices
);
}
/**
@ -54,7 +56,6 @@ export class ForecastsTable extends Component {
static contextType = context;
componentDidMount() {
this.mlForecastService = forecastServiceFactory(this.context.services.mlServices.mlApiServices);
const dataCounts = this.props.job.data_counts;
if (dataCounts.processed_record_count > 0) {
// Get the list of all the forecasts with results at or later than the specified 'from' time.

View file

@ -11,7 +11,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@el
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { extractErrorMessage } from '@kbn/ml-error-utils';
import { ml } from '../../../../services/ml_api_service';
import { JobMessages } from '../../../../components/job_messages';
import type { JobMessage } from '../../../../../../common/types/audit_message';
import { useToastNotificationService } from '../../../../services/toast_notification_service';
@ -38,9 +37,7 @@ export const JobMessagesPane: FC<JobMessagesPaneProps> = React.memo(
const [isClearing, setIsClearing] = useState<boolean>(false);
const toastNotificationService = useToastNotificationService();
const {
jobs: { clearJobAuditMessages },
} = useMlApiContext();
const ml = useMlApiContext();
const fetchMessages = async () => {
setIsLoading(true);
@ -70,7 +67,7 @@ export const JobMessagesPane: FC<JobMessagesPaneProps> = React.memo(
const clearMessages = useCallback(async () => {
setIsClearing(true);
try {
await clearJobAuditMessages(jobId, notificationIndices);
await ml.jobs.clearJobAuditMessages(jobId, notificationIndices);
setIsClearing(false);
if (typeof refreshJobList === 'function') {
refreshJobList();

View file

@ -11,6 +11,7 @@ import { sortBy } from 'lodash';
import moment from 'moment';
import { TIME_FORMAT } from '@kbn/ml-date-utils';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { toLocaleString } from '../../../../util/string_utils';
import { JobIcon } from '../../../../components/job_message_icon';
@ -31,10 +32,12 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { AnomalyDetectionJobIdLink } from './job_id_link';
import { isManagedJob } from '../../../jobs_utils';
import { mlJobServiceFactory } from '../../../../services/job_service';
import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service';
const PAGE_SIZE_OPTIONS = [10, 25, 50];
export class JobsList extends Component {
export class JobsListUI extends Component {
constructor(props) {
super(props);
@ -42,6 +45,12 @@ export class JobsList extends Component {
jobsSummaryList: props.jobsSummaryList,
itemIdToExpandedRowMap: {},
};
this.mlApiServices = props.kibana.services.mlServices.mlApiServices;
this.mlJobService = mlJobServiceFactory(
toastNotificationServiceProvider(props.kibana.services.notifications.toasts),
this.mlApiServices
);
}
static getDerivedStateFromProps(props) {
@ -329,6 +338,10 @@ export class JobsList extends Component {
defaultMessage: 'Actions',
}),
actions: actionsMenuContent(
this.props.kibana.services.notifications.toasts,
this.props.kibana.services.application,
this.mlApiServices,
this.mlJobService,
this.props.showEditJobFlyout,
this.props.showDatafeedChartFlyout,
this.props.showDeleteJobModal,
@ -399,7 +412,7 @@ export class JobsList extends Component {
);
}
}
JobsList.propTypes = {
JobsListUI.propTypes = {
jobsSummaryList: PropTypes.array.isRequired,
fullJobsList: PropTypes.object.isRequired,
isMlEnabledInSpace: PropTypes.bool,
@ -419,7 +432,9 @@ JobsList.propTypes = {
jobsViewState: PropTypes.object,
onJobsViewStateUpdate: PropTypes.func,
};
JobsList.defaultProps = {
JobsListUI.defaultProps = {
isMlEnabledInSpace: true,
loading: false,
};
export const JobsList = withKibana(JobsListUI);

View file

@ -8,7 +8,8 @@
import React, { Component } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { ml } from '../../../../services/ml_api_service';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import { checkForAutoStartDatafeed, filterJobs, loadFullJob } from '../utils';
import { JobsList } from '../jobs_list';
import { JobDetails } from '../job_details';
@ -36,10 +37,12 @@ import { StopDatafeedsConfirmModal } from '../confirm_modals/stop_datafeeds_conf
import { CloseJobsConfirmModal } from '../confirm_modals/close_jobs_confirm_modal';
import { AnomalyDetectionEmptyState } from '../anomaly_detection_empty_state';
import { removeNodeInfo } from '../../../../../../common/util/job_utils';
import { mlJobServiceFactory } from '../../../../services/job_service';
import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service';
let blockingJobsRefreshTimeout = null;
export class JobsListView extends Component {
export class JobsListViewUI extends Component {
constructor(props) {
super(props);
@ -77,6 +80,11 @@ export class JobsListView extends Component {
* @private
*/
this._isFiltersSet = false;
this.mlJobService = mlJobServiceFactory(
toastNotificationServiceProvider(props.kibana.services.notifications.toasts),
props.kibana.services.mlServices.mlApiServices
);
}
componentDidMount() {
@ -98,7 +106,7 @@ export class JobsListView extends Component {
}
openAutoStartDatafeedModal() {
const job = checkForAutoStartDatafeed();
const job = checkForAutoStartDatafeed(this.mlJobService);
if (job !== undefined) {
this.showStartDatafeedModal([job]);
}
@ -139,7 +147,7 @@ export class JobsListView extends Component {
}
this.setState({ itemIdToExpandedRowMap }, () => {
loadFullJob(jobId)
loadFullJob(this.props.kibana.services.mlServices.mlApiServices, jobId)
.then((job) => {
const fullJobsList = { ...this.state.fullJobsList };
if (this.props.showNodeInfo === false) {
@ -316,6 +324,7 @@ export class JobsListView extends Component {
this.setState({ loading: true });
}
const ml = this.props.kibana.services.mlServices.mlApiServices;
const expandedJobsIds = Object.keys(this.state.itemIdToExpandedRowMap);
try {
let jobsAwaitingNodeCount = 0;
@ -378,6 +387,7 @@ export class JobsListView extends Component {
return;
}
const ml = this.props.kibana.services.mlServices.mlApiServices;
const { jobs } = await ml.jobs.blockingJobTasks();
const blockingJobIds = jobs.map((j) => Object.keys(j)[0]).sort();
const taskListHasChanged = blockingJobIds.join() !== this.state.blockingJobIds.join();
@ -552,3 +562,5 @@ export class JobsListView extends Component {
return <div>{this.renderJobsListComponents()}</div>;
}
}
export const JobsListView = withKibana(JobsListViewUI);

View file

@ -5,13 +5,22 @@
* 2.0.
*/
import { checkPermission } from '../../../../capabilities/check_capabilities';
import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { context } from '@kbn/kibana-react-plugin/public';
import { checkPermission } from '../../../../capabilities/check_capabilities';
import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes';
import { mlJobServiceFactory } from '../../../../services/job_service';
import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service';
import { isManagedJob } from '../../../jobs_utils';
import {
closeJobs,
stopDatafeeds,
@ -20,13 +29,12 @@ import {
isClosable,
isResettable,
} from '../utils';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { isManagedJob } from '../../../jobs_utils';
class MultiJobActionsMenuUI extends Component {
constructor(props) {
super(props);
static contextType = context;
constructor(props, constructorContext) {
super(props, constructorContext);
this.state = {
isOpen: false,
@ -37,6 +45,13 @@ class MultiJobActionsMenuUI extends Component {
this.canCloseJob = checkPermission('canCloseJob') && mlNodesAvailable();
this.canResetJob = checkPermission('canResetJob') && mlNodesAvailable();
this.canCreateMlAlerts = checkPermission('canCreateMlAlerts');
this.toastNoticiations = constructorContext.services.notifications.toasts;
const mlApiServices = constructorContext.services.mlServices.mlApiServices;
const toastNotificationService = toastNotificationServiceProvider(
constructorContext.services.notifications.toasts
);
this.mlJobService = mlJobServiceFactory(toastNotificationService, mlApiServices);
}
onButtonClick = () => {
@ -101,7 +116,7 @@ class MultiJobActionsMenuUI extends Component {
if (this.props.jobs.some((j) => isManagedJob(j))) {
this.props.showCloseJobsConfirmModal(this.props.jobs);
} else {
closeJobs(this.props.jobs);
closeJobs(this.toastNotifications, this.mlJobService, this.props.jobs);
}
this.closePopover();

View file

@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import {
EuiButton,
@ -24,11 +25,10 @@ import {
import { cloneDeep } from 'lodash';
import { ml } from '../../../../../services/ml_api_service';
import { checkPermission } from '../../../../../capabilities/check_capabilities';
import { GroupList } from './group_list';
import { NewGroupInput } from './new_group_input';
import { getToastNotificationService } from '../../../../../services/toast_notification_service';
import { toastNotificationServiceProvider } from '../../../../../services/toast_notification_service';
function createSelectedGroups(jobs, groups) {
const jobIds = jobs.map((j) => j.id);
@ -54,15 +54,15 @@ function createSelectedGroups(jobs, groups) {
return selectedGroups;
}
export class GroupSelector extends Component {
export class GroupSelectorUI extends Component {
static propTypes = {
jobs: PropTypes.array.isRequired,
allJobIds: PropTypes.array.isRequired,
refreshJobs: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
constructor(props, constructorContext) {
super(props, constructorContext);
this.state = {
isPopoverOpen: false,
@ -73,6 +73,9 @@ export class GroupSelector extends Component {
this.refreshJobs = this.props.refreshJobs;
this.canUpdateJob = checkPermission('canUpdateJob');
this.toastNotificationsService = toastNotificationServiceProvider(
props.kibana.services.notifications.toasts
);
}
static getDerivedStateFromProps(props, state) {
@ -88,6 +91,7 @@ export class GroupSelector extends Component {
if (this.state.isPopoverOpen) {
this.closePopover();
} else {
const ml = this.props.kibana.services.mlServices.mlApiServices;
ml.jobs
.groups()
.then((groups) => {
@ -133,6 +137,7 @@ export class GroupSelector extends Component {
};
applyChanges = () => {
const toastNotificationsService = this.toastNotificationsService;
const { selectedGroups } = this.state;
const { jobs } = this.props;
const newJobs = jobs.map((j) => ({
@ -153,6 +158,7 @@ export class GroupSelector extends Component {
}
const tempJobs = newJobs.map((j) => ({ jobId: j.id, groups: j.newGroups }));
const ml = this.props.kibana.services.mlServices.mlApiServices;
ml.jobs
.updateGroups(tempJobs)
.then((resp) => {
@ -161,7 +167,7 @@ export class GroupSelector extends Component {
// check success of each job update
if (Object.hasOwn(resp, jobId)) {
if (resp[jobId].success === false) {
getToastNotificationService().displayErrorToast(resp[jobId].error);
toastNotificationsService.displayErrorToast(resp[jobId].error);
success = false;
}
}
@ -176,7 +182,7 @@ export class GroupSelector extends Component {
}
})
.catch((error) => {
getToastNotificationService().displayErrorToast(error);
toastNotificationsService.displayErrorToast(error);
console.error(error);
});
};
@ -271,3 +277,5 @@ export class GroupSelector extends Component {
);
}
}
export const GroupSelector = withKibana(GroupSelectorUI);

View file

@ -25,6 +25,8 @@ import { i18n } from '@kbn/i18n';
import { resetJobs } from '../utils';
import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs';
import { RESETTING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list';
import { useMlKibana } from '../../../../contexts/kibana';
import { useMlJobService } from '../../../../services/job_service';
import { OpenJobsWarningCallout } from './open_jobs_warning_callout';
import { isManagedJob } from '../../../jobs_utils';
import { ManagedJobsWarningCallout } from '../confirm_modals/managed_jobs_warning_callout';
@ -38,6 +40,12 @@ interface Props {
}
export const ResetJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction, refreshJobs }) => {
const {
services: {
notifications: { toasts },
},
} = useMlKibana();
const mlJobService = useMlJobService();
const [resetting, setResetting] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [jobIds, setJobIds] = useState<string[]>([]);
@ -73,11 +81,13 @@ export const ResetJobModal: FC<Props> = ({ setShowFunction, unsetShowFunction, r
const resetJob = useCallback(async () => {
setResetting(true);
await resetJobs(jobIds, deleteUserAnnotations);
await resetJobs(toasts, mlJobService, jobIds, deleteUserAnnotations);
closeModal();
setTimeout(() => {
refreshJobs();
}, RESETTING_JOBS_REFRESH_INTERVAL_MS);
// exclude mlJobservice from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [closeModal, deleteUserAnnotations, jobIds, refreshJobs]);
if (modalVisible === false || jobIds.length === 0) {

View file

@ -7,6 +7,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import moment from 'moment';
import {
EuiButton,
@ -20,18 +21,23 @@ import {
EuiCheckbox,
} from '@elastic/eui';
import moment from 'moment';
import { FormattedMessage } from '@kbn/i18n-react';
import { context } from '@kbn/kibana-react-plugin/public';
import { mlJobServiceFactory } from '../../../../services/job_service';
import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service';
import { isManagedJob } from '../../../jobs_utils';
import { forceStartDatafeeds } from '../utils';
import { TimeRangeSelector } from './time_range_selector';
import { FormattedMessage } from '@kbn/i18n-react';
import { isManagedJob } from '../../../jobs_utils';
export class StartDatafeedModal extends Component {
constructor(props) {
super(props);
static contextType = context;
constructor(props, constructorContext) {
super(props, constructorContext);
const now = moment();
this.state = {
@ -50,6 +56,11 @@ export class StartDatafeedModal extends Component {
this.initialSpecifiedStartTime = now;
this.refreshJobs = this.props.refreshJobs;
this.getShowCreateAlertFlyoutFunction = this.props.getShowCreateAlertFlyoutFunction;
this.toastNotifications = constructorContext.services.notifications.toasts;
this.mlJobService = mlJobServiceFactory(
toastNotificationServiceProvider(this.toastNotifications),
constructorContext.services.mlServices.mlApiServices
);
}
componentDidMount() {
@ -114,7 +125,7 @@ export class StartDatafeedModal extends Component {
? this.state.endTime.valueOf()
: this.state.endTime;
forceStartDatafeeds(jobs, start, end, () => {
forceStartDatafeeds(this.toastNotifications, this.mlJobService, jobs, start, end, () => {
if (this.state.createAlert && jobs.length > 0) {
this.getShowCreateAlertFlyoutFunction()(jobs.map((job) => job.id));
}

View file

@ -5,19 +5,79 @@
* 2.0.
*/
import type { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs';
import type { ApplicationStart, ToastsStart } from '@kbn/core/public';
export function stopDatafeeds(jobs: Array<{ id: string }>, callback?: () => void): Promise<void>;
export function closeJobs(jobs: Array<{ id: string }>, callback?: () => void): Promise<void>;
import type { DATAFEED_STATE } from '../../../../../common/constants/states';
import type {
CombinedJobWithStats,
MlSummaryJob,
} from '../../../../../common/types/anomaly_detection_jobs';
import type { MlJobService } from '../../../services/job_service';
import type { MlApiServices } from '../../../services/ml_api_service';
export function loadFullJob(
mlApiServices: MlApiServices,
jobId: string
): Promise<CombinedJobWithStats>;
export function loadJobForCloning(mlApiServices: MlApiServices, jobId: string): Promise<any>;
export function isStartable(jobs: CombinedJobWithStats[]): boolean;
export function isClosable(jobs: CombinedJobWithStats[]): boolean;
export function isResettable(jobs: CombinedJobWithStats[]): boolean;
export function forceStartDatafeeds(
toastNotifications: ToastsStart,
mlJobService: MlJobService,
jobs: CombinedJobWithStats[],
start: number | undefined,
end: number | undefined,
finish?: () => void
): Promise<void>;
export function stopDatafeeds(
toastNotifications: ToastsStart,
mlJobService: MlJobService,
jobs: CombinedJobWithStats[] | MlSummaryJob[],
finish?: () => void
): Promise<void>;
export function showResults(
toastNotifications: ToastsStart,
resp: any,
action: DATAFEED_STATE
): void;
export function cloneJob(
toastNotifications: ToastsStart,
application: ApplicationStart,
mlApiServices: MlApiServices,
mlJobService: MlJobService,
jobId: string
): Promise<void>;
export function closeJobs(
toastNotifications: ToastsStart,
mlJobService: MlJobService,
jobs: CombinedJobWithStats[] | MlSummaryJob[],
finish?: () => void
): Promise<void>;
export function deleteJobs(
toastNotifications: ToastsStart,
mlJobService: MlJobService,
jobs: Array<{ id: string }>,
deleteUserAnnotations?: boolean,
deleteAlertingRules?: boolean,
callback?: () => void
finish?: () => void
): Promise<void>;
export function resetJobs(
toastNotifications: ToastsStart,
mlJobService: MlJobService,
jobIds: string[],
deleteUserAnnotations?: boolean,
callback?: () => void
finish?: () => void
): Promise<void>;
export function loadFullJob(jobId: string): Promise<CombinedJobWithStats>;
export function filterJobs(
jobs: CombinedJobWithStats[],
clauses: Array<{ field: string; match: string; type: string; value: any }>
): CombinedJobWithStats[];
export function jobProperty(job: CombinedJobWithStats, prop: string): any;
export function jobTagFilter(jobs: CombinedJobWithStats[], value: string): CombinedJobWithStats[];
export function checkForAutoStartDatafeed(
mlJobService: MlJobService
):
| { id: string; hasDatafeed: boolean; latestTimestampSortValue: number; datafeedId: string }
| undefined;

Some files were not shown because too many files have changed in this diff Show more