[Index Management] Add validation of index settings in template form (#208419)

Addresses https://github.com/elastic/kibana/issues/207350

Follow-up to https://github.com/elastic/kibana/pull/207413

## Summary

This PR adds validation to the index settings step in the template
creation flow so that the `index.number_of_shards` setting can only be
set to 1 if the Lookup index is selected.

<img width="900" alt="Screenshot 2025-01-28 at 15 33 59"
src="https://github.com/user-attachments/assets/a867fc6d-460d-4ab6-86b2-2ec54ac7203f"
/>


How to test:
1. Go to Index Management -> Index templates and start creating a
template
2. In the Logistics step, select Lookup index mode
3. In the index settings step, add the `index.number_of_shards` setting
and verify that only the values `1` and `null` are allowed.
4. Change the index mode and verify that for all other index modes,
there is no restriction on this index setting.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Elena Stoeva 2025-01-29 13:13:24 +00:00 committed by GitHub
parent c7e62fc01b
commit af553b531a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 57 additions and 11 deletions

View file

@ -8,7 +8,7 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { API_BASE_PATH } from '../../../common/constants';
import { API_BASE_PATH, LOOKUP_INDEX_MODE } from '../../../common/constants';
import { setupEnvironment } from '../helpers';
import {
@ -298,7 +298,11 @@ describe('<TemplateCreate />', () => {
beforeEach(async () => {
const { actions } = testBed;
// Logistics
await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] });
await actions.completeStepOne({
name: TEMPLATE_NAME,
indexPatterns: ['index1'],
indexMode: LOOKUP_INDEX_MODE,
});
// Component templates
await actions.completeStepTwo();
});
@ -315,7 +319,7 @@ describe('<TemplateCreate />', () => {
expect(exists('indexModeCallout')).toBe(true);
expect(find('indexModeCallout').text()).toContain(
'The index.mode setting has been set to Standard within the Logistics step.'
'The index.mode setting has been set to Lookup within the Logistics step.'
);
});
@ -326,6 +330,17 @@ describe('<TemplateCreate />', () => {
expect(form.getErrorsMessages()).toContain('Invalid JSON format.');
});
it('should not allow setting number_of_shards to a value different from 1 for Lookup index mode', async () => {
// The Lookup index mode was already selected in the first (Logistics) step
const { form, actions } = testBed;
await actions.completeStepThree('{ "index.number_of_shards": 2 }');
expect(form.getErrorsMessages()).toContain(
'Number of shards for lookup index mode can only be 1 or unset.'
);
});
});
describe('mappings (step 4)', () => {

View file

@ -33,7 +33,7 @@ export type DataStreamIndexFromEs = IndicesDataStreamIndex;
export type Health = 'green' | 'yellow' | 'red';
export type IndexMode = 'standard' | 'logsdb' | 'time_series';
export type IndexMode = 'standard' | 'logsdb' | 'time_series' | 'lookup';
export interface EnhancedDataStreamFromEs extends IndicesDataStream {
global_max_retention?: string;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
@ -22,7 +22,8 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { CodeEditor } from '@kbn/code-editor';
import { Forms } from '../../../../../shared_imports';
import { LOOKUP_INDEX_MODE } from '../../../../../../common/constants';
import { Forms, isJSON } from '../../../../../shared_imports';
import { useJsonStep } from './use_json_step';
import { documentationService } from '../../../mappings_editor/shared_imports';
import { indexModeLabels } from '../../../../lib/index_mode_labels';
@ -35,12 +36,38 @@ interface Props {
indexMode?: IndexMode;
}
// The value of the number_of_shards setting that is allowed for lookup index mode
const NUMBER_OF_SHARDS_LOOKUP_MODE = 1;
export const StepSettings: React.FunctionComponent<Props> = React.memo(
({ defaultValue = {}, onChange, esDocsBase, indexMode }) => {
const { navigateToStep } = Forms.useFormWizardContext();
const customValidate = useCallback(
(json: string) => {
if (!isJSON(json)) return null;
const settings = JSON.parse(json);
const numberOfShardsValue =
settings['index.number_of_shards'] ?? settings?.index?.number_of_shards;
if (
numberOfShardsValue != null &&
indexMode === LOOKUP_INDEX_MODE &&
(isNaN(numberOfShardsValue) || numberOfShardsValue !== NUMBER_OF_SHARDS_LOOKUP_MODE)
) {
return i18n.translate(
'xpack.idxMgmt.formWizard.stepSettings.validations.lookupIndexModeNumberOfShardsError',
{
defaultMessage: 'Number of shards for lookup index mode can only be 1 or unset.',
}
);
}
return null;
},
[indexMode]
);
const { jsonContent, setJsonContent, error } = useJsonStep({
defaultValue,
onChange,
customValidate,
});
return (

View file

@ -13,29 +13,33 @@ import { isJSON, Forms } from '../../../../../shared_imports';
interface Parameters {
onChange: (content: Forms.Content) => void;
defaultValue?: object;
customValidate?: (json: string) => string | null;
}
const stringifyJson = (json: any) =>
Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}';
export const useJsonStep = ({ defaultValue, onChange }: Parameters) => {
export const useJsonStep = ({ defaultValue, onChange, customValidate }: Parameters) => {
const [jsonContent, setJsonContent] = useState<string>(stringifyJson(defaultValue ?? {}));
const [error, setError] = useState<string | null>(null);
const validateContent = useCallback(() => {
// We allow empty string as it will be converted to "{}""
const isValid = jsonContent.trim() === '' ? true : isJSON(jsonContent);
if (!isValid) {
const isValidJson = jsonContent.trim() === '' ? true : isJSON(jsonContent);
const customValidationError = customValidate ? customValidate(jsonContent) : null;
if (!isValidJson) {
setError(
i18n.translate('xpack.idxMgmt.validators.string.invalidJSONError', {
defaultMessage: 'Invalid JSON format.',
})
);
} else if (customValidationError) {
setError(customValidationError);
} else {
setError(null);
}
return isValid;
}, [jsonContent]);
return isValidJson && !customValidationError;
}, [customValidate, jsonContent]);
useEffect(() => {
const isValid = validateContent();