Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ test('MLLink produces the correct URL', async () => {
);

expect(href).toMatchInlineSnapshot(
`"/app/ml/jobs?mlManagement=(groupIds:!(apm),jobId:!(something))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"`
`"/app/ml/jobs?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"`
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

import { EuiCard, EuiIcon, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { SetupStatus } from '../../../../../common/log_analysis';
import { CreateJobButton, RecreateJobButton } from '../../log_analysis_setup/create_job_button';
import { useLinkProps } from '../../../../hooks/use_link_props';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { mountReactNode } from '../../../../../../../../src/core/public/utils';

export const LogAnalysisModuleListCard: React.FC<{
jobId: string;
Expand All @@ -26,19 +27,46 @@ export const LogAnalysisModuleListCard: React.FC<{
moduleStatus,
onViewSetup,
}) => {
const {
services: {
ml,
application: { navigateToUrl },
notifications: { toasts },
},
} = useKibanaContextForPlugin();

const [viewInMlLink, setViewInMlLink] = useState<string>('');

const getMlUrl = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can skip this check. If the ML module is not available I don't think the setup flyout will ever be mounted anyway.

if (!ml.urlGenerator) {
toasts.addWarning({
title: mountReactNode(
<FormattedMessage
id="xpack.infra.logs.analysis.mlNotAvailable"
defaultMessage="ML plugin is not available"
/>
),
});
return;
}
setViewInMlLink(await ml.urlGenerator.createUrl({ page: 'jobs', pageState: { jobId } }));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR but I'm curious: how come the createUrl() method is async? Does it fetch anything in the background to generate the URL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a question for the Kibana team. Check the exposed public contract of the URL generator

createUrl: async (state: UrlGeneratorStateMapping[Id]['State']) => {

};

useEffect(() => {
getMlUrl();
});

const navigateToMlApp = async () => {
await navigateToUrl(viewInMlLink);
};

const moduleIcon =
moduleStatus.type === 'required' ? (
<EuiIcon size="xxl" type="machineLearningApp" />
) : (
<EuiIcon color="secondary" size="xxl" type="check" />
);

const viewInMlLinkProps = useLinkProps({
app: 'ml',
pathname: '/jobs',
search: { mlManagement: `(jobId:${jobId})` },
});

const moduleSetupButton =
moduleStatus.type === 'required' ? (
<CreateJobButton hasSetupCapabilities={hasSetupCapabilities} onClick={onViewSetup}>
Expand All @@ -50,13 +78,17 @@ export const LogAnalysisModuleListCard: React.FC<{
) : (
<>
<RecreateJobButton hasSetupCapabilities={hasSetupCapabilities} onClick={onViewSetup} />
<EuiSpacer size="xs" />
<EuiButtonEmpty {...viewInMlLinkProps}>
<FormattedMessage
id="xpack.infra.logs.analysis.viewInMlButtonLabel"
defaultMessage="View in Machine Learning"
/>
</EuiButtonEmpty>
{viewInMlLink ? (
<>
<EuiSpacer size="xs" />
<EuiButtonEmpty onClick={navigateToMlApp}>
<FormattedMessage
id="xpack.infra.logs.analysis.viewInMlButtonLabel"
defaultMessage="View in Machine Learning"
/>
</EuiButtonEmpty>
</>
) : null}
</>
);

Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/infra/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
ObservabilityPluginStart,
} from '../../observability/public';
import type { SpacesPluginStart } from '../../spaces/public';
import { MlPluginStart } from '../../ml/public';

// Our own setup and start contract values
export type InfraClientSetupExports = void;
Expand All @@ -38,6 +39,7 @@ export interface InfraClientStartDeps {
spaces: SpacesPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
usageCollection: UsageCollectionStart;
ml: MlPluginStart;
}

export type InfraClientCoreSetup = CoreSetup<InfraClientStartDeps, InfraClientStartExports>;
Expand Down
22 changes: 21 additions & 1 deletion x-pack/plugins/ml/common/util/string_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { renderTemplate, getMedianStringLength, stringHash } from './string_utils';
import {
renderTemplate,
getMedianStringLength,
stringHash,
getGroupQueryText,
} from './string_utils';

const strings: string[] = [
'foo',
Expand Down Expand Up @@ -54,4 +59,19 @@ describe('ML - string utils', () => {
expect(hash1).not.toBe(hash2);
});
});

describe('getGroupQueryText', () => {
const groupIdOne = 'test_group_id_1';
const groupIdTwo = 'test_group_id_2';

it('should get query string for selected group ids', () => {
const actual = getGroupQueryText([groupIdOne, groupIdTwo]);
expect(actual).toBe(`groups:(${groupIdOne} or ${groupIdTwo})`);
});

it('should get query string for selected group id', () => {
const actual = getGroupQueryText([groupIdOne]);
expect(actual).toBe(`groups:(${groupIdOne})`);
});
});
});
8 changes: 8 additions & 0 deletions x-pack/plugins/ml/common/util/string_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,11 @@ export function stringHash(str: string): number {
}
return hash < 0 ? hash * -2 : hash;
}

export function getGroupQueryText(groupIds: string[]): string {
return `groups:(${groupIds.join(' or ')})`;
}

export function getJobQueryText(jobIds: string | string[]): string {
return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,13 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns';
import { ExpandedRow } from './expanded_row';
import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar';
import { CreateAnalyticsButton } from '../create_analytics_button';
import {
getSelectedIdFromUrl,
getGroupQueryText,
} from '../../../../../jobs/jobs_list/components/utils';
import { getSelectedIdFromUrl } from '../../../../../jobs/jobs_list/components/utils';
import { SourceSelection } from '../source_selection';
import { filterAnalytics, AnalyticsSearchBar } from '../analytics_search_bar';
import { AnalyticsEmptyPrompt } from './empty_prompt';
import { useTableSettings } from './use_table_settings';
import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button';
import { getGroupQueryText } from '../../../../../../../common/util/string_utils';

const filters: EuiSearchBarProps['filters'] = [
{
Expand Down

This file was deleted.

Loading