[Enterprise Search] Show success toast on index creation (#137284)

This commit is contained in:
Sander Philipse 2022-07-27 17:09:56 +02:00 committed by GitHub
parent 0fe8d3f468
commit 8bf7c32c8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 177 additions and 38 deletions

View file

@ -92,3 +92,7 @@ export const ENTERPRISE_SEARCH_KIBANA_COOKIE = '_enterprise_search';
export const ENTERPRISE_SEARCH_RELEVANCE_LOGS_SOURCE_ID = 'ent-search-logs';
export const ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID = 'ent-search-audit-logs';
export const APP_SEARCH_URL = '/app/enterprise_search/app_search';
export const ENTERPRISE_SEARCH_ELASTICSEARCH_URL = '/app/enterprise_search/elasticsearch';
export const WORKPLACE_SEARCH_URL = '/app/enterprise_search/workplace_search';

View file

@ -12,6 +12,7 @@ import { useValues } from 'kea';
import { EuiButton, EuiEmptyPrompt, EuiImage, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { DOCS_URL } from '../../routes';
import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation';
import illustration from '../document_creation/illustration.svg';
@ -58,14 +59,18 @@ export const EmptyEngineOverview: React.FC = () => {
})}
</p>
<EuiSpacer size="m" />
<EuiButton fill href="/app/management/data/index_management/indices">
<EuiButtonTo
fill
to={'/app/management/data/index_management/indices'}
shouldNotCreateHref
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.elasticsearchEngine.emptyStateButton',
{
defaultMessage: 'Manage indices',
}
)}
</EuiButton>
</EuiButtonTo>
</>
}
/>

View file

