[Enterprise Search] Engines - Add Indices to Engines (#149619)

## Summary

Adds a flyout to the engines indices page to add new indices to the
engine.



https://user-images.githubusercontent.com/1699281/215191898-2ed85520-775b-4dc3-b8b9-69ff497d9228.mov


### Checklist


- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Sloane Perrault 2023-01-31 09:16:34 -05:00 committed by GitHub
parent 05c8fe8a6d
commit f7bac2d971
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 338 additions and 7 deletions

View file

@ -0,0 +1,121 @@
/*
* 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, useMemo } from 'react';
import { useActions, useValues } from 'kea';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFormRow,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Status } from '../../../../../common/types/api';
import { isNotNullish } from '../../../../../common/utils/is_not_nullish';
import { getErrorsFromHttpResponse } from '../../../shared/flash_messages/handle_api_errors';
import {
IndicesSelectComboBox,
IndicesSelectComboBoxOption,
indexToOption,
} from '../engines/components/indices_select_combobox';
import { AddIndicesLogic } from './add_indices_logic';
export interface AddIndicesFlyoutProps {
onClose: () => void;
}
export const AddIndicesFlyout: React.FC<AddIndicesFlyoutProps> = ({ onClose }) => {
const { selectedIndices, updateEngineStatus, updateEngineError } = useValues(AddIndicesLogic);
const { setSelectedIndices, submitSelectedIndices } = useActions(AddIndicesLogic);
const selectedOptions = useMemo(() => selectedIndices.map(indexToOption), [selectedIndices]);
const onIndicesChange = useCallback(
(options: IndicesSelectComboBoxOption[]) => {
setSelectedIndices(options.map(({ value }) => value).filter(isNotNullish));
},
[setSelectedIndices]
);
return (
<EuiFlyout onClose={onClose}>
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2>
{i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.addIndicesFlyout.title',
{ defaultMessage: 'Add new indices' }
)}
</h2>
</EuiTitle>
{updateEngineStatus === Status.ERROR && updateEngineError && (
<>
<EuiSpacer />
<EuiCallOut
color="danger"
title={i18n.translate(
'xpack.enterpriseSearch.content.engines.indices.addIndicesFlyout.updateError.title',
{ defaultMessage: 'Error updating engine' }
)}
>
{getErrorsFromHttpResponse(updateEngineError).map((errMessage, i) => (
<p id={`createErrorMsg.${i}`}>{errMessage}</p>
))}
</EuiCallOut>
</>
)}
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.addIndicesFlyout.selectableLabel',
{ defaultMessage: 'Select searchable indices' }
)}
>
<IndicesSelectComboBox
fullWidth
onChange={onIndicesChange}
selectedOptions={selectedOptions}
/>
</EuiFormRow>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween" direction="rowReverse">
<EuiFlexItem grow={false}>
<EuiButton fill iconType="plusInCircle" onClick={submitSelectedIndices}>
{i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.addIndicesFlyout.submitButton',
{ defaultMessage: 'Add selected' }
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty flush="left" onClick={onClose}>
{i18n.translate(
'xpack.enterpriseSearch.content.engine.indices.addIndicesFlyout.cancelButton',
{ defaultMessage: 'Cancel' }
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -0,0 +1,121 @@
/*
* 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 { LogicMounter } from '../../../__mocks__/kea_logic';
import { Status } from '../../../../../common/types/api';
import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices';
import { AddIndicesLogic, AddIndicesLogicValues } from './add_indices_logic';
const DEFAULT_VALUES: AddIndicesLogicValues = {
selectedIndices: [],
updateEngineError: undefined,
updateEngineStatus: Status.IDLE,
};
const makeIndexData = (name: string): ElasticsearchIndexWithIngestion => ({
count: 0,
hidden: false,
name,
total: {
docs: { count: 0, deleted: 0 },
store: { size_in_bytes: 'n/a' },
},
});
describe('AddIndicesLogic', () => {
const { mount: mountAddIndicesLogic } = new LogicMounter(AddIndicesLogic);
const { mount: mountEngineIndicesLogic } = new LogicMounter(AddIndicesLogic);
beforeEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
mountAddIndicesLogic();
mountEngineIndicesLogic();
});
it('has expected default values', () => {
expect(AddIndicesLogic.values).toEqual(DEFAULT_VALUES);
});
describe('actions', () => {
describe('setSelectedIndices', () => {
it('adds the indices to selectedIndices', () => {
AddIndicesLogic.actions.setSelectedIndices([
makeIndexData('index-001'),
makeIndexData('index-002'),
]);
expect(AddIndicesLogic.values.selectedIndices).toEqual([
makeIndexData('index-001'),
makeIndexData('index-002'),
]);
});
it('replaces any existing indices', () => {
AddIndicesLogic.actions.setSelectedIndices([
makeIndexData('index-001'),
makeIndexData('index-002'),
]);
AddIndicesLogic.actions.setSelectedIndices([
makeIndexData('index-003'),
makeIndexData('index-004'),
]);
expect(AddIndicesLogic.values.selectedIndices).toEqual([
makeIndexData('index-003'),
makeIndexData('index-004'),
]);
});
});
});
describe('listeners', () => {
describe('engineUpdated', () => {
it('closes the add indices flyout', () => {
jest.spyOn(AddIndicesLogic.actions, 'closeAddIndicesFlyout');
AddIndicesLogic.actions.engineUpdated({
created: '1999-12-31T23:59:59Z',
indices: [],
name: 'engine-name',
updated: '1999-12-31T23:59:59Z',
});
expect(AddIndicesLogic.actions.closeAddIndicesFlyout).toHaveBeenCalledTimes(1);
});
});
describe('submitSelectedIndices', () => {
it('does not make a request if there are no selectedIndices', () => {
jest.spyOn(AddIndicesLogic.actions, 'addIndicesToEngine');
AddIndicesLogic.actions.submitSelectedIndices();
expect(AddIndicesLogic.actions.addIndicesToEngine).toHaveBeenCalledTimes(0);
});
it('calls addIndicesToEngine when there are selectedIndices', () => {
jest.spyOn(AddIndicesLogic.actions, 'addIndicesToEngine');
AddIndicesLogic.actions.setSelectedIndices([
makeIndexData('index-001'),
makeIndexData('index-002'),
]);
AddIndicesLogic.actions.submitSelectedIndices();
expect(AddIndicesLogic.actions.addIndicesToEngine).toHaveBeenCalledTimes(1);
expect(AddIndicesLogic.actions.addIndicesToEngine).toHaveBeenCalledWith([
'index-001',
'index-002',
]);
});
});
});
});

View file

@ -0,0 +1,62 @@
/*
* 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 { kea, MakeLogicType } from 'kea';
import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices';
import { UpdateEngineApiLogic } from '../../api/engines/update_engine_api_logic';
import { EngineIndicesLogic, EngineIndicesLogicActions } from './engine_indices_logic';
export interface AddIndicesLogicActions {
addIndicesToEngine: EngineIndicesLogicActions['addIndicesToEngine'];
closeAddIndicesFlyout: EngineIndicesLogicActions['closeAddIndicesFlyout'];
engineUpdated: EngineIndicesLogicActions['engineUpdated'];
setSelectedIndices: (indices: ElasticsearchIndexWithIngestion[]) => {
indices: ElasticsearchIndexWithIngestion[];
};
submitSelectedIndices: () => void;
}
export interface AddIndicesLogicValues {
selectedIndices: ElasticsearchIndexWithIngestion[];
updateEngineError: typeof UpdateEngineApiLogic.values.error | undefined;
updateEngineStatus: typeof UpdateEngineApiLogic.values.status;
}
export const AddIndicesLogic = kea<MakeLogicType<AddIndicesLogicValues, AddIndicesLogicActions>>({
actions: {
setSelectedIndices: (indices: ElasticsearchIndexWithIngestion[]) => ({ indices }),
submitSelectedIndices: () => true,
},
connect: {
actions: [EngineIndicesLogic, ['addIndicesToEngine', 'engineUpdated', 'closeAddIndicesFlyout']],
values: [UpdateEngineApiLogic, ['status as updateEngineStatus', 'error as updateEngineError']],
},
listeners: ({ actions, values }) => ({
engineUpdated: () => {
actions.closeAddIndicesFlyout();
},
submitSelectedIndices: () => {
const { selectedIndices } = values;
if (selectedIndices.length === 0) return;
actions.addIndicesToEngine(selectedIndices.map(({ name }) => name));
},
}),
path: ['enterprise_search', 'content', 'add_indices_logic'],
reducers: {
selectedIndices: [
[],
{
closeAddIndicesFlyout: () => [],
setSelectedIndices: (_, { indices }) => indices,
},
],
},
});

View file

@ -31,15 +31,17 @@ import { IngestionMethod } from '../../types';
import { ingestionMethodToText } from '../../utils/indices';
import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template';
import { AddIndicesFlyout } from './add_indices_flyout';
import { EngineIndicesLogic } from './engine_indices_logic';
import { EngineViewLogic } from './engine_view_logic';
export const EngineIndices: React.FC = () => {
const { engineName, isLoadingEngine } = useValues(EngineViewLogic);
const { engineData } = useValues(EngineIndicesLogic);
const { removeIndexFromEngine } = useActions(EngineIndicesLogic);
const { engineData, engineName, isLoadingEngine, addIndicesFlyoutOpen } =
useValues(EngineIndicesLogic);
const { removeIndexFromEngine, openAddIndicesFlyout, closeAddIndicesFlyout } =
useActions(EngineIndicesLogic);
const { navigateToUrl } = useValues(KibanaLogic);
const [removeIndexConfirm, setConfirmRemoveIndex] = useState<string | null>(null);
if (!engineData) return null;
const { indices } = engineData;
@ -170,7 +172,12 @@ export const EngineIndices: React.FC = () => {
defaultMessage: 'Indices',
}),
rightSideItems: [
<EuiButton data-test-subj="engine-add-new-indices-btn" iconType="plusInCircle" fill>
<EuiButton
data-test-subj="engine-add-new-indices-btn"
iconType="plusInCircle"
fill
onClick={openAddIndicesFlyout}
>
{i18n.translate('xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', {
defaultMessage: 'Add new indices',
})}
@ -231,6 +238,7 @@ export const EngineIndices: React.FC = () => {
</EuiText>
</EuiConfirmModal>
)}
{addIndicesFlyoutOpen && <AddIndicesFlyout onClose={closeAddIndicesFlyout} />}
</>
</EnterpriseSearchEnginesPageTemplate>
);

View file

@ -13,8 +13,10 @@ import { FetchEngineApiLogic } from '../../api/engines/fetch_engine_api_logic';
import { EngineIndicesLogic, EngineIndicesLogicValues } from './engine_indices_logic';
const DEFAULT_VALUES: EngineIndicesLogicValues = {
addIndicesFlyoutOpen: false,
engineData: undefined,
engineName: 'my-test-engine',
isLoadingEngine: true,
};
const mockEngineData: EnterpriseSearchEngineDetails = {

View file

@ -16,15 +16,19 @@ import { EngineViewActions, EngineViewLogic, EngineViewValues } from './engine_v
export interface EngineIndicesLogicActions {
addIndicesToEngine: (indices: string[]) => { indices: string[] };
closeAddIndicesFlyout: () => void;
engineUpdated: UpdateEngineApiLogicActions['apiSuccess'];
fetchEngine: EngineViewActions['fetchEngine'];
openAddIndicesFlyout: () => void;
removeIndexFromEngine: (indexName: string) => { indexName: string };
updateEngineRequest: UpdateEngineApiLogicActions['makeRequest'];
}
export interface EngineIndicesLogicValues {
addIndicesFlyoutOpen: boolean;
engineData: EngineViewValues['engineData'];
engineName: EngineViewValues['engineName'];
isLoadingEngine: EngineViewValues['isLoadingEngine'];
}
export const EngineIndicesLogic = kea<
@ -32,6 +36,8 @@ export const EngineIndicesLogic = kea<
>({
actions: {
addIndicesToEngine: (indices) => ({ indices }),
closeAddIndicesFlyout: () => true,
openAddIndicesFlyout: () => true,
removeIndexFromEngine: (indexName) => ({ indexName }),
},
connect: {
@ -41,7 +47,7 @@ export const EngineIndicesLogic = kea<
UpdateEngineApiLogic,
['makeRequest as updateEngineRequest', 'apiSuccess as engineUpdated'],
],
values: [EngineViewLogic, ['engineData', 'engineName']],
values: [EngineViewLogic, ['engineData', 'engineName', 'isLoadingEngine']],
},
listeners: ({ actions, values }) => ({
addIndicesToEngine: ({ indices }) => {
@ -68,4 +74,13 @@ export const EngineIndicesLogic = kea<
},
}),
path: ['enterprise_search', 'content', 'engine_indices_logic'],
reducers: {
addIndicesFlyoutOpen: [
false,
{
closeAddIndicesFlyout: () => false,
openAddIndicesFlyout: () => true,
},
],
},
});

View file

@ -28,6 +28,8 @@ import { ElasticsearchIndexWithIngestion } from '../../../../../../common/types/
import { indexHealthToHealthColor } from '../../../../shared/constants/health_colors';
import { FetchIndicesForEnginesAPILogic } from '../../../api/engines/fetch_indices_api_logic';
export type IndicesSelectComboBoxOption = EuiComboBoxOptionOption<ElasticsearchIndexWithIngestion>;
export type IndicesSelectComboBoxProps = Omit<
EuiComboBoxProps<ElasticsearchIndexWithIngestion>,
'onCreateOption' | 'onSearchChange' | 'noSuggestions' | 'async'
@ -83,7 +85,7 @@ export const IndicesSelectComboBox = (props: IndicesSelectComboBoxProps) => {
export const indexToOption = (
index: ElasticsearchIndexWithIngestion
): EuiComboBoxOptionOption<ElasticsearchIndexWithIngestion> => ({
): IndicesSelectComboBoxOption => ({
label: index.name,
value: index,
});