Skip to content

Commit

Permalink
[ML][Fleet] Adds link to anomaly detection configurations from Integr…
Browse files Browse the repository at this point in the history
…ation > Assets tab (#193105)

## Summary

Related issue: #182199

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.

<img width="429" alt="Screenshot 2024-09-19 at 13 46 47"
src="https://github.com/user-attachments/assets/9fcc3606-cc08-483f-88b4-00c07de3fc57">

<img width="717" alt="Screenshot 2024-09-19 at 13 47 52"
src="https://github.com/user-attachments/assets/847f3d6e-95a1-491c-aa3e-7d54da7df98a">


### 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 <[email protected]>
  • Loading branch information
alvarezmelissa87 and elasticmachine authored Sep 19, 2024
1 parent a940293 commit 052187c
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -31,9 +54,14 @@ export const SuppliedConfigurations = () => {
},
} = useMlKibana();

const [suppliedConfigurationsPageState, setSuppliedConfigurationsPageState] =
usePageUrlState<PageUrlState>(ML_PAGES.SUPPLIED_CONFIGURATIONS, {
queryText: '',
});

const [modules, setModules] = useState<Module[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [query, setQuery] = useState(EuiSearchBar.Query.MATCH_ALL);
const [searchError, setSearchError] = useState<string | undefined>();
const [isFlyoutVisible, setIsFlyoutVisible] = useState<boolean>(false);
const [selectedModuleId, setSelectedModuleId] = useState<string | undefined>();

Expand Down Expand Up @@ -82,22 +110,34 @@ 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(() => {
const clauses = query?.ast?.clauses ?? [];
return clauses.length > 0 ? filterModules(modules, clauses) : modules;
}, [query, modules]);

const onChange = useCallback(({ query: onChangeQuery }) => setQuery(onChangeQuery), [setQuery]);

if (isLoading === true) return <LoadingIndicator />;

return (
Expand All @@ -112,11 +152,18 @@ export const SuppliedConfigurations = () => {
}
),
incremental: true,
schema,
schema: SCHEMA,
}}
filters={filters}
onChange={onChange}
/>
<EuiFormRow
data-test-subj="mlAnomalyJobSelectionControls"
isInvalid={searchError !== undefined}
error={searchError}
>
<></>
</EuiFormRow>
<EuiSpacer size="l" />
<EuiFlexGrid gutterSize="l" columns={4}>
{filteredModules.map(({ description, id, logo, title }) => {
Expand Down
16 changes: 16 additions & 0 deletions x-pack/plugins/ml/public/application/util/string_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
mlEscape,
escapeForElasticsearchQuery,
escapeKueryForEmbeddableFieldValuePair,
stringMatch,
} from './string_utils';

describe('ML - string utils', () => {
Expand Down Expand Up @@ -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);
});
});
});
14 changes: 9 additions & 5 deletions x-pack/plugins/ml/public/application/util/string_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
25 changes: 24 additions & 1 deletion x-pack/plugins/ml/server/saved_objects/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -30,12 +42,23 @@ export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) {
migrations,
mappings: mlTrainedModel,
});
savedObjects.registerType({
savedObjects.registerType<MlModuleAttributes>({
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) => {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 052187c

Please sign in to comment.