diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml index 5134d96f043c8..affdaca9cb539 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_quality_gate/mki_quality_gate_rule_management.yml @@ -22,7 +22,7 @@ steps: limit: 1 - command: .buildkite/scripts/pipelines/security_solution_quality_gate/security_solution_cypress/mki_security_solution_cypress.sh cypress:run:qa:serverless:rule_management:prebuilt_rules - label: "Cypress MKI - Rule Management - Prebuilt Rules + label: "Cypress MKI - Rule Management - Prebuilt Rules" key: test_rule_management_prebuilt_rules env: BK_TEST_SUITE_KEY: "serverless-cypress-rule-management" diff --git a/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx b/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx index 5e0079342a6df..6776a24e1874d 100644 --- a/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx +++ b/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx @@ -147,6 +147,7 @@ export const DataControlEditor = (false); + /** TODO: Make `editorConfig` work when refactoring the `ControlGroupRenderer` */ // const editorConfig = controlGroup.getEditorConfig(); @@ -193,7 +194,6 @@ export const DataControlEditor = setEditorState({ ...editorState, ...newState })} setControlEditorValid={setControlEditorValid} /> ); - }, [fieldRegistry, selectedControlType, editorState, initialState]); + }, [fieldRegistry, selectedControlType, editorState]); return ( <> diff --git a/examples/controls_example/public/react_controls/data_controls/initialize_data_control.tsx b/examples/controls_example/public/react_controls/data_controls/initialize_data_control.tsx index 2f5babd94951d..31cb979e5ba39 100644 --- a/examples/controls_example/public/react_controls/data_controls/initialize_data_control.tsx +++ b/examples/controls_example/public/react_controls/data_controls/initialize_data_control.tsx @@ -139,7 +139,7 @@ export const initializeDataControl = ( ); } else { // replace the control with a new one of the updated type - controlGroup.replacePanel(controlId, { panelType: newType, initialState }); + controlGroup.replacePanel(controlId, { panelType: newType, initialState: newState }); } }, initialState: { diff --git a/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx b/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx index 73aa593c0ce56..23ff4c4d20634 100644 --- a/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx +++ b/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx @@ -248,7 +248,7 @@ describe('RangesliderControlApi', () => { const CustomSettings = factory.CustomOptionsComponent!; const component = render( @@ -263,7 +263,7 @@ describe('RangesliderControlApi', () => { const CustomSettings = factory.CustomOptionsComponent!; const component = render( diff --git a/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.tsx b/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.tsx index ed882873f73a7..b6ba29b7d0095 100644 --- a/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.tsx +++ b/examples/controls_example/public/react_controls/data_controls/range_slider/get_range_slider_control_factory.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo } from 'react'; import deepEqual from 'react-fast-compare'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; @@ -38,9 +38,8 @@ export const getRangesliderControlFactory = ( isFieldCompatible: (field) => { return field.aggregatable && field.type === 'number'; }, - CustomOptionsComponent: ({ initialState, updateState, setControlEditorValid }) => { - const [step, setStep] = useState(initialState.step ?? 1); - + CustomOptionsComponent: ({ currentState, updateState, setControlEditorValid }) => { + const step = currentState.step ?? 1; return ( <> @@ -48,7 +47,6 @@ export const getRangesliderControlFactory = ( value={step} onChange={(event) => { const newStep = event.target.valueAsNumber; - setStep(newStep); updateState({ step: newStep }); setControlEditorValid(newStep > 0); }} diff --git a/examples/controls_example/public/react_controls/data_controls/search_control/get_search_control_factory.tsx b/examples/controls_example/public/react_controls/data_controls/search_control/get_search_control_factory.tsx index 1fde5a4ef6ca7..9997fd4e6d64e 100644 --- a/examples/controls_example/public/react_controls/data_controls/search_control/get_search_control_factory.tsx +++ b/examples/controls_example/public/react_controls/data_controls/search_control/get_search_control_factory.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import deepEqual from 'react-fast-compare'; import { BehaviorSubject, combineLatest, debounceTime, distinctUntilChanged } from 'rxjs'; @@ -65,17 +65,15 @@ export const getSearchControlFactory = ({ (field.spec.esTypes ?? []).includes('text') ); }, - CustomOptionsComponent: ({ initialState, updateState }) => { - const [searchTechnique, setSearchTechnique] = useState(initialState.searchTechnique); - + CustomOptionsComponent: ({ currentState, updateState }) => { + const searchTechnique = currentState.searchTechnique ?? DEFAULT_SEARCH_TECHNIQUE; return ( { const newSearchTechnique = id as SearchControlTechniques; - setSearchTechnique(newSearchTechnique); updateState({ searchTechnique: newSearchTechnique }); }} /> diff --git a/examples/controls_example/public/react_controls/data_controls/types.ts b/examples/controls_example/public/react_controls/data_controls/types.ts index a4cf9cab9fc53..b3379889f4223 100644 --- a/examples/controls_example/public/react_controls/data_controls/types.ts +++ b/examples/controls_example/public/react_controls/data_controls/types.ts @@ -30,7 +30,7 @@ export interface DataControlFactory< > extends ControlFactory { isFieldCompatible: (field: DataViewField) => boolean; CustomOptionsComponent?: React.FC<{ - initialState: Omit; + currentState: Partial; updateState: (newState: Partial) => void; setControlEditorValid: (valid: boolean) => void; }>; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 94983e4929883..36a62af378c31 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -744,13 +744,23 @@ class AgentPolicyService { ); } + const policyNeedsBump = baseAgentPolicy.package_policies || baseAgentPolicy.is_protected; + + // bump revision if agent policy is updated after creation + if (policyNeedsBump) { + await this.bumpRevision(soClient, esClient, newAgentPolicy.id, { + user: options?.user, + }); + } else { + await this.deployPolicy(soClient, newAgentPolicy.id); + } + // Get updated agent policy with package policies and adjusted tamper protection const updatedAgentPolicy = await this.get(soClient, newAgentPolicy.id, true); if (!updatedAgentPolicy) { throw new AgentPolicyNotFoundError('Copied agent policy not found'); } - await this.deployPolicy(soClient, newAgentPolicy.id); logger.debug(`Completed copy of agent policy ${id}`); return updatedAgentPolicy; } diff --git a/x-pack/plugins/index_management/common/types/component_templates.ts b/x-pack/plugins/index_management/common/types/component_templates.ts index be285170f668e..a84e1a3fe6eb0 100644 --- a/x-pack/plugins/index_management/common/types/component_templates.ts +++ b/x-pack/plugins/index_management/common/types/component_templates.ts @@ -49,3 +49,11 @@ export interface ComponentTemplateListItem { export interface ComponentTemplateDatastreams { data_streams: string[]; } + +export interface ComponentTemplateMeta { + managed: boolean; + managed_by: string; + package: { + name: string; + }; +} diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index 678d5c854470d..0e8a8725a56cd 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -11,6 +11,7 @@ interface IndexModule { number_of_shards: number | string; codec: string; routing_partition_size: number; + refresh_interval: string; load_fixed_bitset_filters_eagerly: boolean; shard: { check_on_startup: boolean | 'checksum'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx index 555e4030b92c1..1929be89aa339 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -78,7 +78,6 @@ describe('', () => { template: { settings: { number_of_shards: 1 }, }, - _kbnMeta: { usedBy: [], isManaged: false }, }; beforeEach(async () => { @@ -166,27 +165,30 @@ describe('', () => { }), }) ); - // Mapping rollout modal should not be opened if the component template is not managed by Fleet + // Mapping rollout modal should not be opened if the component template is not managed expect(coreStart.overlays.openModal).not.toBeCalled(); }); }); - describe('managed by fleet', () => { + describe('can rollover linked datastreams', () => { const DATASTREAM_NAME = 'logs-test-default'; + const CUSTOM_COMPONENT_TEMPLATE = 'comp-1@custom'; + const ENCODED_CUSTOM_COMPONENT_TEMPLATE = encodeURIComponent(CUSTOM_COMPONENT_TEMPLATE); + beforeEach(async () => { httpRequestsMockHelpers.setLoadComponentTemplateResponse( - COMPONENT_TEMPLATE_TO_EDIT.name, + ENCODED_CUSTOM_COMPONENT_TEMPLATE, Object.assign({}, COMPONENT_TEMPLATE_TO_EDIT, { - _meta: { managed_by: 'fleet' }, + name: CUSTOM_COMPONENT_TEMPLATE, }) ); - httpRequestsMockHelpers.setGetComponentTemplateDatastream(COMPONENT_TEMPLATE_TO_EDIT.name, { + httpRequestsMockHelpers.setGetComponentTemplateDatastream(ENCODED_CUSTOM_COMPONENT_TEMPLATE, { data_streams: [DATASTREAM_NAME], }); await act(async () => { - testBed = await setup(httpSetup); + testBed = await setup(httpSetup, '@custom'); }); testBed.component.update(); @@ -221,7 +223,7 @@ describe('', () => { component.update(); expect(httpSetup.put).toHaveBeenLastCalledWith( - `${API_BASE_PATH}/component_templates/${COMPONENT_TEMPLATE_TO_EDIT.name}`, + `${API_BASE_PATH}/component_templates/${ENCODED_CUSTOM_COMPONENT_TEMPLATE}`, expect.anything() ); expect(httpSetup.post).toHaveBeenLastCalledWith( @@ -259,7 +261,7 @@ describe('', () => { component.update(); expect(httpSetup.put).toHaveBeenLastCalledWith( - `${API_BASE_PATH}/component_templates/${COMPONENT_TEMPLATE_TO_EDIT.name}`, + `${API_BASE_PATH}/component_templates/${ENCODED_CUSTOM_COMPONENT_TEMPLATE}`, expect.anything() ); expect(httpSetup.post).toHaveBeenLastCalledWith( @@ -269,5 +271,76 @@ describe('', () => { expect(coreStart.overlays.openModal).not.toBeCalled(); }); + + it('should show mappings rollover modal on save if referenced index template is managed and packaged', async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse( + COMPONENT_TEMPLATE_TO_EDIT.name, + Object.assign({}, COMPONENT_TEMPLATE_TO_EDIT, { + _meta: {}, + }) + ); + + httpRequestsMockHelpers.setGetComponentTemplateDatastream(COMPONENT_TEMPLATE_TO_EDIT.name, { + data_streams: [DATASTREAM_NAME], + }); + + httpRequestsMockHelpers.setLoadReferencedIndexTemplateMetaResponse( + COMPONENT_TEMPLATE_TO_EDIT.name, + { + package: { + name: 'security', + }, + managed_by: 'security', + managed: true, + } + ); + + await act(async () => { + testBed = await setup(httpSetup); + }); + + testBed.component.update(); + + httpRequestsMockHelpers.setPostDatastreamMappingsFromTemplate( + DATASTREAM_NAME, + {}, + { message: 'Bad request', statusCode: 400 } + ); + const { exists, actions, component, form, coreStart } = testBed; + + await act(async () => { + form.setInputValue('versionField.input', '1'); + }); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + await actions.completeStepSettings(); + await actions.completeStepMappings(); + await actions.completeStepAliases(); + + // Make sure the list of affected mappings is shown + expect(exists('affectedMappingsList')).toBe(true); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + expect(httpSetup.put).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/component_templates/${COMPONENT_TEMPLATE_TO_EDIT.name}`, + expect.anything() + ); + expect(httpSetup.post).toHaveBeenLastCalledWith( + `${API_BASE_PATH}/data_streams/${DATASTREAM_NAME}/mappings_from_template`, + expect.anything() + ); + + expect(coreStart.overlays.openModal).toBeCalled(); + }); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts index 03aff69a548af..d8b2452ce9bee 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts @@ -189,4 +189,5 @@ export type ComponentTemplateFormTestSubjects = | 'aliasesEditor' | 'mappingsEditor' | 'settingsEditor' + | 'affectedMappingsList' | 'versionField.input'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts index 8ce2a34db397b..cc3124fb7a19f 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts @@ -76,6 +76,18 @@ const registerHttpRequestMockHelpers = ( error?: ResponseError ) => mockResponse('GET', `${API_BASE_PATH}/component_templates/${templateId}`, response, error); + const setLoadReferencedIndexTemplateMetaResponse = ( + templateId: string, + response?: HttpResponse, + error?: ResponseError + ) => + mockResponse( + 'GET', + `${API_BASE_PATH}/component_templates/${templateId}/referenced_index_template_meta`, + response, + error + ); + const setDeleteComponentTemplateResponse = ( templateId: string, response?: HttpResponse, @@ -100,6 +112,7 @@ const registerHttpRequestMockHelpers = ( return { setLoadComponentTemplatesResponse, + setLoadReferencedIndexTemplateMetaResponse, setDeleteComponentTemplateResponse, setLoadComponentTemplateResponse, setCreateComponentTemplateResponse, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx index 736f4410b9237..09c2edb834445 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx @@ -17,7 +17,6 @@ import { useComponentTemplatesContext } from '../../component_templates_context' import { ComponentTemplateForm } from '../component_template_form'; import { useStepFromQueryString } from '../use_step_from_query_string'; import { useDatastreamsRollover } from '../component_template_datastreams_rollover/use_datastreams_rollover'; -import { MANAGED_BY_FLEET } from '../../constants'; interface Props { /** @@ -33,13 +32,38 @@ export const ComponentTemplateCreate: React.FunctionComponent(false); const [saveError, setSaveError] = useState(null); const redirectTo = useRedirectPath(history); + const [currentStep, setCurrentStep] = useState('logistics'); + const [componentName, setComponentName] = useState(); + const [canRollover, setCanRollover] = useState(false); + const { api } = useComponentTemplatesContext(); const { activeStep: defaultActiveStep, updateStep } = useStepFromQueryString(history); const locationSearchParams = useMemo(() => { return new URLSearchParams(history.location.search); }, [history.location.search]); + // Effect for computing if we should allow the user to rollover attached datastreams + useEffect(() => { + async function computeCanRollover() { + // When the current step is not logistics, we have an available component template + // name that we can use to query the referenced index template. + if (currentStep !== 'logistics') { + // If the component template is referenced by an index template that is part of + // a package and is managed we can allow the user to roll it over if possible. + const { data: refIndexTemplate } = await api.getReferencedIndexTemplateMeta( + componentName as string + ); + + setCanRollover(Boolean(refIndexTemplate?.managed_by && refIndexTemplate?.package)); + } + + setCanRollover(false); + } + + computeCanRollover(); + }, [api, currentStep, componentName, setCanRollover]); + const defaultValue = useMemo(() => { if (sourceComponentTemplate) { return sourceComponentTemplate; @@ -59,7 +83,6 @@ export const ComponentTemplateCreate: React.FunctionComponent { @@ -76,7 +99,11 @@ export const ComponentTemplateCreate: React.FunctionComponent { + setCurrentStep(step); + updateStep(step); + }} defaultValue={defaultValue} onSave={onSave} isSaving={isSaving} saveError={saveError} + setComponentName={setComponentName} clearSaveError={clearSaveError} /> diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index 8944973a0d480..61402135c7fa0 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -21,7 +21,6 @@ import { } from '../../shared_imports'; import { ComponentTemplateForm } from '../component_template_form'; import { useRedirectPath } from '../../../../hooks/redirect_path'; -import { MANAGED_BY_FLEET } from '../../constants'; import { useStepFromQueryString } from '../use_step_from_query_string'; import { useDatastreamsRollover } from '../component_template_datastreams_rollover/use_datastreams_rollover'; @@ -48,6 +47,13 @@ export const ComponentTemplateEdit: React.FunctionComponent dataStreamResponse?.data_streams ?? [], [dataStreamResponse]); + // If the component template is referenced by an index template that is part of + // a package and is managed we can allow the user to roll it over if possible. + const { data: refIndexTemplate } = api.useLoadReferencedIndexTemplateMeta(decodedName); + const canRollover = useMemo( + () => Boolean(refIndexTemplate?.managed_by && refIndexTemplate?.package), + [refIndexTemplate] + ); const { showDatastreamRolloverModal } = useDatastreamsRollover(); @@ -68,9 +74,13 @@ export const ComponentTemplateEdit: React.FunctionComponent void; clearSaveError: () => void; + setComponentName?: (name: string) => void; isSaving: boolean; saveError: any; defaultValue?: ComponentTemplateDeserialized; @@ -45,6 +46,7 @@ interface Props { defaultActiveWizardSection?: WizardSection; onStepChange?: (stepId: string) => void; dataStreams?: string[]; + canRollover?: boolean; } const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { @@ -90,7 +92,9 @@ export const ComponentTemplateForm = ({ isManaged: false, }, }, + setComponentName, dataStreams, + canRollover, isEditing, isSaving, saveError, @@ -237,6 +241,16 @@ export const ComponentTemplateForm = ({ texts={i18nTexts} defaultActiveStep={defaultActiveStepIndex} onStepChange={onStepChange} + onChange={(attrs) => { + // Let the parent component know the name of the component template in the + // form has changed, so that it can re-compute the canRollover prop. + // This is needed for determinating if the user should see a rollover + // attached datastreams modal or not. + const data = attrs.getData(); + if (setComponentName) { + setComponentName(data.logistics.name); + } + }} > diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx index c2e9a37cab408..6a421c50bcd32 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx @@ -27,7 +27,6 @@ import { serializers, serializeComponentTemplate, } from '../../../shared_imports'; -import { MANAGED_BY_FLEET } from '../../../constants'; import { getLifecycleValue } from '../../../../../lib/data_streams'; const INFINITE_AS_ICON = true; @@ -52,10 +51,11 @@ const getDescriptionText = (data: any) => { interface Props { componentTemplate: ComponentTemplateDeserialized; dataStreams?: string[]; + canRollover?: boolean; } export const StepReview: React.FunctionComponent = React.memo( - ({ dataStreams, componentTemplate }) => { + ({ dataStreams, canRollover, componentTemplate }) => { const { name } = componentTemplate; const serializedComponentTemplate = serializeComponentTemplate( @@ -70,8 +70,8 @@ export const StepReview: React.FunctionComponent = React.memo( version: serializedVersion, } = serializedComponentTemplate; - const isFleetDatastreamsVisible = - Boolean(dataStreams?.length) && componentTemplate._meta?.managed_by === MANAGED_BY_FLEET; + const areDatastreamsVisible = + Boolean(dataStreams?.length) && (componentTemplate.name.endsWith('@custom') || canRollover); const SummaryTab = () => (
@@ -138,8 +138,8 @@ export const StepReview: React.FunctionComponent = React.memo( - {isFleetDatastreamsVisible && dataStreams && ( - + {areDatastreamsVisible && dataStreams && ( + {/* Datastream mappings */} = React.memo( {request} - {isFleetDatastreamsVisible && ( + {areDatastreamsVisible && ( <> diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx index b2f2fbd0d57fd..8f6c077617308 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx @@ -14,16 +14,23 @@ import { StepReview } from './step_review'; interface Props { getComponentTemplateData: (wizardContent: WizardContent) => ComponentTemplateDeserialized; dataStreams?: string[]; + canRollover?: boolean; } export const StepReviewContainer = React.memo( - ({ getComponentTemplateData, dataStreams }: Props) => { + ({ getComponentTemplateData, dataStreams, canRollover }: Props) => { const { getData } = Forms.useMultiContentContext(); const wizardContent = getData(); // Build the final template object, providing the wizard content data const componentTemplate = getComponentTemplateData(wizardContent); - return ; + return ( + + ); } ); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index ca6f8f242d288..f743ce5da777d 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -11,6 +11,7 @@ import { ComponentTemplateDeserialized, ComponentTemplateSerialized, ComponentTemplateDatastreams, + ComponentTemplateMeta, } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, @@ -109,13 +110,33 @@ export const getApi = ( }); } + function useLoadReferencedIndexTemplateMeta(name: string) { + return useRequest({ + path: `${apiBasePath}/component_templates/${encodeURIComponent( + name + )}/referenced_index_template_meta`, + method: 'get', + }); + } + + async function getReferencedIndexTemplateMeta(name: string) { + return sendRequest({ + path: `${apiBasePath}/component_templates/${encodeURIComponent( + name + )}/referenced_index_template_meta`, + method: 'get', + }); + } + return { useLoadComponentTemplates, deleteComponentTemplates, useLoadComponentTemplate, createComponentTemplate, updateComponentTemplate, + useLoadReferencedIndexTemplateMeta, useLoadComponentTemplatesDatastream, + getReferencedIndexTemplateMeta, getComponentTemplateDatastreams, postDataStreamRollover, postDataStreamMappingsFromTemplate, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index d70ed32e2cdf8..05bdd8e2f3068 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -68,6 +68,7 @@ export type { ComponentTemplateDeserialized, ComponentTemplateListItem, ComponentTemplateDatastreams, + ComponentTemplateMeta, } from '../../../../common'; export { serializeComponentTemplate } from '../../../../common/lib'; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts index 9a3e22068a7f3..535d6c9352a73 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts @@ -12,13 +12,17 @@ import { registerCreateRoute } from './register_create_route'; import { registerUpdateRoute } from './register_update_route'; import { registerDeleteRoute } from './register_delete_route'; import { registerPrivilegesRoute } from './register_privileges_route'; -import { registerGetDatastreams } from './register_datastream_route'; +import { + registerGetDatastreams, + registerReferencedIndexTemplateMeta, +} from './register_datastream_route'; export function registerComponentTemplateRoutes(dependencies: RouteDependencies) { registerGetAllRoute(dependencies); registerCreateRoute(dependencies); registerUpdateRoute(dependencies); registerGetDatastreams(dependencies); + registerReferencedIndexTemplateMeta(dependencies); registerDeleteRoute(dependencies); registerPrivilegesRoute(dependencies); } diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/register_datastream_route.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/register_datastream_route.ts index 8a7f1917a4218..8474c0d4c9660 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/register_datastream_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/register_datastream_route.ts @@ -77,3 +77,41 @@ export const registerGetDatastreams = ({ } ); }; + +export const registerReferencedIndexTemplateMeta = ({ + router, + lib: { handleEsError }, +}: RouteDependencies): void => { + router.get( + { + path: addBasePath('/component_templates/{name}/referenced_index_template_meta'), + validate: { + params: paramsSchema, + }, + }, + async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + const { name } = request.params; + + try { + const { index_templates: indexTemplates } = + await client.asCurrentUser.indices.getIndexTemplate(); + const result = indexTemplates.filter((indexTemplate) => + indexTemplate.index_template?.composed_of?.includes(name) + ); + + // We should always match against the first result which should yield + // the index template we are after. + if (result[0]) { + return response.ok({ + body: result[0].index_template._meta, + }); + } + + return response.notFound(); + } catch (error) { + return handleEsError({ error, response }); + } + } + ); +}; diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx index 29a0bb4eae1ec..892d17490e3c0 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx @@ -11,6 +11,7 @@ import { entityCentricExperience } from '@kbn/observability-plugin/common'; import { ObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; import React, { useContext } from 'react'; +import { i18n } from '@kbn/i18n'; import { useLocation } from 'react-router-dom'; import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context'; @@ -26,6 +27,8 @@ import { ApmEnvironmentFilter } from '../../shared/environment_filter'; import { getNoDataConfig } from './no_data_config'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { EntityEnablement } from '../../shared/entity_enablement'; +import { CustomNoDataTemplate } from './custom_no_data_template'; +import { ServiceInventoryView } from '../../../context/entity_manager_context/entity_manager_context'; // Paths that must skip the no data screen const bypassNoDataScreenPaths = ['/settings', '/diagnostics']; @@ -76,7 +79,8 @@ export function ApmMainTemplate({ entityCentricExperience, false ); - const { isEntityCentricExperienceViewEnabled } = useEntityManagerEnablementContext(); + const { isEntityCentricExperienceViewEnabled, serviceInventoryViewLocalStorageSetting } = + useEntityManagerEnablementContext(); const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; @@ -114,6 +118,10 @@ export function ApmMainTemplate({ const hasApmData = !!data?.hasData; const hasApmIntegrations = !!fleetApmPoliciesData?.hasApmPolicies; + const showCustomEmptyState = + !hasApmData && + isEntityCentricExperienceSettingEnabled && + serviceInventoryViewLocalStorageSetting === ServiceInventoryView.classic; const noDataConfig = getNoDataConfig({ basePath, @@ -160,9 +168,16 @@ export function ApmMainTemplate({ ); - const pageTemplate = ( + const pageTemplate = showCustomEmptyState ? ( + + ) : ( + ) : null} {showServiceGroupsNav && selectedNavButton && ( diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/custom_no_data_template.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/custom_no_data_template.tsx new file mode 100644 index 0000000000000..677bbdad6f78a --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/custom_no_data_template.tsx @@ -0,0 +1,124 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTextColor, + EuiText, + EuiButton, + EuiPageTemplate, + EuiCard, + EuiImage, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution'; +import { NoDataConfig } from '@kbn/shared-ux-page-no-data-config-types'; +import { ApmPluginStartDeps } from '../../../plugin'; +import { EntityEnablement } from '../../shared/entity_enablement'; + +export function CustomNoDataTemplate({ + isPageDataLoaded, + noDataConfig, +}: { + isPageDataLoaded: boolean; + noDataConfig?: NoDataConfig; +}) { + const { services } = useKibana(); + const { http, observabilityShared } = services; + const basePath = http?.basePath.get(); + + const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; + const imageUrl = `${basePath}/plugins/kibanaReact/assets/elastic_agent_card.svg`; + + return ( + + + + + + +

+ {i18n.translate('xpack.apm.customEmtpyState.title', { + defaultMessage: 'Detect and resolve problems with your application', + })} +

+ +

+ {i18n.translate('xpack.apm.customEmtpyState.description', { + defaultMessage: + 'Start collecting data for your applications and services so you can detect and resolve problems faster.', + })} +

+
+
+ + + + {i18n.translate('xpack.apm.customEmtpyState.title.reader', { + defaultMessage: 'Add APM data', + })} + + + } + description={i18n.translate('xpack.apm.customEmtpyState.card.description', { + defaultMessage: + 'Use APM agents to collect APM data. We make it easy with agents for many popular languages.', + })} + footer={ + + + + {noDataConfig?.action.elasticAgent.title} + + + +

+ +

+
+
+
+ } + image={ + + } + /> +
+
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx index 06fd44e2950fe..daf588fbc5b41 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx @@ -28,7 +28,7 @@ import { FeedbackModal } from './feedback_modal'; import { ServiceInventoryView } from '../../../context/entity_manager_context/entity_manager_context'; import { Unauthorized } from './unauthorized_modal'; -export function EntityEnablement() { +export function EntityEnablement({ label, tooltip }: { label: string; tooltip?: string }) { const [isFeedbackModalVisible, setsIsFeedbackModalVisible] = useState(false); const [isUnauthorizedModalVisible, setsIsUnauthorizedModalVisible] = useState(false); @@ -37,6 +37,7 @@ export function EntityEnablement() { } = useKibana(); const { + isEntityManagerEnabled, isEnablementPending, refetch, setServiceInventoryViewLocalStorageSetting, @@ -52,6 +53,11 @@ export function EntityEnablement() { }; const handleEnablement = async () => { + if (isEntityManagerEnabled) { + setServiceInventoryViewLocalStorageSetting(ServiceInventoryView.entity); + return; + } + setIsLoading(true); try { const response = await entityManager.entityClient.enableManagedEntityDiscovery(); @@ -82,11 +88,15 @@ export function EntityEnablement() { ) : ( - {isLoading ? : } + {isLoading ? ( + + ) : ( + + )} @@ -94,54 +104,54 @@ export function EntityEnablement() { ? i18n.translate('xpack.apm.eemEnablement.enabled.', { defaultMessage: 'Viewing our new experience', }) - : i18n.translate('xpack.apm.eemEnablement.tryItButton.', { - defaultMessage: 'Try our new experience!', - })} + : label} - - - } - isOpen={isPopoverOpen} - closePopover={togglePopover} - anchorPosition="downLeft" - > -
- -

- {i18n.translate('xpack.apm.entityEnablement.content', { - defaultMessage: - 'Our new experience combines both APM-instrumented services with services detected from logs in a single service inventory.', - })} -

-
-
- - - - {i18n.translate('xpack.apm.entityEnablement.footer', { - defaultMessage: 'Learn more', + {tooltip && ( + + - - -
-
+ /> + } + isOpen={isPopoverOpen} + closePopover={togglePopover} + anchorPosition="downLeft" + > +
+ +

+ {i18n.translate('xpack.apm.entityEnablement.content', { + defaultMessage: + 'Our new experience combines both APM-instrumented services with services detected from logs in a single service inventory.', + })} +

+
+
+ + + + {i18n.translate('xpack.apm.entityEnablement.footer', { + defaultMessage: 'Learn more', + })} + + + + +
+ )} {isEntityCentricExperienceViewEnabled && ( @@ -158,6 +168,7 @@ export function EntityEnablement() { setsIsUnauthorizedModalVisible(false)} + label={label} /> ); diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/unauthorized_modal.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/unauthorized_modal.tsx index 8dd3682b61ff3..acf50474a08af 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/unauthorized_modal.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/unauthorized_modal.tsx @@ -22,6 +22,7 @@ import { EuiFlexItem, EuiIcon, EuiImage, + EuiLink, EuiPanel, EuiSpacer, EuiText, @@ -32,9 +33,11 @@ import { useKibanaUrl } from '../../../hooks/use_kibana_url'; export function Unauthorized({ isUnauthorizedModalVisible = false, onClose, + label, }: { isUnauthorizedModalVisible?: boolean; onClose: () => void; + label: string; }) { const servicesInventory = useKibanaUrl('/plugins/apm/assets/services_inventory.png'); @@ -54,6 +57,18 @@ export function Unauthorized({ })} } + cancelButtonText={ + + {i18n.translate('xpack.apm.unauthorized.linkLinkLabel', { + defaultMessage: 'See how to enable EEM', + })} + + } defaultFocusedButton="confirm" > @@ -83,7 +98,8 @@ export function Unauthorized({

{i18n.translate('xpack.apm.unauthorised.body', { defaultMessage: - 'To see services detected from logs and APM-instrumented services in our new service inventory, please ask an administrator to visit this page and try our new experience. ', + 'To see services detected from logs and APM-instrumented services in our new service inventory, please ask an administrator to visit this page and {label}. ', + values: { label: label.toLowerCase() }, })}

diff --git a/x-pack/plugins/observability_solution/apm/tsconfig.json b/x-pack/plugins/observability_solution/apm/tsconfig.json index c081fce2e783b..371119d697f9f 100644 --- a/x-pack/plugins/observability_solution/apm/tsconfig.json +++ b/x-pack/plugins/observability_solution/apm/tsconfig.json @@ -123,7 +123,9 @@ "@kbn/test-jest-helpers", "@kbn/security-plugin-types-common", "@kbn/entityManager-plugin", - "@kbn/react-hooks" + "@kbn/react-hooks", + "@kbn/shared-ux-avatar-solution", + "@kbn/shared-ux-page-no-data-config-types" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/observability_solution/observability_shared/public/components/page_template/page_template.tsx b/x-pack/plugins/observability_solution/observability_shared/public/components/page_template/page_template.tsx index f0daadda636ed..f485da7047a50 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/components/page_template/page_template.tsx +++ b/x-pack/plugins/observability_solution/observability_shared/public/components/page_template/page_template.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiSideNavItemType, EuiPageSectionProps } from '@elastic/eui'; +import { EuiSideNavItemType, EuiPageSectionProps, EuiEmptyPromptProps } from '@elastic/eui'; import { _EuiPageBottomBarProps } from '@elastic/eui/src/components/page_template/bottom_bar/page_bottom_bar'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; @@ -39,7 +39,7 @@ export type WrappedPageTemplateProps = Pick< bottomBar?: React.ReactNode; bottomBarProps?: _EuiPageBottomBarProps; topSearchBar?: React.ReactNode; -}; +} & Pick; export interface NavigationEntry { // the label of the menu entry, should be translated diff --git a/x-pack/plugins/observability_solution/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts b/x-pack/plugins/observability_solution/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts index 3fc0c535639ee..4c42069188b3a 100644 --- a/x-pack/plugins/observability_solution/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts +++ b/x-pack/plugins/observability_solution/profiling/e2e/cypress/e2e/profiling_views/functions.cy.ts @@ -36,8 +36,8 @@ describe('Functions page', () => { const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]'; cy.get(firstRowSelector).eq(1).contains('1'); cy.get(firstRowSelector).eq(2).contains('vmlinux'); - cy.get(firstRowSelector).eq(3).contains('0.16%'); - cy.get(firstRowSelector).eq(4).contains('0.16%'); + cy.get(firstRowSelector).eq(3).contains('5.46%'); + cy.get(firstRowSelector).eq(4).contains('5.46%'); cy.get(firstRowSelector).eq(5).contains('3.97 lbs / 1.8 kg'); cy.get(firstRowSelector).eq(6).contains('$17.37'); cy.get(firstRowSelector).eq(7).contains('28'); @@ -56,8 +56,8 @@ describe('Functions page', () => { { parentKey: 'informationRows', key: 'executable', value: 'vmlinux' }, { parentKey: 'informationRows', key: 'function', value: 'N/A' }, { parentKey: 'informationRows', key: 'sourceFile', value: 'N/A' }, - { parentKey: 'impactEstimates', key: 'totalCPU', value: '0.16%' }, - { parentKey: 'impactEstimates', key: 'selfCPU', value: '0.16%' }, + { parentKey: 'impactEstimates', key: 'totalCPU', value: '5.46%' }, + { parentKey: 'impactEstimates', key: 'selfCPU', value: '5.46%' }, { parentKey: 'impactEstimates', key: 'samples', value: '28' }, { parentKey: 'impactEstimates', key: 'selfSamples', value: '28' }, { parentKey: 'impactEstimates', key: 'coreSeconds', value: '1.4 seconds' }, @@ -118,7 +118,7 @@ describe('Functions page', () => { columnIndex: 3, highRank: 1, lowRank: 389, - highValue: '0.16%', + highValue: '5.46%', lowValue: '0.00%', }, { @@ -126,8 +126,8 @@ describe('Functions page', () => { columnIndex: 4, highRank: 693, lowRank: 44, - highValue: '1.80%', - lowValue: '0.01%', + highValue: '60.43%', + lowValue: '0.19%', }, { columnKey: 'annualizedCo2', diff --git a/x-pack/plugins/observability_solution/profiling/public/components/topn_functions/utils.test.ts b/x-pack/plugins/observability_solution/profiling/public/components/topn_functions/utils.test.ts index c7598f55acb0b..9142427790450 100644 --- a/x-pack/plugins/observability_solution/profiling/public/components/topn_functions/utils.test.ts +++ b/x-pack/plugins/observability_solution/profiling/public/components/topn_functions/utils.test.ts @@ -46,7 +46,7 @@ describe('Top N functions: Utils', () => { expect(getTotalCount()).toEqual(0); }); it('returns value from topNFunctions', () => { - expect(getTotalCount({ TotalCount: 10 } as TopNFunctions)).toEqual(10); + expect(getTotalCount({ selfCPU: 10 } as TopNFunctions)).toEqual(10); }); }); }); diff --git a/x-pack/plugins/observability_solution/profiling/public/components/topn_functions/utils.ts b/x-pack/plugins/observability_solution/profiling/public/components/topn_functions/utils.ts index 74f63f1dd5689..6cb68bd54f153 100644 --- a/x-pack/plugins/observability_solution/profiling/public/components/topn_functions/utils.ts +++ b/x-pack/plugins/observability_solution/profiling/public/components/topn_functions/utils.ts @@ -32,7 +32,7 @@ export function scaleValue({ value, scaleFactor = 1 }: { value: number; scaleFac return value * scaleFactor; } -export const getTotalCount = (topNFunctions?: TopNFunctions) => topNFunctions?.TotalCount ?? 0; +export const getTotalCount = (topNFunctions?: TopNFunctions) => topNFunctions?.selfCPU ?? 0; export interface IFunctionRow { id: string; @@ -95,15 +95,15 @@ export function getFunctionsRows({ scaleFactor: baselineScaleFactor, }); - const totalCPUPerc = (topN.CountInclusive / topNFunctions.TotalCount) * 100; - const selfCPUPerc = (topN.CountExclusive / topNFunctions.TotalCount) * 100; + const totalCPUPerc = (topN.CountInclusive / topNFunctions.selfCPU) * 100; + const selfCPUPerc = (topN.CountExclusive / topNFunctions.selfCPU) * 100; const impactEstimates = totalSeconds > 0 ? calculateImpactEstimates({ countExclusive: topN.CountExclusive, countInclusive: topN.CountInclusive, - totalSamples: topNFunctions.TotalCount, + totalSamples: topNFunctions.selfCPU, totalSeconds, }) : undefined; @@ -124,10 +124,9 @@ export function getFunctionsRows({ selfCPU: comparisonRow.CountExclusive, totalCPU: comparisonRow.CountInclusive, selfCPUPerc: - selfCPUPerc - (comparisonRow.CountExclusive / comparisonTopNFunctions.TotalCount) * 100, + selfCPUPerc - (comparisonRow.CountExclusive / comparisonTopNFunctions.selfCPU) * 100, totalCPUPerc: - totalCPUPerc - - (comparisonRow.CountInclusive / comparisonTopNFunctions.TotalCount) * 100, + totalCPUPerc - (comparisonRow.CountInclusive / comparisonTopNFunctions.selfCPU) * 100, selfAnnualCO2kgs: comparisonRow.selfAnnualCO2kgs, selfAnnualCostUSD: comparisonRow.selfAnnualCostUSD, totalAnnualCO2kgs: comparisonRow.totalAnnualCO2kgs, diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md index fad01b9698b9b..e73b976d0b44e 100644 --- a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/upgrade_review_algorithms.md @@ -24,6 +24,8 @@ Status: `in progress`. - [**Scenario: `ABB` - Rule field is any type**](#scenario-abb---rule-field-is-any-type) - [Rule field has an update and a custom value that are NOT the same - `ABC`](#rule-field-has-an-update-and-a-custom-value-that-are-not-the-same---abc) - [**Scenario: `ABC` - Rule field is a number or single line string**](#scenario-abc---rule-field-is-a-number-or-single-line-string) + - [**Scenario: `ABC` - Rule field is a mergable multi line string**](#scenario-abc---rule-field-is-a-mergable-multi-line-string) + - [**Scenario: `ABC` - Rule field is a non-mergable multi line string**](#scenario-abc---rule-field-is-a-non-mergable-multi-line-string) - [**Scenario: `ABC` - Rule field is an array of scalar values**](#scenario-abc---rule-field-is-an-array-of-scalar-values) - [Rule field has an update and a custom value that are the same and the rule base version doesn't exist - `-AA`](#rule-field-has-an-update-and-a-custom-value-that-are-the-same-and-the-rule-base-version-doesnt-exist----aa) - [**Scenario: `-AA` - Rule field is any type**](#scenario--aa---rule-field-is-any-type) @@ -61,7 +63,7 @@ Status: `in progress`. #### **Scenario: `AAA` - Rule field is any type** -**Automation**: 3 integration tests with mock rules + a set of unit tests for each algorithm +**Automation**: 4 integration tests with mock rules + a set of unit tests for each algorithm ```Gherkin Given field is not customized by the user (current version == base version) @@ -71,10 +73,11 @@ And field should not be returned from the `upgrade/_review` API end And field should not be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | "A" | "A" | "A" | "A" | -| number | risk_score | 1 | 1 | 1 | 1 | -| array of scalars | tags | ["one", "two", "three"] | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "A" | "A" | "A" | +| multi line string | description | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | +| number | risk_score | 1 | 1 | 1 | 1 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | ``` ### Rule field doesn't have an update but has a custom value - `ABA` @@ -91,10 +94,11 @@ And field should be returned from the `upgrade/_review` API endpoin And field should be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | "A" | "B" | "A" | "B" | -| number | risk_score | 1 | 2 | 1 | 2 | -| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "three"] | ["one", "two", "four"] | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "B" | "A" | "B" | +| multi line string | description | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | +| number | risk_score | 1 | 2 | 1 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "three"] | ["one", "two", "four"] | ``` ### Rule field has an update and doesn't have a custom value - `AAB` @@ -111,10 +115,11 @@ And field should be returned from the `upgrade/_review` API endpoin And field should be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | "A" | "A" | "B" | "B" | -| number | risk_score | 1 | 1 | 2 | 2 | -| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "A" | "B" | "B" | +| multi line string | description | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | +| number | risk_score | 1 | 1 | 2 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | ``` ### Rule field has an update and a custom value that are the same - `ABB` @@ -132,10 +137,11 @@ And field should be returned from the `upgrade/_review` API endpoin And field should be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | "A" | "B" | "B" | "B" | -| number | risk_score | 1 | 2 | 2 | 2 | -| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | ["one", "two", "four"] | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | "A" | "B" | "B" | "B" | +| multi line string | description | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | +| number | risk_score | 1 | 2 | 2 | 2 | +| array of scalars | tags | ["one", "two", "three"] | ["one", "two", "four"] | ["one", "two", "four"] | ["one", "two", "four"] | ``` ### Rule field has an update and a custom value that are NOT the same - `ABC` @@ -158,6 +164,42 @@ Examples: | number | risk_score | 1 | 2 | 3 | 2 | ``` +#### **Scenario: `ABC` - Rule field is a mergable multi line string** + +**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithms + +```Gherkin +Given field is customized by the user (current version != base version) +And field is updated by Elastic in this upgrade (target version != base version) +And customized field is different than the Elastic update in this upgrade (current version != target version) +And the 3-way diff of fields are determined to be mergable +Then for field the diff algorithm should output a merged version as the merged one with a solvable conflict +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| multi line string | description | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My description.\nThis is a second line, now longer." | "My GREAT description.\nThis is a second line, now longer." | +``` + +#### **Scenario: `ABC` - Rule field is a non-mergable multi line string** + +**Automation**: 2 integration tests with mock rules + a set of unit tests for the algorithms + +```Gherkin +Given field is customized by the user (current version != base version) +And field is updated by Elastic in this upgrade (target version != base version) +And customized field is different than the Elastic update in this upgrade (current version != target version) +And the 3-way diff of fields are determined to be unmergable +Then for field the diff algorithm should output the current version as the merged one with a non-solvable conflict +And field should be returned from the `upgrade/_review` API endpoint +And field should be shown in the upgrade preview UI + +Examples: +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| multi line string | description | "My description.\nThis is a second line." | "My GREAT description.\nThis is a third line." | "My EXCELLENT description.\nThis is a fourth." | "My GREAT description.\nThis is a third line." | +``` + #### **Scenario: `ABC` - Rule field is an array of scalar values** **Automation**: 5 integration tests with mock rules + a set of unit tests for the algorithm @@ -198,10 +240,11 @@ And field should not be returned from the `upgrade/_review` API end And field should not be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | N/A | "A" | "A" | "A" | -| number | risk_score | N/A | 1 | 1 | 1 | -| array of scalars | tags | N/A | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | N/A | "A" | "A" | "A" | +| multi line string | description | N/A | "My description.\nThis is a second line." | "My description.\nThis is a second line." | "My description.\nThis is a second line." | +| number | risk_score | N/A | 1 | 1 | 1 | +| array of scalars | tags | N/A | ["one", "three", "two"] | ["three", "one", "two"] | ["one", "three", "two"] | ``` ### Rule field has an update and a custom value that are NOT the same and the rule base version doesn't exist - `-BC` @@ -219,9 +262,10 @@ And field should be returned from the `upgrade/_review` API endpoin And field should be shown in the upgrade preview UI Examples: -| algorithm | field_name | base_version | current_version | target_version | merged_version | -| single line string | name | N/A | "B" | "C" | "C" | -| number | risk_score | N/A | 2 | 3 | 3 | +| algorithm | field_name | base_version | current_version | target_version | merged_version | +| single line string | name | N/A | "B" | "C" | "C" | +| multi line string | description | N/A | "My description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | "My GREAT description.\nThis is a second line." | +| number | risk_score | N/A | 2 | 3 | 3 | ``` #### **Scenario: `-BC` - Rule field is an array of scalar values** diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index 86d1c13884321..7594778062857 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPageHeader, @@ -63,6 +64,8 @@ interface State { features: KibanaFeature[]; originalSpace?: Partial; showAlteringActiveSpaceDialog: boolean; + haveDisabledFeaturesChanged: boolean; + hasSolutionViewChanged: boolean; isLoading: boolean; saveInProgress: boolean; formError?: { @@ -74,7 +77,6 @@ interface State { export class ManageSpacePage extends Component { private readonly validator: SpaceValidator; - private initialSpaceState: State['space'] | null = null; constructor(props: Props) { super(props); @@ -88,6 +90,8 @@ export class ManageSpacePage extends Component { }, features: [], isSolutionNavEnabled: false, + haveDisabledFeaturesChanged: false, + hasSolutionViewChanged: false, }; } @@ -118,7 +122,32 @@ export class ManageSpacePage extends Component { }); } - public async componentDidUpdate(previousProps: Props) { + public async componentDidUpdate(previousProps: Props, prevState: State) { + const { originalSpace, space } = this.state; + + if (originalSpace && space) { + let haveDisabledFeaturesChanged = prevState.haveDisabledFeaturesChanged; + if (prevState.space.disabledFeatures !== space.disabledFeatures) { + haveDisabledFeaturesChanged = + space.disabledFeatures?.length !== originalSpace.disabledFeatures?.length || + difference(space.disabledFeatures, originalSpace.disabledFeatures ?? []).length > 0; + } + const hasSolutionViewChanged = + originalSpace.solution !== undefined + ? space.solution !== originalSpace.solution + : !!space.solution && space.solution !== 'classic'; + + if ( + prevState.haveDisabledFeaturesChanged !== haveDisabledFeaturesChanged || + prevState.hasSolutionViewChanged !== hasSolutionViewChanged + ) { + this.setState({ + haveDisabledFeaturesChanged, + hasSolutionViewChanged, + }); + } + } + if (this.props.spaceId !== previousProps.spaceId && this.props.spaceId) { await this.loadSpace(this.props.spaceId, Promise.resolve(this.state.features)); } @@ -190,6 +219,8 @@ export class ManageSpacePage extends Component { + {this.getChangeImpactWarning()} + {this.getFormButtons()} {showAlteringActiveSpaceDialog && ( @@ -221,6 +252,31 @@ export class ManageSpacePage extends Component { ); }; + public getChangeImpactWarning = () => { + if (!this.editingExistingSpace()) return null; + const { haveDisabledFeaturesChanged, hasSolutionViewChanged } = this.state; + if (!haveDisabledFeaturesChanged && !hasSolutionViewChanged) return null; + + return ( + <> + + + + + + ); + }; + public getFormButtons = () => { const createSpaceText = i18n.translate( 'xpack.spaces.management.manageSpacePage.createSpaceButton', @@ -244,6 +300,7 @@ export class ManageSpacePage extends Component { ); const saveText = this.editingExistingSpace() ? updateSpaceText : createSpaceText; + return ( @@ -296,6 +353,7 @@ export class ManageSpacePage extends Component { const originalSpace: Space = this.state.originalSpace as Space; const space: Space = this.state.space as Space; + const { haveDisabledFeaturesChanged, hasSolutionViewChanged } = this.state; const result = this.validator.validateForSave(space); if (result.isInvalid) { this.setState({ @@ -311,12 +369,6 @@ export class ManageSpacePage extends Component { spacesManager.getActiveSpace().then((activeSpace) => { const editingActiveSpace = activeSpace.id === originalSpace.id; - const haveDisabledFeaturesChanged = - space.disabledFeatures.length !== originalSpace.disabledFeatures.length || - difference(space.disabledFeatures, originalSpace.disabledFeatures).length > 0; - const hasSolutionViewChanged = - this.state.space.solution !== this.initialSpaceState?.solution; - if (editingActiveSpace && (haveDisabledFeaturesChanged || hasSolutionViewChanged)) { this.setState({ showAlteringActiveSpaceDialog: true, @@ -344,19 +396,17 @@ export class ManageSpacePage extends Component { onLoadSpace(space); } - this.initialSpaceState = { - ...space, - avatarType: space.imageUrl ? 'image' : 'initials', - initials: space.initials || getSpaceInitials(space), - color: space.color || getSpaceColor(space), - customIdentifier: false, - customAvatarInitials: - !!space.initials && getSpaceInitials({ name: space.name }) !== space.initials, - customAvatarColor: !!space.color && getSpaceColor({ name: space.name }) !== space.color, - }; - this.setState({ - space: { ...this.initialSpaceState }, + space: { + ...space, + avatarType: space.imageUrl ? 'image' : 'initials', + initials: space.initials || getSpaceInitials(space), + color: space.color || getSpaceColor(space), + customIdentifier: false, + customAvatarInitials: + !!space.initials && getSpaceInitials({ name: space.name }) !== space.initials, + customAvatarColor: !!space.color && getSpaceColor({ name: space.name }) !== space.color, + }, features, originalSpace: space, isLoading: false, diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index 634d1921d1612..261e210a979cd 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -526,7 +526,7 @@ export default function (providerContext: FtrProviderContext) { is_managed: false, namespace: 'default', monitoring_enabled: ['logs', 'metrics'], - revision: 1, + revision: 2, schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION, updated_by: 'elastic', package_policies: [], @@ -650,6 +650,7 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(newPolicy.is_protected).to.eql(true); + expect(newPolicy.revision).to.eql(2); }); it('should increment package policy copy names', async () => { diff --git a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts index b6b5eff6ef5f2..3f00dda32c878 100644 --- a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts @@ -57,7 +57,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToUrl('management', 'kibana/spaces/edit/default', { shouldUseHashForSubUrl: false, }); + + await testSubjects.missingOrFail('userImpactWarning'); await PageObjects.spaceSelector.changeSolutionView('classic'); + await testSubjects.existOrFail('userImpactWarning'); // Warn that the change will impact other users + await PageObjects.spaceSelector.clickSaveSpaceCreation(); await PageObjects.spaceSelector.confirmModal(); diff --git a/x-pack/test_serverless/api_integration/services/index_management/svl_templates.api.ts b/x-pack/test_serverless/api_integration/services/index_management/svl_templates.api.ts index 10a95efbcd9f8..5960bfdd87e8e 100644 --- a/x-pack/test_serverless/api_integration/services/index_management/svl_templates.api.ts +++ b/x-pack/test_serverless/api_integration/services/index_management/svl_templates.api.ts @@ -7,7 +7,7 @@ import { TemplateDeserialized, TemplateSerialized } from '@kbn/index-management-plugin/common'; import { API_BASE_PATH } from './constants'; -import { InternalRequestHeader, RoleCredentials } from '../../../shared/services'; +import { RoleCredentials } from '../../../shared/services'; import { FtrProviderContext } from '../../ftr_provider_context'; export function SvlTemplatesApi({ getService }: FtrProviderContext) { @@ -16,8 +16,11 @@ export function SvlTemplatesApi({ getService }: FtrProviderContext) { let templatesCreated: Array<{ name: string; isLegacy?: boolean }> = []; - const getAllTemplates = (internalReqHeader: InternalRequestHeader, roleAuthc: RoleCredentials) => - supertestWithoutAuth.get(`${API_BASE_PATH}/index_templates`).set(roleAuthc.apiKeyHeader); + const getAllTemplates = (roleAuthc: RoleCredentials) => + supertestWithoutAuth + .get(`${API_BASE_PATH}/index_templates`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader); const getOneTemplate = (name: string, isLegacy: boolean = false) => supertestWithoutAuth.get(`${API_BASE_PATH}/index_templates/${name}?legacy=${isLegacy}`); diff --git a/x-pack/test_serverless/api_integration/services/index_management/svl_templates.helpers.ts b/x-pack/test_serverless/api_integration/services/index_management/svl_templates.helpers.ts index 16e453e10edf5..0f6006fd91470 100644 --- a/x-pack/test_serverless/api_integration/services/index_management/svl_templates.helpers.ts +++ b/x-pack/test_serverless/api_integration/services/index_management/svl_templates.helpers.ts @@ -10,9 +10,7 @@ import { INDEX_PATTERNS } from './constants'; import { FtrProviderContext } from '../../ftr_provider_context'; const templateMock = { - settings: { - number_of_shards: 1, - }, + settings: {}, mappings: { properties: { host_name: { diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts index 90f38c9e9801b..1e0dff8bef632 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts @@ -27,8 +27,7 @@ export default function ({ getService }: FtrProviderContext) { const getRandomString: () => string = () => randomness.string({ casing: 'lower', alpha: true }); - // see details: https://github.com/elastic/kibana/issues/187368 - describe.skip('Index templates', function () { + describe('Index templates', function () { before(async () => { roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); internalReqHeader = svlCommonApi.getInternalRequestHeader(); @@ -136,6 +135,7 @@ export default function ({ getService }: FtrProviderContext) { undefined, false ); + const { status } = await svlTemplatesApi.createTemplate(payload, roleAuthc); expect(status).to.eql(200); }); @@ -214,12 +214,13 @@ export default function ({ getService }: FtrProviderContext) { const { status } = await svlTemplatesApi.createTemplate(indexTemplate, roleAuthc); expect(status).to.eql(200); - let { body: catTemplateResponse } = await svlTemplatesHelpers.catTemplate(templateName); - + const { body: templates } = await svlTemplatesApi.getAllTemplates(roleAuthc); const { name, version } = indexTemplate; expect( - catTemplateResponse.find(({ name: catTemplateName }) => catTemplateName === name)?.version - ).to.equal(version?.toString()); + templates.templates.find( + ({ name: catTemplateName }: { name: string }) => catTemplateName === name + )?.version + ).to.equal(version); // Update template with new version const updatedVersion = 2; @@ -230,11 +231,13 @@ export default function ({ getService }: FtrProviderContext) { ); expect(updateStatus).to.eql(200); - ({ body: catTemplateResponse } = await svlTemplatesHelpers.catTemplate(templateName)); + const { body: templates2 } = await svlTemplatesApi.getAllTemplates(roleAuthc); expect( - catTemplateResponse.find(({ name: catTemplateName }) => catTemplateName === name)?.version - ).to.equal(updatedVersion.toString()); + templates2.templates.find( + ({ name: catTemplateName }: { name: string }) => catTemplateName === name + )?.version + ).to.equal(updatedVersion); }); it('should parse the ES error and return the cause', async () => { @@ -267,7 +270,8 @@ export default function ({ getService }: FtrProviderContext) { templateName, roleAuthc ); - expect(updateStatus).to.eql(404); + + expect(updateStatus).to.eql(400); expect(body.attributes).an('object'); // one of the item of the cause array should point to our script @@ -292,10 +296,10 @@ export default function ({ getService }: FtrProviderContext) { if (createStatus !== 200) throw new Error(`Error creating template: ${createStatus} ${createBody.message}`); - const { body: catTemplateResponse } = await svlTemplatesHelpers.catTemplate(templateName); + const { body: allTemplates } = await svlTemplatesApi.getAllTemplates(roleAuthc); expect( - catTemplateResponse.find((template) => template.name === payload.name)?.name + allTemplates.templates.find(({ name }: { name: string }) => name === payload.name)?.name ).to.equal(templateName); const { status: deleteStatus, body: deleteBody } = await svlTemplatesApi.deleteTemplates( @@ -308,11 +312,11 @@ export default function ({ getService }: FtrProviderContext) { expect(deleteBody.errors).to.be.empty(); expect(deleteBody.templatesDeleted[0]).to.equal(templateName); - const { body: catTemplateResponse2 } = await svlTemplatesHelpers.catTemplate(templateName); + const { body: allTemplates2 } = await svlTemplatesApi.getAllTemplates(roleAuthc); - expect(catTemplateResponse2.find((template) => template.name === payload.name)).to.equal( - undefined - ); + expect( + allTemplates2.templates.find(({ name }: { name: string }) => name === payload.name) + ).to.equal(undefined); }); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/settings.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/settings.ts index 62756e70d753b..b54393da6f6f7 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index_management/settings.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/settings.ts @@ -17,8 +17,7 @@ export default function ({ getService }: FtrProviderContext) { const svlIndicesHelpers = getService('svlIndicesHelpers'); let roleAuthc: RoleCredentials; - // see details: https://github.com/elastic/kibana/issues/187369 - describe.skip('settings', function () { + describe('settings', function () { before(async () => { roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); }); @@ -34,60 +33,16 @@ export default function ({ getService }: FtrProviderContext) { const { status, body } = await svlSettingsApi.getIndexSettings(index, roleAuthc); svlCommonApi.assertResponseStatusCode(200, status, body); - // Verify we fetch the corret index settings - expect(body.settings.index.provided_name).to.be(index); - const expectedSettings = [ - 'max_inner_result_window', - 'unassigned', - 'max_terms_count', 'lifecycle', - 'routing_partition_size', - 'max_docvalue_fields_search', 'merge', - 'max_refresh_listeners', - 'max_regex_length', - 'load_fixed_bitset_filters_eagerly', - 'number_of_routing_shards', - 'write', - 'verified_before_close', 'mapping', - 'source_only', - 'soft_deletes', - 'max_script_fields', 'query', - 'format', - 'frozen', 'sort', - 'priority', 'codec', - 'max_rescore_window', - 'analyze', - 'gc_deletes', - 'max_ngram_diff', - 'translog', - 'auto_expand_replicas', - 'requests', - 'data_path', - 'highlight', - 'routing', - 'search', - 'fielddata', 'default_pipeline', - 'max_slices_per_scroll', - 'shard', - 'xpack', - 'percolator', - 'allocation', 'refresh_interval', - 'indexing', - 'compound_format', 'blocks', - 'max_result_window', - 'store', - 'queries', - 'warmer', - 'max_shingle_diff', 'query_string', ]; @@ -105,17 +60,20 @@ export default function ({ getService }: FtrProviderContext) { const index = await svlIndicesHelpers.createIndex(); const { body: body1 } = await svlSettingsApi.getIndexSettings(index, roleAuthc); - expect(body1.settings.index.number_of_replicas).to.be('1'); + + // There are no settings by default + expect(body1.settings?.index?.number_of_replicas).to.be(undefined); const settings = { index: { - number_of_replicas: 2, + refresh_interval: '7s', }, }; await svlSettingsApi.updateIndexSettings(index, settings, roleAuthc); const { body: body2 } = await svlSettingsApi.getIndexSettings(index, roleAuthc); - expect(body2.settings.index.number_of_replicas).to.be('2'); + + expect(body2.settings.index.refresh_interval).to.be('7s'); }); }); }