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

Adds a link to discover from the transform list to the actions menu. Conditions for the link to be enabled:
- Kibana index pattern must be available
- Transform must have been started once and done some progress so there's the destination index available
This commit is contained in:
Walter Rafelsberger 2021-04-27 16:49:20 +02:00 committed by GitHub
parent 6c46e4107c
commit 18d9d435af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 508 additions and 75 deletions

View file

@ -9,7 +9,8 @@
"licensing",
"management",
"features",
"savedObjects"
"savedObjects",
"share"
],
"optionalPlugins": [
"security",

View file

@ -7,17 +7,28 @@
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 { SharePluginStart } from '../../../../../../src/plugins/share/public';
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 = {
// Replace mock to support syntax using `.then()` as used in transform code.
coreStart.savedObjects.client.find = jest.fn().mockResolvedValue({ savedObjects: [] });
const appDependencies: AppDependencies = {
application: coreStart.application,
chrome: coreStart.chrome,
data: dataStart,
docLinks: coreStart.docLinks,
@ -28,11 +39,15 @@ const appDependencies = {
storage: ({ get: jest.fn() } as unknown) as Storage,
overlays: coreStart.overlays,
http: coreSetup.http,
history: {} as ScopedHistory,
savedObjectsPlugin: savedObjectsPluginMock.createStartContract(),
share: ({ urlGenerators: { getUrlGenerator: jest.fn() } } as unknown) as SharePluginStart,
ml: {} as GetMlSharedImportsReturnType,
};
export const useAppDependencies = () => {
const ml = useContext(MlSharedContext);
return { ...appDependencies, ml, savedObjects: jest.fn() };
return { ...appDependencies, ml };
};
export const useToastNotifications = () => {

View file

@ -5,17 +5,19 @@
* 2.0.
*/
import { CoreSetup, CoreStart } from 'src/core/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { SavedObjectsStart } from 'src/plugins/saved_objects/public';
import { ScopedHistory } from 'kibana/public';
import type { CoreSetup, CoreStart } from 'src/core/public';
import type { DataPublicPluginStart } from 'src/plugins/data/public';
import type { SavedObjectsStart } from 'src/plugins/saved_objects/public';
import type { ScopedHistory } from 'kibana/public';
import type { SharePluginStart } from 'src/plugins/share/public';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import type { 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'];
@ -28,6 +30,7 @@ export interface AppDependencies {
overlays: CoreStart['overlays'];
history: ScopedHistory;
savedObjectsPlugin: SavedObjectsStart;
share: SharePluginStart;
ml: GetMlSharedImportsReturnType;
}

View file

@ -28,7 +28,6 @@ export {
} from './transform';
export { TRANSFORM_LIST_COLUMN, TransformListAction, TransformListRow } from './transform_list';
export { getTransformProgress, isCompletedBatchTransform } from './transform_stats';
export { getDiscoverUrl } from './navigation';
export {
getEsAggFromAggConfig,
isPivotAggsConfigWithUiSupport,

View file

@ -1,16 +0,0 @@
/*
* 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 { getDiscoverUrl } from './navigation';
describe('navigation', () => {
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)'
);
});
});

View file

@ -7,28 +7,9 @@
import React, { FC } from 'react';
import { Redirect } from 'react-router-dom';
import rison from 'rison-node';
import { SECTION_SLUG } from '../constants';
/**
* 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 {
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}`;
return `${baseUrl}/app${hash}`;
}
export const RedirectToTransformManagement: FC = () => <Redirect to={`/${SECTION_SLUG.HOME}`} />;
export const RedirectToCreateTransform: FC<{ savedObjectId: string }> = ({ savedObjectId }) => (

View file

@ -28,8 +28,8 @@ 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 { data } = plugins;
const { application, chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core;
const { data, share } = plugins;
const { docTitle } = chrome;
// Initialize services
@ -39,6 +39,7 @@ export async function mountManagementSection(
// AppCore/AppPlugins to be passed on as React context
const appDependencies: AppDependencies = {
application,
chrome,
data,
docLinks,
@ -51,6 +52,7 @@ export async function mountManagementSection(
uiSettings,
history,
savedObjectsPlugin: plugins.savedObjects,
share,
ml: await getMlSharedImports(),
};

View file

@ -26,6 +26,11 @@ import {
import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
import {
DISCOVER_APP_URL_GENERATOR,
DiscoverUrlGeneratorState,
} from '../../../../../../../../../src/plugins/discover/public';
import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms';
import {
isGetTransformsStatsResponseSchema,
@ -36,7 +41,7 @@ import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants
import { getErrorMessage } from '../../../../../../common/utils/errors';
import { getTransformProgress, getDiscoverUrl } from '../../../../common';
import { getTransformProgress } from '../../../../common';
import { useApi } from '../../../../hooks/use_api';
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
import { RedirectToTransformManagement } from '../../../../common/navigation';
@ -86,13 +91,45 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
const [progressPercentComplete, setProgressPercentComplete] = useState<undefined | number>(
undefined
);
const [discoverLink, setDiscoverLink] = useState<string>();
const deps = useAppDependencies();
const indexPatterns = deps.data.indexPatterns;
const toastNotifications = useToastNotifications();
const { getUrlGenerator } = deps.share.urlGenerators;
const isDiscoverAvailable = deps.application.capabilities.discover?.show ?? false;
useEffect(() => {
let unmounted = false;
onChange({ created, started, indexPatternId });
const getDiscoverUrl = async (): Promise<void> => {
const state: DiscoverUrlGeneratorState = {
indexPatternId,
};
let discoverUrlGenerator;
try {
discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR);
} catch (error) {
// ignore error thrown when url generator is not available
return;
}
const discoverUrl = await discoverUrlGenerator.createUrl(state);
if (!unmounted) {
setDiscoverLink(discoverUrl);
}
};
if (started === true && indexPatternId !== undefined && isDiscoverAvailable) {
getDiscoverUrl();
}
return () => {
unmounted = true;
};
// custom comparison
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [created, started, indexPatternId]);
@ -477,7 +514,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
</EuiPanel>
</EuiFlexItem>
)}
{started === true && indexPatternId !== undefined && (
{isDiscoverAvailable && discoverLink !== undefined && (
<EuiFlexItem style={PANEL_ITEM_STYLE}>
<EuiCard
icon={<EuiIcon size="xxl" type="discoverApp" />}
@ -490,7 +527,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
defaultMessage: 'Use Discover to explore the transform.',
}
)}
href={getDiscoverUrl(indexPatternId, deps.http.basePath.get())}
href={discoverLink}
data-test-subj="transformWizardCardDiscover"
/>
</EuiFlexItem>

View file

@ -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]} indexPatternExists={true} />
</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]} indexPatternExists={false} />
</IntlProvider>
);
// assert
await waitFor(() => {
expect(screen.queryByTestId('transformDiscoverActionNameText disabled')).toBeInTheDocument();
expect(
screen.queryByTestId('transformDiscoverActionNameText enabled')
).not.toBeInTheDocument();
expect(screen.queryByText('View in Discover')).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,97 @@
/*
* 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 {
indexPatternExists: boolean;
items: TransformListRow[];
}
export const DiscoverActionName: FC<DiscoverActionNameProps> = ({ indexPatternExists, 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.',
}
);
} else if (!indexPatternExists) {
disabledTransformMessage = i18n.translate(
'xpack.transform.transformList.discoverTransformNoIndexPatternToolTip',
{
defaultMessage: `A Kibana index pattern is required for the destination index to be viewable in Discover`,
}
);
} else if (transformNeverStarted) {
disabledTransformMessage = i18n.translate(
'xpack.transform.transformList.discoverTransformToolTip',
{
defaultMessage: `The transform needs to be started before it's available in Discover.`,
}
);
}
if (typeof disabledTransformMessage !== 'undefined') {
return (
<EuiToolTip position="top" content={disabledTransformMessage}>
<span data-test-subj="transformDiscoverActionNameText disabled">
{discoverActionNameText}
</span>
</EuiToolTip>
);
}
return (
<span data-test-subj="transformDiscoverActionNameText enabled">{discoverActionNameText}</span>
);
};

View file

@ -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';

View file

@ -0,0 +1,99 @@
/*
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
import {
DiscoverUrlGeneratorState,
DISCOVER_APP_URL_GENERATOR,
} from '../../../../../../../../../src/plugins/discover/public';
import { TransformListAction, TransformListRow } from '../../../../common';
import { useSearchItems } from '../../../../hooks/use_search_items';
import { useAppDependencies } from '../../../../app_dependencies';
import {
isDiscoverActionDisabled,
discoverActionNameText,
DiscoverActionName,
} from './discover_action_name';
const getIndexPatternTitleFromTargetIndex = (item: TransformListRow) =>
Array.isArray(item.config.dest.index) ? item.config.dest.index.join(',') : item.config.dest.index;
export type DiscoverAction = ReturnType<typeof useDiscoverAction>;
export const useDiscoverAction = (forceDisable: boolean) => {
const appDeps = useAppDependencies();
const savedObjectsClient = appDeps.savedObjects.client;
const indexPatterns = appDeps.data.indexPatterns;
const { getUrlGenerator } = appDeps.share.urlGenerators;
const isDiscoverAvailable = !!appDeps.application.capabilities.discover?.show;
const { getIndexPatternIdByTitle, loadIndexPatterns } = useSearchItems(undefined);
const [indexPatternsLoaded, setIndexPatternsLoaded] = useState(false);
useEffect(() => {
async function checkIndexPatternAvailability() {
await loadIndexPatterns(savedObjectsClient, indexPatterns);
setIndexPatternsLoaded(true);
}
checkIndexPatternAvailability();
}, [indexPatterns, loadIndexPatterns, savedObjectsClient]);
const clickHandler = useCallback(
async (item: TransformListRow) => {
let discoverUrlGenerator;
try {
discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR);
} catch (error) {
// ignore error thrown when url generator is not available
return;
}
const indexPatternTitle = getIndexPatternTitleFromTargetIndex(item);
const indexPatternId = getIndexPatternIdByTitle(indexPatternTitle);
const state: DiscoverUrlGeneratorState = {
indexPatternId,
};
const path = await discoverUrlGenerator.createUrl(state);
appDeps.application.navigateToApp('discover', { path });
},
[appDeps.application, getIndexPatternIdByTitle, getUrlGenerator]
);
const indexPatternExists = useCallback(
(item: TransformListRow) => {
const indexPatternTitle = getIndexPatternTitleFromTargetIndex(item);
const indexPatternId = getIndexPatternIdByTitle(indexPatternTitle);
return indexPatternId !== undefined;
},
[getIndexPatternIdByTitle]
);
const action: TransformListAction = useMemo(
() => ({
name: (item: TransformListRow) => {
return <DiscoverActionName items={[item]} indexPatternExists={indexPatternExists(item)} />;
},
available: () => isDiscoverAvailable,
enabled: (item: TransformListRow) =>
indexPatternsLoaded &&
!isDiscoverActionDisabled([item], forceDisable, indexPatternExists(item)),
description: discoverActionNameText,
icon: 'visTable',
type: 'icon',
onClick: clickHandler,
'data-test-subj': 'transformActionDiscover',
}),
[forceDisable, indexPatternExists, indexPatternsLoaded, isDiscoverAvailable, clickHandler]
);
return { action };
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { render, fireEvent } from '@testing-library/react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import moment from 'moment-timezone';
import { TransformListRow } from '../../../../common';
@ -41,20 +41,26 @@ describe('Transform: Transform List <ExpandedRow />', () => {
</MlSharedContext.Provider>
);
expect(getByText('Details')).toBeInTheDocument();
expect(getByText('Stats')).toBeInTheDocument();
expect(getByText('JSON')).toBeInTheDocument();
expect(getByText('Messages')).toBeInTheDocument();
expect(getByText('Preview')).toBeInTheDocument();
await waitFor(() => {
expect(getByText('Details')).toBeInTheDocument();
expect(getByText('Stats')).toBeInTheDocument();
expect(getByText('JSON')).toBeInTheDocument();
expect(getByText('Messages')).toBeInTheDocument();
expect(getByText('Preview')).toBeInTheDocument();
const tabContent = getByTestId('transformDetailsTabContent');
expect(tabContent).toBeInTheDocument();
const tabContent = getByTestId('transformDetailsTabContent');
expect(tabContent).toBeInTheDocument();
expect(getByTestId('transformDetailsTab')).toHaveAttribute('aria-selected', 'true');
expect(within(tabContent).getByText('General')).toBeInTheDocument();
expect(getByTestId('transformDetailsTab')).toHaveAttribute('aria-selected', 'true');
expect(within(tabContent).getByText('General')).toBeInTheDocument();
});
fireEvent.click(getByTestId('transformStatsTab'));
expect(getByTestId('transformStatsTab')).toHaveAttribute('aria-selected', 'true');
expect(within(tabContent).getByText('Stats')).toBeInTheDocument();
await waitFor(() => {
expect(getByTestId('transformStatsTab')).toHaveAttribute('aria-selected', 'true');
const tabContent = getByTestId('transformDetailsTabContent');
expect(within(tabContent).getByText('Stats')).toBeInTheDocument();
});
});
});

View file

@ -7,20 +7,26 @@
import { renderHook } from '@testing-library/react-hooks';
import { useActions } from './use_actions';
jest.mock('../../../../../shared_imports');
jest.mock('../../../../../app/app_dependencies');
import { useActions } from './use_actions';
describe('Transform: Transform List Actions', () => {
test('useActions()', () => {
const { result } = renderHook(() => useActions({ forceDisable: false, transformNodes: 1 }));
test('useActions()', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useActions({ forceDisable: false, transformNodes: 1 })
);
await waitForNextUpdate();
const actions = result.current.actions;
// Using `any` for the callback. Somehow the EUI types don't pass
// on the `data-test-subj` attribute correctly. We're interested
// in the runtime result here anyway.
expect(actions.map((a: any) => a['data-test-subj'])).toStrictEqual([
'transformActionDiscover',
'transformActionStart',
'transformActionStop',
'transformActionEdit',

View file

@ -13,6 +13,7 @@ import { TransformListRow } from '../../../../common';
import { useCloneAction } from '../action_clone';
import { useDeleteAction, DeleteActionModal } from '../action_delete';
import { useDiscoverAction } from '../action_discover';
import { EditTransformFlyout } from '../edit_transform_flyout';
import { useEditAction } from '../action_edit';
import { useStartAction, StartActionModal } from '../action_start';
@ -30,6 +31,7 @@ export const useActions = ({
} => {
const cloneAction = useCloneAction(forceDisable, transformNodes);
const deleteAction = useDeleteAction(forceDisable);
const discoverAction = useDiscoverAction(forceDisable);
const editAction = useEditAction(forceDisable, transformNodes);
const startAction = useStartAction(forceDisable, transformNodes);
const stopAction = useStopAction(forceDisable);
@ -45,6 +47,7 @@ export const useActions = ({
</>
),
actions: [
discoverAction.action,
startAction.action,
stopAction.action,
editAction.action,

View file

@ -13,8 +13,11 @@ jest.mock('../../../../../shared_imports');
jest.mock('../../../../../app/app_dependencies');
describe('Transform: Job List Columns', () => {
test('useColumns()', () => {
const { result } = renderHook(() => useColumns([], () => {}, 1, []));
test('useColumns()', async () => {
const { result, waitForNextUpdate } = renderHook(() => useColumns([], () => {}, 1, []));
await waitForNextUpdate();
const columns: ReturnType<typeof useColumns>['columns'] = result.current.columns;
expect(columns).toHaveLength(7);

View file

@ -7,11 +7,12 @@
import { i18n as kbnI18n } from '@kbn/i18n';
import { CoreSetup } from 'src/core/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { HomePublicPluginSetup } from 'src/plugins/home/public';
import { SavedObjectsStart } from 'src/plugins/saved_objects/public';
import { ManagementSetup } from '../../../../src/plugins/management/public';
import type { CoreSetup } from 'src/core/public';
import type { DataPublicPluginStart } from 'src/plugins/data/public';
import type { HomePublicPluginSetup } from 'src/plugins/home/public';
import type { SavedObjectsStart } from 'src/plugins/saved_objects/public';
import type { ManagementSetup } from 'src/plugins/management/public';
import type { SharePluginStart } from 'src/plugins/share/public';
import { registerFeature } from './register_feature';
export interface PluginsDependencies {
@ -19,6 +20,7 @@ export interface PluginsDependencies {
management: ManagementSetup;
home: HomePublicPluginSetup;
savedObjects: SavedObjectsStart;
share: SharePluginStart;
}
export class TransformUiPlugin {

View file

@ -89,6 +89,7 @@ export default function ({ getService }: FtrProviderContext) {
get destinationIndex(): string {
return `user-${this.transformId}`;
},
discoverAdjustSuperDatePicker: true,
expected: {
pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "category.keyword": {'],
pivotAdvancedEditorValue: {
@ -210,6 +211,7 @@ export default function ({ getService }: FtrProviderContext) {
],
},
],
discoverQueryHits: '7,270',
},
} as PivotTransformTestData,
{
@ -247,6 +249,7 @@ export default function ({ getService }: FtrProviderContext) {
get destinationIndex(): string {
return `user-${this.transformId}`;
},
discoverAdjustSuperDatePicker: false,
expected: {
pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "geoip.country_iso_code": {'],
pivotAdvancedEditorValue: {
@ -294,6 +297,7 @@ export default function ({ getService }: FtrProviderContext) {
rows: 5,
},
histogramCharts: [],
discoverQueryHits: '10',
},
} as PivotTransformTestData,
{
@ -317,6 +321,7 @@ export default function ({ getService }: FtrProviderContext) {
get destinationIndex(): string {
return `user-${this.transformId}`;
},
discoverAdjustSuperDatePicker: true,
expected: {
latestPreview: {
column: 0,
@ -342,6 +347,7 @@ export default function ({ getService }: FtrProviderContext) {
'July 12th 2019, 23:31:12',
],
},
discoverQueryHits: '10',
},
} as LatestTransformTestData,
];
@ -533,6 +539,26 @@ export default function ({ getService }: FtrProviderContext) {
progress: testData.expected.row.progress,
});
});
it('navigates to discover and displays results of the destination index', async () => {
await transform.testExecution.logTestStep('should show the actions popover');
await transform.table.assertTransformRowActions(testData.transformId, false);
await transform.testExecution.logTestStep('should navigate to discover');
await transform.table.clickTransformRowAction('Discover');
if (testData.discoverAdjustSuperDatePicker) {
await transform.discover.assertNoResults(testData.destinationIndex);
await transform.testExecution.logTestStep(
'should switch quick select lookback to years'
);
await transform.discover.assertSuperDatePickerToggleQuickMenuButtonExists();
await transform.discover.openSuperDatePicker();
await transform.discover.quickSelectYears();
}
await transform.discover.assertDiscoverQueryHits(testData.expected.discoverQueryHits);
});
});
}
});

View file

@ -66,6 +66,7 @@ export interface BaseTransformTestData {
transformDescription: string;
expected: any;
destinationIndex: string;
discoverAdjustSuperDatePicker: boolean;
}
export interface PivotTransformTestData extends BaseTransformTestData {

View file

@ -0,0 +1,65 @@
/*
* 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 '../../ftr_provider_context';
export function TransformDiscoverProvider({ getService }: FtrProviderContext) {
const find = getService('find');
const testSubjects = getService('testSubjects');
return {
async assertDiscoverQueryHits(expectedDiscoverQueryHits: string) {
await testSubjects.existOrFail('discoverQueryHits');
const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits');
expect(actualDiscoverQueryHits).to.eql(
expectedDiscoverQueryHits,
`Discover query hits should be ${expectedDiscoverQueryHits}, got ${actualDiscoverQueryHits}`
);
},
async assertNoResults(expectedDestinationIndex: string) {
// Discover should use the destination index pattern
const actualIndexPatternSwitchLinkText = await (
await testSubjects.find('indexPattern-switch-link')
).getVisibleText();
expect(actualIndexPatternSwitchLinkText).to.eql(
expectedDestinationIndex,
`Destination index should be ${expectedDestinationIndex}, got ${actualIndexPatternSwitchLinkText}`
);
await testSubjects.existOrFail('discoverNoResults');
},
async assertSuperDatePickerToggleQuickMenuButtonExists() {
await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton');
},
async openSuperDatePicker() {
await testSubjects.click('superDatePickerToggleQuickMenuButton');
await testSubjects.existOrFail('superDatePickerQuickMenu');
},
async quickSelectYears() {
const quickMenuElement = await testSubjects.find('superDatePickerQuickMenu');
// No test subject, select "Years" to look back 15 years instead of 15 minutes.
await find.selectValue(`[aria-label*="Time unit"]`, 'y');
// Apply
const applyButton = await quickMenuElement.findByClassName('euiQuickSelect__applyButton');
const actualApplyButtonText = await applyButton.getVisibleText();
expect(actualApplyButtonText).to.be('Apply');
await applyButton.click();
await testSubjects.existOrFail('discoverQueryHits');
},
};
}

View file

@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
import { TransformAPIProvider } from './api';
import { TransformEditFlyoutProvider } from './edit_flyout';
import { TransformDiscoverProvider } from './discover';
import { TransformManagementProvider } from './management';
import { TransformNavigationProvider } from './navigation';
import { TransformSecurityCommonProvider } from './security_common';
@ -22,6 +23,7 @@ import { MachineLearningTestResourcesProvider } from '../ml/test_resources';
export function TransformProvider(context: FtrProviderContext) {
const api = TransformAPIProvider(context);
const discover = TransformDiscoverProvider(context);
const editFlyout = TransformEditFlyoutProvider(context);
const management = TransformManagementProvider(context);
const navigation = TransformNavigationProvider(context);
@ -35,6 +37,7 @@ export function TransformProvider(context: FtrProviderContext) {
return {
api,
discover,
editFlyout,
management,
navigation,

View file

@ -9,6 +9,8 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
type TransformRowActionName = 'Clone' | 'Delete' | 'Edit' | 'Start' | 'Stop' | 'Discover';
export function TransformTableProvider({ getService }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
@ -238,6 +240,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
await testSubjects.existOrFail('transformActionClone');
await testSubjects.existOrFail('transformActionDelete');
await testSubjects.existOrFail('transformActionDiscover');
await testSubjects.existOrFail('transformActionEdit');
if (isTransformRunning) {
@ -251,7 +254,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
public async assertTransformRowActionEnabled(
transformId: string,
action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit',
action: TransformRowActionName,
expectedValue: boolean
) {
const selector = `transformAction${action}`;
@ -274,7 +277,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
public async clickTransformRowActionWithRetry(
transformId: string,
action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit'
action: TransformRowActionName
) {
await retry.tryForTime(30 * 1000, async () => {
await browser.pressKeys(browser.keys.ESCAPE);
@ -285,7 +288,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
});
}
public async clickTransformRowAction(action: string) {
public async clickTransformRowAction(action: TransformRowActionName) {
await testSubjects.click(`transformAction${action}`);
}