mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
b79f5abed3
commit
38f4aa0776
247 changed files with 2579 additions and 3150 deletions
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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: () => {},
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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[];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: ['*'],
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() } } },
|
||||
}),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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('');
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[]
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`;
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
[],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
|
|
|
@ -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 }));
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue