[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:
parent
6c46e4107c
commit
18d9d435af
|
@ -9,7 +9,8 @@
|
|||
"licensing",
|
||||
"management",
|
||||
"features",
|
||||
"savedObjects"
|
||||
"savedObjects",
|
||||
"share"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"security",
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 }) => (
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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 };
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -66,6 +66,7 @@ export interface BaseTransformTestData {
|
|||
transformDescription: string;
|
||||
expected: any;
|
||||
destinationIndex: string;
|
||||
discoverAdjustSuperDatePicker: boolean;
|
||||
}
|
||||
|
||||
export interface PivotTransformTestData extends BaseTransformTestData {
|
||||
|
|
65
x-pack/test/functional/services/transform/discover.ts
Normal file
65
x-pack/test/functional/services/transform/discover.ts
Normal 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');
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue