From 09faf897c9c5ac6d164767a6239519e53f09b08e Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 13 Sep 2023 20:07:55 +0100 Subject: [PATCH] [ML] Serverless compatibility fixes (#163724) **ML Management page** - Fixes general page loading issues. - Ensures only enabled features are shown as tabs - Ensures only jobs for enabled features can be exported and imported. - Ensures only enabled features are listed in the saved object sync output. - On trained models tab: - Only lists DFA models if NLP is disabled. - Only lists non-DFA models if DFA is disabled. **Anomaly Detection** - Hides node information in anomaly detection jobs list. - Hides the Exclude frozen data option in the Use full time range selector in job wizards. **Data frame analytics** - Hides all node and license level information. **Trained models** - Only lists DFA models if NLP is disabled. - Only lists non-DFA models if DFA is disabled. - Hides all node and license level information. - Hides DFA nodes **Notifications and memory usage** - Ensures only enabled features are mentioned. Including selectable types in the search bar filters. **Integrations with other plugins** - Changes registration for integrations into other plugins so they only happen if the relevant feature is enabled. - Client side: UI actions, cases, embeddables, alerts, maps. - Server side: Sample data sets, cases **AIOPS** - Hides the Exclude frozen data option in the Use full time range selector on all pages **Notes for non ML team reviewers** **response-ops** I've divided the [persistable_state.ts](https://github.com/elastic/kibana/pull/163724#diff-e02dc0b6cb5b63965372b1f4a84d2287cba31a15ab525ab7983f02d09f23879f) test into basic and trial version. The ML cases attachments should only be registered if anomaly detection is available in a trial or platinum license. This was a bug which I noticed when making serverless changes. **Observability** I've made a few minor changes to the nav menu, fixing names of ML features and adding the missing Change point detection AIOPs page. **Security solution** I've made a few minor changes to the nav menu, fixing names of ML features and adding some missing ML features. I think the icons being used will need to be revisited before release as we have [official ML icons](https://elastic.github.io/eui/#/display/icons#apps) but not for every page. So we should probably either have new icons created or all agree on which standard non-ML icons should be used for the ones which are missing. **Search** The NLP feature is currently disabled in main, I believe this was an attempt to stop ML anomaly detection alert rules from being registered. I've reenabled NLP and changed the way we're registering the alerts. They will now only be registered if the anomaly detection feature is enabled. Fixes https://github.com/elastic/kibana/issues/163372 --- packages/default-nav/ml/default_navigation.ts | 6 + .../chrome/navigation/mocks/src/navlinks.ts | 1 + .../default_navigation.test.tsx.snap | 20 + .../components/full_time_range_selector.tsx | 75 ++- .../src/hooks/use_date_picker_context.tsx | 4 + .../change_point_detection_root.tsx | 12 +- .../log_categorization_app_state.tsx | 8 +- .../log_rate_analysis_app_state.tsx | 8 +- .../log_rate_analysis_content_wrapper.tsx | 3 + .../public/hooks/use_aiops_app_context.ts | 1 + .../index_data_visualizer.tsx | 12 +- x-pack/plugins/ml/common/constants/app.ts | 1 + .../plugins/ml/common/util/job_utils.test.ts | 38 +- x-pack/plugins/ml/common/util/job_utils.ts | 14 +- .../ml/public/alerting/register_ml_alerts.ts | 4 +- .../aiops/change_point_detection.tsx | 4 +- .../application/aiops/log_categorization.tsx | 4 +- .../application/aiops/log_rate_analysis.tsx | 4 +- x-pack/plugins/ml/public/application/app.tsx | 29 +- .../capabilities/check_capabilities.ts | 77 ++- .../export_jobs_flyout/export_jobs_flyout.tsx | 187 ++++--- .../import_jobs_flyout/import_jobs_flyout.tsx | 48 +- .../components/job_messages/job_messages.tsx | 103 ++-- .../components/job_spaces_sync/sync_list.tsx | 15 +- .../jobs_awaiting_node_warning.tsx | 4 +- .../new_job_awaiting_node.tsx | 18 +- .../ml_entity_selector.test.tsx | 1 + .../ml_entity_selector/ml_entity_selector.tsx | 15 +- .../components/ml_page/ml_page.tsx | 17 +- .../saved_objects_warning.tsx | 16 +- .../application/contexts/kibana/index.ts | 1 + .../contexts/kibana/use_is_serverless.ts} | 6 +- .../contexts/kibana/use_navigate_to_path.ts | 4 +- .../pages/job_map/components/controls.tsx | 52 +- .../index_based/index_data_visualizer.tsx | 8 +- .../jobs_list_view/jobs_list_view.js | 12 +- .../jobs_stats_bar/jobs_stats_bar.js | 31 +- .../application/jobs/jobs_list/jobs.tsx | 4 +- .../components/time_range_step/time_range.tsx | 4 +- .../ml/public/application/management/index.ts | 5 +- .../jobs_list_page/jobs_list_page.tsx | 24 +- .../space_management/space_management.tsx | 70 ++- .../application/management/jobs_list/index.ts | 6 +- .../memory_usage/memory_tree_map/tree_map.tsx | 50 +- .../memory_usage/memory_usage_page.tsx | 11 +- .../model_management/expanded_row.tsx | 12 +- .../model_management/model_actions.tsx | 4 +- .../model_management/models_list.tsx | 14 +- .../components/notifications_list.test.tsx | 1 + .../components/notifications_list.tsx | 70 ++- .../anomaly_detection_panel.tsx | 5 +- .../anomaly_detection_panel/utils.ts | 26 +- .../routing/routes/memory_usage.tsx | 7 +- .../application/services/job_service.js | 45 -- .../anomaly_charts_embeddable_factory.test.ts | 2 +- .../anomaly_charts_embeddable_factory.ts | 5 +- .../anomaly_charts_setup_flyout.tsx | 3 +- ...omaly_swimlane_embeddable_factory.test.tsx | 2 +- .../anomaly_swimlane_embeddable_factory.ts | 5 +- .../anomaly_swimlane_setup_flyout.tsx | 3 +- .../common/resolve_job_selection.tsx | 5 +- x-pack/plugins/ml/public/embeddables/index.ts | 14 +- .../job_creation/common/create_flyout.tsx | 3 +- .../job_creation/lens/show_flyout.tsx | 4 +- .../job_creation/map/show_flyout.tsx | 13 +- x-pack/plugins/ml/public/plugin.ts | 103 ++-- .../register_search_links.ts | 5 +- .../search_deep_links.ts | 485 +++++++++--------- ...er_feature.ts => register_home_feature.ts} | 6 +- .../edit_anomaly_charts_panel_action.tsx | 4 +- .../ui_actions/edit_swimlane_panel_action.tsx | 9 +- x-pack/plugins/ml/public/ui_actions/index.ts | 15 +- .../ui_actions/open_vis_in_ml_action.tsx | 22 +- .../server/lib/alerts/register_ml_alerts.ts | 6 +- .../lib/capabilities/capabilities_switcher.ts | 32 +- .../plugins/ml/server/lib/register_cases.ts | 22 + ....ts => register_sameple_data_set_links.ts} | 9 +- .../ml/server/lib/register_settings.ts | 82 +-- .../data_frame_analytics/analytics_manager.ts | 29 +- .../model_management/memory_usage.test.ts | 8 +- .../models/model_management/memory_usage.ts | 15 +- x-pack/plugins/ml/server/plugin.ts | 54 +- .../ml/server/routes/data_frame_analytics.ts | 52 +- x-pack/plugins/ml/server/routes/management.ts | 7 +- .../ml/server/routes/model_management.ts | 10 +- .../plugins/ml/server/routes/notifications.ts | 10 +- .../ml/server/routes/trained_models.ts | 39 +- x-pack/plugins/ml/server/types.ts | 3 +- .../public/navigation/links/constants.ts | 3 +- .../navigation/links/sections/ml_links.ts | 18 +- .../links/sections/ml_translations.ts | 36 +- .../components/side_navigation/index.tsx | 5 +- .../serverless_search/server/plugin.ts | 2 +- .../translations/translations/fr-FR.json | 6 - .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../registered_persistable_state_basic.ts | 43 ++ .../security_and_spaces/tests/basic/index.ts | 1 + .../persistable_state.ts | 24 - .../registered_persistable_state_trial.ts | 45 ++ .../security_and_spaces/tests/trial/index.ts | 1 + .../cypress/e2e/navigation.cy.ts | 5 +- 102 files changed, 1579 insertions(+), 929 deletions(-) rename x-pack/plugins/ml/{server/lib/sample_data_sets/index.ts => public/application/contexts/kibana/use_is_serverless.ts} (62%) rename x-pack/plugins/ml/public/{register_feature.ts => register_home_feature.ts} (86%) create mode 100644 x-pack/plugins/ml/server/lib/register_cases.ts rename x-pack/plugins/ml/server/lib/{sample_data_sets/sample_data_sets.ts => register_sameple_data_set_links.ts} (87%) create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/basic/attachments_framework/registered_persistable_state_basic.ts create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts 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();