[Enterprise Search] promote copy ingest pipeline (#143396)

Promited the copy and customize action from within the modal to the
ingest pipeline card. This is to make it more discoverable for users and
give better UX.
This commit is contained in:
Rodney Norris 2022-10-18 10:24:55 -05:00 committed by GitHub
parent 61f0889d66
commit 608a05d2d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 257 additions and 84 deletions

View file

@ -0,0 +1,68 @@
/*
* 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 { setMockValues } from '../../../../../__mocks__/kea_logic';
import { crawlerIndex } from '../../../../__mocks__/view_index.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiText, EuiButtonEmpty } from '@elastic/eui';
import { CustomizeIngestPipelineItem } from './customize_pipeline_item';
const DEFAULT_VALUES = {
// LicensingLogic
hasPlatinumLicense: true,
// IndexViewLogic
indexName: crawlerIndex.name,
ingestionMethod: 'crawler',
// KibanaLogic
isCloud: false,
};
describe('CustomizeIngestPipelineItem', () => {
beforeEach(() => {
jest.clearAllMocks();
setMockValues({ ...DEFAULT_VALUES });
});
it('renders cta with license', () => {
const wrapper = shallow(<CustomizeIngestPipelineItem />);
expect(wrapper.find(EuiButtonEmpty)).toHaveLength(1);
expect(wrapper.find(EuiText)).toHaveLength(1);
expect(wrapper.find(EuiText).children().text()).toContain('create an index-specific version');
expect(wrapper.find(EuiText).children().text()).not.toContain('With a platinum license');
});
it('renders cta on cloud', () => {
setMockValues({
...DEFAULT_VALUES,
hasPlatinumLicense: false,
isCloud: true,
});
const wrapper = shallow(<CustomizeIngestPipelineItem />);
expect(wrapper.find(EuiText)).toHaveLength(1);
expect(wrapper.find(EuiText).children().text()).toContain('create an index-specific version');
expect(wrapper.find(EuiText).children().text()).not.toContain('With a platinum license');
});
it('gates cta without license', () => {
setMockValues({
...DEFAULT_VALUES,
hasPlatinumLicense: false,
isCloud: false,
});
const wrapper = shallow(<CustomizeIngestPipelineItem />);
expect(wrapper.find(EuiButtonEmpty)).toHaveLength(1);
expect(wrapper.find(EuiText)).toHaveLength(1);
const ctaButton = wrapper.find(EuiButtonEmpty);
expect(ctaButton.prop('disabled')).toBe(true);
expect(ctaButton.prop('iconType')).toBe('lock');
expect(wrapper.find(EuiText).children().text()).toContain('With a platinum license');
});
});

View file

@ -0,0 +1,67 @@
/*
* 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 { useActions, useValues } from 'kea';
import { EuiButtonEmpty, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { KibanaLogic } from '../../../../../shared/kibana';
import { LicensingLogic } from '../../../../../shared/licensing';
import { CreateCustomPipelineApiLogic } from '../../../../api/index/create_custom_pipeline_api_logic';
import { IndexViewLogic } from '../../index_view_logic';
export const CustomizeIngestPipelineItem: React.FC = () => {
const { indexName, ingestionMethod } = useValues(IndexViewLogic);
const { isCloud } = useValues(KibanaLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { makeRequest: createCustomPipeline } = useActions(CreateCustomPipelineApiLogic);
const isGated = !isCloud && !hasPlatinumLicense;
return (
<>
{isGated ? (
<EuiText color="subdued" size="s" grow={false}>
{i18n.translate(
'xpack.enterpriseSearch.content.index.pipelines.copyAndCustomize.platinumText',
{
defaultMessage:
'With a platinum license, you can create an index-specific version of this configuration and modify it for your use case.',
}
)}
</EuiText>
) : (
<EuiText color="subdued" size="s" grow={false}>
{i18n.translate(
'xpack.enterpriseSearch.content.index.pipelines.copyAndCustomize.description',
{
defaultMessage:
'You can create an index-specific version of this configuration and modify it for your use case.',
}
)}
</EuiText>
)}
<EuiFlexGroup justifyContent="flexEnd">
<EuiButtonEmpty
data-telemetry-id={`entSearchContent-${ingestionMethod}-pipelines-ingestPipelines-copyAndCustomize`}
disabled={isGated}
iconType={isGated ? 'lock' : undefined}
onClick={() => createCustomPipeline({ indexName })}
>
{i18n.translate(
'xpack.enterpriseSearch.content.index.pipelines.ingestModal.copyButtonLabel',
{ defaultMessage: 'Copy and customize' }
)}
</EuiButtonEmpty>
</EuiFlexGroup>
<EuiSpacer size="m" />
</>
);
};

View file

@ -27,8 +27,6 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { DEFAULT_PIPELINE_NAME } from '../../../../../../../common/constants';
import { IngestPipelineParams } from '../../../../../../../common/types/connectors';
import { CurlRequest } from '../../components/curl_request/curl_request';
@ -37,11 +35,9 @@ import { PipelineSettingsForm } from '../pipeline_settings_form';
interface IngestPipelineModalProps {
closeModal: () => void;
createCustomPipelines: () => void;
displayOnly: boolean;
indexName: string;
ingestionMethod: string;
isGated: boolean;
isLoading: boolean;
pipeline: IngestPipelineParams;
savePipeline: () => void;
@ -50,11 +46,9 @@ interface IngestPipelineModalProps {
export const IngestPipelineModal: React.FC<IngestPipelineModalProps> = ({
closeModal,
createCustomPipelines,
displayOnly,
indexName,
ingestionMethod,
isGated,
isLoading,
pipeline,
savePipeline,
@ -62,9 +56,6 @@ export const IngestPipelineModal: React.FC<IngestPipelineModalProps> = ({
}) => {
const { name } = pipeline;
// can't customize if you already have a custom pipeline!
const canCustomize = name === DEFAULT_PIPELINE_NAME;
return (
<EuiModal onClose={closeModal} maxWidth={'40rem'}>
<EuiModalHeader>
@ -192,38 +183,6 @@ export const IngestPipelineModal: React.FC<IngestPipelineModalProps> = ({
</EuiFlexItem>
</>
)}
{canCustomize && (
<>
<EuiSpacer />
<EuiFlexItem>
<EuiText color="subdued" size="s" grow={false}>
{i18n.translate(
'xpack.enterpriseSearch.content.index.pipelines.ingestModal.platinumText',
{
defaultMessage:
'With a platinum license, you can create an index-specific version of this configuration and modify it for your use case.',
}
)}
</EuiText>
</EuiFlexItem>
<EuiSpacer />
<EuiFlexItem grow={false}>
<EuiFlexGroup justifyContent="flexStart">
<EuiButtonEmpty
data-telemetry-id={`entSearchContent-${ingestionMethod}-pipelines-ingestPipelines-copyAndCustomize`}
disabled={isGated}
iconType={isGated ? 'lock' : undefined}
onClick={createCustomPipelines}
>
{i18n.translate(
'xpack.enterpriseSearch.content.index.pipelines.ingestModal.copyButtonLabel',
{ defaultMessage: 'Copy and customize' }
)}
</EuiButtonEmpty>
</EuiFlexGroup>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
</EuiModalBody>
<EuiModalFooter>

View file

@ -0,0 +1,79 @@
/*
* 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 { setMockValues } from '../../../../../__mocks__/kea_logic';
import { crawlerIndex } from '../../../../__mocks__/view_index.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
import { DEFAULT_PIPELINE_NAME } from '../../../../../../../common/constants';
import { CustomPipelineItem } from './custom_pipeline_item';
import { CustomizeIngestPipelineItem } from './customize_pipeline_item';
import { DefaultPipelineItem } from './default_pipeline_item';
import { IngestPipelinesCard } from './ingest_pipelines_card';
const DEFAULT_VALUES = {
// IndexViewLogic
indexName: crawlerIndex.name,
ingestionMethod: 'crawler',
// FetchCustomPipelineApiLogic
data: undefined,
// PipelinesLogic
canSetPipeline: true,
hasIndexIngestionPipeline: false,
index: crawlerIndex,
pipelineName: DEFAULT_PIPELINE_NAME,
pipelineState: {
extract_binary_content: true,
name: DEFAULT_PIPELINE_NAME,
reduce_whitespace: true,
run_ml_inference: false,
},
showModal: false,
};
describe('IngestPipelinesCard', () => {
beforeEach(() => {
jest.clearAllMocks();
setMockValues({ ...DEFAULT_VALUES });
});
it('renders with default ingest pipeline', () => {
const wrapper = shallow(<IngestPipelinesCard />);
expect(wrapper.find(DefaultPipelineItem)).toHaveLength(1);
expect(wrapper.find(CustomizeIngestPipelineItem)).toHaveLength(1);
expect(wrapper.find(CustomPipelineItem)).toHaveLength(0);
});
it('does not render customize cta with index ingest pipeline', () => {
const pipelineName = crawlerIndex.name;
const pipelines: Record<string, IngestPipeline | undefined> = {
[pipelineName]: {},
[`${pipelineName}@custom`]: {
processors: [],
},
};
setMockValues({
...DEFAULT_VALUES,
data: pipelines,
hasIndexIngestionPipeline: true,
pipelineName,
pipelineState: {
...DEFAULT_VALUES.pipelineState,
name: pipelineName,
},
});
const wrapper = shallow(<IngestPipelinesCard />);
expect(wrapper.find(CustomizeIngestPipelineItem)).toHaveLength(0);
expect(wrapper.find(DefaultPipelineItem)).toHaveLength(1);
expect(wrapper.find(CustomPipelineItem)).toHaveLength(1);
});
});

View file

@ -11,32 +11,31 @@ import { useActions, useValues } from 'kea';
import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { KibanaLogic } from '../../../../../shared/kibana';
import { LicensingLogic } from '../../../../../shared/licensing';
import { CreateCustomPipelineApiLogic } from '../../../../api/index/create_custom_pipeline_api_logic';
import { FetchCustomPipelineApiLogic } from '../../../../api/index/fetch_custom_pipeline_api_logic';
import { IndexViewLogic } from '../../index_view_logic';
import { PipelinesLogic } from '../pipelines_logic';
import { CustomPipelineItem } from './custom_pipeline_item';
import { CustomizeIngestPipelineItem } from './customize_pipeline_item';
import { DefaultPipelineItem } from './default_pipeline_item';
import { IngestPipelineModal } from './ingest_pipeline_modal';
export const IngestPipelinesCard: React.FC = () => {
const { indexName, ingestionMethod } = useValues(IndexViewLogic);
const { canSetPipeline, index, pipelineName, pipelineState, showModal } =
useValues(PipelinesLogic);
const {
canSetPipeline,
hasIndexIngestionPipeline,
index,
pipelineName,
pipelineState,
showModal,
} = useValues(PipelinesLogic);
const { closeModal, openModal, setPipelineState, savePipeline } = useActions(PipelinesLogic);
const { makeRequest: fetchCustomPipeline } = useActions(FetchCustomPipelineApiLogic);
const { makeRequest: createCustomPipeline } = useActions(CreateCustomPipelineApiLogic);
const { data: customPipelines } = useValues(FetchCustomPipelineApiLogic);
const { isCloud } = useValues(KibanaLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
const isGated = !isCloud && !hasPlatinumLicense;
const customPipeline = customPipelines ? customPipelines[`${indexName}@custom`] : undefined;
useEffect(() => {
@ -44,45 +43,46 @@ export const IngestPipelinesCard: React.FC = () => {
}, [indexName]);
return (
<EuiFlexGroup direction="column" gutterSize="s">
{showModal && (
<IngestPipelineModal
closeModal={closeModal}
createCustomPipelines={() => createCustomPipeline({ indexName })}
displayOnly={!canSetPipeline}
indexName={indexName}
ingestionMethod={ingestionMethod}
isGated={isGated}
isLoading={false}
pipeline={{ ...pipelineState, name: pipelineName }}
savePipeline={savePipeline}
setPipeline={setPipelineState}
/>
)}
<EuiFlexItem>
<EuiPanel color="subdued">
<DefaultPipelineItem
index={index}
openModal={openModal}
pipelineName={pipelineName}
ingestionMethod={ingestionMethod}
<>
{!hasIndexIngestionPipeline && <CustomizeIngestPipelineItem />}
<EuiFlexGroup direction="column" gutterSize="s">
{showModal && (
<IngestPipelineModal
closeModal={closeModal}
displayOnly={!canSetPipeline}
indexName={indexName}
pipelineState={pipelineState}
ingestionMethod={ingestionMethod}
isLoading={false}
pipeline={{ ...pipelineState, name: pipelineName }}
savePipeline={savePipeline}
setPipeline={setPipelineState}
/>
</EuiPanel>
</EuiFlexItem>
{customPipeline && (
)}
<EuiFlexItem>
<EuiPanel color="primary">
<CustomPipelineItem
indexName={indexName}
<EuiPanel color="subdued">
<DefaultPipelineItem
index={index}
openModal={openModal}
pipelineName={pipelineName}
ingestionMethod={ingestionMethod}
pipelineSuffix="custom"
processorsCount={customPipeline.processors?.length ?? 0}
indexName={indexName}
pipelineState={pipelineState}
/>
</EuiPanel>
</EuiFlexItem>
)}
</EuiFlexGroup>
{customPipeline && (
<EuiFlexItem>
<EuiPanel color="primary">
<CustomPipelineItem
indexName={indexName}
ingestionMethod={ingestionMethod}
pipelineSuffix="custom"
processorsCount={customPipeline.processors?.length ?? 0}
/>
</EuiPanel>
</EuiFlexItem>
)}
</EuiFlexGroup>
</>
);
};