diff --git a/packages/default-nav/ml/default_navigation.ts b/packages/default-nav/ml/default_navigation.ts index ebd72f5915201..829c2307299d3 100644 --- a/packages/default-nav/ml/default_navigation.ts +++ b/packages/default-nav/ml/default_navigation.ts @@ -112,6 +112,12 @@ export const defaultNavigation: MlNodeDefinition = { }), link: 'ml:indexDataVisualizer', }, + { + title: i18n.translate('defaultNavigation.ml.dataComparison', { + defaultMessage: 'Data comparison', + }), + link: 'ml:dataComparison', + }, ], }, { diff --git a/packages/shared-ux/chrome/navigation/mocks/src/navlinks.ts b/packages/shared-ux/chrome/navigation/mocks/src/navlinks.ts index b9b1c287dce73..c3d3b756eaae4 100644 --- a/packages/shared-ux/chrome/navigation/mocks/src/navlinks.ts +++ b/packages/shared-ux/chrome/navigation/mocks/src/navlinks.ts @@ -62,6 +62,7 @@ const allNavLinks: AppDeepLinkId[] = [ 'ml:fileUpload', 'ml:filterListsSettings', 'ml:indexDataVisualizer', + 'ml:dataComparison', 'ml:logPatternAnalysis', 'ml:logRateAnalysis', 'ml:memoryUsage', diff --git a/packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap b/packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap index 97d241f6c957f..f7f08fe075f56 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap +++ b/packages/shared-ux/chrome/navigation/src/ui/__snapshots__/default_navigation.test.tsx.snap @@ -469,6 +469,26 @@ Array [ "renderItem": undefined, "title": "Data view", }, + Object { + "children": undefined, + "deepLink": Object { + "baseUrl": "/mocked", + "href": "http://mocked/ml:dataComparison", + "id": "ml:dataComparison", + "title": "Deeplink ml:dataComparison", + "url": "/mocked/ml:dataComparison", + }, + "href": undefined, + "id": "ml:dataComparison", + "isActive": false, + "path": Array [ + "rootNav:ml", + "data_visualizer", + "ml:dataComparison", + ], + "renderItem": undefined, + "title": "Data comparison", + }, ], "deepLink": undefined, "href": undefined, diff --git a/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx b/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx index ee8922a8e7631..e3e5a1de3d65f 100644 --- a/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx +++ b/x-pack/packages/ml/date_picker/src/components/full_time_range_selector.tsx @@ -72,6 +72,10 @@ export interface FullTimeRangeSelectorProps { * @param value - The time field range response. */ apiPath?: SetFullTimeRangeApiPath; + /** + * Optional flag to disable the frozen data tier choice. + */ + hideFrozenDataTierChoice?: boolean; } /** @@ -92,10 +96,12 @@ export const FullTimeRangeSelector: FC = (props) => disabled, callback, apiPath, + hideFrozenDataTierChoice = false, } = props; const { http, notifications: { toasts }, + isServerless, } = useDatePickerContext(); // wrapper around setFullTimeRange to allow for the calling of the optional callBack prop @@ -107,7 +113,9 @@ export const FullTimeRangeSelector: FC = (props) => toasts, http, query, - frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE, + isServerless || hideFrozenDataTierChoice + ? false + : frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE, apiPath ); if (typeof callback === 'function' && fullTimeRange !== undefined) { @@ -123,7 +131,18 @@ export const FullTimeRangeSelector: FC = (props) => ) ); } - }, [callback, dataView, frozenDataPreference, http, query, timefilter, toasts, apiPath]); + }, [ + timefilter, + dataView, + toasts, + http, + query, + isServerless, + hideFrozenDataTierChoice, + frozenDataPreference, + apiPath, + callback, + ]); const [isPopoverOpen, setPopover] = useState(false); @@ -210,31 +229,33 @@ export const FullTimeRangeSelector: FC = (props) => /> - - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - anchorPosition="downRight" - > - {popoverContent} - - + {isServerless || hideFrozenDataTierChoice ? null : ( + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downRight" + > + {popoverContent} + + + )} ); }; diff --git a/x-pack/packages/ml/date_picker/src/hooks/use_date_picker_context.tsx b/x-pack/packages/ml/date_picker/src/hooks/use_date_picker_context.tsx index 28ad6a12de745..60b1c66f95984 100644 --- a/x-pack/packages/ml/date_picker/src/hooks/use_date_picker_context.tsx +++ b/x-pack/packages/ml/date_picker/src/hooks/use_date_picker_context.tsx @@ -44,6 +44,10 @@ export interface DatePickerDependencies { * Internationalisation service */ i18n: I18nStart; + /** + * Optional flag to indicate whether kibana is running in serverless + */ + isServerless?: boolean; } /** diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx index dd2380fedfa32..c18764e45797d 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_root.tsx @@ -16,7 +16,11 @@ import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { StorageContextProvider } from '@kbn/ml-local-storage'; import { UrlStateProvider } from '@kbn/ml-url-state'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { DatePickerContextProvider, mlTimefilterRefresh$ } from '@kbn/ml-date-picker'; +import { + DatePickerContextProvider, + type DatePickerDependencies, + mlTimefilterRefresh$, +} from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { type Observable } from 'rxjs'; @@ -46,16 +50,20 @@ export interface ChangePointDetectionAppStateProps { savedSearch: SavedSearch | null; /** App dependencies */ appDependencies: AiopsAppDependencies; + /** Optional flag to indicate whether kibana is running in serverless */ + isServerless?: boolean; } export const ChangePointDetectionAppState: FC = ({ dataView, savedSearch, appDependencies, + isServerless = false, }) => { - const datePickerDeps = { + const datePickerDeps: DatePickerDependencies = { ...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']), uiSettingsKeys: UI_SETTINGS, + isServerless, }; const warning = timeSeriesDataViewWarning(dataView, 'change_point_detection'); diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx index a7c9bc17b6fe9..5ddf65d5b938d 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx @@ -12,7 +12,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import { StorageContextProvider } from '@kbn/ml-local-storage'; import { UrlStateProvider } from '@kbn/ml-url-state'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { DatePickerContextProvider } from '@kbn/ml-date-picker'; +import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { DataSourceContext } from '../../hooks/use_data_source'; @@ -35,12 +35,15 @@ export interface LogCategorizationAppStateProps { savedSearch: SavedSearch | null; /** App dependencies */ appDependencies: AiopsAppDependencies; + /** Optional flag to indicate whether kibana is running in serverless */ + isServerless?: boolean; } export const LogCategorizationAppState: FC = ({ dataView, savedSearch, appDependencies, + isServerless = false, }) => { if (!dataView) return null; @@ -50,9 +53,10 @@ export const LogCategorizationAppState: FC = ({ return <>{warning}; } - const datePickerDeps = { + const datePickerDeps: DatePickerDependencies = { ...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']), uiSettingsKeys: UI_SETTINGS, + isServerless, }; return ( diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_app_state.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_app_state.tsx index 212d0465b9cc0..0a41900feb9fb 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_app_state.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_app_state.tsx @@ -13,7 +13,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import { StorageContextProvider } from '@kbn/ml-local-storage'; import { UrlStateProvider } from '@kbn/ml-url-state'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { DatePickerContextProvider } from '@kbn/ml-date-picker'; +import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context'; @@ -40,6 +40,8 @@ export interface LogRateAnalysisAppStateProps { appDependencies: AiopsAppDependencies; /** Option to make main histogram sticky */ stickyHistogram?: boolean; + /** Optional flag to indicate whether kibana is running in serverless */ + isServerless?: boolean; } export const LogRateAnalysisAppState: FC = ({ @@ -47,6 +49,7 @@ export const LogRateAnalysisAppState: FC = ({ savedSearch, appDependencies, stickyHistogram, + isServerless = false, }) => { if (!dataView) return null; @@ -56,9 +59,10 @@ export const LogRateAnalysisAppState: FC = ({ return <>{warning}; } - const datePickerDeps = { + const datePickerDeps: DatePickerDependencies = { ...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']), uiSettingsKeys: UI_SETTINGS, + isServerless, }; return ( diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content_wrapper.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content_wrapper.tsx index ec258081d840d..b19e72e7c4b5a 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content_wrapper.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content_wrapper.tsx @@ -57,6 +57,8 @@ export interface LogRateAnalysisContentWrapperProps { * @param d Log rate analysis results data */ onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void; + /** Optional flag to indicate whether kibana is running in serverless */ + isServerless?: boolean; } export const LogRateAnalysisContentWrapper: FC = ({ @@ -70,6 +72,7 @@ export const LogRateAnalysisContentWrapper: FC { if (!dataView) return null; diff --git a/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts b/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts index 6b8cc0700f28f..aa364a416a046 100644 --- a/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts +++ b/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts @@ -114,6 +114,7 @@ export interface AiopsAppDependencies { presentationUtil?: PresentationUtilPluginStart; embeddable?: EmbeddableStart; cases?: CasesUiStart; + isServerless?: boolean; } /** diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx index 9669188f45123..7e1ea426f0637 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx @@ -17,7 +17,7 @@ import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-pl import { StorageContextProvider } from '@kbn/ml-local-storage'; import { DataView } from '@kbn/data-views-plugin/public'; import { getNestedProperty } from '@kbn/ml-nested-property'; -import { DatePickerContextProvider } from '@kbn/ml-date-picker'; +import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { Provider as UrlStateContextProvider, @@ -263,9 +263,12 @@ export const DataVisualizerStateContextProvider: FC = ({ getAdditionalLinks }) => { + isServerless?: boolean; +} + +export const IndexDataVisualizer: FC = ({ getAdditionalLinks, isServerless = false }) => { const coreStart = getCoreStart(); const { data, @@ -296,9 +299,10 @@ export const IndexDataVisualizer: FC<{ unifiedSearch, ...coreStart, }; - const datePickerDeps = { + const datePickerDeps: DatePickerDependencies = { ...pick(services, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']), uiSettingsKeys: UI_SETTINGS, + isServerless, }; return ( diff --git a/x-pack/plugins/ml/common/constants/app.ts b/x-pack/plugins/ml/common/constants/app.ts index 970505f485739..00f6fa7a42d05 100644 --- a/x-pack/plugins/ml/common/constants/app.ts +++ b/x-pack/plugins/ml/common/constants/app.ts @@ -13,5 +13,6 @@ export const PLUGIN_ICON_SOLUTION = 'logoKibana'; export const ML_APP_NAME = i18n.translate('xpack.ml.navMenu.mlAppNameText', { defaultMessage: 'Machine Learning', }); +export const ML_APP_ROUTE = '/app/ml'; export const ML_INTERNAL_BASE_PATH = '/internal/ml'; export const ML_EXTERNAL_BASE_PATH = '/api/ml'; diff --git a/x-pack/plugins/ml/common/util/job_utils.test.ts b/x-pack/plugins/ml/common/util/job_utils.test.ts index d003710bb57e4..5b07c1018d701 100644 --- a/x-pack/plugins/ml/common/util/job_utils.test.ts +++ b/x-pack/plugins/ml/common/util/job_utils.test.ts @@ -22,8 +22,9 @@ import { resolveMaxTimeInterval, getFiltersForDSLQuery, isKnownEmptyQuery, + removeNodeInfo, } from './job_utils'; -import { CombinedJob, Job } from '../types/anomaly_detection_jobs'; +import type { CombinedJob, CombinedJobWithStats, Job } from '../types/anomaly_detection_jobs'; import { FilterStateStore } from '@kbn/es-query'; import moment from 'moment'; @@ -778,4 +779,39 @@ describe('getFiltersForDSLQuery', () => { expect(result).toBe(false); }); }); + + test('removes node info and returns a copy of the job', () => { + const job = { + job_id: 'test', + datafeed_config: { + datafeed_id: 'datafeed-test', + job_id: 'test', + indices: ['index1'], + query: { + match_all: {}, + }, + node: { + name: 'node-1', + ephemeral_id: '1234', + transport_address: 'localhost:9200', + attributes: {}, + }, + }, + node: { + name: 'node-1', + ephemeral_id: '1234', + transport_address: 'localhost:9200', + attributes: {}, + }, + } as never as CombinedJobWithStats; + + const result = removeNodeInfo(job); + expect(result.job_id).toBe('test'); + expect(result.node).toBe(undefined); + expect(result.datafeed_config.node).toBe(undefined); + + expect(job.job_id).toBe('test'); + expect(job.node).not.toBe(undefined); + expect(job.datafeed_config.node).not.toBe(undefined); + }); }); diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 61420a61e7f09..f52019214f873 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { each, isEmpty, isEqual, pick } from 'lodash'; +import { cloneDeep, each, isEmpty, isEqual, pick } from 'lodash'; import semverGte from 'semver/functions/gte'; import moment, { Duration } from 'moment'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -29,6 +29,7 @@ import { maxLengthValidator } from './validators'; import { CREATED_BY_LABEL } from '../constants/new_job'; import type { CombinedJob, + CombinedJobWithStats, CustomSettings, Datafeed, Job, @@ -936,3 +937,14 @@ export function extractInfluencers(jobs: Job | Job[]): string[] { } return Array.from(influencers); } + +export function removeNodeInfo(job: CombinedJobWithStats) { + const newJob = cloneDeep(job); + if (newJob.node !== undefined) { + delete newJob.node; + } + if (newJob.datafeed_config?.node !== undefined) { + delete newJob.datafeed_config.node; + } + return newJob; +} diff --git a/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts b/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts index 86446dfb1012e..3379c145bf364 100644 --- a/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts +++ b/x-pack/plugins/ml/public/alerting/register_ml_alerts.ts @@ -11,7 +11,7 @@ import type { TriggersAndActionsUIPublicPluginSetup } from '@kbn/triggers-action import type { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/public'; import { ML_ALERT_TYPES } from '../../common/constants/alerts'; import type { MlAnomalyDetectionAlertParams } from '../../common/types/alerts'; -import { PLUGIN_ID } from '../../common/constants/app'; +import { ML_APP_ROUTE, PLUGIN_ID } from '../../common/constants/app'; import { formatExplorerUrl } from '../locator/formatters/anomaly_detection'; import { validateLookbackInterval, validateTopNBucket } from './validators'; import { registerJobsHealthAlertingRule } from './jobs_health_rule'; @@ -149,6 +149,6 @@ export function registerNavigation(alerting: AlertingSetup) { ]), ]; - return formatExplorerUrl('/app/ml', { jobIds }); + return formatExplorerUrl(ML_APP_ROUTE, { jobIds }); }); } diff --git a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx index 65ec83b2cabbf..e0c1728d893a5 100644 --- a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx +++ b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx @@ -15,7 +15,7 @@ import { ChangePointDetection } from '@kbn/aiops-plugin/public'; import { useDataSource } from '../contexts/ml/data_source_context'; import { useFieldStatsTrigger, FieldStatsFlyoutProvider } from '../components/field_stats_flyout'; -import { useMlKibana } from '../contexts/kibana'; +import { useMlKibana, useIsServerless } from '../contexts/kibana'; import { HelpMenu } from '../components/help_menu'; import { TechnicalPreviewBadge } from '../components/technical_preview_badge'; @@ -23,6 +23,7 @@ import { MlPageHeader } from '../components/page_header'; export const ChangePointDetectionPage: FC = () => { const { services } = useMlKibana(); + const isServerless = useIsServerless(); const { selectedDataView: dataView, selectedSavedSearch: savedSearch } = useDataSource(); @@ -45,6 +46,7 @@ export const ChangePointDetectionPage: FC = () => { { const { services } = useMlKibana(); + const isServerless = useIsServerless(); const { selectedDataView: dataView, selectedSavedSearch: savedSearch } = useDataSource(); @@ -40,6 +41,7 @@ export const LogCategorizationPage: FC = () => { { const { services } = useMlKibana(); + const isServerless = useIsServerless(); const { selectedDataView: dataView, selectedSavedSearch: savedSearch } = useDataSource(); @@ -43,6 +44,7 @@ export const LogRateAnalysisPage: FC = () => { stickyHistogram={false} dataView={dataView} savedSearch={savedSearch} + isServerless={isServerless} appDependencies={pick(services, [ 'application', 'data', diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index ca1a398de1cbf..a5993f99e4a9c 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -12,7 +12,7 @@ import { pick } from 'lodash'; import type { AppMountParameters, CoreStart, HttpStart } from '@kbn/core/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import { DatePickerContextProvider } from '@kbn/ml-date-picker'; +import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; @@ -41,6 +41,7 @@ interface AppProps { coreStart: CoreStart; deps: MlDependencies; appMountParams: AppMountParameters; + isServerless: boolean; } const localStorage = new Storage(window.localStorage); @@ -48,7 +49,11 @@ const localStorage = new Storage(window.localStorage); /** * Provides global services available across the entire ML app. */ -export function getMlGlobalServices(httpStart: HttpStart, usageCollection?: UsageCollectionSetup) { +export function getMlGlobalServices( + httpStart: HttpStart, + isServerless: boolean, + usageCollection?: UsageCollectionSetup +) { const httpService = new HttpService(httpStart); const mlApiServices = mlApiServicesProvider(httpService); @@ -58,6 +63,7 @@ export function getMlGlobalServices(httpStart: HttpStart, usageCollection?: Usag mlUsageCollection: mlUsageCollectionProvider(usageCollection), mlCapabilities: new MlCapabilitiesService(mlApiServices), mlLicense: new MlLicense(), + isServerless, }; } @@ -67,7 +73,7 @@ export interface MlServicesContext { export type MlGlobalServices = ReturnType; -const App: FC = ({ coreStart, deps, appMountParams }) => { +const App: FC = ({ coreStart, deps, appMountParams, isServerless }) => { const pageDeps: PageDependencies = { history: appMountParams.history, setHeaderActionMenu: appMountParams.setHeaderActionMenu, @@ -99,9 +105,9 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { contentManagement: deps.contentManagement, presentationUtil: deps.presentationUtil, ...coreStart, - mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection), + mlServices: getMlGlobalServices(coreStart.http, isServerless, deps.usageCollection), }; - }, [deps, coreStart]); + }, [deps, coreStart, isServerless]); useLifecycles( function setupLicenseOnMount() { @@ -122,9 +128,10 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { if (!licenseReady || !mlCapabilities) return null; - const datePickerDeps = { + const datePickerDeps: DatePickerDependencies = { ...pick(services, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']), uiSettingsKeys: UI_SETTINGS, + isServerless, }; const I18nContext = coreStart.i18n.Context; @@ -151,7 +158,8 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { export const renderApp = ( coreStart: CoreStart, deps: MlDependencies, - appMountParams: AppMountParameters + appMountParams: AppMountParameters, + isServerless: boolean ) => { setDependencyCache({ timefilter: deps.data.query.timefilter, @@ -180,7 +188,12 @@ export const renderApp = ( appMountParams.onAppLeave((actions) => actions.default()); ReactDOM.render( - , + , appMountParams.element ); diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index c5325f4aa7614..ef0bf69c59c50 100644 --- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -6,8 +6,15 @@ */ import { i18n } from '@kbn/i18n'; -import { BehaviorSubject, combineLatest, from, type Subscription, timer } from 'rxjs'; -import { distinctUntilChanged, retry, switchMap, tap } from 'rxjs/operators'; +import { + BehaviorSubject, + combineLatest, + from, + type Subscription, + timer, + firstValueFrom, +} from 'rxjs'; +import { distinctUntilChanged, filter, retry, switchMap, tap } from 'rxjs/operators'; import { isEqual } from 'lodash'; import useObservable from 'react-use/lib/useObservable'; import { useMemo, useRef } from 'react'; @@ -16,11 +23,12 @@ import { hasLicenseExpired } from '../license'; import { getDefaultCapabilities, - MlCapabilities, - MlCapabilitiesKey, + type MlCapabilities, + type MlCapabilitiesKey, } from '../../../common/types/capabilities'; import { getCapabilities } from './get_capabilities'; -import { type MlApiServices } from '../services/ml_api_service'; +import type { MlApiServices } from '../services/ml_api_service'; +import type { MlGlobalServices } from '../app'; let _capabilities: MlCapabilities = getDefaultCapabilities(); @@ -36,6 +44,10 @@ export class MlCapabilitiesService { private _updateRequested$ = new BehaviorSubject(Date.now()); private _capabilities$ = new BehaviorSubject(null); + private _capabilitiesObs$ = this._capabilities$.asObservable(); + + private _isPlatinumOrTrialLicense$ = new BehaviorSubject(null); + private _mlFeatureEnabledInSpace$ = new BehaviorSubject(null); public capabilities$ = this._capabilities$.pipe(distinctUntilChanged(isEqual)); @@ -59,6 +71,8 @@ export class MlCapabilitiesService { ) .subscribe((results) => { this._capabilities$.next(results.capabilities); + this._isPlatinumOrTrialLicense$.next(results.isPlatinumOrTrialLicense); + this._mlFeatureEnabledInSpace$.next(results.mlFeatureEnabledInSpace); this._isLoading$.next(false); /** @@ -72,6 +86,18 @@ export class MlCapabilitiesService { return this._capabilities$.getValue(); } + public isPlatinumOrTrialLicense(): boolean | null { + return this._isPlatinumOrTrialLicense$.getValue(); + } + + public mlFeatureEnabledInSpace(): boolean | null { + return this._mlFeatureEnabledInSpace$.getValue(); + } + + public getCapabilities$() { + return this._capabilitiesObs$; + } + public refreshCapabilities() { this._updateRequested$.next(Date.now()); } @@ -111,23 +137,34 @@ export function usePermissionCheck((resolve, reject) => { - checkMlCapabilities() - .then(({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { - _capabilities = capabilities; - // Loop through all capabilities to ensure they are all set to true. - const isManageML = Object.values(_capabilities).every((p) => p === true); +export function checkGetManagementMlJobsResolver({ mlCapabilities }: MlGlobalServices) { + return new Promise(async (resolve, reject) => { + try { + const capabilities = await firstValueFrom( + mlCapabilities.getCapabilities$().pipe(filter((c) => !!c)) + ); - if (isManageML === true && isPlatinumOrTrialLicense === true) { - return resolve({ mlFeatureEnabledInSpace }); - } else { - return reject({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }); - } - }) - .catch((e) => { + if (capabilities === null) { return reject(); - }); + } + _capabilities = capabilities; + const isManageML = + (capabilities.isADEnabled && capabilities.canCreateJob) || + (capabilities.isDFAEnabled && capabilities.canCreateDataFrameAnalytics) || + (capabilities.isNLPEnabled && capabilities.canCreateTrainedModels); + if (isManageML === true) { + return resolve(); + } else { + // reject with possible reasons why capabilities are false + return reject({ + capabilities, + isPlatinumOrTrialLicense: mlCapabilities.isPlatinumOrTrialLicense(), + mlFeatureEnabledInSpace: mlCapabilities.mlFeatureEnabledInSpace(), + }); + } + } catch (error) { + reject(error); + } }); } diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index 4c53bbd1c4a17..5bf0e73b2cee1 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useState, useEffect, useMemo, useCallback } from 'react'; +import React, { FC, useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { @@ -26,12 +26,13 @@ import { EuiConfirmModal, } from '@elastic/eui'; -import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; +import { useMlKibana } from '../../../contexts/kibana'; import { ExportJobDependenciesWarningCallout } from './export_job_warning_callout'; import { JobsExportService } from './jobs_export_service'; import type { JobDependencies } from './jobs_export_service'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import type { JobType } from '../../../../../common/types/saved_objects'; +import { usePermissionCheck } from '../../../capabilities/check_capabilities'; interface Props { isDisabled: boolean; @@ -39,21 +40,19 @@ interface Props { } export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { - const mlApiServices = useMlApiContext(); - const { - getJobs, - dataFrameAnalytics: { getDataFrameAnalytics }, - } = mlApiServices; - const { services: { notifications: { toasts }, - mlServices: { mlUsageCollection }, + mlServices: { mlUsageCollection, mlApiServices }, }, } = useMlKibana(); - // eslint-disable-next-line react-hooks/exhaustive-deps - const jobsExportService = useMemo(() => new JobsExportService(mlApiServices), []); + const { + getJobs, + dataFrameAnalytics: { getDataFrameAnalytics }, + } = mlApiServices; + + const jobsExportService = useMemo(() => new JobsExportService(mlApiServices), [mlApiServices]); const [loadingADJobs, setLoadingADJobs] = useState(true); const [loadingDFAJobs, setLoadingDFAJobs] = useState(true); @@ -69,10 +68,18 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { () => toastNotificationServiceProvider(toasts), [toasts] ); + const [isADEnabled, isDFAEnabled] = usePermissionCheck(['isADEnabled', 'isDFAEnabled']); const [jobDependencies, setJobDependencies] = useState([]); const [selectedJobDependencies, setSelectedJobDependencies] = useState([]); + const isMounted = useRef(true); + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + useEffect( function onFlyoutChange() { setLoadingADJobs(true); @@ -84,49 +91,68 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { setSwitchTabConfirmVisible(false); if (showFlyout) { - getJobs() - .then(({ jobs }) => { - setLoadingADJobs(false); - setAdJobIds(jobs.map((j) => j.job_id)); - - jobsExportService - .getJobDependencies(jobs) - .then((jobDeps) => { - setJobDependencies(jobDeps); - setLoadingADJobs(false); - }) - .catch((error) => { - const errorTitle = i18n.translate( - 'xpack.ml.importExport.exportFlyout.calendarsError', - { - defaultMessage: 'Could not load calendars', - } - ); - displayErrorToast(error, errorTitle); + if (isADEnabled) { + getJobs() + .then(({ jobs }) => { + if (isMounted.current === false) return; + setLoadingADJobs(false); + setAdJobIds(jobs.map((j) => j.job_id)); + + jobsExportService + .getJobDependencies(jobs) + .then((jobDeps) => { + if (isMounted.current === false) return; + setJobDependencies(jobDeps); + setLoadingADJobs(false); + }) + .catch((error) => { + if (isMounted.current === false) return; + const errorTitle = i18n.translate( + 'xpack.ml.importExport.exportFlyout.calendarsError', + { + defaultMessage: 'Could not load calendars', + } + ); + displayErrorToast(error, errorTitle); + }); + }) + .catch((error) => { + if (isMounted.current === false) return; + const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.adJobsError', { + defaultMessage: 'Could not load anomaly detection jobs', }); - }) - .catch((error) => { - const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.adJobsError', { - defaultMessage: 'Could not load anomaly detection jobs', + displayErrorToast(error, errorTitle); }); - displayErrorToast(error, errorTitle); - }); - - getDataFrameAnalytics() - .then(({ data_frame_analytics: dataFrameAnalytics }) => { - setLoadingDFAJobs(false); - setDfaJobIds(dataFrameAnalytics.map((j) => j.id)); - }) - .catch((error) => { - const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.dfaJobsError', { - defaultMessage: 'Could not load data frame analytics jobs', + } + + if (isDFAEnabled) { + getDataFrameAnalytics() + .then(({ data_frame_analytics: dataFrameAnalytics }) => { + if (isMounted.current === false) return; + setLoadingDFAJobs(false); + setDfaJobIds(dataFrameAnalytics.map((j) => j.id)); + }) + .catch((error) => { + if (isMounted.current === false) return; + + const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.dfaJobsError', { + defaultMessage: 'Could not load data frame analytics jobs', + }); + displayErrorToast(error, errorTitle); }); - displayErrorToast(error, errorTitle); - }); + } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [showFlyout] + [ + currentTab, + displayErrorToast, + getDataFrameAnalytics, + getJobs, + isADEnabled, + isDFAEnabled, + jobsExportService, + showFlyout, + ] ); function toggleFlyout() { @@ -188,16 +214,15 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { switchTab(jobType); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [selectedJobIds] + + [selectedJobIds, selectedJobType] ); useEffect(() => { setSelectedJobDependencies( jobDependencies.filter(({ jobId }) => selectedJobIds.includes(jobId)) ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedJobIds]); + }, [jobDependencies, selectedJobIds]); function switchTab(jobType: JobType) { setSwitchTabConfirmVisible(false); @@ -214,6 +239,10 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { } } + if (isADEnabled === false && isDFAEnabled === false) { + return null; + } + return ( <> @@ -239,32 +268,36 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { - attemptTabSwitch('anomaly-detector')} - disabled={exporting} - data-test-subj="mlJobMgmtExportJobsADTab" - > - - - attemptTabSwitch('data-frame-analytics')} - disabled={exporting} - data-test-subj="mlJobMgmtExportJobsDFATab" - > - - + {isADEnabled === true ? ( + attemptTabSwitch('anomaly-detector')} + disabled={exporting} + data-test-subj="mlJobMgmtExportJobsADTab" + > + + + ) : null} + {isDFAEnabled === true ? ( + attemptTabSwitch('data-frame-analytics')} + disabled={exporting} + data-test-subj="mlJobMgmtExportJobsDFATab" + > + + + ) : null} <> - {selectedJobType === 'anomaly-detector' && ( + {isADEnabled === true && selectedJobType === 'anomaly-detector' && ( <> {loadingADJobs === true ? ( @@ -308,7 +341,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { )} )} - {selectedJobType === 'data-frame-analytics' && ( + {isDFAEnabled === true && selectedJobType === 'data-frame-analytics' && ( <> {loadingDFAJobs === true ? ( diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index 6c6dc10f085f5..61e6df17676cb 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -33,30 +33,33 @@ import { type ErrorType, extractErrorProperties } from '@kbn/ml-error-utils'; import type { DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-utils'; import type { JobType } from '../../../../../common/types/saved_objects'; -import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; +import { useMlKibana } from '../../../contexts/kibana'; import { CannotImportJobsCallout } from './cannot_import_jobs_callout'; import { CannotReadFileCallout } from './cannot_read_file_callout'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import { JobImportService } from './jobs_import_service'; import { useValidateIds } from './validate'; import type { ImportedAdJob, JobIdObject, SkippedJobs } from './jobs_import_service'; +import { usePermissionCheck } from '../../../capabilities/check_capabilities'; interface Props { isDisabled: boolean; } export const ImportJobsFlyout: FC = ({ isDisabled }) => { - const { - jobs: { bulkCreateJobs }, - dataFrameAnalytics: { createDataFrameAnalytics }, - filters: { filters: getFilters }, - } = useMlApiContext(); const { services: { data: { dataViews: { getTitles: getDataViewTitles }, }, notifications: { toasts }, - mlServices: { mlUsageCollection }, + mlServices: { + mlUsageCollection, + mlApiServices: { + jobs: { bulkCreateJobs }, + dataFrameAnalytics: { createDataFrameAnalytics }, + filters: { filters: getFilters }, + }, + }, }, } = useMlKibana(); @@ -79,6 +82,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { () => toastNotificationServiceProvider(toasts), [toasts] ); + const [isADEnabled, isDFAEnabled] = usePermissionCheck(['isADEnabled', 'isDFAEnabled']); const [validateIds] = useValidateIds( jobType, @@ -123,7 +127,11 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { try { const loadedFile = await jobImportService.readJobConfigs(files[0]); - if (loadedFile.jobType === null) { + if ( + loadedFile.jobType === null || + (loadedFile.jobType === 'anomaly-detector' && isADEnabled === false) || + (loadedFile.jobType === 'data-frame-analytics' && isDFAEnabled === false) + ) { reset(true); return; } @@ -177,19 +185,19 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { const onImport = useCallback(async () => { setImporting(true); - if (jobType === 'anomaly-detector') { - const renamedJobs = jobImportService.renameAdJobs(jobIdObjects, adJobs); - try { + try { + if (jobType === 'anomaly-detector' && isADEnabled === true) { + const renamedJobs = jobImportService.renameAdJobs(jobIdObjects, adJobs); await bulkCreateADJobs(renamedJobs); mlUsageCollection.count('imported_anomaly_detector_jobs', renamedJobs.length); - } catch (error) { - // display unexpected error - displayErrorToast(error); + } else if (jobType === 'data-frame-analytics' && isDFAEnabled === true) { + const renamedJobs = jobImportService.renameDfaJobs(jobIdObjects, dfaJobs); + await bulkCreateDfaJobs(renamedJobs); + mlUsageCollection.count('imported_data_frame_analytics_jobs', renamedJobs.length); } - } else if (jobType === 'data-frame-analytics') { - const renamedJobs = jobImportService.renameDfaJobs(jobIdObjects, dfaJobs); - await bulkCreateDfaJobs(renamedJobs); - mlUsageCollection.count('imported_data_frame_analytics_jobs', renamedJobs.length); + } catch (error) { + // display unexpected error + displayErrorToast(error); } setImporting(false); @@ -347,6 +355,10 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { /> ); + if (isADEnabled === false && isDFAEnabled === false) { + return null; + } + return ( <> diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index 16bbf4490e9ad..20faeaf0bdfd2 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { EuiBasicTableColumn, @@ -25,6 +25,7 @@ import { JobMessage } from '../../../../common/types/audit_message'; import { blurButtonOnClick } from '../../util/component_utils'; import { JobIcon } from '../job_message_icon'; +import { useIsServerless } from '../../contexts/kibana'; interface JobMessagesProps { messages: JobMessage[]; @@ -45,54 +46,62 @@ export const JobMessages: FC = ({ refreshMessage, actionHandler, }) => { - const columns: Array> = [ - { - name: refreshMessage ? ( - - { - refreshMessage(); - })} - iconType="refresh" - aria-label={i18n.translate('xpack.ml.jobMessages.refreshAriaLabel', { + const isServerless = useIsServerless(); + const columns: Array> = useMemo(() => { + const cols = [ + { + name: refreshMessage ? ( + - - ) : ( - '' - ), - render: (message: JobMessage) => , - width: `${theme.euiSizeL}`, - }, - { - field: 'timestamp', - name: i18n.translate('xpack.ml.jobMessages.timeLabel', { - defaultMessage: 'Time', - }), - render: timeFormatter, - width: '120px', - sortable: true, - }, - { - field: 'node_name', - name: i18n.translate('xpack.ml.jobMessages.nodeLabel', { - defaultMessage: 'Node', - }), - width: '150px', - }, - { - field: 'message', - name: i18n.translate('xpack.ml.jobMessages.messageLabel', { - defaultMessage: 'Message', - }), - width: '50%', - }, - ]; + > + { + refreshMessage(); + })} + iconType="refresh" + aria-label={i18n.translate('xpack.ml.jobMessages.refreshAriaLabel', { + defaultMessage: 'Refresh', + })} + /> + + ) : ( + '' + ), + render: (message: JobMessage) => , + width: `${theme.euiSizeL}`, + }, + { + field: 'timestamp', + name: i18n.translate('xpack.ml.jobMessages.timeLabel', { + defaultMessage: 'Time', + }), + render: timeFormatter, + width: '120px', + sortable: true, + }, + { + field: 'message', + name: i18n.translate('xpack.ml.jobMessages.messageLabel', { + defaultMessage: 'Message', + }), + width: '50%', + }, + ]; + + if (isServerless === false) { + cols.splice(2, 0, { + field: 'node_name', + name: i18n.translate('xpack.ml.jobMessages.nodeLabel', { + defaultMessage: 'Node', + }), + width: '150px', + }); + } + + return cols; + }, [isServerless, refreshMessage]); if (typeof actionHandler === 'function') { columns.push({ diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_sync/sync_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_sync/sync_list.tsx index 8d724f5b6187e..211dc1aa8c57b 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_sync/sync_list.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_sync/sync_list.tsx @@ -18,8 +18,11 @@ import { } from '@elastic/eui'; import type { SyncSavedObjectResponse, SyncResult } from '../../../../common/types/saved_objects'; +import { usePermissionCheck } from '../../capabilities/check_capabilities'; export const SyncList: FC<{ syncItems: SyncSavedObjectResponse | null }> = ({ syncItems }) => { + const [isADEnabled] = usePermissionCheck(['isADEnabled']); + if (syncItems === null) { return null; } @@ -34,13 +37,17 @@ export const SyncList: FC<{ syncItems: SyncSavedObjectResponse | null }> = ({ sy - + {isADEnabled ? ( + <> + - + - + - + + + ) : null} ); }; diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx index 07a10b6a36c81..a85776aeacc82 100644 --- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx @@ -10,13 +10,15 @@ import React, { FC } from 'react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { lazyMlNodesAvailable } from '../../ml_nodes_check'; +import { useIsServerless } from '../../contexts/kibana'; interface Props { jobCount: number; } export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => { - if (lazyMlNodesAvailable() === false || jobCount === 0) { + const isServerless = useIsServerless(); + if (isServerless || lazyMlNodesAvailable() === false || jobCount === 0) { return null; } diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx index 760ebc521d251..5055efad210f0 100644 --- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/new_job_awaiting_node.tsx @@ -11,17 +11,33 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { JobType } from '../../../../common/types/saved_objects'; import { lazyMlNodesAvailable } from '../../ml_nodes_check'; +import { useIsServerless } from '../../contexts/kibana'; interface Props { jobType: JobType; } export const NewJobAwaitingNodeWarning: FC = () => { + const isServerless = useIsServerless(); if (lazyMlNodesAvailable() === false) { return null; } - return ( + return isServerless ? ( + <> + + } + color="primary" + iconType="iInCircle" + /> + + + ) : ( <> { const getAllJobAndGroupIds = jest.fn(() => { diff --git a/x-pack/plugins/ml/public/application/components/ml_entity_selector/ml_entity_selector.tsx b/x-pack/plugins/ml/public/application/components/ml_entity_selector/ml_entity_selector.tsx index c31d091fdf67c..22c14ff7d72a9 100644 --- a/x-pack/plugins/ml/public/application/components/ml_entity_selector/ml_entity_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_entity_selector/ml_entity_selector.tsx @@ -17,6 +17,7 @@ import { countBy } from 'lodash'; import useMount from 'react-use/lib/useMount'; import { useMlApiContext } from '../../contexts/kibana'; import { useToastNotificationService } from '../../services/toast_notification_service'; +import { usePermissionCheck } from '../../capabilities/check_capabilities'; type EntityType = 'anomaly_detector' | 'data_frame_analytics' | 'trained_models'; @@ -60,6 +61,11 @@ export const MlEntitySelector: FC = ({ onSelectionChange, handleDuplicates = false, }) => { + const [isADEnabled, isDFAEnabled, isNLPEnabled] = usePermissionCheck([ + 'isADEnabled', + 'isDFAEnabled', + 'isNLPEnabled', + ]); const { jobs: jobsApi, trainedModels, dataFrameAnalytics } = useMlApiContext(); const { displayErrorToast } = useToastNotificationService(); const visColorsBehindText = euiPaletteColorBlindBehindText(); @@ -70,7 +76,7 @@ export const MlEntitySelector: FC = ({ const fetchOptions = useCallback(async () => { try { const newOptions: Array> = []; - if (entityTypes?.anomaly_detector) { + if (isADEnabled && entityTypes?.anomaly_detector) { const { jobIds: jobIdOptions } = await jobsApi.getAllJobAndGroupIds(); newOptions.push({ @@ -90,7 +96,7 @@ export const MlEntitySelector: FC = ({ }); } - if (entityTypes?.data_frame_analytics) { + if (isDFAEnabled && entityTypes?.data_frame_analytics) { const dfa = await dataFrameAnalytics.getDataFrameAnalytics(); if (dfa.count > 0) { newOptions.push({ @@ -110,7 +116,7 @@ export const MlEntitySelector: FC = ({ } } - if (entityTypes?.trained_models) { + if ((isDFAEnabled || isNLPEnabled) && entityTypes?.trained_models) { const models = await trainedModels.getTrainedModels(); if (models.length > 0) { newOptions.push({ @@ -147,6 +153,9 @@ export const MlEntitySelector: FC = ({ entityTypes, visColorsBehindText, displayErrorToast, + isADEnabled, + isDFAEnabled, + isNLPEnabled, ]); useMount(function fetchOptionsOnMount() { diff --git a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx index 6c981351d5f63..3eb1deb83e7ae 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx @@ -20,7 +20,7 @@ import { DatePickerWrapper } from '@kbn/ml-date-picker'; import * as routes from '../../routing/routes'; import { MlPageWrapper } from '../../routing/ml_page_wrapper'; -import { useMlKibana, useNavigateToPath } from '../../contexts/kibana'; +import { useMlKibana, useNavigateToPath, useIsServerless } from '../../contexts/kibana'; import type { MlRoute, PageDependencies } from '../../routing/router'; import { useActiveRoute } from '../../routing/use_active_route'; import { useDocTitle } from '../../routing/use_doc_title'; @@ -28,7 +28,6 @@ import { useDocTitle } from '../../routing/use_doc_title'; import { MlPageHeaderRenderer } from '../page_header/page_header'; import { useSideNavItems } from './side_nav'; -import { usePermissionCheck } from '../../capabilities/check_capabilities'; const ML_APP_SELECTOR = '[data-test-subj="mlApp"]'; @@ -56,22 +55,12 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps mlServices: { httpService }, }, } = useMlKibana(); + const isServerless = useIsServerless(); const headerPortalNode = useMemo(() => createHtmlPortalNode(), []); const [isHeaderMounted, setIsHeaderMounted] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [isADEnabled, isDFAEnabled, isNLPEnabled] = usePermissionCheck([ - 'isADEnabled', - 'isDFAEnabled', - 'isNLPEnabled', - ]); - - const navMenuEnabled = useMemo( - () => isADEnabled && isDFAEnabled && isNLPEnabled, - [isADEnabled, isDFAEnabled, isNLPEnabled] - ); - useEffect(() => { const subscriptions = new Subscription(); @@ -138,7 +127,7 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps data-test-subj={'mlApp'} restrictWidth={false} solutionNav={ - navMenuEnabled + isServerless === false ? { name: i18n.translate('xpack.ml.plugin.title', { defaultMessage: 'Machine Learning', diff --git a/x-pack/plugins/ml/public/application/components/saved_objects_warning/saved_objects_warning.tsx b/x-pack/plugins/ml/public/application/components/saved_objects_warning/saved_objects_warning.tsx index 342caa5225883..1e935e657a1e2 100644 --- a/x-pack/plugins/ml/public/application/components/saved_objects_warning/saved_objects_warning.tsx +++ b/x-pack/plugins/ml/public/application/components/saved_objects_warning/saved_objects_warning.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { MlSavedObjectType } from '../../../../common/types/saved_objects'; @@ -31,7 +31,17 @@ export const SavedObjectsWarning: FC = ({ const mounted = useRef(false); const [showWarning, setShowWarning] = useState(false); const [showSyncFlyout, setShowSyncFlyout] = useState(false); - const canCreateJob = usePermissionCheck('canCreateJob'); + + const [canCreateJob, canCreateDataFrameAnalytics, canCreateTrainedModels] = usePermissionCheck([ + 'canCreateJob', + 'canCreateDataFrameAnalytics', + 'canCreateTrainedModels', + ]); + + const canSync = useMemo( + () => canCreateJob || canCreateDataFrameAnalytics || canCreateTrainedModels, + [canCreateDataFrameAnalytics, canCreateJob, canCreateTrainedModels] + ); const checkStatus = useCallback(async () => { try { @@ -101,7 +111,7 @@ export const SavedObjectsWarning: FC = ({ id="xpack.ml.jobsList.missingSavedObjectWarning.description" defaultMessage="Some jobs or trained models are missing or have incomplete saved objects. " /> - {canCreateJob ? ( + {canSync ? ( { /** * Handle urls generated by MlUrlGenerator where '/app/ml' is automatically prepended */ - const url = modifiedPath.includes('/app/ml') + const url = modifiedPath.includes(ML_APP_ROUTE) ? modifiedPath : getUrlForApp(PLUGIN_ID, { path: modifiedPath, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx index c86808bd9b255..74356242ae8aa 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/components/controls.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useEffect, useState, useContext, useCallback } from 'react'; +import React, { FC, useEffect, useState, useContext, useCallback, useMemo } from 'react'; import cytoscape from 'cytoscape'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -37,6 +37,7 @@ import { useNotifications, useNavigateToPath, useMlKibana, + useIsServerless, } from '../../../../contexts/kibana'; import { getDataViewIdFromName } from '../../../../util/index_utils'; import { useNavigateToWizardWithClonedJob } from '../../analytics_management/components/action_clone/clone_action_name'; @@ -47,34 +48,40 @@ import { import { DeleteSpaceAwareItemCheckModal } from '../../../../components/delete_space_aware_item_check_modal'; interface Props { - details: any; + details: Record; getNodeData: any; modelId?: string; updateElements: (nodeId: string, nodeLabel: string, destIndexNode?: string) => void; refreshJobsCallback: () => void; } -function getListItems(details: object): EuiDescriptionListProps['listItems'] { - return Object.entries(details).map(([key, value]) => { - let description; - if (key === 'create_time') { - description = formatHumanReadableDateTimeSeconds(moment(value).unix() * 1000); - } else { - description = - typeof value === 'object' ? ( - - {JSON.stringify(value, null, 2)} - - ) : ( - value - ); +function getListItemsFactory(isServerless: boolean) { + return (details: Record): EuiDescriptionListProps['listItems'] => { + if (isServerless) { + delete details.license_level; } - return { - title: key, - description, - }; - }); + return Object.entries(details).map(([key, value]) => { + let description; + if (key === 'create_time') { + description = formatHumanReadableDateTimeSeconds(moment(value).unix() * 1000); + } else { + description = + typeof value === 'object' ? ( + + {JSON.stringify(value, null, 2)} + + ) : ( + value + ); + } + + return { + title: key, + description, + }; + }); + }; } export const Controls: FC = React.memo( @@ -87,6 +94,9 @@ export const Controls: FC = React.memo( const canCreateDataFrameAnalytics: boolean = usePermissionCheck('canCreateDataFrameAnalytics'); const canDeleteDataFrameAnalytics: boolean = usePermissionCheck('canDeleteDataFrameAnalytics'); const deleteAction = useDeleteAction(canDeleteDataFrameAnalytics); + const isServerless = useIsServerless(); + const getListItems = useMemo(() => getListItemsFactory(isServerless), [isServerless]); + const { closeDeleteJobCheckModal, deleteItem, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx index 10c7003e6e18a..3debbda8021c3 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx @@ -15,7 +15,7 @@ import type { GetAdditionalLinksParams, } from '@kbn/data-visualizer-plugin/public'; import { useTimefilter } from '@kbn/ml-date-picker'; -import { useMlKibana, useMlLocator } from '../../contexts/kibana'; +import { useMlKibana, useMlLocator, useIsServerless } from '../../contexts/kibana'; import { HelpMenu } from '../../components/help_menu'; import { ML_PAGES } from '../../../../common/constants/locator'; import { isFullLicense } from '../../license'; @@ -37,6 +37,7 @@ export const IndexDataVisualizerPage: FC = () => { }, }, } = useMlKibana(); + const isServerless = useIsServerless(); const mlLocator = useMlLocator()!; const mlFeaturesDisabled = !isFullLicense(); getMlNodeCount(); @@ -188,7 +189,10 @@ export const IndexDataVisualizerPage: FC = () => { defaultMessage="Data Visualizer" /> - + ) : null} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 75ada48dbdc4c..dcecda5575d87 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -31,6 +31,7 @@ import { JobListMlAnomalyAlertFlyout } from '../../../../../alerting/ml_alerting import { StopDatafeedsConfirmModal } from '../confirm_modals/stop_datafeeds_confirm_modal'; import { CloseJobsConfirmModal } from '../confirm_modals/close_jobs_confirm_modal'; import { AnomalyDetectionEmptyState } from '../anomaly_detection_empty_state'; +import { removeNodeInfo } from '../../../../../../common/util/job_utils'; let blockingJobsRefreshTimeout = null; @@ -136,6 +137,9 @@ export class JobsListView extends Component { loadFullJob(jobId) .then((job) => { const fullJobsList = { ...this.state.fullJobsList }; + if (this.props.isServerless) { + job = removeNodeInfo(job); + } fullJobsList[jobId] = job; this.setState({ fullJobsList }, () => { // take a fresh copy of the itemIdToExpandedRowMap object @@ -314,6 +318,9 @@ export class JobsListView extends Component { const fullJobsList = {}; const jobsSummaryList = jobs.map((job) => { if (job.fullJob !== undefined) { + if (this.props.isServerless) { + job.fullJob = removeNodeInfo(job.fullJob); + } fullJobsList[job.id] = job.fullJob; delete job.fullJob; } @@ -408,7 +415,10 @@ export class JobsListView extends Component { <> - + diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js index 8358c68e473ef..c8b2868267d80 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js @@ -12,15 +12,21 @@ import PropTypes from 'prop-types'; import React from 'react'; import { i18n } from '@kbn/i18n'; -function createJobStats(jobsSummaryList) { +function createJobStats(jobsSummaryList, isServerless) { + const displayNodeInfo = isServerless === false; + const jobStats = { - activeNodes: { - label: i18n.translate('xpack.ml.jobsList.statsBar.activeMLNodesLabel', { - defaultMessage: 'Active ML nodes', - }), - value: 0, - show: true, - }, + ...(displayNodeInfo + ? { + activeNodes: { + label: i18n.translate('xpack.ml.jobsList.statsBar.activeMLNodesLabel', { + defaultMessage: 'Active ML nodes', + }), + value: 0, + show: true, + }, + } + : {}), total: { label: i18n.translate('xpack.ml.jobsList.statsBar.totalJobsLabel', { defaultMessage: 'Total jobs', @@ -94,17 +100,20 @@ function createJobStats(jobsSummaryList) { jobStats.failed.show = false; } - jobStats.activeNodes.value = Object.keys(mlNodes).length; + if (displayNodeInfo) { + jobStats.activeNodes.value = Object.keys(mlNodes).length; + } return jobStats; } -export const JobStatsBar = ({ jobsSummaryList }) => { - const jobStats = createJobStats(jobsSummaryList); +export const JobStatsBar = ({ jobsSummaryList, isServerless }) => { + const jobStats = createJobStats(jobsSummaryList, isServerless); return ; }; JobStatsBar.propTypes = { jobsSummaryList: PropTypes.array.isRequired, + isServerless: PropTypes.bool.isRequired, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index f93f1642b10f0..f413b97cb3602 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -12,7 +12,7 @@ import { JobsListView } from './components/jobs_list_view'; import { ML_PAGES } from '../../../../common/constants/locator'; import { ListingPageUrlState } from '../../../../common/types/common'; import { HelpMenu } from '../../components/help_menu'; -import { useMlKibana } from '../../contexts/kibana'; +import { useIsServerless, useMlKibana } from '../../contexts/kibana'; import { MlPageHeader } from '../../components/page_header'; import { HeaderMenuPortal } from '../../components/header_menu_portal'; import { JobsActionMenu } from '../components/jobs_action_menu'; @@ -42,6 +42,7 @@ export const JobsPage: FC = ({ isMlEnabledInSpace, lastRefresh }) const { services: { docLinks }, } = useMlKibana(); + const isServerless = useIsServerless(); const helpLink = docLinks.links.ml.anomalyDetection; return ( <> @@ -56,6 +57,7 @@ export const JobsPage: FC = ({ isMlEnabledInSpace, lastRefresh }) lastRefresh={lastRefresh} jobsViewState={pageState} onJobsViewStateUpdate={setPageState} + isServerless={isServerless} /> diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx index d93eb690c31d9..d9fa9e542c0c5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx @@ -22,7 +22,7 @@ import { EventRateChart } from '../charts/event_rate_chart'; import { LineChartPoint } from '../../../common/chart_loader'; import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { TimeRangePicker, TimeRange } from '../../../common/components'; -import { useMlKibana } from '../../../../../contexts/kibana'; +import { useMlKibana, useIsServerless } from '../../../../../contexts/kibana'; import { ML_FROZEN_TIER_PREFERENCE, type MlStorageKey, @@ -33,6 +33,7 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) const timefilter = useTimefilter(); const { services } = useMlKibana(); const dataSourceContext = useDataSource(); + const isServerless = useIsServerless(); const { jobCreator, jobCreatorUpdate, jobCreatorUpdated, chartLoader, chartInterval } = useContext(JobCreatorContext); @@ -137,6 +138,7 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) callback={fullTimeRangeCallback} timefilter={timefilter} apiPath={`${ML_INTERNAL_BASE_PATH}/fields_service/time_field_range`} + hideFrozenDataTierChoice={isServerless} /> diff --git a/x-pack/plugins/ml/public/application/management/index.ts b/x-pack/plugins/ml/public/application/management/index.ts index f64d7cbb5bb64..dea3ba6cd8afa 100644 --- a/x-pack/plugins/ml/public/application/management/index.ts +++ b/x-pack/plugins/ml/public/application/management/index.ts @@ -16,7 +16,8 @@ import type { MlStartDependencies } from '../../plugin'; export function registerManagementSection( management: ManagementSetup, core: CoreSetup, - deps: { usageCollection?: UsageCollectionSetup } + deps: { usageCollection?: UsageCollectionSetup }, + isServerless: boolean ) { return management.sections.section.insightsAndAlerting.registerApp({ id: 'jobsListLink', @@ -26,7 +27,7 @@ export function registerManagementSection( order: 4, async mount(params: ManagementAppMountParams) { const { mountApp } = await import('./jobs_list'); - return mountApp(core, params, deps); + return mountApp(core, params, deps, isServerless); }, }); } diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 2ce2e9ddd9d56..2cf92278cfa63 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -40,9 +40,7 @@ import { JobSpacesSyncFlyout } from '../../../../components/job_spaces_sync'; import { getMlGlobalServices } from '../../../../app'; import { ExportJobsFlyout, ImportJobsFlyout } from '../../../../components/import_export_jobs'; import type { MlSavedObjectType } from '../../../../../../common/types/saved_objects'; -import { mlApiServicesProvider } from '../../../../services/ml_api_service'; -import { HttpService } from '../../../../services/http_service'; import { SpaceManagement } from './space_management'; import { DocsLink } from './docs_link'; @@ -56,11 +54,17 @@ export const JobsListPage: FC<{ data: DataPublicPluginStart; usageCollection?: UsageCollectionSetup; fieldFormats: FieldFormatsStart; -}> = ({ coreStart, share, history, spacesApi, data, usageCollection, fieldFormats }) => { - const mlApiServices = useMemo( - () => mlApiServicesProvider(new HttpService(coreStart.http)), - [coreStart.http] - ); + isServerless: boolean; +}> = ({ + coreStart, + share, + history, + spacesApi, + data, + usageCollection, + fieldFormats, + isServerless, +}) => { const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [isPlatinumOrTrialLicense, setIsPlatinumOrTrialLicense] = useState(true); @@ -70,13 +74,13 @@ export const JobsListPage: FC<{ const theme$ = coreStart.theme.theme$; const mlServices = useMemo( - () => getMlGlobalServices(coreStart.http, usageCollection), - [coreStart.http, usageCollection] + () => getMlGlobalServices(coreStart.http, isServerless, usageCollection), + [coreStart.http, isServerless, usageCollection] ); const check = async () => { try { - await checkGetManagementMlJobsResolver(mlApiServices); + await checkGetManagementMlJobsResolver(mlServices); } catch (e) { if (e.mlFeatureEnabledInSpace && e.isPlatinumOrTrialLicense === false) { setIsPlatinumOrTrialLicense(false); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/space_management/space_management.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/space_management/space_management.tsx index 2564f7c55da1e..bb21b971a8c12 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/space_management/space_management.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/space_management/space_management.tsx @@ -21,6 +21,7 @@ import { import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import { useTableState } from '@kbn/ml-in-memory-table'; +import { usePermissionCheck } from '../../../../../capabilities/check_capabilities'; import type { JobType, MlSavedObjectType } from '../../../../../../../common/types/saved_objects'; import type { ManagementListResponse, @@ -38,12 +39,19 @@ interface Props { export const SpaceManagement: FC = ({ spacesApi, setCurrentTab }) => { const { getList } = useManagementApiService(); - const [currentTabId, setCurrentTabId] = useState('anomaly-detector'); + + const [currentTabId, setCurrentTabId] = useState(null); const [items, setItems] = useState(); const [columns, setColumns] = useState>>([]); const [filters, setFilters] = useState(); const [isLoading, setIsLoading] = useState(false); + const [isADEnabled, isDFAEnabled, isNLPEnabled] = usePermissionCheck([ + 'isADEnabled', + 'isDFAEnabled', + 'isNLPEnabled', + ]); + const { onTableChange, pagination, sorting, setPageIndex } = useTableState( items ?? [], 'id' @@ -56,9 +64,26 @@ export const SpaceManagement: FC = ({ spacesApi, setCurrentTab }) => { }; }, []); + useEffect( + function setInitialSelectedTab() { + if (isADEnabled === true) { + setCurrentTabId('anomaly-detector'); + } else if (isDFAEnabled === true) { + setCurrentTabId('data-frame-analytics'); + } else if (isNLPEnabled === true) { + setCurrentTabId('trained-model'); + } + }, + [isADEnabled, isDFAEnabled, isNLPEnabled] + ); + const loadingTab = useRef(null); const refresh = useCallback( - (tabId: MlSavedObjectType) => { + (tabId: MlSavedObjectType | null) => { + if (tabId === null) { + return; + } + loadingTab.current = tabId; setIsLoading(true); getList(tabId) @@ -83,16 +108,21 @@ export const SpaceManagement: FC = ({ spacesApi, setCurrentTab }) => { useEffect( function refreshOnTabChange() { setItems(undefined); - setColumns(createColumns()); - setCurrentTab(currentTabId); - refresh(currentTabId); - setPageIndex(0); + if (currentTabId !== null) { + setColumns(createColumns()); + setCurrentTab(currentTabId); + refresh(currentTabId); + setPageIndex(0); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [currentTabId] ); const createColumns = useCallback(() => { + if (currentTabId === null) { + return []; + } return [ ...getColumns(currentTabId), ...(spacesApi !== undefined @@ -165,35 +195,41 @@ export const SpaceManagement: FC = ({ spacesApi, setCurrentTab }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [items, columns, isLoading, filters, currentTabId, refresh, onTableChange]); - const tabs = useMemo( - () => [ - { + const tabs = useMemo(() => { + const tempTabs = []; + + if (isADEnabled === true) { + tempTabs.push({ 'data-test-subj': 'mlStackManagementAnomalyDetectionTab', id: 'anomaly-detector', name: i18n.translate('xpack.ml.management.list.anomalyDetectionTab', { defaultMessage: 'Anomaly detection', }), content: getTable(), - }, - { + }); + } + if (isDFAEnabled === true) { + tempTabs.push({ 'data-test-subj': 'mlStackManagementAnalyticsTab', id: 'data-frame-analytics', name: i18n.translate('xpack.ml.management.list.analyticsTab', { defaultMessage: 'Analytics', }), content: getTable(), - }, - { + }); + } + if (isNLPEnabled === true || isDFAEnabled === true) { + tempTabs.push({ 'data-test-subj': 'mlStackManagementTrainedModelsTab', id: 'trained-model', name: i18n.translate('xpack.ml.management.list.trainedModelsTab', { defaultMessage: 'Trained models', }), content: getTable(), - }, - ], - [getTable] - ); + }); + } + return tempTabs; + }, [getTable, isADEnabled, isDFAEnabled, isNLPEnabled]); return ( { @@ -37,6 +38,7 @@ const renderApp = ( spacesApi, usageCollection, fieldFormats, + isServerless, }), element ); @@ -48,7 +50,8 @@ const renderApp = ( export async function mountApp( core: CoreSetup, params: ManagementAppMountParams, - deps: { usageCollection?: UsageCollectionSetup } + deps: { usageCollection?: UsageCollectionSetup }, + isServerless: boolean ) { const [coreStart, pluginsStart] = await core.getStartServices(); @@ -60,6 +63,7 @@ export async function mountApp( pluginsStart.share, pluginsStart.data, pluginsStart.fieldFormats, + isServerless, pluginsStart.spaces, deps.usageCollection ); diff --git a/x-pack/plugins/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx b/x-pack/plugins/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx index a90c088b40e26..f134b2e5fbb83 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx @@ -29,6 +29,7 @@ import { useFieldFormatter, useMlKibana } from '../../contexts/kibana'; import { useRefresh } from '../../routing/use_refresh'; import { getMemoryItemColor } from '../memory_item_colors'; import { useToastNotificationService } from '../../services/toast_notification_service'; +import { usePermissionCheck } from '../../capabilities/check_capabilities'; interface Props { node?: string; @@ -58,13 +59,6 @@ const TYPE_LABELS_INVERTED = Object.entries(TYPE_LABELS).reduce ); -const TYPE_OPTIONS: EuiComboBoxOptionOption[] = Object.entries(TYPE_LABELS).map( - ([label, type]) => ({ - label, - color: getMemoryItemColor(type), - }) -); - export const JobMemoryTreeMap: FC = ({ node, type, height }) => { const { services: { theme: themeService }, @@ -79,6 +73,12 @@ export const JobMemoryTreeMap: FC = ({ node, type, height }) => { [isDarkTheme] ); + const [isADEnabled, isDFAEnabled, isNLPEnabled] = usePermissionCheck([ + 'isADEnabled', + 'isDFAEnabled', + 'isNLPEnabled', + ]); + const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES); const { displayErrorToast } = useToastNotificationService(); const refresh = useRefresh(); @@ -88,10 +88,40 @@ export const JobMemoryTreeMap: FC = ({ node, type, height }) => { const [allData, setAllData] = useState([]); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); - const [selectedOptions, setSelectedOptions] = useState(TYPE_OPTIONS); + const [selectedOptions, setSelectedOptions] = useState(null); + const typeOptions = useMemo(() => { + return Object.entries(TYPE_LABELS) + .filter(([, t]) => { + if ( + (t === 'anomaly-detector' && isADEnabled === false) || + (t === 'data-frame-analytics' && isDFAEnabled === false) || + (t === 'trained-model' && isNLPEnabled === false && isDFAEnabled === false) + ) { + return false; + } + + return true; + }) + .map(([label, t]) => ({ + label, + color: getMemoryItemColor(t), + })); + }, [isADEnabled, isDFAEnabled, isNLPEnabled]); + + useEffect( + function initSelectedOptions() { + if (selectedOptions === null) { + setSelectedOptions(typeOptions); + } + }, + [selectedOptions, typeOptions] + ); const filterData = useCallback( (dataIn: MemoryUsageInfo[]) => { + if (selectedOptions === null) { + return dataIn; + } const types = selectedOptions.map((o) => TYPE_LABELS[o.label]); return dataIn.filter((d) => types.includes(d.type)); }, @@ -137,8 +167,8 @@ export const JobMemoryTreeMap: FC = ({ node, type, height }) => { 0} loading={loading}> diff --git a/x-pack/plugins/ml/public/application/memory_usage/memory_usage_page.tsx b/x-pack/plugins/ml/public/application/memory_usage/memory_usage_page.tsx index fd0c2f4157a58..249d9c959809d 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/memory_usage_page.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/memory_usage_page.tsx @@ -13,7 +13,7 @@ import { NodesList } from './nodes_overview'; import { MlPageHeader } from '../components/page_header'; import { MemoryPage, JobMemoryTreeMap } from './memory_tree_map'; import { SavedObjectsWarning } from '../components/saved_objects_warning'; -import { usePermissionCheck } from '../capabilities/check_capabilities'; +import { useIsServerless } from '../contexts/kibana'; enum TAB { NODES, @@ -23,11 +23,8 @@ enum TAB { export const MemoryUsagePage: FC = () => { const [selectedTab, setSelectedTab] = useState(TAB.NODES); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true }); - const [isADEnabled, isDFAEnabled, isNLPEnabled] = usePermissionCheck([ - 'isADEnabled', - 'isDFAEnabled', - 'isNLPEnabled', - ]); + + const isServerless = useIsServerless(); const refresh = useCallback(() => { mlTimefilterRefresh$.next({ @@ -50,7 +47,7 @@ export const MemoryUsagePage: FC = () => { - {isADEnabled && isDFAEnabled && isNLPEnabled ? ( + {isServerless === false ? ( <> = ({ item }) => { const formatToListItems = useListItemsFormatter(); + const isServerless = useIsServerless(); const { inference_config: inferenceConfig, @@ -149,7 +151,7 @@ export const ExpandedRow: FC = ({ item }) => { estimated_operations, estimated_heap_memory_usage_bytes, default_field_map, - license_level, + ...(isServerless ? {} : { license_level }), }; }, [ default_field_map, @@ -159,6 +161,7 @@ export const ExpandedRow: FC = ({ item }) => { license_level, tags, version, + isServerless, ]); const deploymentStatItems: AllocatedModel[] = useMemo(() => { @@ -196,6 +199,10 @@ export const ExpandedRow: FC = ({ item }) => { return items; }, [stats]); + const hideColumns = useMemo(() => { + return isServerless ? ['model_id', 'node_name'] : ['model_id']; + }, [isServerless]); + const tabs = useMemo(() => { return [ { @@ -341,7 +348,7 @@ export const ExpandedRow: FC = ({ item }) => { - + @@ -455,6 +462,7 @@ export const ExpandedRow: FC = ({ item }) => { restMetaData, stats, item.model_id, + hideColumns, ]); const initialSelectedTab = diff --git a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx index 2dc8e3f3ff5d5..80eb6e91843f8 100644 --- a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx +++ b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx @@ -534,7 +534,9 @@ export function useModelActions({ isPrimary: true, available: isTestable, onClick: (item) => onTestAction(item), - enabled: (item) => canTestTrainedModels && isTestable(item, true) && !isLoading, + enabled: (item) => { + return canTestTrainedModels && isTestable(item, true) && !isLoading; + }, }, ], [ diff --git a/x-pack/plugins/ml/public/application/model_management/models_list.tsx b/x-pack/plugins/ml/public/application/model_management/models_list.tsx index c90142da28eeb..5ddcc2096b914 100644 --- a/x-pack/plugins/ml/public/application/model_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/model_management/models_list.tsx @@ -63,6 +63,7 @@ import { useRefresh } from '../routing/use_refresh'; import { SavedObjectsWarning } from '../components/saved_objects_warning'; import { TestTrainedModelFlyout } from './test_models'; import { AddInferencePipelineFlyout } from '../components/ml_inference'; +import { usePermissionCheck } from '../capabilities/check_capabilities'; type Stats = Omit; @@ -104,6 +105,8 @@ export const ModelsList: FC = ({ }, } = useMlKibana(); + const [isNLPEnabled] = usePermissionCheck(['isNLPEnabled']); + useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true }); const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); @@ -582,6 +585,11 @@ export const ModelsList: FC = ({ }; const resultItems = useMemo(() => { + if (isNLPEnabled === false) { + // don't add any of the built in models (e.g. elser) if NLP is disabled + return items; + } + const idSet = new Set(items.map((i) => i.model_id)); const notDownloaded: ModelItem[] = Object.entries(ELASTIC_MODEL_DEFINITIONS) .filter(([modelId]) => !idSet.has(modelId)) @@ -594,8 +602,10 @@ export const ModelsList: FC = ({ description: modelDefinition.description, } as ModelItem; }); - return [...items, ...notDownloaded]; - }, [items]); + const result = [...items, ...notDownloaded]; + + return result; + }, [isNLPEnabled, items]); if (!isInitialized) return null; diff --git a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.test.tsx b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.test.tsx index f9f7478be6e3e..91e6650a544f7 100644 --- a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.test.tsx +++ b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.test.tsx @@ -17,6 +17,7 @@ jest.mock('../../services/toast_notification_service'); jest.mock('../../contexts/ml/ml_notifications_context'); jest.mock('../../contexts/kibana/use_field_formatter'); jest.mock('../../components/saved_objects_warning'); +jest.mock('../../capabilities/check_capabilities'); const getMockedTimefilter = () => { // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx index 4c04333b71e61..5edd19f4d8119 100644 --- a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx +++ b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx @@ -40,6 +40,7 @@ import type { NotificationItem, } from '../../../../common/types/notifications'; import { useMlKibana } from '../../contexts/kibana'; +import { usePermissionCheck } from '../../capabilities/check_capabilities'; const levelBadgeMap: Record = { [ML_NOTIFICATIONS_MESSAGE_LEVEL.ERROR]: 'danger', @@ -65,6 +66,13 @@ export const NotificationsList: FC = () => { mlServices: { mlApiServices }, }, } = useMlKibana(); + + const [isADEnabled, isDFAEnabled, isNLPEnabled] = usePermissionCheck([ + 'isADEnabled', + 'isDFAEnabled', + 'isNLPEnabled', + ]); + const { displayErrorToast } = useToastNotificationService(); const { lastCheckedAt, setLastCheckedAt, notificationsCounts, latestRequestedAt } = @@ -221,6 +229,39 @@ export const NotificationsList: FC = () => { }, [dateFormatter]); const filters: SearchFilterConfig[] = useMemo(() => { + const jobTypeOptions = []; + if (isADEnabled === true) { + jobTypeOptions.push({ + value: 'anomaly_detector', + name: i18n.translate('xpack.ml.notifications.filters.type.anomalyDetector', { + defaultMessage: 'Anomaly Detection', + }), + }); + } + if (isDFAEnabled === true) { + jobTypeOptions.push({ + value: 'data_frame_analytics', + name: i18n.translate('xpack.ml.notifications.filters.type.dfa', { + defaultMessage: 'Data Frame Analytics', + }), + }); + } + if (isNLPEnabled === true || isDFAEnabled === true) { + jobTypeOptions.push({ + value: 'inference', + name: i18n.translate('xpack.ml.notifications.filters.type.inference', { + defaultMessage: 'Inference', + }), + }); + } + + jobTypeOptions.push({ + value: 'system', + name: i18n.translate('xpack.ml.notifications.filters.type.system', { + defaultMessage: 'System', + }), + }); + return [ { type: 'field_value_selection', @@ -260,39 +301,14 @@ export const NotificationsList: FC = () => { defaultMessage: 'Type', }), multiSelect: 'or', - options: [ - { - value: 'anomaly_detector', - name: i18n.translate('xpack.ml.notifications.filters.type.anomalyDetector', { - defaultMessage: 'Anomaly Detection', - }), - }, - { - value: 'data_frame_analytics', - name: i18n.translate('xpack.ml.notifications.filters.type.dfa', { - defaultMessage: 'Data Frame Analytics', - }), - }, - { - value: 'inference', - name: i18n.translate('xpack.ml.notifications.filters.type.inference', { - defaultMessage: 'Inference', - }), - }, - { - value: 'system', - name: i18n.translate('xpack.ml.notifications.filters.type.system', { - defaultMessage: 'System', - }), - }, - ], + options: jobTypeOptions, }, { type: 'custom_component', component: EntityFilter, }, ]; - }, []); + }, [isADEnabled, isDFAEnabled, isNLPEnabled]); const newNotificationsCount = Object.values(notificationsCounts).reduce((a, b) => a + b); diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 30aa0f6d22dfb..9707515deb800 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -19,7 +19,7 @@ import { import { ML_PAGES } from '../../../../../common/constants/locator'; import { OverviewStatsBar } from '../../../components/collapsible_panel/collapsible_panel'; import { CollapsiblePanel } from '../../../components/collapsible_panel'; -import { useMlKibana, useMlLink } from '../../../contexts/kibana'; +import { useMlKibana, useMlLink, useIsServerless } from '../../../contexts/kibana'; import { AnomalyDetectionTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData } from './utils'; @@ -57,6 +57,7 @@ export const AnomalyDetectionPanel: FC = ({ anomalyTimelineService, setLa } = useMlKibana(); const { displayErrorToast } = useToastNotificationService(); + const isServerless = useIsServerless(); const refresh = useRefresh(); @@ -91,7 +92,7 @@ export const AnomalyDetectionPanel: FC = ({ anomalyTimelineService, setLa return job; }); const { groups: jobsGroups, count } = getGroupsFromJobs(jobsSummaryList); - const stats = getStatsBarData(jobsSummaryList); + const stats = getStatsBarData(jobsSummaryList, isServerless); const statGroups = groupBy( Object.entries(stats) diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts index a5a864e139eae..3e46b258c3a05 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts @@ -74,7 +74,7 @@ export function getGroupsFromJobs(jobs: MlSummaryJobs): { return { groups, count }; } -export function getStatsBarData(jobsList: any) { +export function getStatsBarData(jobsList: MlSummaryJob[] | undefined, isServerless: boolean) { const jobStats = { total: { label: i18n.translate('xpack.ml.overviewJobsList.statsBar.totalJobsLabel', { @@ -108,14 +108,18 @@ export function getStatsBarData(jobsList: any) { show: false, group: 0, }, - activeNodes: { - label: i18n.translate('xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel', { - defaultMessage: 'Active ML nodes', - }), - value: 0, - show: true, - group: 1, - }, + ...(isServerless + ? {} + : { + activeNodes: { + label: i18n.translate('xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel', { + defaultMessage: 'Active ML nodes', + }), + value: 0, + show: true, + group: 1, + }, + }), activeDatafeeds: { label: i18n.translate('xpack.ml.jobsList.statsBar.activeDatafeedsLabel', { defaultMessage: 'Active datafeeds', @@ -162,7 +166,9 @@ export function getStatsBarData(jobsList: any) { jobStats.failed.show = false; } - jobStats.activeNodes.value = Object.keys(mlNodes).length; + if (isServerless === false) { + jobStats.activeNodes!.value = Object.keys(mlNodes).length; + } if (jobStats.total.value === 0) { for (const [statKey, val] of Object.entries(jobStats)) { diff --git a/x-pack/plugins/ml/public/application/routing/routes/memory_usage.tsx b/x-pack/plugins/ml/public/application/routing/routes/memory_usage.tsx index 9f78410615586..dce72c080e94f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/memory_usage.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/memory_usage.tsx @@ -38,7 +38,12 @@ export const nodesListRouteFactory = ( const PageWrapper: FC = () => { const { context } = useRouteResolver( 'full', - ['canGetJobs', 'canGetDataFrameAnalytics', 'canGetTrainedModels'], + // only enabled in non-serverless mode + // if a serverless project ever contains all three features + // this check will have to be changed to an + // explicit isServerless check which will probably + // require a change in useRouteResolver + ['isADEnabled', 'isDFAEnabled', 'isNLPEnabled'], basicResolvers() ); diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index e2b183a7855a2..5f2be59156762 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -8,7 +8,6 @@ import { cloneDeep, each, find, get, isNumber } from 'lodash'; import moment from 'moment'; -import { i18n } from '@kbn/i18n'; import { validateTimeRange, TIME_FORMAT } from '@kbn/ml-date-utils'; import { parseInterval } from '../../../common/util/parse_interval'; @@ -47,50 +46,6 @@ class JobService { this.jobDescriptions = {}; this.detectorsByJob = {}; this.customUrlsByJob = {}; - this.jobStats = { - activeNodes: { - label: i18n.translate('xpack.ml.jobService.activeMLNodesLabel', { - defaultMessage: 'Active ML nodes', - }), - value: 0, - show: true, - }, - total: { - label: i18n.translate('xpack.ml.jobService.totalJobsLabel', { - defaultMessage: 'Total jobs', - }), - value: 0, - show: true, - }, - open: { - label: i18n.translate('xpack.ml.jobService.openJobsLabel', { - defaultMessage: 'Open jobs', - }), - value: 0, - show: true, - }, - closed: { - label: i18n.translate('xpack.ml.jobService.closedJobsLabel', { - defaultMessage: 'Closed jobs', - }), - value: 0, - show: true, - }, - failed: { - label: i18n.translate('xpack.ml.jobService.failedJobsLabel', { - defaultMessage: 'Failed jobs', - }), - value: 0, - show: false, - }, - activeDatafeeds: { - label: i18n.translate('xpack.ml.jobService.activeDatafeedsLabel', { - defaultMessage: 'Active datafeeds', - }), - value: 0, - show: true, - }, - }; } loadJobs() { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.test.ts index dd7408287cbf4..4e1771c25d0ef 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.test.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.test.ts @@ -27,7 +27,7 @@ describe('AnomalyChartsEmbeddableFactory', () => { const [coreStart, pluginsStart] = await getStartServices(); // act - const factory = new AnomalyChartsEmbeddableFactory(getStartServices); + const factory = new AnomalyChartsEmbeddableFactory(getStartServices, false); await factory.create({ jobIds: ['test-job'], diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts index e502d03bcd964..a6056e84a87be 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.ts @@ -35,7 +35,8 @@ export class AnomalyChartsEmbeddableFactory ]; constructor( - private getStartServices: StartServicesAccessor + private getStartServices: StartServicesAccessor, + private isServerless: boolean ) {} public async isEditable() { @@ -61,7 +62,7 @@ export class AnomalyChartsEmbeddableFactory const { resolveEmbeddableAnomalyChartsUserInput } = await import( './anomaly_charts_setup_flyout' ); - return await resolveEmbeddableAnomalyChartsUserInput(coreStart); + return await resolveEmbeddableAnomalyChartsUserInput(coreStart, this.isServerless); } catch (e) { return Promise.reject(); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx index 92aea068c5b15..13849c8e6064c 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx @@ -19,6 +19,7 @@ import { mlApiServicesProvider } from '../../application/services/ml_api_service export async function resolveEmbeddableAnomalyChartsUserInput( coreStart: CoreStart, + isServerless: boolean, input?: AnomalyChartsEmbeddableInput ): Promise> { const { http, overlays, theme, i18n } = coreStart; @@ -27,7 +28,7 @@ export async function resolveEmbeddableAnomalyChartsUserInput( return new Promise(async (resolve, reject) => { try { - const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); + const { jobIds } = await resolveJobSelection(coreStart, isServerless, input?.jobIds); const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds); const { jobs } = await getJobs({ jobId: jobIds.join(',') }); const influencers = extractInfluencers(jobs); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx index cb759f6783b46..2b75202095e3f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx @@ -27,7 +27,7 @@ describe('AnomalySwimlaneEmbeddableFactory', () => { const [coreStart, pluginsStart] = await getStartServices(); // act - const factory = new AnomalySwimlaneEmbeddableFactory(getStartServices); + const factory = new AnomalySwimlaneEmbeddableFactory(getStartServices, false); await factory.create({ jobIds: ['test-job'], diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts index 3e7f958ea778e..421d12193e56f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts @@ -35,7 +35,8 @@ export class AnomalySwimlaneEmbeddableFactory ]; constructor( - private getStartServices: StartServicesAccessor + private getStartServices: StartServicesAccessor, + private isServerless: boolean ) {} public async isEditable() { @@ -59,7 +60,7 @@ export class AnomalySwimlaneEmbeddableFactory try { const { resolveAnomalySwimlaneUserInput } = await import('./anomaly_swimlane_setup_flyout'); - return await resolveAnomalySwimlaneUserInput(coreStart); + return await resolveAnomalySwimlaneUserInput(coreStart, this.isServerless); } catch (e) { return Promise.reject(); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index dc2ca931cc805..2c0e6c5e2d963 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -19,6 +19,7 @@ import { mlApiServicesProvider } from '../../application/services/ml_api_service export async function resolveAnomalySwimlaneUserInput( coreStart: CoreStart, + isServerless: boolean, input?: AnomalySwimlaneEmbeddableInput ): Promise> { const { http, overlays, theme, i18n } = coreStart; @@ -27,7 +28,7 @@ export async function resolveAnomalySwimlaneUserInput( return new Promise(async (resolve, reject) => { try { - const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); + const { jobIds } = await resolveJobSelection(coreStart, isServerless, input?.jobIds); const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds); const { jobs } = await getJobs({ jobId: jobIds.join(',') }); const influencers = extractInfluencers(jobs); diff --git a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx index 00c4a02d4e929..3cd49afd8c361 100644 --- a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx +++ b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx @@ -26,6 +26,7 @@ import { JobSelectorFlyout } from './components/job_selector_flyout'; */ export async function resolveJobSelection( coreStart: CoreStart, + isServerless: boolean, selectedJobIds?: JobId[] ): Promise<{ jobIds: string[]; groups: Array<{ groupId: string; jobIds: string[] }> }> { const { @@ -69,7 +70,9 @@ export async function resolveJobSelection( const flyoutSession = coreStart.overlays.openFlyout( toMountPoint( - + { const { @@ -53,7 +54,7 @@ export function createFlyout( data, lens, dashboardService, - mlServices: getMlGlobalServices(http), + mlServices: getMlGlobalServices(http, isServerless), }} > { return createFlyout( LensLayerSelectionFlyout, @@ -29,6 +30,7 @@ export async function showLensVisToADJobFlyout( share, data, dashboardService, + isServerless, lens ); } diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/map/show_flyout.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/map/show_flyout.tsx index 5380513f1dc97..da2d8bb2e0d4c 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/map/show_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/map/show_flyout.tsx @@ -19,7 +19,16 @@ export async function showMapVisToADJobFlyout( coreStart: CoreStart, share: SharePluginStart, data: DataPublicPluginStart, - dashboardService: DashboardStart + dashboardService: DashboardStart, + isServerless: boolean ): Promise { - return createFlyout(GeoJobFlyout, embeddable, coreStart, share, data, dashboardService); + return createFlyout( + GeoJobFlyout, + embeddable, + coreStart, + share, + data, + dashboardService, + isServerless + ); } diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index c66d4c6ec8d33..3a32f8b25ae89 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -51,9 +51,9 @@ import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/ import { registerManagementSection } from './application/management'; import { MlLocatorDefinition, type MlLocator } from './locator'; import { setDependencyCache } from './application/util/dependency_cache'; -import { registerFeature } from './register_feature'; +import { registerHomeFeature } from './register_home_feature'; import { isFullLicense, isMlEnabled } from '../common/license'; -import { PLUGIN_ICON_SOLUTION, PLUGIN_ID } from '../common/constants/app'; +import { ML_APP_ROUTE, PLUGIN_ICON_SOLUTION, PLUGIN_ID } from '../common/constants/app'; import type { MlCapabilities } from './shared'; export interface MlStartDependencies { @@ -103,8 +103,11 @@ export class MlPlugin implements Plugin { private appUpdater$ = new BehaviorSubject(() => ({})); private locator: undefined | MlLocator; + private isServerless: boolean = false; - constructor(private initializerContext: PluginInitializerContext) {} + constructor(private initializerContext: PluginInitializerContext) { + this.isServerless = initializerContext.env.packageInfo.buildFlavor === 'serverless'; + } setup(core: MlCoreSetup, pluginsSetup: MlSetupDependencies) { core.application.register({ @@ -114,12 +117,11 @@ export class MlPlugin implements Plugin { }), order: 5000, euiIconType: PLUGIN_ICON_SOLUTION, - appRoute: '/app/ml', + appRoute: ML_APP_ROUTE, category: DEFAULT_APP_CATEGORIES.kibana, updater$: this.appUpdater$, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); - const kibanaVersion = this.initializerContext.env.packageInfo.version; const { renderApp } = await import('./application/app'); return renderApp( coreStart, @@ -137,7 +139,7 @@ export class MlPlugin implements Plugin { embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable }, maps: pluginsStart.maps, uiActions: pluginsStart.uiActions, - kibanaVersion, + kibanaVersion: this.initializerContext.env.packageInfo.version, triggersActionsUi: pluginsStart.triggersActionsUi, dataVisualizer: pluginsStart.dataVisualizer, usageCollection: pluginsSetup.usageCollection, @@ -149,7 +151,8 @@ export class MlPlugin implements Plugin { contentManagement: pluginsStart.contentManagement, presentationUtil: pluginsStart.presentationUtil, }, - params + params, + this.isServerless ); }, }); @@ -159,9 +162,14 @@ export class MlPlugin implements Plugin { } if (pluginsSetup.management) { - registerManagementSection(pluginsSetup.management, core, { - usageCollection: pluginsSetup.usageCollection, - }).enable(); + registerManagementSection( + pluginsSetup.management, + core, + { + usageCollection: pluginsSetup.usageCollection, + }, + this.isServerless + ).enable(); } const licensing = pluginsSetup.licensing.license$.pipe(take(1)); @@ -170,53 +178,58 @@ export class MlPlugin implements Plugin { const fullLicense = isFullLicense(license); const [coreStart, pluginStart] = await core.getStartServices(); const { capabilities } = coreStart.application; + const mlCapabilities = capabilities.ml as MlCapabilities; + // register various ML plugin features which require a full license + // note including registerHomeFeature in register_helper would cause the page bundle size to increase significantly if (mlEnabled) { // add ML to home page if (pluginsSetup.home) { - registerFeature(pluginsSetup.home); + registerHomeFeature(pluginsSetup.home); } - } else { - // if ml is disabled in elasticsearch, disable ML in kibana - this.appUpdater$.next(() => ({ - status: AppStatus.inaccessible, - })); - } - - // register various ML plugin features which require a full license - // note including registerFeature in register_helper would cause the page bundle size to increase significantly - const { - registerEmbeddables, - registerMlUiActions, - registerSearchLinks, - registerMlAlerts, - registerMapExtension, - registerCasesAttachments, - } = await import('./register_helper'); - - if (pluginsSetup.maps) { - // Pass capabilites.ml.canGetJobs as minimum permission to show anomalies card in maps layers - const canGetJobs = capabilities.ml?.canGetJobs === true; - const canCreateJobs = capabilities.ml?.canCreateJob === true; - await registerMapExtension(pluginsSetup.maps, core, { canGetJobs, canCreateJobs }); - } - if (mlEnabled) { - registerSearchLinks(this.appUpdater$, fullLicense, capabilities.ml as MlCapabilities); + const { + registerEmbeddables, + registerMlUiActions, + registerSearchLinks, + registerMlAlerts, + registerMapExtension, + registerCasesAttachments, + } = await import('./register_helper'); + registerSearchLinks(this.appUpdater$, fullLicense, mlCapabilities, this.isServerless); if (fullLicense) { - registerEmbeddables(pluginsSetup.embeddable, core); - registerMlUiActions(pluginsSetup.uiActions, core); + registerMlUiActions(pluginsSetup.uiActions, core, this.isServerless); - if (pluginsSetup.cases) { - registerCasesAttachments(pluginsSetup.cases, coreStart, pluginStart); - } + if (mlCapabilities.isADEnabled) { + registerEmbeddables(pluginsSetup.embeddable, core, this.isServerless); + + if (pluginsSetup.cases) { + registerCasesAttachments(pluginsSetup.cases, coreStart, pluginStart); + } + + if ( + pluginsSetup.triggersActionsUi && + mlCapabilities.canUseMlAlerts && + mlCapabilities.canGetJobs + ) { + registerMlAlerts(pluginsSetup.triggersActionsUi, pluginsSetup.alerting); + } - const canUseMlAlerts = capabilities.ml?.canUseMlAlerts; - if (pluginsSetup.triggersActionsUi && canUseMlAlerts) { - registerMlAlerts(pluginsSetup.triggersActionsUi, pluginsSetup.alerting); + if (pluginsSetup.maps) { + // Pass canGetJobs as minimum permission to show anomalies card in maps layers + await registerMapExtension(pluginsSetup.maps, core, { + canGetJobs: mlCapabilities.canGetJobs, + canCreateJobs: mlCapabilities.canCreateJob, + }); + } } } + } else { + // if ml is disabled in elasticsearch, disable ML in kibana + this.appUpdater$.next(() => ({ + status: AppStatus.inaccessible, + })); } }); diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts index 35c37c56ecdff..6b7f10103b440 100644 --- a/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts @@ -15,7 +15,8 @@ import type { MlCapabilities } from '../../shared'; export function registerSearchLinks( appUpdater: BehaviorSubject, isFullLicense: boolean, - mlCapabilities: MlCapabilities + mlCapabilities: MlCapabilities, + isServerless: boolean ) { appUpdater.next(() => ({ keywords: [ @@ -23,6 +24,6 @@ export function registerSearchLinks( defaultMessage: 'ML', }), ], - deepLinks: getDeepLinks(isFullLicense, mlCapabilities), + deepLinks: getDeepLinks(isFullLicense, mlCapabilities, isServerless), })); } diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts index dfe2b4bbaf200..f9bdd2b50e4a4 100644 --- a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts @@ -12,276 +12,275 @@ import { type AppDeepLink, AppNavLinkStatus } from '@kbn/core/public'; import { ML_PAGES } from '../../../common/constants/locator'; import type { MlCapabilities } from '../../shared'; -function getNavStatus( +function createDeepLinks( mlCapabilities: MlCapabilities, - statusIfServerless: boolean -): AppNavLinkStatus | undefined { - if (mlCapabilities.isADEnabled && mlCapabilities.isDFAEnabled && mlCapabilities.isNLPEnabled) { - // if all features are enabled we can assume that we are not running in serverless mode. - // returning default will not add the link to the nav menu, but the link will be registered for searching - return AppNavLinkStatus.default; - } - - return statusIfServerless ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden; -} + isFullLicense: boolean, + isServerless: boolean +) { + function getNavStatus( + visible: boolean, + showInServerless: boolean = true + ): AppNavLinkStatus | undefined { + if (isServerless) { + // in serverless the status needs to be "visible" rather than "default" + // for the links to appear in the nav menu. + return showInServerless && visible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden; + } -function getOverviewLinkDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - return { - id: 'overview', - title: i18n.translate('xpack.ml.deepLink.overview', { - defaultMessage: 'Overview', - }), - path: `/${ML_PAGES.OVERVIEW}`, - navLinkStatus: getNavStatus(mlCapabilities, false), - }; -} + return visible ? AppNavLinkStatus.default : AppNavLinkStatus.hidden; + } -function getAnomalyDetectionDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - const navLinkStatus = getNavStatus(mlCapabilities, mlCapabilities.isADEnabled); return { - id: 'anomalyDetection', - title: i18n.translate('xpack.ml.deepLink.anomalyDetection', { - defaultMessage: 'Anomaly Detection', - }), - path: `/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`, - navLinkStatus, - deepLinks: [ - { - id: 'anomalyExplorer', - title: i18n.translate('xpack.ml.deepLink.anomalyExplorer', { - defaultMessage: 'Anomaly explorer', - }), - path: `/${ML_PAGES.ANOMALY_EXPLORER}`, - navLinkStatus, - }, - { - id: 'singleMetricViewer', - title: i18n.translate('xpack.ml.deepLink.singleMetricViewer', { - defaultMessage: 'Single metric viewer', + getOverviewLinkDeepLink: (): AppDeepLink => { + const navLinkStatus = getNavStatus(mlCapabilities.isADEnabled || mlCapabilities.isDFAEnabled); + return { + id: 'overview', + title: i18n.translate('xpack.ml.deepLink.overview', { + defaultMessage: 'Overview', }), - path: `/${ML_PAGES.SINGLE_METRIC_VIEWER}`, + path: `/${ML_PAGES.OVERVIEW}`, navLinkStatus, - }, - ], - }; -} + }; + }, -function getDataFrameAnalyticsDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - const navLinkStatus = getNavStatus(mlCapabilities, mlCapabilities.isDFAEnabled); - return { - id: 'dataFrameAnalytics', - title: i18n.translate('xpack.ml.deepLink.dataFrameAnalytics', { - defaultMessage: 'Data Frame Analytics', - }), - path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`, - navLinkStatus, - deepLinks: [ - { - id: 'resultExplorer', - title: i18n.translate('xpack.ml.deepLink.resultExplorer', { - defaultMessage: 'Results explorer', + getAnomalyDetectionDeepLink: (): AppDeepLink => { + const navLinkStatus = getNavStatus(mlCapabilities.isADEnabled); + return { + id: 'anomalyDetection', + title: i18n.translate('xpack.ml.deepLink.anomalyDetection', { + defaultMessage: 'Anomaly Detection', }), - path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`, + path: `/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`, navLinkStatus, - }, - { - id: 'analyticsMap', - title: i18n.translate('xpack.ml.deepLink.analyticsMap', { - defaultMessage: 'Analytics map', + deepLinks: [ + { + id: 'anomalyExplorer', + title: i18n.translate('xpack.ml.deepLink.anomalyExplorer', { + defaultMessage: 'Anomaly explorer', + }), + path: `/${ML_PAGES.ANOMALY_EXPLORER}`, + navLinkStatus, + }, + { + id: 'singleMetricViewer', + title: i18n.translate('xpack.ml.deepLink.singleMetricViewer', { + defaultMessage: 'Single metric viewer', + }), + path: `/${ML_PAGES.SINGLE_METRIC_VIEWER}`, + navLinkStatus, + }, + ], + }; + }, + + getDataFrameAnalyticsDeepLink: (): AppDeepLink => { + const navLinkStatus = getNavStatus(mlCapabilities.isDFAEnabled); + return { + id: 'dataFrameAnalytics', + title: i18n.translate('xpack.ml.deepLink.dataFrameAnalytics', { + defaultMessage: 'Data Frame Analytics', }), - path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_MAP}`, + path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`, navLinkStatus, - }, - ], - }; -} + deepLinks: [ + { + id: 'resultExplorer', + title: i18n.translate('xpack.ml.deepLink.resultExplorer', { + defaultMessage: 'Results explorer', + }), + path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`, + navLinkStatus, + }, + { + id: 'analyticsMap', + title: i18n.translate('xpack.ml.deepLink.analyticsMap', { + defaultMessage: 'Analytics map', + }), + path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_MAP}`, + navLinkStatus, + }, + ], + }; + }, -function getAiopsDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - const navLinkStatus = getNavStatus(mlCapabilities, mlCapabilities.canUseAiops); - return { - id: 'aiOps', - title: i18n.translate('xpack.ml.deepLink.aiOps', { - defaultMessage: 'AIOps', - }), - // Default to the index select page for log rate analysis since we don't have an AIops overview page - path: `/${ML_PAGES.AIOPS_LOG_RATE_ANALYSIS_INDEX_SELECT}`, - navLinkStatus, - deepLinks: [ - { - id: 'logRateAnalysis', - title: i18n.translate('xpack.ml.deepLink.logRateAnalysis', { - defaultMessage: 'Log Rate Analysis', + getModelManagementDeepLink: (): AppDeepLink => { + const navLinkStatus = getNavStatus( + mlCapabilities.isDFAEnabled || mlCapabilities.isNLPEnabled + ); + return { + id: 'modelManagement', + title: i18n.translate('xpack.ml.deepLink.modelManagement', { + defaultMessage: 'Model Management', }), - path: `/${ML_PAGES.AIOPS_LOG_RATE_ANALYSIS_INDEX_SELECT}`, + path: `/${ML_PAGES.TRAINED_MODELS_MANAGE}`, navLinkStatus, - }, - { - id: 'logPatternAnalysis', - title: i18n.translate('xpack.ml.deepLink.logPatternAnalysis', { - defaultMessage: 'Log Pattern Analysis', + deepLinks: [ + { + id: 'nodesOverview', + title: i18n.translate('xpack.ml.deepLink.trainedModels', { + defaultMessage: 'Trained Models', + }), + path: `/${ML_PAGES.TRAINED_MODELS_MANAGE}`, + navLinkStatus, + }, + { + id: 'nodes', + title: i18n.translate('xpack.ml.deepLink.nodes', { + defaultMessage: 'Nodes', + }), + path: `/${ML_PAGES.NODES}`, + navLinkStatus: getNavStatus( + mlCapabilities.isDFAEnabled || mlCapabilities.isNLPEnabled, + false + ), + }, + ], + }; + }, + + getMemoryUsageDeepLink: (): AppDeepLink => { + return { + id: 'memoryUsage', + title: i18n.translate('xpack.ml.deepLink.memoryUsage', { + defaultMessage: 'Memory Usage', }), - path: `/${ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT}`, - navLinkStatus, - }, - { - id: 'changePointDetections', - title: i18n.translate('xpack.ml.deepLink.changePointDetection', { - defaultMessage: 'Change Point Detection', + path: `/${ML_PAGES.MEMORY_USAGE}`, + navLinkStatus: getNavStatus(isFullLicense, false), + }; + }, + + getSettingsDeepLink: (): AppDeepLink => { + const navLinkStatus = getNavStatus(mlCapabilities.isADEnabled); + return { + id: 'settings', + title: i18n.translate('xpack.ml.deepLink.settings', { + defaultMessage: 'Settings', }), - path: `/${ML_PAGES.AIOPS_CHANGE_POINT_DETECTION_INDEX_SELECT}`, + path: `/${ML_PAGES.SETTINGS}`, navLinkStatus, - }, - ], - }; -} + deepLinks: [ + { + id: 'calendarSettings', + title: i18n.translate('xpack.ml.deepLink.calendarSettings', { + defaultMessage: 'Calendars', + }), + path: `/${ML_PAGES.CALENDARS_MANAGE}`, + navLinkStatus, + }, + { + id: 'filterListsSettings', + title: i18n.translate('xpack.ml.deepLink.filterListsSettings', { + defaultMessage: 'Filter Lists', + }), + path: `/${ML_PAGES.SETTINGS}`, // Link to settings page as read only users cannot view filter lists. + navLinkStatus, + }, + ], + }; + }, -function getModelManagementDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - const navLinkStatus = getNavStatus(mlCapabilities, mlCapabilities.isNLPEnabled); - return { - id: 'modelManagement', - title: i18n.translate('xpack.ml.deepLink.modelManagement', { - defaultMessage: 'Model Management', - }), - path: `/${ML_PAGES.TRAINED_MODELS_MANAGE}`, - navLinkStatus, - deepLinks: [ - { - id: 'nodesOverview', - title: i18n.translate('xpack.ml.deepLink.trainedModels', { - defaultMessage: 'Trained Models', + getAiopsDeepLink: (): AppDeepLink => { + const navLinkStatus = getNavStatus(mlCapabilities.canUseAiops); + return { + id: 'aiOps', + title: i18n.translate('xpack.ml.deepLink.aiOps', { + defaultMessage: 'AIOps', }), - path: `/${ML_PAGES.TRAINED_MODELS_MANAGE}`, + // Default to the index select page for log rate analysis since we don't have an AIops overview page + path: `/${ML_PAGES.AIOPS_LOG_RATE_ANALYSIS_INDEX_SELECT}`, navLinkStatus, - }, - { - id: 'nodes', - title: i18n.translate('xpack.ml.deepLink.nodes', { - defaultMessage: 'Nodes', - }), - path: `/${ML_PAGES.NODES}`, - navLinkStatus: getNavStatus(mlCapabilities, false), - }, - ], - }; -} + deepLinks: [ + { + id: 'logRateAnalysis', + title: i18n.translate('xpack.ml.deepLink.logRateAnalysis', { + defaultMessage: 'Log Rate Analysis', + }), + path: `/${ML_PAGES.AIOPS_LOG_RATE_ANALYSIS_INDEX_SELECT}`, + navLinkStatus, + }, + { + id: 'logPatternAnalysis', + title: i18n.translate('xpack.ml.deepLink.logPatternAnalysis', { + defaultMessage: 'Log Pattern Analysis', + }), + path: `/${ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT}`, + navLinkStatus, + }, + { + id: 'changePointDetections', + title: i18n.translate('xpack.ml.deepLink.changePointDetection', { + defaultMessage: 'Change Point Detection', + }), + path: `/${ML_PAGES.AIOPS_CHANGE_POINT_DETECTION_INDEX_SELECT}`, + navLinkStatus, + }, + ], + }; + }, -function getMemoryUsageDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - return { - id: 'memoryUsage', - title: i18n.translate('xpack.ml.deepLink.memoryUsage', { - defaultMessage: 'Memory Usage', - }), - path: `/${ML_PAGES.MEMORY_USAGE}`, - navLinkStatus: getNavStatus(mlCapabilities, false), - }; -} - -function getDataVisualizerDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - return { - id: 'dataVisualizer', - title: i18n.translate('xpack.ml.deepLink.dataVisualizer', { - defaultMessage: 'Data Visualizer', - }), - path: `/${ML_PAGES.DATA_VISUALIZER}`, - navLinkStatus: getNavStatus(mlCapabilities, false), - }; -} - -function getFileUploadDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - return { - id: 'fileUpload', - title: i18n.translate('xpack.ml.deepLink.fileUpload', { - defaultMessage: 'File Upload', - }), - keywords: ['CSV', 'JSON'], - path: `/${ML_PAGES.DATA_VISUALIZER_FILE}`, - navLinkStatus: getNavStatus(mlCapabilities, false), - }; -} - -function getIndexDataVisualizerDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - return { - id: 'indexDataVisualizer', - title: i18n.translate('xpack.ml.deepLink.indexDataVisualizer', { - defaultMessage: 'Index Data Visualizer', - }), - path: `/${ML_PAGES.DATA_VISUALIZER_INDEX_SELECT}`, - navLinkStatus: getNavStatus(mlCapabilities, false), - }; -} + getNotificationsDeepLink: (): AppDeepLink => { + return { + id: 'notifications', + title: i18n.translate('xpack.ml.deepLink.notifications', { + defaultMessage: 'Notifications', + }), + path: `/${ML_PAGES.NOTIFICATIONS}`, + navLinkStatus: getNavStatus(isFullLicense), + }; + }, -function getDataComparisonDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - return { - id: 'dataComparison', - title: i18n.translate('xpack.ml.deepLink.dataComparison', { - defaultMessage: 'Data Comparison', - }), - path: `/${ML_PAGES.DATA_COMPARISON_INDEX_SELECT}`, - navLinkStatus: getNavStatus(mlCapabilities, false), - }; -} + getDataVisualizerDeepLink: (): AppDeepLink => { + return { + id: 'dataVisualizer', + title: i18n.translate('xpack.ml.deepLink.dataVisualizer', { + defaultMessage: 'Data Visualizer', + }), + path: `/${ML_PAGES.DATA_VISUALIZER}`, + navLinkStatus: getNavStatus(true), + }; + }, -function getSettingsDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - const navLinkStatus = getNavStatus(mlCapabilities, mlCapabilities.isADEnabled); - return { - id: 'settings', - title: i18n.translate('xpack.ml.deepLink.settings', { - defaultMessage: 'Settings', - }), - path: `/${ML_PAGES.SETTINGS}`, - navLinkStatus, - deepLinks: [ - { - id: 'calendarSettings', - title: i18n.translate('xpack.ml.deepLink.calendarSettings', { - defaultMessage: 'Calendars', + getFileUploadDeepLink: (): AppDeepLink => { + return { + id: 'fileUpload', + title: i18n.translate('xpack.ml.deepLink.fileUpload', { + defaultMessage: 'File Upload', }), - path: `/${ML_PAGES.CALENDARS_MANAGE}`, - navLinkStatus, - }, - { - id: 'filterListsSettings', - title: i18n.translate('xpack.ml.deepLink.filterListsSettings', { - defaultMessage: 'Filter Lists', + keywords: ['CSV', 'JSON'], + path: `/${ML_PAGES.DATA_VISUALIZER_FILE}`, + navLinkStatus: getNavStatus(true), + }; + }, + + getIndexDataVisualizerDeepLink: (): AppDeepLink => { + return { + id: 'indexDataVisualizer', + title: i18n.translate('xpack.ml.deepLink.indexDataVisualizer', { + defaultMessage: 'Index Data Visualizer', }), - path: `/${ML_PAGES.SETTINGS}`, // Link to settings page as read only users cannot view filter lists. - navLinkStatus, - }, - ], - }; -} + path: `/${ML_PAGES.DATA_VISUALIZER_INDEX_SELECT}`, + navLinkStatus: getNavStatus(true), + }; + }, -function getNotificationsDeepLink(mlCapabilities: MlCapabilities): AppDeepLink { - return { - id: 'notifications', - title: i18n.translate('xpack.ml.deepLink.notifications', { - defaultMessage: 'Notifications', - }), - path: `/${ML_PAGES.NOTIFICATIONS}`, - navLinkStatus: getNavStatus(mlCapabilities, true), + getDataComparisonDeepLink: (): AppDeepLink => { + return { + id: 'dataComparison', + title: i18n.translate('xpack.ml.deepLink.dataComparison', { + defaultMessage: 'Data Comparison', + }), + path: `/${ML_PAGES.DATA_COMPARISON_INDEX_SELECT}`, + navLinkStatus: getNavStatus(true), + }; + }, }; } -export function getDeepLinks(isFullLicense: boolean, mlCapabilities: MlCapabilities) { - const deepLinks: Array> = [ - getDataVisualizerDeepLink(mlCapabilities), - getFileUploadDeepLink(mlCapabilities), - getIndexDataVisualizerDeepLink(mlCapabilities), - getDataComparisonDeepLink(mlCapabilities), - ]; - - if (isFullLicense === true) { - deepLinks.push( - getOverviewLinkDeepLink(mlCapabilities), - getAnomalyDetectionDeepLink(mlCapabilities), - getDataFrameAnalyticsDeepLink(mlCapabilities), - getModelManagementDeepLink(mlCapabilities), - getMemoryUsageDeepLink(mlCapabilities), - getSettingsDeepLink(mlCapabilities), - getAiopsDeepLink(mlCapabilities), - getNotificationsDeepLink(mlCapabilities) - ); - } - - return deepLinks; +export function getDeepLinks( + isFullLicense: boolean, + mlCapabilities: MlCapabilities, + isServerless: boolean +) { + const links = createDeepLinks(mlCapabilities, isFullLicense, isServerless); + return Object.values(links).map((link) => link()); } diff --git a/x-pack/plugins/ml/public/register_feature.ts b/x-pack/plugins/ml/public/register_home_feature.ts similarity index 86% rename from x-pack/plugins/ml/public/register_feature.ts rename to x-pack/plugins/ml/public/register_home_feature.ts index bc0f6f751351e..baae66f72eea5 100644 --- a/x-pack/plugins/ml/public/register_feature.ts +++ b/x-pack/plugins/ml/public/register_home_feature.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; -import { PLUGIN_ID } from '../common/constants/app'; +import { ML_APP_ROUTE, PLUGIN_ID } from '../common/constants/app'; -export const registerFeature = (home: HomePublicPluginSetup) => { +export const registerHomeFeature = (home: HomePublicPluginSetup) => { // register ML so it appears on the Kibana home page home.featureCatalogue.register({ id: PLUGIN_ID, @@ -24,7 +24,7 @@ export const registerFeature = (home: HomePublicPluginSetup) => { 'Automatically model the normal behavior of your time series data to detect anomalies.', }), icon: 'machineLearningApp', - path: '/app/ml', + path: ML_APP_ROUTE, showOnHomePage: false, category: 'data', solutionId: 'kibana', diff --git a/x-pack/plugins/ml/public/ui_actions/edit_anomaly_charts_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_anomaly_charts_panel_action.tsx index d79c897958554..e4f765f87c598 100644 --- a/x-pack/plugins/ml/public/ui_actions/edit_anomaly_charts_panel_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/edit_anomaly_charts_panel_action.tsx @@ -17,7 +17,8 @@ import { export const EDIT_ANOMALY_CHARTS_PANEL_ACTION = 'editAnomalyChartsPanelAction'; export function createEditAnomalyChartsPanelAction( - getStartServices: MlCoreSetup['getStartServices'] + getStartServices: MlCoreSetup['getStartServices'], + isServerless: boolean ): UiActionsActionDefinition { return { id: 'edit-anomaly-charts', @@ -43,6 +44,7 @@ export function createEditAnomalyChartsPanelAction( const result = await resolveEmbeddableAnomalyChartsUserInput( coreStart, + isServerless, embeddable.getInput() ); embeddable.updateInput(result); diff --git a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx index 4352dc2df89bf..5070862023598 100644 --- a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx @@ -14,7 +14,8 @@ import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, EditSwimlanePanelContext } from '../e export const EDIT_SWIMLANE_PANEL_ACTION = 'editSwimlanePanelAction'; export function createEditSwimlanePanelAction( - getStartServices: MlCoreSetup['getStartServices'] + getStartServices: MlCoreSetup['getStartServices'], + isServerless: boolean ): UiActionsActionDefinition { return { id: 'edit-anomaly-swimlane', @@ -38,7 +39,11 @@ export function createEditSwimlanePanelAction( '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout' ); - const result = await resolveAnomalySwimlaneUserInput(coreStart, embeddable.getInput()); + const result = await resolveAnomalySwimlaneUserInput( + coreStart, + isServerless, + embeddable.getInput() + ); embeddable.updateInput(result); } catch (e) { return Promise.reject(); diff --git a/x-pack/plugins/ml/public/ui_actions/index.ts b/x-pack/plugins/ml/public/ui_actions/index.ts index 4067547e08956..f08f5bcd886bc 100644 --- a/x-pack/plugins/ml/public/ui_actions/index.ts +++ b/x-pack/plugins/ml/public/ui_actions/index.ts @@ -34,17 +34,24 @@ export { SWIM_LANE_SELECTION_TRIGGER }; */ export function registerMlUiActions( uiActions: UiActionsSetup, - core: CoreSetup + core: CoreSetup, + isServerless: boolean ) { // Initialize actions - const editSwimlanePanelAction = createEditSwimlanePanelAction(core.getStartServices); + const editSwimlanePanelAction = createEditSwimlanePanelAction( + core.getStartServices, + isServerless + ); const openInExplorerAction = createOpenInExplorerAction(core.getStartServices); const applyInfluencerFiltersAction = createApplyInfluencerFiltersAction(core.getStartServices); const applyEntityFieldFilterAction = createApplyEntityFieldFiltersAction(core.getStartServices); const applyTimeRangeSelectionAction = createApplyTimeRangeSelectionAction(core.getStartServices); const clearSelectionAction = createClearSelectionAction(core.getStartServices); - const editExplorerPanelAction = createEditAnomalyChartsPanelAction(core.getStartServices); - const visToAdJobAction = createVisToADJobAction(core.getStartServices); + const editExplorerPanelAction = createEditAnomalyChartsPanelAction( + core.getStartServices, + isServerless + ); + const visToAdJobAction = createVisToADJobAction(core.getStartServices, isServerless); // Register actions uiActions.registerAction(editSwimlanePanelAction); diff --git a/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx index fb0aa38e44d90..3e06b6175d61e 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx @@ -15,7 +15,8 @@ import { isLensEmbeddable, isMapEmbeddable } from '../application/jobs/new_job/j export const CREATE_LENS_VIS_TO_ML_AD_JOB_ACTION = 'createMLADJobAction'; export function createVisToADJobAction( - getStartServices: MlCoreSetup['getStartServices'] + getStartServices: MlCoreSetup['getStartServices'], + isServerless: boolean ): UiActionsActionDefinition<{ embeddable: Embeddable | MapEmbeddable }> { return { id: 'create-ml-ad-job-action', @@ -39,11 +40,26 @@ export function createVisToADJobAction( if (lens === undefined) { return; } - await showLensVisToADJobFlyout(embeddable, coreStart, share, data, lens, dashboard); + await showLensVisToADJobFlyout( + embeddable, + coreStart, + share, + data, + lens, + dashboard, + isServerless + ); } else if (isMapEmbeddable(embeddable)) { const [{ showMapVisToADJobFlyout }, [coreStart, { share, data, dashboard }]] = await Promise.all([import('../embeddables/job_creation/map'), getStartServices()]); - await showMapVisToADJobFlyout(embeddable, coreStart, share, data, dashboard); + await showMapVisToADJobFlyout( + embeddable, + coreStart, + share, + data, + dashboard, + isServerless + ); } } catch (e) { return Promise.reject(); diff --git a/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts b/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts index ab1136b5d1edc..1095280ebdae9 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts @@ -19,7 +19,7 @@ export interface RegisterAlertParams { mlServicesProviders: MlServicesProviders; } -export function registerMlAlerts(params: RegisterAlertParams) { - registerAnomalyDetectionAlertType(params); - registerJobsMonitoringRuleType(params); +export function registerMlAlerts(alertParams: RegisterAlertParams) { + registerAnomalyDetectionAlertType(alertParams); + registerJobsMonitoringRuleType(alertParams); } diff --git a/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts index 2be5bd877552a..dadb15b8a46b8 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts @@ -59,7 +59,7 @@ function getSwitcher( basicLicenseMlCapabilities.forEach((c) => (mlCaps[c] = originalCapabilities[c])); } - return { ml: applyEnabledFeatures(mlCaps, enabledFeatures) }; + return { ml: mlCaps }; } catch (e) { logger.debug(`Error updating capabilities for ML based on licensing: ${e}`); return {}; @@ -67,22 +67,26 @@ function getSwitcher( }; } -function applyEnabledFeatures(mlCaps: MlCapabilities, enabledFeatures: MlFeatures) { - mlCaps.isADEnabled = enabledFeatures.ad; - mlCaps.isDFAEnabled = enabledFeatures.dfa; - mlCaps.isNLPEnabled = enabledFeatures.nlp; +function applyEnabledFeatures(mlCaps: MlCapabilities, { ad, dfa, nlp }: MlFeatures) { + mlCaps.isADEnabled = ad; + mlCaps.isDFAEnabled = dfa; + mlCaps.isNLPEnabled = nlp; + mlCaps.canViewMlNodes = mlCaps.canViewMlNodes && ad && dfa && nlp; - mlCaps.canViewMlNodes = - mlCaps.canViewMlNodes && mlCaps.isADEnabled && mlCaps.isDFAEnabled && mlCaps.isNLPEnabled; - - if (enabledFeatures.ad === false) { - featureCapabilities.ad.forEach((c) => (mlCaps[c] = false)); + if (ad === false) { + for (const c of featureCapabilities.ad) { + mlCaps[c] = false; + } } - if (enabledFeatures.dfa === false) { - featureCapabilities.dfa.forEach((c) => (mlCaps[c] = false)); + if (dfa === false) { + for (const c of featureCapabilities.dfa) { + mlCaps[c] = false; + } } - if (enabledFeatures.nlp === false) { - featureCapabilities.nlp.forEach((c) => (mlCaps[c] = false)); + if (nlp === false && dfa === false) { + for (const c of featureCapabilities.nlp) { + mlCaps[c] = false; + } } return mlCaps; diff --git a/x-pack/plugins/ml/server/lib/register_cases.ts b/x-pack/plugins/ml/server/lib/register_cases.ts new file mode 100644 index 0000000000000..f916176a4d3f4 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/register_cases.ts @@ -0,0 +1,22 @@ +/* + * 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 type { CasesSetup } from '@kbn/cases-plugin/server'; +import { + CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, + CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, +} from '../../common/constants/cases'; + +export function registerCasesPersistableState(cases: CasesSetup) { + cases.attachmentFramework.registerPersistableState({ + id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, + }); + + cases.attachmentFramework.registerPersistableState({ + id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, + }); +} diff --git a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts b/x-pack/plugins/ml/server/lib/register_sameple_data_set_links.ts similarity index 87% rename from x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts rename to x-pack/plugins/ml/server/lib/register_sameple_data_set_links.ts index 393490793004a..6e7a0946d0369 100644 --- a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts +++ b/x-pack/plugins/ml/server/lib/register_sameple_data_set_links.ts @@ -7,10 +7,13 @@ import { i18n } from '@kbn/i18n'; import type { HomeServerPluginSetup } from '@kbn/home-plugin/server'; -import { MlLicense } from '../../../common/license'; +import type { MlFeatures } from '../types'; -export function initSampleDataSets(mlLicense: MlLicense, home: HomeServerPluginSetup) { - if (mlLicense.isMlEnabled() && mlLicense.isFullLicense()) { +export function registerSampleDataSetLinks( + enabledFeatures: MlFeatures, + home: HomeServerPluginSetup +) { + if (enabledFeatures.ad === true) { const sampleDataLinkLabel = i18n.translate('xpack.ml.sampleDataLinkLabel', { defaultMessage: 'ML jobs', }); diff --git a/x-pack/plugins/ml/server/lib/register_settings.ts b/x-pack/plugins/ml/server/lib/register_settings.ts index fae6e810b187e..4bd3f0a35bb0a 100644 --- a/x-pack/plugins/ml/server/lib/register_settings.ts +++ b/x-pack/plugins/ml/server/lib/register_settings.ts @@ -14,43 +14,49 @@ import { DEFAULT_AD_RESULTS_TIME_FILTER, DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER, } from '../../common/constants/settings'; +import type { MlFeatures } from '../types'; -export function registerKibanaSettings(coreSetup: CoreSetup) { - coreSetup.uiSettings.register({ - [ANOMALY_DETECTION_ENABLE_TIME_RANGE]: { - name: i18n.translate('xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeName', { - defaultMessage: 'Enable time filter defaults for anomaly detection results', - }), - value: DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER, - schema: schema.boolean(), - description: i18n.translate( - 'xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeDesc', - { - defaultMessage: - 'Use the default time filter in the Single Metric Viewer and Anomaly Explorer. If not enabled, the results for the full time range of the job are displayed.', - } - ), - category: ['machineLearning'], - }, - [ANOMALY_DETECTION_DEFAULT_TIME_RANGE]: { - name: i18n.translate('xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeName', { - defaultMessage: 'Time filter defaults for anomaly detection results', - }), - type: 'json', - value: JSON.stringify(DEFAULT_AD_RESULTS_TIME_FILTER, null, 2), - description: i18n.translate( - 'xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeDesc', - { - defaultMessage: - 'The time filter selection to use when viewing anomaly detection job results.', - } - ), - schema: schema.object({ - from: schema.string(), - to: schema.string(), - }), - requiresPageReload: true, - category: ['machineLearning'], - }, - }); +export function registerKibanaSettings(enabledFeatures: MlFeatures, coreSetup: CoreSetup) { + if (enabledFeatures.ad === true) { + coreSetup.uiSettings.register({ + [ANOMALY_DETECTION_ENABLE_TIME_RANGE]: { + name: i18n.translate( + 'xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeName', + { + defaultMessage: 'Enable time filter defaults for anomaly detection results', + } + ), + value: DEFAULT_ENABLE_AD_RESULTS_TIME_FILTER, + schema: schema.boolean(), + description: i18n.translate( + 'xpack.ml.advancedSettings.enableAnomalyDetectionDefaultTimeRangeDesc', + { + defaultMessage: + 'Use the default time filter in the Single Metric Viewer and Anomaly Explorer. If not enabled, the results for the full time range of the job are displayed.', + } + ), + category: ['machineLearning'], + }, + [ANOMALY_DETECTION_DEFAULT_TIME_RANGE]: { + name: i18n.translate('xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeName', { + defaultMessage: 'Time filter defaults for anomaly detection results', + }), + type: 'json', + value: JSON.stringify(DEFAULT_AD_RESULTS_TIME_FILTER, null, 2), + description: i18n.translate( + 'xpack.ml.advancedSettings.anomalyDetectionDefaultTimeRangeDesc', + { + defaultMessage: + 'The time filter selection to use when viewing anomaly detection job results.', + } + ), + schema: schema.object({ + from: schema.string(), + to: schema.string(), + }), + requiresPageReload: true, + category: ['machineLearning'], + }, + }); + } } diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts index 05c2764297d2d..4f1796782a6ae 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IScopedClusterClient } from '@kbn/core/server'; +import type { IScopedClusterClient } from '@kbn/core/server'; import { getAnalysisType, INDEX_CREATED_BY, @@ -23,20 +23,21 @@ import { flatten } from 'lodash'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { modelsProvider } from '../model_management'; import { - ExtendAnalyticsMapArgs, - GetAnalyticsMapArgs, - InitialElementsReturnType, + type ExtendAnalyticsMapArgs, + type GetAnalyticsMapArgs, + type InitialElementsReturnType, + type NextLinkReturnType, + type GetAnalyticsJobIdArg, + type GetAnalyticsModelIdArg, isCompleteInitialReturnType, isAnalyticsMapEdgeElement, isAnalyticsMapNodeElement, isIndexPatternLinkReturnType, isJobDataLinkReturnType, isTransformLinkReturnType, - NextLinkReturnType, - GetAnalyticsJobIdArg, - GetAnalyticsModelIdArg, } from './types'; import type { MlClient } from '../../lib/ml_client'; +import type { MlFeatures } from '../../types'; import { DEFAULT_TRAINED_MODELS_PAGE_SIZE } from '../../routes/trained_models'; export class AnalyticsManager { @@ -44,12 +45,20 @@ export class AnalyticsManager { private _jobs: estypes.MlDataframeAnalyticsSummary[] = []; private _transforms?: TransformGetTransformTransformSummary[]; - constructor(private _mlClient: MlClient, private _client: IScopedClusterClient) {} + constructor( + private readonly _mlClient: MlClient, + private readonly _client: IScopedClusterClient, + private readonly _enabledFeatures: MlFeatures + ) {} private async initData() { const [models, jobs] = await Promise.all([ - this._mlClient.getTrainedModels({ size: DEFAULT_TRAINED_MODELS_PAGE_SIZE }), - this._mlClient.getDataFrameAnalytics({ size: 1000 }), + this._enabledFeatures.nlp || this._enabledFeatures.dfa + ? this._mlClient.getTrainedModels({ size: DEFAULT_TRAINED_MODELS_PAGE_SIZE }) + : { trained_model_configs: [] }, + this._enabledFeatures.dfa + ? this._mlClient.getDataFrameAnalytics({ size: 1000 }) + : { data_frame_analytics: [] }, ]); this._trainedModels = models.trained_model_configs; this._jobs = jobs.data_frame_analytics; diff --git a/x-pack/plugins/ml/server/models/model_management/memory_usage.test.ts b/x-pack/plugins/ml/server/models/model_management/memory_usage.test.ts index 3f85487a4cbf3..31857da674d52 100644 --- a/x-pack/plugins/ml/server/models/model_management/memory_usage.test.ts +++ b/x-pack/plugins/ml/server/models/model_management/memory_usage.test.ts @@ -133,10 +133,16 @@ describe('Model service', () => { }), } as unknown as jest.Mocked; + const mlFeatures = { + ad: true, + dfa: true, + nlp: true, + }; + let service: MemoryUsageService; beforeEach(() => { - service = new MemoryUsageService(mlClient); + service = new MemoryUsageService(mlClient, mlFeatures); }); afterEach(() => {}); diff --git a/x-pack/plugins/ml/server/models/model_management/memory_usage.ts b/x-pack/plugins/ml/server/models/model_management/memory_usage.ts index 541b396b0a6e5..cd665c387302f 100644 --- a/x-pack/plugins/ml/server/models/model_management/memory_usage.ts +++ b/x-pack/plugins/ml/server/models/model_management/memory_usage.ts @@ -22,6 +22,7 @@ import type { NodeDeploymentStatsResponse, NodesOverviewResponse, } from '../../../common/types/trained_models'; +import type { MlFeatures } from '../../types'; // @ts-expect-error numeral missing value const AD_EXTRA_MEMORY = numeral('10MB').value(); @@ -33,7 +34,7 @@ const NODE_FIELDS = ['attributes', 'name', 'roles'] as const; export type RequiredNodeFields = Pick; export class MemoryUsageService { - constructor(private readonly mlClient: MlClient) {} + constructor(private readonly mlClient: MlClient, private readonly mlFeatures: MlFeatures) {} public async getMemorySizes(itemType?: MlSavedObjectType, node?: string, showClosedJobs = false) { let memories: MemoryUsageInfo[] = []; @@ -60,11 +61,19 @@ export class MemoryUsageService { } private async getADJobsSizes() { + if (this.mlFeatures.ad === false) { + return []; + } + const jobs = await this.mlClient.getJobStats(); return jobs.jobs.map(this.getADJobMemorySize); } private async getTrainedModelsSizes() { + if (this.mlFeatures.nlp === false) { + return []; + } + const [models, stats] = await Promise.all([ this.mlClient.getTrainedModels(), this.mlClient.getTrainedModelsStats(), @@ -83,6 +92,10 @@ export class MemoryUsageService { } private async getDFAJobsSizes() { + if (this.mlFeatures.dfa === false) { + return []; + } + const [jobs, jobsStats] = await Promise.all([ this.mlClient.getDataFrameAnalytics(), this.mlClient.getDataFrameAnalyticsStats(), diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index b3f829faf170b..a0982e4721b06 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -29,10 +29,7 @@ import { notificationsRoutes } from './routes/notifications'; import type { MlFeatures, PluginsSetup, PluginsStart, RouteInitialization } from './types'; import { PLUGIN_ID } from '../common/constants/app'; import type { MlCapabilities } from '../common/types/capabilities'; - import { initMlServerLog } from './lib/log'; -import { initSampleDataSets } from './lib/sample_data_sets'; - import { annotationRoutes } from './routes/annotations'; import { calendars } from './routes/calendars'; import { dataFeedRoutes } from './routes/datafeeds'; @@ -68,10 +65,8 @@ import { ML_ALERT_TYPES } from '../common/constants/alerts'; import { alertingRoutes } from './routes/alerting'; import { registerCollector } from './usage'; import { SavedObjectsSyncService } from './saved_objects/sync_task'; -import { - CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, - CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, -} from '../common/constants/cases'; +import { registerCasesPersistableState } from './lib/register_cases'; +import { registerSampleDataSetLinks } from './lib/register_sameple_data_set_links'; type SetFeaturesEnabled = (features: MlFeatures) => void; @@ -104,12 +99,17 @@ export class MlServerPlugin dfa: true, nlp: true, }; + private isServerless: boolean = false; + private registerCases: () => void = () => {}; + private registerSampleDatasetsIntegration: () => void = () => {}; + private registerKibanaSettings: () => void = () => {}; constructor(ctx: PluginInitializerContext) { this.log = ctx.logger.get(); this.mlLicense = new MlLicense(); this.isMlReady = new Promise((resolve) => (this.setMlReady = resolve)); this.savedObjectsSyncService = new SavedObjectsSyncService(this.log); + this.isServerless = ctx.env.packageInfo.buildFlavor === 'serverless'; } public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup { @@ -157,8 +157,6 @@ export class MlServerPlugin }, }); - registerKibanaSettings(coreSetup); - // initialize capabilities switcher to add license filter to ml capabilities setupCapabilitiesSwitcher( coreSetup, @@ -224,7 +222,8 @@ export class MlServerPlugin coreSetup.getStartServices ), mlLicense: this.mlLicense, - enabledFeatures: this.enabledFeatures, + getEnabledFeatures: () => Object.assign({}, this.enabledFeatures), + isServerless: this.isServerless, }; annotationRoutes(routeInit, plugins.security); @@ -268,6 +267,24 @@ export class MlServerPlugin }); } + this.registerCases = () => { + if (plugins.cases) { + registerCasesPersistableState(plugins.cases); + } + }; + + this.registerSampleDatasetsIntegration = () => { + // called in start once enabledFeatures is available + if (this.home) { + registerSampleDataSetLinks(this.enabledFeatures, this.home); + } + }; + + this.registerKibanaSettings = () => { + // called in start once enabledFeatures is available + registerKibanaSettings(this.enabledFeatures, coreSetup); + }; + if (plugins.usageCollection) { const getIndexForType = (type: string) => coreSetup @@ -276,16 +293,6 @@ export class MlServerPlugin registerCollector(plugins.usageCollection, getIndexForType); } - if (plugins.cases) { - plugins.cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE, - }); - - plugins.cases.attachmentFramework.registerPersistableState({ - id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS, - }); - } - const setFeaturesEnabled = (features: MlFeatures) => { if (features.ad !== undefined) { this.enabledFeatures.ad = features.ad; @@ -319,10 +326,11 @@ export class MlServerPlugin return; } - if (this.home) { - initSampleDataSets(mlLicense, this.home); + if (mlLicense.isMlEnabled() && mlLicense.isFullLicense()) { + this.registerCases(); + this.registerSampleDatasetsIntegration(); + this.registerKibanaSettings(); } - // check whether the job saved objects exist // and create them if needed. const { initializeJobs } = jobSavedObjectsInitializationFactory( diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 56e4e4ee4f625..fb5eca48d8fa1 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -15,7 +15,7 @@ import { import { ML_INTERNAL_BASE_PATH } from '../../common/constants/app'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; -import type { RouteInitialization } from '../types'; +import type { MlFeatures, RouteInitialization } from '../types'; import { dataAnalyticsJobConfigSchema, dataAnalyticsJobUpdateSchema, @@ -51,9 +51,10 @@ function deleteDestDataViewById(dataViewsService: DataViewsService, dataViewId: function getExtendedMap( mlClient: MlClient, client: IScopedClusterClient, - idOptions: ExtendAnalyticsMapArgs + idOptions: ExtendAnalyticsMapArgs, + enabledFeatures: MlFeatures ) { - const analytics = new AnalyticsManager(mlClient, client); + const analytics = new AnalyticsManager(mlClient, client, enabledFeatures); return analytics.extendAnalyticsMapForAnalyticsJob(idOptions); } @@ -63,17 +64,13 @@ function getExtendedModelsMap( idOptions: { analyticsId?: string; modelId?: string; - } + }, + enabledFeatures: MlFeatures ) { - const analytics = new AnalyticsManager(mlClient, client); + const analytics = new AnalyticsManager(mlClient, client, enabledFeatures); return analytics.extendModelsMap(idOptions); } -export function getAnalyticsManager(mlClient: MlClient, client: IScopedClusterClient) { - const analytics = new AnalyticsManager(mlClient, client); - return analytics; -} - // replace the recursive field and agg references with a // map of ids to allow it to be stringified for transportation // over the network. @@ -95,7 +92,12 @@ function convertForStringify(aggs: Aggregation[], fields: Field[]): void { /** * Routes for the data frame analytics */ -export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: RouteInitialization) { +export function dataFrameAnalyticsRoutes({ + router, + mlLicense, + routeGuard, + getEnabledFeatures, +}: RouteInitialization) { async function userCanDeleteIndex( client: IScopedClusterClient, destinationIndex: string @@ -795,16 +797,26 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout let results; if (treatAsRoot === 'true' || treatAsRoot === true) { - // @ts-expect-error never used as analyticsId - results = await getExtendedMap(mlClient, client, { - analyticsId: type !== JOB_MAP_NODE_TYPES.INDEX ? analyticsId : undefined, - index: type === JOB_MAP_NODE_TYPES.INDEX ? analyticsId : undefined, - }); + results = await getExtendedMap( + mlClient, + client, + // @ts-expect-error never used as analyticsId + { + analyticsId: type !== JOB_MAP_NODE_TYPES.INDEX ? analyticsId : undefined, + index: type === JOB_MAP_NODE_TYPES.INDEX ? analyticsId : undefined, + }, + getEnabledFeatures() + ); } else { - results = await getExtendedModelsMap(mlClient, client, { - analyticsId: type !== JOB_MAP_NODE_TYPES.TRAINED_MODEL ? analyticsId : undefined, - modelId: type === JOB_MAP_NODE_TYPES.TRAINED_MODEL ? analyticsId : undefined, - }); + results = await getExtendedModelsMap( + mlClient, + client, + { + analyticsId: type !== JOB_MAP_NODE_TYPES.TRAINED_MODEL ? analyticsId : undefined, + modelId: type === JOB_MAP_NODE_TYPES.TRAINED_MODEL ? analyticsId : undefined, + }, + getEnabledFeatures() + ); } return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/management.ts b/x-pack/plugins/ml/server/routes/management.ts index 9c68c8fcef261..7a74bac8d5128 100644 --- a/x-pack/plugins/ml/server/routes/management.ts +++ b/x-pack/plugins/ml/server/routes/management.ts @@ -19,11 +19,12 @@ import type { AnalyticsManagementItems, TrainedModelsManagementItems, } from '../../common/types/management'; +import { filterForEnabledFeatureModels } from './trained_models'; /** * Routes for management service */ -export function managementRoutes({ router, routeGuard }: RouteInitialization) { +export function managementRoutes({ router, routeGuard, getEnabledFeatures }: RouteInitialization) { /** * @apiGroup Management * @@ -126,12 +127,14 @@ export function managementRoutes({ router, routeGuard }: RouteInitialization) { trainedModelsSpaces(), ]); + const filteredModels = filterForEnabledFeatureModels(models, getEnabledFeatures()); + const modelStatsMapped = modelsStats.reduce((acc, cur) => { acc[cur.model_id] = cur; return acc; }, {} as Record); - const modelsWithSpaces: TrainedModelsManagementItems[] = models.map((m) => { + const modelsWithSpaces: TrainedModelsManagementItems[] = filteredModels.map((m) => { const id = m.model_id; return { id, diff --git a/x-pack/plugins/ml/server/routes/model_management.ts b/x-pack/plugins/ml/server/routes/model_management.ts index 853ee6582a7f7..31709dd4c32d0 100644 --- a/x-pack/plugins/ml/server/routes/model_management.ts +++ b/x-pack/plugins/ml/server/routes/model_management.ts @@ -20,7 +20,11 @@ import { wrapError } from '../client/error_wrapper'; import { MemoryUsageService } from '../models/model_management'; import { itemTypeLiterals } from './schemas/saved_objects'; -export function modelManagementRoutes({ router, routeGuard }: RouteInitialization) { +export function modelManagementRoutes({ + router, + routeGuard, + getEnabledFeatures, +}: RouteInitialization) { /** * @apiGroup ModelManagement * @@ -48,7 +52,7 @@ export function modelManagementRoutes({ router, routeGuard }: RouteInitializatio }, routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, response }) => { try { - const memoryUsageService = new MemoryUsageService(mlClient); + const memoryUsageService = new MemoryUsageService(mlClient, getEnabledFeatures()); const result = await memoryUsageService.getNodesOverview(); return response.ok({ body: result, @@ -95,7 +99,7 @@ export function modelManagementRoutes({ router, routeGuard }: RouteInitializatio routeGuard.fullLicenseAPIGuard(async ({ mlClient, response, request }) => { try { - const memoryUsageService = new MemoryUsageService(mlClient); + const memoryUsageService = new MemoryUsageService(mlClient, getEnabledFeatures()); return response.ok({ body: await memoryUsageService.getMemorySizes( request.query.type, diff --git a/x-pack/plugins/ml/server/routes/notifications.ts b/x-pack/plugins/ml/server/routes/notifications.ts index f9c64543dd042..c248399ff001a 100644 --- a/x-pack/plugins/ml/server/routes/notifications.ts +++ b/x-pack/plugins/ml/server/routes/notifications.ts @@ -14,7 +14,11 @@ import { import { wrapError } from '../client/error_wrapper'; import type { RouteInitialization } from '../types'; -export function notificationsRoutes({ router, routeGuard, enabledFeatures }: RouteInitialization) { +export function notificationsRoutes({ + router, + routeGuard, + getEnabledFeatures, +}: RouteInitialization) { /** * @apiGroup Notifications * @@ -49,7 +53,7 @@ export function notificationsRoutes({ router, routeGuard, enabledFeatures }: Rou const notificationsService = new NotificationsService( client, mlSavedObjectService, - enabledFeatures + getEnabledFeatures() ); const results = await notificationsService.searchMessages(request.query); @@ -98,7 +102,7 @@ export function notificationsRoutes({ router, routeGuard, enabledFeatures }: Rou const notificationsService = new NotificationsService( client, mlSavedObjectService, - enabledFeatures + getEnabledFeatures() ); const results = await notificationsService.countMessages(request.query); diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index 065b5878da9b2..ab5d3a87e8f46 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -5,11 +5,12 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { schema } from '@kbn/config-schema'; -import { ErrorType } from '@kbn/ml-error-utils'; -import { type MlGetTrainedModelsRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ErrorType } from '@kbn/ml-error-utils'; +import type { MlGetTrainedModelsRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ML_INTERNAL_BASE_PATH } from '../../common/constants/app'; -import { RouteInitialization } from '../types'; +import type { MlFeatures, RouteInitialization } from '../types'; import { wrapError } from '../client/error_wrapper'; import { deleteTrainedModelQuerySchema, @@ -25,14 +26,34 @@ import { updateDeploymentParamsSchema, createIngestPipelineSchema, } from './schemas/inference_schema'; -import { TrainedModelConfigResponse } from '../../common/types/trained_models'; +import type { TrainedModelConfigResponse } from '../../common/types/trained_models'; import { mlLog } from '../lib/log'; import { forceQuerySchema } from './schemas/anomaly_detectors_schema'; import { modelsProvider } from '../models/model_management'; export const DEFAULT_TRAINED_MODELS_PAGE_SIZE = 10000; -export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) { +export function filterForEnabledFeatureModels( + models: TrainedModelConfigResponse[] | estypes.MlTrainedModelConfig[], + enabledFeatures: MlFeatures +) { + let filteredModels = models; + if (enabledFeatures.nlp === false) { + filteredModels = filteredModels.filter((m) => m.model_type === 'tree_ensemble'); + } + + if (enabledFeatures.dfa === false) { + filteredModels = filteredModels.filter((m) => m.model_type !== 'tree_ensemble'); + } + + return filteredModels; +} + +export function trainedModelsRoutes({ + router, + routeGuard, + getEnabledFeatures, +}: RouteInitialization) { /** * @apiGroup TrainedModels * @@ -62,14 +83,14 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) try { const { modelId } = request.params; const { with_pipelines: withPipelines, ...query } = request.query; - const body = await mlClient.getTrainedModels({ + const resp = await mlClient.getTrainedModels({ ...query, ...(modelId ? { model_id: modelId } : {}), size: DEFAULT_TRAINED_MODELS_PAGE_SIZE, } as MlGetTrainedModelsRequest); // model_type is missing // @ts-ignore - const result = body.trained_model_configs as TrainedModelConfigResponse[]; + const result = resp.trained_model_configs as TrainedModelConfigResponse[]; try { if (withPipelines) { // Also need to retrieve the list of deployment IDs from stats @@ -123,8 +144,10 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) mlLog.debug(e); } + const body = filterForEnabledFeatureModels(result, getEnabledFeatures()); + return response.ok({ - body: result, + body, }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index 4bca1336ae0b9..4024e13420e7c 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -80,7 +80,8 @@ export interface RouteInitialization { router: IRouter; mlLicense: MlLicense; routeGuard: RouteGuard; - enabledFeatures: MlFeatures; + getEnabledFeatures: () => MlFeatures; + isServerless: boolean; } export type MlFeatures = Record<'ad' | 'dfa' | 'nlp', boolean>; diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/constants.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/constants.ts index 9c6073c028731..479323f5688c5 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/constants.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/constants.ts @@ -50,7 +50,8 @@ export enum ExternalPageName { mlNodes = 'ml:nodes', mlFileUpload = 'ml:fileUpload', mlIndexDataVisualizer = 'ml:indexDataVisualizer', - mlExplainLogRateSpikes = 'ml:explainLogRateSpikes', + mlDataComparison = 'ml:dataComparison', + mlExplainLogRateSpikes = 'ml:logRateAnalysis', mlLogPatternAnalysis = 'ml:logPatternAnalysis', mlChangePointDetections = 'ml:changePointDetections', // Dev Tools diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/ml_links.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/ml_links.ts index ed324cfb6f52c..bdd0f739e3e7c 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/ml_links.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/ml_links.ts @@ -62,12 +62,16 @@ export const mlNavCategories: ProjectLinkCategory[] = [ { type: LinkCategoryType.title, label: i18n.MODEL_MANAGEMENT_CATEGORY, - linkIds: [ExternalPageName.mlNodesOverview, ExternalPageName.mlNodes], + linkIds: [ExternalPageName.mlNodesOverview], }, { type: LinkCategoryType.title, label: i18n.DATA_VISUALIZER_CATEGORY, - linkIds: [ExternalPageName.mlFileUpload, ExternalPageName.mlIndexDataVisualizer], + linkIds: [ + ExternalPageName.mlFileUpload, + ExternalPageName.mlIndexDataVisualizer, + ExternalPageName.mlDataComparison, + ], }, { type: LinkCategoryType.title, @@ -160,11 +164,17 @@ export const mlNavLinks: ProjectNavigationLink[] = [ landingIcon: IconEndpointLazy, description: i18n.INDEX_DATA_VISUALIZER_DESC, }, + { + id: ExternalPageName.mlDataComparison, + title: i18n.DATA_COMPARISON_TITLE, + landingIcon: IconEndpointLazy, + description: i18n.DATA_COMPARISON_DESC, + }, { id: ExternalPageName.mlExplainLogRateSpikes, - title: i18n.EXPLAIN_LOG_RATE_SPIKES_TITLE, + title: i18n.LOG_RATE_ANALYSIS_TITLE, landingIcon: IconEndpointLazy, - description: i18n.EXPLAIN_LOG_RATE_SPIKES_DESC, + description: i18n.LOG_RATE_ANALYSIS_DESC, }, { id: ExternalPageName.mlLogPatternAnalysis, diff --git a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/ml_translations.ts b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/ml_translations.ts index 301e5b05bfa67..500fcdd9da427 100644 --- a/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/ml_translations.ts +++ b/x-pack/plugins/security_solution_serverless/public/navigation/links/sections/ml_translations.ts @@ -156,13 +156,13 @@ export const ANALYTICS_MAP_DESC = i18n.translate( export const NODES_OVERVIEW_TITLE = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.nodesOverview.title', { - defaultMessage: 'Nodes overview', + defaultMessage: 'Model Management', } ); export const NODES_OVERVIEW_DESC = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.nodesOverview.desc', { - defaultMessage: 'Nodes overview page', + defaultMessage: 'Model Management page', } ); export const NODES_TITLE = i18n.translate( @@ -180,37 +180,49 @@ export const NODES_DESC = i18n.translate( export const FILE_UPLOAD_TITLE = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.fileUpload.title', { - defaultMessage: 'File', + defaultMessage: 'File data visualizer', } ); export const FILE_UPLOAD_DESC = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.fileUpload.desc', { - defaultMessage: 'File page', + defaultMessage: 'File data visualizer page', } ); export const INDEX_DATA_VISUALIZER_TITLE = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.indexDataVisualizer.title', { - defaultMessage: 'Data view', + defaultMessage: 'Data view data visualizer', } ); export const INDEX_DATA_VISUALIZER_DESC = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.indexDataVisualizer.desc', { - defaultMessage: 'Data view page', + defaultMessage: 'Data view data visualizer page', } ); -export const EXPLAIN_LOG_RATE_SPIKES_TITLE = i18n.translate( +export const DATA_COMPARISON_TITLE = i18n.translate( + 'xpack.securitySolutionServerless.navLinks.ml.datComparison.title', + { + defaultMessage: 'Data comparison', + } +); +export const DATA_COMPARISON_DESC = i18n.translate( + 'xpack.securitySolutionServerless.navLinks.ml.datComparison.desc', + { + defaultMessage: 'Data comparison page', + } +); +export const LOG_RATE_ANALYSIS_TITLE = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.explainLogRateSpikes.title', { - defaultMessage: 'Explain log rate spikes', + defaultMessage: 'Log Rate Analysis', } ); -export const EXPLAIN_LOG_RATE_SPIKES_DESC = i18n.translate( +export const LOG_RATE_ANALYSIS_DESC = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.explainLogRateSpikes.desc', { - defaultMessage: 'Explain log rate spikes page', + defaultMessage: 'Log Rate Analysis Page', } ); export const LOG_PATTERN_ANALYSIS_TITLE = i18n.translate( @@ -228,12 +240,12 @@ export const LOG_PATTERN_ANALYSIS_DESC = i18n.translate( export const CHANGE_POINT_DETECTIONS_TITLE = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.changePointDetections.title', { - defaultMessage: 'Change point detections', + defaultMessage: 'Change point detection', } ); export const CHANGE_POINT_DETECTIONS_DESC = i18n.translate( 'xpack.securitySolutionServerless.navLinks.ml.changePointDetections.desc', { - defaultMessage: 'Change point detections page', + defaultMessage: 'Change point detection page', } ); diff --git a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx index 3808a64e3baff..55cf3960ab0bd 100644 --- a/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_observability/public/components/side_navigation/index.tsx @@ -66,14 +66,15 @@ const navigationTree: NavigationTreeDefinition = { defaultMessage: 'Log rate analysis', }), link: 'ml:logRateAnalysis', - icon: 'beaker', getIsActive: ({ pathNameSerialized, prepend }) => { return pathNameSerialized.includes(prepend('/app/ml/aiops/log_rate_analysis')); }, }, { + title: i18n.translate('xpack.serverlessObservability.ml.changePointDetection', { + defaultMessage: 'Change point detection', + }), link: 'ml:changePointDetections', - icon: 'beaker', getIsActive: ({ pathNameSerialized, prepend }) => { return pathNameSerialized.includes( prepend('/app/ml/aiops/change_point_detection') diff --git a/x-pack/plugins/serverless_search/server/plugin.ts b/x-pack/plugins/serverless_search/server/plugin.ts index e43c5b0f18bbb..a77ff8dfd9cc3 100644 --- a/x-pack/plugins/serverless_search/server/plugin.ts +++ b/x-pack/plugins/serverless_search/server/plugin.ts @@ -71,7 +71,7 @@ export class ServerlessSearchPlugin registerIndicesRoutes(dependencies); }); - pluginsSetup.ml.setFeaturesEnabled({ ad: false, dfa: false, nlp: false }); + pluginsSetup.ml.setFeaturesEnabled({ ad: false, dfa: false, nlp: true }); pluginsSetup.serverless.setupProjectSettings(SEARCH_PROJECT_SETTINGS); return {}; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0bb7f28f877ab..1fa00f4c26eb8 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -24115,13 +24115,7 @@ "xpack.ml.jobSelector.noResultsForJobLabel": "Aucun résultat", "xpack.ml.jobSelector.selectAllGroupLabel": "Tout sélectionner", "xpack.ml.jobSelector.selectAllOptionLabel": "*", - "xpack.ml.jobService.activeDatafeedsLabel": "Flux de données actifs", - "xpack.ml.jobService.activeMLNodesLabel": "Nœuds de ML actifs", - "xpack.ml.jobService.closedJobsLabel": "Tâches fermées", - "xpack.ml.jobService.failedJobsLabel": "Tâches échouées", "xpack.ml.jobService.jobAuditMessagesErrorTitle": "Erreur lors du chargement des messages liés à la tâche", - "xpack.ml.jobService.openJobsLabel": "Ouvrir les tâches", - "xpack.ml.jobService.totalJobsLabel": "Total de tâches", "xpack.ml.jobService.validateJobErrorTitle": "Erreur de validation de tâche", "xpack.ml.jobsHealthAlertingRule.actionGroupName": "Problème détecté", "xpack.ml.jobsHealthAlertingRule.name": "Intégrité des tâches de détection des anomalies", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a8cc0445a4a12..dc973df003738 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -24115,13 +24115,7 @@ "xpack.ml.jobSelector.noResultsForJobLabel": "成果がありません", "xpack.ml.jobSelector.selectAllGroupLabel": "すべて選択", "xpack.ml.jobSelector.selectAllOptionLabel": "*", - "xpack.ml.jobService.activeDatafeedsLabel": "アクティブなデータフィード", - "xpack.ml.jobService.activeMLNodesLabel": "アクティブな ML ノード", - "xpack.ml.jobService.closedJobsLabel": "ジョブを作成", - "xpack.ml.jobService.failedJobsLabel": "失敗したジョブ", "xpack.ml.jobService.jobAuditMessagesErrorTitle": "ジョブメッセージの読み込みエラー", - "xpack.ml.jobService.openJobsLabel": "ジョブを開く", - "xpack.ml.jobService.totalJobsLabel": "合計ジョブ数", "xpack.ml.jobService.validateJobErrorTitle": "ジョブ検証エラー", "xpack.ml.jobsHealthAlertingRule.actionGroupName": "問題が検出されました", "xpack.ml.jobsHealthAlertingRule.name": "異常検知ジョブヘルス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9c5c310d41a15..621d57a7d1533 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -24114,13 +24114,7 @@ "xpack.ml.jobSelector.noResultsForJobLabel": "无结果", "xpack.ml.jobSelector.selectAllGroupLabel": "全选", "xpack.ml.jobSelector.selectAllOptionLabel": "*", - "xpack.ml.jobService.activeDatafeedsLabel": "活动数据馈送", - "xpack.ml.jobService.activeMLNodesLabel": "活动 ML 节点", - "xpack.ml.jobService.closedJobsLabel": "已关闭的作业", - "xpack.ml.jobService.failedJobsLabel": "失败的作业", "xpack.ml.jobService.jobAuditMessagesErrorTitle": "加载作业消息时出错", - "xpack.ml.jobService.openJobsLabel": "打开的作业", - "xpack.ml.jobService.totalJobsLabel": "总计作业数", "xpack.ml.jobService.validateJobErrorTitle": "作业验证错误", "xpack.ml.jobsHealthAlertingRule.actionGroupName": "检测到问题", "xpack.ml.jobsHealthAlertingRule.name": "异常检测作业运行状况", diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts new file mode 100644 index 0000000000000..bb7ae7d04c418 --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts @@ -0,0 +1,43 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + /** + * Attachment types are being registered in + * x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts + */ + describe('Persistable state attachments', () => { + // This test is intended to fail when new persistable state attachment types are registered. + // To resolve, add the new persistable state attachment types ID to this list. This will trigger + // a CODEOWNERS review by Response Ops. + describe('check registered persistable state attachment types', () => { + const getRegisteredTypes = () => { + return supertest + .get('/api/cases_fixture/registered_persistable_state_attachments') + .expect(200) + .then((response) => response.body); + }; + + it('should check changes on all registered persistable state attachment types', async () => { + const types = await getRegisteredTypes(); + + expect(types).to.eql({ + '.lens': '78559fd806809ac3a1008942ead2a079864054f5', + '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', + aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', + }); + }); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts index 6fc840f873ba1..2750559ca3fb5 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts @@ -30,6 +30,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/assignees')); loadTestFile(require.resolve('./cases/push_case')); loadTestFile(require.resolve('./configure/get_connectors')); + loadTestFile(require.resolve('./attachments_framework/registered_persistable_state_basic')); // Internal routes loadTestFile(require.resolve('./internal/suggest_user_profiles')); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/attachments_framework/persistable_state.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/attachments_framework/persistable_state.ts index 8c9701ed58e33..24d9cc5132c64 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/attachments_framework/persistable_state.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/attachments_framework/persistable_state.ts @@ -273,29 +273,5 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }); - - // This test is intended to fail when new persistable state attachment types are registered. - // To resolve, add the new persistable state attachment types ID to this list. This will trigger - // a CODEOWNERS review by Response Ops. - describe('check registered persistable state attachment types', () => { - const getRegisteredTypes = () => { - return supertest - .get('/api/cases_fixture/registered_persistable_state_attachments') - .expect(200) - .then((response) => response.body); - }; - - it('should check changes on all registered persistable state attachment types', async () => { - const types = await getRegisteredTypes(); - - expect(types).to.eql({ - '.lens': '78559fd806809ac3a1008942ead2a079864054f5', - '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', - aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', - ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', - ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f', - }); - }); - }); }); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts new file mode 100644 index 0000000000000..3b2b536b3c88d --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts @@ -0,0 +1,45 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + /** + * Attachment types are being registered in + * x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts + */ + describe('Persistable state attachments', () => { + // This test is intended to fail when new persistable state attachment types are registered. + // To resolve, add the new persistable state attachment types ID to this list. This will trigger + // a CODEOWNERS review by Response Ops. + describe('check registered persistable state attachment types', () => { + const getRegisteredTypes = () => { + return supertest + .get('/api/cases_fixture/registered_persistable_state_attachments') + .expect(200) + .then((response) => response.body); + }; + + it('should check changes on all registered persistable state attachment types', async () => { + const types = await getRegisteredTypes(); + + expect(types).to.eql({ + '.lens': '78559fd806809ac3a1008942ead2a079864054f5', + '.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1', + aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c', + ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', + ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f', + }); + }); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index 9e68a8379b702..f6ef4d3ede478 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -33,6 +33,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/assignees')); loadTestFile(require.resolve('./cases/find_cases')); loadTestFile(require.resolve('./configure')); + loadTestFile(require.resolve('./attachments_framework/registered_persistable_state_trial')); // sub privileges are only available with a license above basic loadTestFile(require.resolve('./delete_sub_privilege')); loadTestFile(require.resolve('./user_profiles/get_current')); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts b/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts index 1e32185187f8d..22b59a9e3ed05 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cypress/e2e/navigation.cy.ts @@ -72,7 +72,10 @@ describe.skip('Serverless', () => { cy.contains('Log rate analysis').click(); cy.url().should('include', '/app/ml/aiops/log_rate_analysis_index_select'); - cy.contains('Change Point Detection').click(); + cy.contains('Log pattern analysis').click(); + cy.url().should('include', '/app/ml/aiops/log_categorization_index_select'); + + cy.contains('Change point detection').click(); cy.url().should('include', '/app/ml/aiops/change_point_detection_index_select'); cy.contains('Job notifications').click();