mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
05c8fe8a6d
commit
f7bac2d971
7 changed files with 338 additions and 7 deletions
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue