From 052187ce180f0cbcb062486f36a562495f840bef Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 19 Sep 2024 14:40:44 -0600 Subject: [PATCH] [ML][Fleet] Adds link to anomaly detection configurations from Integration > Assets tab (#193105) ## Summary Related issue: https://github.com/elastic/kibana/issues/182199?reload=1 This PR adds a link to `ML > Anomaly Detection > Supplied Configurations` from `Integration > Assets tab` for 'ML Modules' assets. The naming of the asset is also updated in Fleet to be consistent with the ML UI. Screenshot 2024-09-19 at 13 46 47 Screenshot 2024-09-19 at 13 47 52 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Elastic Machine --- .../integrations/sections/epm/constants.tsx | 4 +- .../services/epm/packages/get_bulk_assets.ts | 1 - .../supplied_configurations.tsx | 71 +++++++++++++++---- .../application/util/string_utils.test.ts | 16 +++++ .../public/application/util/string_utils.ts | 14 ++-- .../ml/server/saved_objects/saved_objects.ts | 25 ++++++- .../transform_search_bar_filters.tsx | 14 ++-- .../epm/__snapshots__/bulk_get_assets.snap | 2 +- 8 files changed, 120 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx index 1a1fba813bd8..03f0b0b5cee8 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx @@ -66,10 +66,10 @@ export const AssetTitleMap: Record< } ), 'ml-module': i18n.translate('xpack.fleet.epm.assetTitles.mlModules', { - defaultMessage: 'ML modules', + defaultMessage: 'Anomaly detection configurations', }), ml_module: i18n.translate('xpack.fleet.epm.assetTitles.mlModules', { - defaultMessage: 'ML modules', + defaultMessage: 'Anomaly detection configurations', }), tag: i18n.translate('xpack.fleet.epm.assetTitles.tag', { defaultMessage: 'Tags', diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_bulk_assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_bulk_assets.ts index 082691fb0812..ab6868dfaf65 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get_bulk_assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_bulk_assets.ts @@ -71,7 +71,6 @@ export async function getBulkAssets( } // TODO: Ask for Kibana SOs to have `getInAppUrl()` registered so that the above works safely: - // ml-module // security-rule // csp-rule-template // osquery-pack-asset diff --git a/x-pack/plugins/ml/public/application/supplied_configurations/supplied_configurations.tsx b/x-pack/plugins/ml/public/application/supplied_configurations/supplied_configurations.tsx index d8aaf678e372..f98502c1c5f8 100644 --- a/x-pack/plugins/ml/public/application/supplied_configurations/supplied_configurations.tsx +++ b/x-pack/plugins/ml/public/application/supplied_configurations/supplied_configurations.tsx @@ -6,22 +6,45 @@ */ import React, { useCallback, useMemo, useState } from 'react'; -import type { SearchFilterConfig, FieldValueOptionType } from '@elastic/eui'; -import { EuiCard, EuiIcon, EuiFlexGrid, EuiFlexItem, EuiSearchBar, EuiSpacer } from '@elastic/eui'; +import type { SearchFilterConfig, FieldValueOptionType, EuiSearchBarProps } from '@elastic/eui'; +import { + EuiCard, + EuiIcon, + EuiFlexGrid, + EuiFlexItem, + EuiFormRow, + EuiSearchBar, + EuiSpacer, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { usePageUrlState, type PageUrlState } from '@kbn/ml-url-state'; import useMountedState from 'react-use/lib/useMountedState'; import useMount from 'react-use/lib/useMount'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { useMlKibana } from '../contexts/kibana'; import type { Module } from '../../../common/types/modules'; +import { ML_PAGES } from '../../../common/constants/locator'; import { LoadingIndicator } from '../components/loading_indicator'; import { filterModules } from './utils'; import { SuppliedConfigurationsFlyout } from './supplied_configurations_flyout'; +interface SuppliedConfigurationsPageUrlState { + queryText: string; +} + export function isLogoObject(arg: unknown): arg is { icon: string } { return isPopulatedObject(arg) && Object.hasOwn(arg, 'icon'); } +const SCHEMA = { + strict: true, + fields: { + tags: { + type: 'string', + }, + }, +}; + export const SuppliedConfigurations = () => { const { services: { @@ -31,9 +54,14 @@ export const SuppliedConfigurations = () => { }, } = useMlKibana(); + const [suppliedConfigurationsPageState, setSuppliedConfigurationsPageState] = + usePageUrlState(ML_PAGES.SUPPLIED_CONFIGURATIONS, { + queryText: '', + }); + const [modules, setModules] = useState([]); const [isLoading, setIsLoading] = useState(false); - const [query, setQuery] = useState(EuiSearchBar.Query.MATCH_ALL); + const [searchError, setSearchError] = useState(); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [selectedModuleId, setSelectedModuleId] = useState(); @@ -82,13 +110,27 @@ export const SuppliedConfigurations = () => { ]; }, [modules]); - const schema = { - strict: true, - fields: { - tags: { - type: 'string', - }, + const setSearchQueryText = useCallback( + (value: string) => { + setSuppliedConfigurationsPageState({ queryText: value }); }, + [setSuppliedConfigurationsPageState] + ); + + const query = useMemo(() => { + const searchQueryText = (suppliedConfigurationsPageState as SuppliedConfigurationsPageUrlState) + .queryText; + return searchQueryText !== '' ? EuiSearchBar.Query.parse(searchQueryText) : undefined; + }, [suppliedConfigurationsPageState]); + + const onChange: EuiSearchBarProps['onChange'] = (search) => { + if (search.error !== null) { + setSearchError(search.error.message); + return; + } + + setSearchError(undefined); + setSearchQueryText(search.queryText); }; const filteredModules = useMemo(() => { @@ -96,8 +138,6 @@ export const SuppliedConfigurations = () => { return clauses.length > 0 ? filterModules(modules, clauses) : modules; }, [query, modules]); - const onChange = useCallback(({ query: onChangeQuery }) => setQuery(onChangeQuery), [setQuery]); - if (isLoading === true) return ; return ( @@ -112,11 +152,18 @@ export const SuppliedConfigurations = () => { } ), incremental: true, - schema, + schema: SCHEMA, }} filters={filters} onChange={onChange} /> + + <> + {filteredModules.map(({ description, id, logo, title }) => { diff --git a/x-pack/plugins/ml/public/application/util/string_utils.test.ts b/x-pack/plugins/ml/public/application/util/string_utils.test.ts index a149eddec45b..7bdf320557d3 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.test.ts @@ -15,6 +15,7 @@ import { mlEscape, escapeForElasticsearchQuery, escapeKueryForEmbeddableFieldValuePair, + stringMatch, } from './string_utils'; describe('ML - string utils', () => { @@ -170,4 +171,19 @@ describe('ML - string utils', () => { ); }); }); + + describe('stringMatch', () => { + test('should return true for partial match', () => { + expect(stringMatch('foobar', 'Foo')).toBe(true); + }); + test('should return true for exact match', () => { + expect(stringMatch('foobar', 'foobar')).toBe(true); + }); + test('should return false for no match', () => { + expect(stringMatch('foobar', 'nomatch')).toBe(false); + }); + test('should catch error for invalid regex substring and return false', () => { + expect(stringMatch('foobar', '?')).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/ml/public/application/util/string_utils.ts b/x-pack/plugins/ml/public/application/util/string_utils.ts index 72e12febbe5a..36ddd4edbe38 100644 --- a/x-pack/plugins/ml/public/application/util/string_utils.ts +++ b/x-pack/plugins/ml/public/application/util/string_utils.ts @@ -187,9 +187,13 @@ export function calculateTextWidth(txt: string | number, isNumber: boolean) { } export function stringMatch(str: string | undefined, substr: any) { - return ( - typeof str === 'string' && - typeof substr === 'string' && - (str.toLowerCase().match(substr.toLowerCase()) === null) === false - ); + try { + return ( + typeof str === 'string' && + typeof substr === 'string' && + (str.toLowerCase().match(substr.toLowerCase()) === null) === false + ); + } catch (error) { + return false; + } } diff --git a/x-pack/plugins/ml/server/saved_objects/saved_objects.ts b/x-pack/plugins/ml/server/saved_objects/saved_objects.ts index 36f9c29d35e0..98da0bcf8ebe 100644 --- a/x-pack/plugins/ml/server/saved_objects/saved_objects.ts +++ b/x-pack/plugins/ml/server/saved_objects/saved_objects.ts @@ -6,6 +6,7 @@ */ import type { SavedObjectsServiceSetup } from '@kbn/core/server'; +import rison from '@kbn/rison'; import { mlJob, mlTrainedModel, mlModule } from './mappings'; import { migrations } from './migrations'; @@ -15,6 +16,17 @@ import { ML_TRAINED_MODEL_SAVED_OBJECT_TYPE, } from '../../common/types/saved_objects'; +interface MlModuleAttributes { + id: string; + title: string; + description?: string; + type: string; + logo?: object; + query?: string; + jobs: object[]; + datafeeds: object[]; +} + export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) { savedObjects.registerType({ name: ML_JOB_SAVED_OBJECT_TYPE, @@ -30,12 +42,23 @@ export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) { migrations, mappings: mlTrainedModel, }); - savedObjects.registerType({ + savedObjects.registerType({ name: ML_MODULE_SAVED_OBJECT_TYPE, hidden: false, management: { importableAndExportable: true, visibleInManagement: false, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/ml/supplied_configurations/?_a=${encodeURIComponent( + rison.encode({ supplied_configurations: { queryText: obj.attributes.title } }) + )}`, + uiCapabilitiesPath: 'ml.canGetJobs', + }; + }, }, namespaceType: 'agnostic', migrations, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx index 67ba6cffa997..5596f3b5306e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar_filters.tsx @@ -62,11 +62,15 @@ export const transformFilters: SearchFilterConfig[] = [ ]; function stringMatch(str: string | undefined, substr: any) { - return ( - typeof str === 'string' && - typeof substr === 'string' && - (str.toLowerCase().match(substr.toLowerCase()) === null) === false - ); + try { + return ( + typeof str === 'string' && + typeof substr === 'string' && + (str.toLowerCase().match(substr.toLowerCase()) === null) === false + ); + } catch (error) { + return false; + } } export const filterTransforms = (transforms: TransformListRow[], clauses: Clause[]) => { diff --git a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/bulk_get_assets.snap b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/bulk_get_assets.snap index 761ecfa66af7..5fd219958c31 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/bulk_get_assets.snap +++ b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/bulk_get_assets.snap @@ -140,7 +140,7 @@ Array [ "type": "index-pattern", }, Object { - "appLink": "", + "appLink": "/app/ml/supplied_configurations/?_a=(supplied_configurations%3A(queryText%3A'Nginx%20access%20logs'))", "attributes": Object { "description": "Find unusual activity in HTTP access logs from filebeat (ECS).", "title": "Nginx access logs",