Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Transforms: Adds a link to discover from the transform list to the actions menu. #97805

Merged
merged 13 commits into from
Apr 27, 2021
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const createStartContractMock = () => {
bulkUpdate: jest.fn(),
delete: jest.fn(),
bulkGet: jest.fn(),
find: jest.fn(),
find: jest.fn().mockResolvedValue({ savedObjects: [] }),
Copy link
Member

Choose a reason for hiding this comment

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

Do you mind setting the mock if needed on your own tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I refactored the code to apply that mock in the transform plugin code (171a65c)

get: jest.fn(),
update: jest.fn(),
},
Expand Down
13 changes: 11 additions & 2 deletions x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,23 @@

import { useContext } from 'react';

import type { ScopedHistory } from 'kibana/public';

import { coreMock } from '../../../../../../src/core/public/mocks';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { savedObjectsPluginMock } from '../../../../../../src/plugins/saved_objects/public/mocks';
import { Storage } from '../../../../../../src/plugins/kibana_utils/public';

import type { AppDependencies } from '../app_dependencies';
import { MlSharedContext } from './shared_context';
import type { GetMlSharedImportsReturnType } from '../../shared_imports';

const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const dataStart = dataPluginMock.createStartContract();

const appDependencies = {
const appDependencies: AppDependencies = {
application: coreStart.application,
chrome: coreStart.chrome,
data: dataStart,
docLinks: coreStart.docLinks,
Expand All @@ -28,11 +34,14 @@ const appDependencies = {
storage: ({ get: jest.fn() } as unknown) as Storage,
overlays: coreStart.overlays,
http: coreSetup.http,
history: {} as ScopedHistory,
savedObjectsPlugin: savedObjectsPluginMock.createStartContract(),
ml: {} as GetMlSharedImportsReturnType,
};

export const useAppDependencies = () => {
const ml = useContext(MlSharedContext);
return { ...appDependencies, ml, savedObjects: jest.fn() };
return { ...appDependencies, ml };
};

export const useToastNotifications = () => {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/transform/public/app/app_dependencies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import type { GetMlSharedImportsReturnType } from '../shared_imports';

export interface AppDependencies {
application: CoreStart['application'];
chrome: CoreStart['chrome'];
data: DataPublicPluginStart;
docLinks: CoreStart['docLinks'];
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/transform/public/app/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export {
} from './transform';
export { TRANSFORM_LIST_COLUMN, TransformListAction, TransformListRow } from './transform_list';
export { getTransformProgress, isCompletedBatchTransform } from './transform_stats';
export { getDiscoverUrl } from './navigation';
export { getDiscoverUrl, getDiscoverUrlState } from './navigation';
export {
getEsAggFromAggConfig,
isPivotAggsConfigWithUiSupport,
Expand Down
13 changes: 11 additions & 2 deletions x-pack/plugins/transform/public/app/common/navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@
* 2.0.
*/

import { getDiscoverUrl } from './navigation';
import { getDiscoverUrl, getDiscoverUrlState } from './navigation';

describe('navigation', () => {
describe('navigation: getDiscoverUrl', () => {
test('getDiscoverUrl should provide encoded url to Discover page', () => {
expect(getDiscoverUrl('farequote-airline', 'http://example.com')).toBe(
'http://example.com/app/discover#?_g=()&_a=(index:farequote-airline)'
);
});
});

describe('navigation: getDiscoverUrlState', () => {
test('getDiscoverUrlState should provide encoded url state without an index for Discover page', () => {
expect(getDiscoverUrlState()).toBe('_g=()&_a=()');
});
test('getDiscoverUrlState should provide encoded url state with an index for Discover page', () => {
expect(getDiscoverUrlState('farequote-airline')).toBe('_g=()&_a=(index:farequote-airline)');
});
});
28 changes: 19 additions & 9 deletions x-pack/plugins/transform/public/app/common/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,31 @@ import rison from 'rison-node';
import { SECTION_SLUG } from '../constants';

/**
* Gets a url for navigating to Discover page.
* Gets url state for navigating to Discover page with optional link to a specific index pattern.
* @param indexPatternId Index pattern ID.
* @param baseUrl Base url.
*/
export function getDiscoverUrl(indexPatternId: string, baseUrl: string): string {
export function getDiscoverUrlState(indexPatternId?: string): string {
const _g = rison.encode({});

// Add the index pattern ID to the appState part of the URL.
const _a = rison.encode({
index: indexPatternId,
});

const hash = `/discover#?_g=${_g}&_a=${_a}`;
const _a = rison.encode(
typeof indexPatternId === 'string'
? {
index: indexPatternId,
}
: {}
);

return `_g=${_g}&_a=${_a}`;
}

return `${baseUrl}/app${hash}`;
/**
* Gets a url for navigating to Discover page.
* @param indexPatternId Index pattern ID.
* @param baseUrl Base url.
*/
export function getDiscoverUrl(indexPatternId: string, baseUrl: string): string {
return `${baseUrl}/app/discover#?${getDiscoverUrlState(indexPatternId)}`;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

discover plugin provides a URL generator

export class DiscoverUrlGenerator

Copy link
Contributor Author

@walterra walterra Apr 26, 2021

Choose a reason for hiding this comment

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

Good find, we hadn't used those generators yet in the transform plugin. I refactored to make use of it, the result is that these utils are not necessary anymore 🥳 - related update in bb30f3f.


export const RedirectToTransformManagement: FC = () => <Redirect to={`/${SECTION_SLUG.HOME}`} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function mountManagementSection(
const { http, notifications, getStartServices } = coreSetup;
const startServices = await getStartServices();
const [core, plugins] = startServices;
const { chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core;
const { application, chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core;
const { data } = plugins;
const { docTitle } = chrome;

Expand All @@ -39,6 +39,7 @@ export async function mountManagementSection(

// AppCore/AppPlugins to be passed on as React context
const appDependencies: AppDependencies = {
application,
chrome,
data,
docLinks,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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 { cloneDeep } from 'lodash';
import React from 'react';
import { IntlProvider } from 'react-intl';

import { render, waitFor, screen } from '@testing-library/react';

import { TransformListRow } from '../../../../common';
import { isDiscoverActionDisabled, DiscoverActionName } from './discover_action_name';

import transformListRow from '../../../../common/__mocks__/transform_list_row.json';

jest.mock('../../../../../shared_imports');
jest.mock('../../../../../app/app_dependencies');

// @ts-expect-error mock data is too loosely typed
const item: TransformListRow = transformListRow;

describe('Transform: Transform List Actions isDiscoverActionDisabled()', () => {
it('should be disabled when more than one item is passed in', () => {
expect(isDiscoverActionDisabled([item, item], false, true)).toBe(true);
});
it('should be disabled when forceDisable is true', () => {
expect(isDiscoverActionDisabled([item], true, true)).toBe(true);
});
it('should be disabled when the index pattern is not available', () => {
expect(isDiscoverActionDisabled([item], false, false)).toBe(true);
});
it('should be disabled when the transform started but has no index pattern', () => {
const itemCopy = cloneDeep(item);
itemCopy.stats.state = 'started';
expect(isDiscoverActionDisabled([itemCopy], false, false)).toBe(true);
});
it('should be enabled when the transform started and has an index pattern', () => {
const itemCopy = cloneDeep(item);
itemCopy.stats.state = 'started';
expect(isDiscoverActionDisabled([itemCopy], false, true)).toBe(false);
});
it('should be enabled when the index pattern is available', () => {
expect(isDiscoverActionDisabled([item], false, true)).toBe(false);
});
});

describe('Transform: Transform List Actions <StopAction />', () => {
it('renders an enabled button', async () => {
// prepare
render(
<IntlProvider locale="en">
<DiscoverActionName items={[item]} />
</IntlProvider>
);

// assert
await waitFor(() => {
expect(
screen.queryByTestId('transformDiscoverActionNameText disabled')
).not.toBeInTheDocument();
expect(screen.queryByTestId('transformDiscoverActionNameText enabled')).toBeInTheDocument();
expect(screen.queryByText('View in Discover')).toBeInTheDocument();
});
});

it('renders a disabled button', async () => {
// prepare
const itemCopy = cloneDeep(item);
itemCopy.stats.checkpointing.last.checkpoint = 0;
render(
<IntlProvider locale="en">
<DiscoverActionName items={[itemCopy]} />
</IntlProvider>
);

// assert
await waitFor(() => {
expect(screen.queryByTestId('transformDiscoverActionNameText disabled')).toBeInTheDocument();
expect(
screen.queryByTestId('transformDiscoverActionNameText enabled')
).not.toBeInTheDocument();
expect(screen.queryByText('View in Discover')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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 React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip } from '@elastic/eui';

import { TRANSFORM_STATE } from '../../../../../../common/constants';

import { getTransformProgress, TransformListRow } from '../../../../common';

export const discoverActionNameText = i18n.translate(
'xpack.transform.transformList.discoverActionNameText',
{
defaultMessage: 'View in Discover',
}
);

export const isDiscoverActionDisabled = (
items: TransformListRow[],
forceDisable: boolean,
indexPatternExists: boolean
) => {
if (items.length !== 1) {
return true;
}

const item = items[0];

// Disable discover action if it's a batch transform and was never started
const stoppedTransform = item.stats.state === TRANSFORM_STATE.STOPPED;
const transformProgress = getTransformProgress(item);
const isBatchTransform = typeof item.config.sync === 'undefined';
const transformNeverStarted =
stoppedTransform === true && transformProgress === undefined && isBatchTransform === true;

return forceDisable === true || indexPatternExists === false || transformNeverStarted === true;
};

export interface DiscoverActionNameProps {
items: TransformListRow[];
}
export const DiscoverActionName: FC<DiscoverActionNameProps> = ({ items }) => {
const isBulkAction = items.length > 1;

const item = items[0];

// Disable discover action if it's a batch transform and was never started
const stoppedTransform = item.stats.state === TRANSFORM_STATE.STOPPED;
const transformProgress = getTransformProgress(item);
const isBatchTransform = typeof item.config.sync === 'undefined';
const transformNeverStarted =
stoppedTransform && transformProgress === undefined && isBatchTransform === true;

let disabledTransformMessage;
if (isBulkAction === true) {
disabledTransformMessage = i18n.translate(
'xpack.transform.transformList.discoverTransformBulkToolTip',
{
defaultMessage: 'Links to discover are not supported as a bulk action.',
peteharverson marked this conversation as resolved.
Show resolved Hide resolved
}
);
} else {
disabledTransformMessage = i18n.translate(
'xpack.transform.transformList.discoverTransformToolTip',
{
defaultMessage: `The transform needs to be started before it's available in Discover.`,
}
);
}

if (transformNeverStarted) {
return (
<EuiToolTip position="top" content={disabledTransformMessage}>
<span data-test-subj="transformDiscoverActionNameText disabled">
{discoverActionNameText}
</span>
</EuiToolTip>
);
}

return (
<span data-test-subj="transformDiscoverActionNameText enabled">{discoverActionNameText}</span>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export { useDiscoverAction } from './use_action_discover';
export { DiscoverActionName } from './discover_action_name';
Loading