@ -25,7 +25,7 @@ const DEFAULT_VALUES: AddConnectorValues = {
describe('AddConnectorPackageLogic', () => {
const { mount } = new LogicMounter(AddConnectorPackageLogic);
const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers;
const { flashAPIErrors } = mockFlashMessageHelpers;
it('has expected default values', () => {
mount();
@ -56,7 +56,6 @@ describe('AddConnectorPackageLogic', () => {
jest.useFakeTimers();
AddConnectorPackageApiLogic.actions.apiSuccess({ indexName: 'success' } as any);
await nextTick();
expect(flashSuccessToast).toHaveBeenCalled();
jest.advanceTimersByTime(1001);
await nextTick();
expect(KibanaLogic.values.navigateToUrl).toHaveBeenCalledWith(

View file

@ -7,14 +7,12 @@
import { kea, MakeLogicType } from 'kea';
import { i18n } from '@kbn/i18n';
import { ErrorCode } from '../../../../../../common/types/error_codes';
import { generateEncodedPath } from '../../../../app_search/utils/encode_path_params';
import { Actions } from '../../../../shared/api_logic/create_api_logic';
import { flashAPIErrors, flashSuccessToast } from '../../../../shared/flash_messages';
import { flashAPIErrors } from '../../../../shared/flash_messages';
import { KibanaLogic } from '../../../../shared/kibana';
import {
AddConnectorPackageApiLogic,
@ -46,23 +44,7 @@ export const AddConnectorPackageLogic = kea<MakeLogicType<AddConnectorValues, Ad
listeners: {
apiError: (error) => flashAPIErrors(error),
apiSuccess: async ({ indexName }, breakpoint) => {
flashSuccessToast(
i18n.translate(
'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.successToast.label',
{ defaultMessage: 'Index created successfully' }
),
{
text: i18n.translate(
'xpack.enterpriseSearch.content.newIndex.steps.buildConnector.successToast.description',
{
defaultMessage:
'You can use App Search engines to build a search experience for your new Elasticsearch index.',
}
),
}
);
// Flash the success toast so people can read it
// But also give Elasticsearch the chance to propagate the index so we don't end up in an error state after navigating
// Give Elasticsearch the chance to propagate the index so we don't end up in an error state after navigating
await breakpoint(1000);
KibanaLogic.values.navigateToUrl(
generateEncodedPath(SEARCH_INDEX_TAB_PATH, {

View file

@ -21,9 +21,15 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
APP_SEARCH_URL,
ENTERPRISE_SEARCH_ELASTICSEARCH_URL,
} from '../../../../../../common/constants';
import { HttpError, Status } from '../../../../../../common/types/api';
import { ErrorCode } from '../../../../../../common/types/error_codes';
import { docLinks } from '../../../../shared/doc_links';
import { EuiLinkTo } from '../../../../shared/react_router_helpers';
import { AddConnectorPackageApiLogic } from '../../../api/connector_package/add_connector_package_api_logic';
import { NewSearchIndexLogic } from '../new_search_index_logic';
@ -198,13 +204,36 @@ export const MethodConnector: React.FC = () => {
children: (
<EuiText size="s">
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.newIndex.connector.steps.buildSearchExperience.content',
{
defaultMessage:
'After building your connector, your content is ready. Build your first search experience with Elasticsearch, or explore the search experience tools provided by App Search. We recommend that you create a search engine for the best balance of flexible power and turnkey simplicity.',
}
)}
<FormattedMessage
id="xpack.enterpriseSearch.content.newIndex.connector.steps.buildSearchExperience.content"
defaultMessage="After building your connector, your content is ready. Build your first search experience with {elasticsearchLink}, or explore the search experience tools provided by {appSearchLink}. We recommend that you create a {searchEngineLink} for the best balance of flexible power and turnkey simplicity."
values={{
appSearchLink: (
<EuiLinkTo to={APP_SEARCH_URL} shouldNotCreateHref>
{i18n.translate(
'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.buildConnector.appSearchLink',
{ defaultMessage: 'App Search' }
)}
</EuiLinkTo>
),
elasticsearchLink: (
<EuiLinkTo to={ENTERPRISE_SEARCH_ELASTICSEARCH_URL} shouldNotCreateHref>
{i18n.translate(
'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.buildConnector.elasticsearchLink',
{ defaultMessage: 'Elasticsearch' }
)}
</EuiLinkTo>
),
searchEngineLink: (
<EuiLinkTo to={`${APP_SEARCH_URL}/engines/new`} shouldNotCreateHref>
{i18n.translate(
'xpack.enterpriseSearch.content.newIndex.methodConnector.steps.buildConnector.searchEngineLink',
{ defaultMessage: 'search engine' }
)}
</EuiLinkTo>
),
}}
/>
</p>
</EuiText>
),

View file

@ -0,0 +1,50 @@
/*
* 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 from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { APP_SEARCH_URL } from '../../../../../common/constants';
import { flashSuccessToast } from '../../../shared/flash_messages';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
const SuccessToast = (
<>
<EuiText size="s">
{i18n.translate('xpack.enterpriseSearch.content.new_index.successToast.description', {
defaultMessage:
'You can use App Search engines to build a search experience for your new Elasticsearch index.',
})}
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="flexEnd" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiButtonTo to={`${APP_SEARCH_URL}/engines`} shouldNotCreateHref color="success">
{i18n.translate('xpack.enterpriseSearch.content.new_index.successToast.button.label', {
defaultMessage: 'Create an engine',
})}
</EuiButtonTo>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
export function flashIndexCreatedToast(): void {
flashSuccessToast(
i18n.translate('xpack.enterpriseSearch.content.new_index.successToast.title', {
defaultMessage: 'Index created successfully',
}),
{
iconType: 'cheer',
text: SuccessToast,
}
);
}

View file

@ -12,8 +12,11 @@ import { nextTick } from '@kbn/test-jest-helpers';
import { IndexExistsApiLogic } from '../../api/index/index_exists_api_logic';
import { UNIVERSAL_LANGUAGE_VALUE } from './constants';
import { flashIndexCreatedToast } from './new_index_created_toast';
import { NewSearchIndexLogic, NewSearchIndexValues } from './new_search_index_logic';
jest.mock('./new_index_created_toast', () => ({ flashIndexCreatedToast: jest.fn() }));
const DEFAULT_VALUES: NewSearchIndexValues = {
data: undefined as any,
fullIndexName: 'search-',
@ -108,5 +111,26 @@ describe('NewSearchIndexLogic', () => {
});
});
});
describe('apiIndexCreated', () => {
it('calls flash index created toast', () => {
NewSearchIndexLogic.actions.apiIndexCreated({ indexName: 'indexName' });
expect(flashIndexCreatedToast).toHaveBeenCalled();
});
});
describe('connectorIndexCreated', () => {
it('calls flash index created toast', () => {
NewSearchIndexLogic.actions.connectorIndexCreated({
id: 'connectorId',
indexName: 'indexName',
});
expect(flashIndexCreatedToast).toHaveBeenCalled();
});
});
describe('crawlerIndexCreated', () => {
it('calls flash index created toast', () => {
NewSearchIndexLogic.actions.crawlerIndexCreated({ created: 'indexName' });
expect(flashIndexCreatedToast).toHaveBeenCalled();
});
});
});
});

View file

@ -8,6 +8,21 @@
import { kea, MakeLogicType } from 'kea';
import { Actions } from '../../../shared/api_logic/create_api_logic';
import {
AddConnectorPackageApiLogic,
AddConnectorPackageApiLogicArgs,
AddConnectorPackageApiLogicResponse,
} from '../../api/connector_package/add_connector_package_api_logic';
import {
CreateCrawlerIndexApiLogic,
CreateCrawlerIndexArgs,
CreateCrawlerIndexResponse,
} from '../../api/crawler/create_crawler_index_api_logic';
import {
CreateApiIndexApiLogic,
CreateApiIndexApiLogicArgs,
CreateApiIndexApiLogicResponse,
} from '../../api/index/create_api_index_api_logic';
import {
IndexExistsApiLogic,
@ -18,6 +33,7 @@ import {
import { isValidIndexName } from '../../utils/validate_index_name';
import { UNIVERSAL_LANGUAGE_VALUE } from './constants';
import { flashIndexCreatedToast } from './new_index_created_toast';
import { LanguageForOptimization } from './types';
import { getLanguageForOptimization } from './utils';
@ -31,12 +47,22 @@ export interface NewSearchIndexValues {
rawName: string;
}
export type NewSearchIndexActions = Pick<
type NewSearchIndexActions = Pick<
Actions<IndexExistsApiParams, IndexExistsApiResponse>,
'makeRequest'
> & {
apiIndexCreated: Actions<
CreateApiIndexApiLogicArgs,
CreateApiIndexApiLogicResponse
>['apiSuccess'];
connectorIndexCreated: Actions<
AddConnectorPackageApiLogicArgs,
AddConnectorPackageApiLogicResponse
>['apiSuccess'];
crawlerIndexCreated: Actions<CreateCrawlerIndexArgs, CreateCrawlerIndexResponse>['apiSuccess'];
setLanguageSelectValue(language: string): { language: string };
setRawName(rawName: string): { rawName: string };
showIndexCreatedCallout: () => void;
};
export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSearchIndexActions>>({
@ -45,10 +71,28 @@ export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSe
setRawName: (rawName) => ({ rawName }),
},
connect: {
actions: [IndexExistsApiLogic, ['makeRequest']],
actions: [
AddConnectorPackageApiLogic,
['apiSuccess as connectorIndexCreated'],
CreateApiIndexApiLogic,
['apiSuccess as apiIndexCreated'],
CreateCrawlerIndexApiLogic,
['apiSuccess as crawlerIndexCreated'],
IndexExistsApiLogic,
['makeRequest'],
],
values: [IndexExistsApiLogic, ['data']],
},
listeners: ({ actions, values }) => ({
apiIndexCreated: () => {
flashIndexCreatedToast();
},
connectorIndexCreated: () => {
flashIndexCreatedToast();
},
crawlerIndexCreated: () => {
flashIndexCreatedToast();
},
setRawName: async (_, breakpoint) => {
await breakpoint(150);
actions.makeRequest({ indexName: values.fullIndexName });

View file

@ -19,11 +19,11 @@ export const FlashMessages: React.FC = ({ children }) => {
return (
<div aria-live="polite" data-test-subj="FlashMessages">
{messages.map(({ type, message, description }, index) => (
{messages.map(({ type, message, description, iconType }, index) => (
<Fragment key={index}>
<EuiCallOut
color={FLASH_MESSAGE_TYPES[type].color}
iconType={FLASH_MESSAGE_TYPES[type].iconType}
iconType={iconType ?? FLASH_MESSAGE_TYPES[type].iconType}
title={message}
>
{description}

View file

@ -11,14 +11,16 @@ export type FlashMessageTypes = 'success' | 'info' | 'warning' | 'error';
export type FlashMessageColors = 'success' | 'primary' | 'warning' | 'danger';
export interface IFlashMessage {
type: FlashMessageTypes;
message: ReactNode;
description?: ReactNode;
iconType?: string;
message: ReactNode;
type: FlashMessageTypes;
}
// @see EuiGlobalToastListToast for more props
export interface ToastOptions {
iconType?: string;
id?: string;
text?: ReactChild; // Additional text below the message/title, same as IFlashMessage['description']
toastLifeTimeMs?: number; // Allows customing per-toast timeout
id?: string;
}