[ILM] Use global field to set the snapshot repository (#94602)

This commit is contained in:
Sébastien Loix 2021-03-16 15:22:24 +00:00 committed by GitHub
parent c9d1dbf599
commit 310194193a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 373 additions and 168 deletions

View file

@ -67,6 +67,46 @@ describe('<EditPolicy /> searchable snapshots', () => {
expect(actions.hot.searchableSnapshotsExists()).toBeTruthy();
});
test('should set the repository from previously defined repository', async () => {
const { actions } = testBed;
const repository = 'myRepo';
await actions.hot.setSearchableSnapshot(repository);
await actions.cold.enable(true);
await actions.cold.toggleSearchableSnapshot(true);
await actions.frozen.enable(true);
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];
expect(latestRequest.method).toBe('POST');
expect(latestRequest.url).toBe('/api/index_lifecycle_management/policies');
const reqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body);
expect(reqBody.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe(repository);
expect(reqBody.phases.cold.actions.searchable_snapshot.snapshot_repository).toBe(repository);
expect(reqBody.phases.frozen.actions.searchable_snapshot.snapshot_repository).toBe(repository);
});
test('should update the repository in all searchable snapshot actions', async () => {
const { actions } = testBed;
await actions.hot.setSearchableSnapshot('myRepo');
await actions.cold.enable(true);
await actions.cold.toggleSearchableSnapshot(true);
await actions.frozen.enable(true);
// We update the repository in one phase
await actions.frozen.setSearchableSnapshot('changed');
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];
const reqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body);
// And all phases should be updated
expect(reqBody.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe('changed');
expect(reqBody.phases.cold.actions.searchable_snapshot.snapshot_repository).toBe('changed');
expect(reqBody.phases.frozen.actions.searchable_snapshot.snapshot_repository).toBe('changed');
});
describe('on cloud', () => {
describe('new policy', () => {
beforeEach(async () => {
@ -86,6 +126,7 @@ describe('<EditPolicy /> searchable snapshots', () => {
const { component } = testBed;
component.update();
});
test('defaults searchable snapshot to true on cloud', async () => {
const { find, actions } = testBed;
await actions.cold.enable(true);
@ -112,14 +153,17 @@ describe('<EditPolicy /> searchable snapshots', () => {
const { component } = testBed;
component.update();
});
test('correctly sets snapshot repository default to "found-snapshots"', async () => {
const { actions } = testBed;
await actions.cold.enable(true);
await actions.cold.toggleSearchableSnapshot(true);
await actions.savePolicy();
const latestRequest = server.requests[server.requests.length - 1];
const request = JSON.parse(JSON.parse(latestRequest.requestBody).body);
expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual(
expect(latestRequest.method).toBe('POST');
expect(latestRequest.url).toBe('/api/index_lifecycle_management/policies');
const reqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body);
expect(reqBody.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual(
'found-snapshots'
);
});

View file

@ -8,7 +8,7 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { useConfigurationIssues } from '../../../form';
import { useConfiguration } from '../../../form';
import {
DataTierAllocationField,
SearchableSnapshotField,
@ -29,7 +29,7 @@ const i18nTexts = {
};
export const ColdPhase: FunctionComponent = () => {
const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues();
const { isUsingSearchableSnapshotInHotPhase } = useConfiguration();
return (
<Phase phase="cold" topLevelSettings={<SearchableSnapshotField phase="cold" />}>

View file

@ -20,18 +20,16 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { useFormData } from '../../../../../../shared_imports';
import { i18nTexts } from '../../../i18n_texts';
import { usePhaseTimings } from '../../../form';
import { MinAgeField, SnapshotPoliciesField } from '../shared_fields';
import './delete_phase.scss';
import { usePhaseTimings, globalFields } from '../../../form';
import { PhaseIcon } from '../../phase_icon';
import { MinAgeField, SnapshotPoliciesField } from '../shared_fields';
import { PhaseErrorIndicator } from '../phase/phase_error_indicator';
import './delete_phase.scss';
const formFieldPaths = {
enabled: '_meta.delete.enabled',
enabled: globalFields.deleteEnabled.path,
};
export const DeletePhase: FunctionComponent = () => {

View file

@ -23,7 +23,7 @@ import { useFormData, SelectField, NumericField } from '../../../../../../shared
import { i18nTexts } from '../../../i18n_texts';
import { ROLLOVER_EMPTY_VALIDATION, useConfigurationIssues, UseField } from '../../../form';
import { ROLLOVER_EMPTY_VALIDATION, useConfiguration, UseField } from '../../../form';
import { useEditPolicyContext } from '../../../edit_policy_context';
@ -47,7 +47,7 @@ export const HotPhase: FunctionComponent = () => {
const [formData] = useFormData({
watch: isUsingDefaultRolloverPath,
});
const { isUsingRollover } = useConfigurationIssues();
const { isUsingRollover } = useConfiguration();
const isUsingDefaultRollover: boolean = get(formData, isUsingDefaultRolloverPath);
const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false);

View file

@ -22,7 +22,7 @@ import {
import { getFieldValidityAndErrorMessage } from '../../../../../../../shared_imports';
import { UseField, useConfigurationIssues } from '../../../../form';
import { UseField, useConfiguration } from '../../../../form';
import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util';
@ -81,7 +81,7 @@ interface Props {
}
export const MinAgeField: FunctionComponent<Props> = ({ phase }): React.ReactElement => {
const { isUsingRollover } = useConfigurationIssues();
const { isUsingRollover } = useConfiguration();
return (
<UseField path={`phases.${phase}.min_age`}>
{(field) => {

View file

@ -0,0 +1,66 @@
/*
* 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, { useEffect, useRef } from 'react';
import { EuiComboBoxOptionOption } from '@elastic/eui';
import { ComboBoxField, FieldHook } from '../../../../../../../shared_imports';
import { useGlobalFields } from '../../../../form';
interface PropsRepositoryCombobox {
field: FieldHook;
isLoading: boolean;
repos: string[];
noSuggestions: boolean;
globalRepository: string;
}
export const RepositoryComboBoxField = ({
field,
isLoading,
repos,
noSuggestions,
globalRepository,
}: PropsRepositoryCombobox) => {
const isMounted = useRef(false);
const { setValue } = field;
const {
searchableSnapshotRepo: { setValue: setSearchableSnapshotRepository },
} = useGlobalFields();
useEffect(() => {
// We keep our phase searchable action field in sync
// with the default repository field declared globally for the policy
if (isMounted.current) {
setValue(Boolean(globalRepository.trim()) ? [globalRepository] : []);
}
isMounted.current = true;
}, [setValue, globalRepository]);
return (
<ComboBoxField
field={field}
fullWidth={false}
euiFieldProps={{
'data-test-subj': 'searchableSnapshotCombobox',
options: repos.map((repo) => ({ label: repo, value: repo })),
singleSelection: { asPlainText: true },
isLoading,
noSuggestions,
onCreateOption: (newOption: string) => {
setSearchableSnapshotRepository(newOption);
},
onChange: (options: EuiComboBoxOptionOption[]) => {
if (options.length > 0) {
setSearchableSnapshotRepository(options[0].label);
} else {
setSearchableSnapshotRepository('');
}
},
}}
/>
);
};

View file

@ -5,24 +5,18 @@
* 2.0.
*/
import React, { FunctionComponent, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import React, { FunctionComponent, useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiComboBoxOptionOption,
EuiTextColor,
EuiSpacer,
EuiCallOut,
EuiLink,
} from '@elastic/eui';
import { ComboBoxField, useKibana, useFormData } from '../../../../../../../shared_imports';
import { EuiTextColor, EuiSpacer, EuiCallOut, EuiLink } from '@elastic/eui';
import { useKibana, useFormData } from '../../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../../edit_policy_context';
import { useConfigurationIssues, UseField, searchableSnapshotFields } from '../../../../form';
import { useConfiguration, UseField, globalFields } from '../../../../form';
import { FieldLoadingError, DescribedFormRow, LearnMoreLink } from '../../../';
import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provider';
import { RepositoryComboBoxField } from './repository_combobox_field';
import './_searchable_snapshot_field.scss';
@ -31,12 +25,6 @@ export interface Props {
canBeDisabled?: boolean;
}
/**
* This repository is provisioned by Elastic Cloud and will always
* exist as a "managed" repository.
*/
const CLOUD_DEFAULT_REPO = 'found-snapshots';
const geti18nTexts = (phase: Props['phase']) => ({
title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', {
defaultMessage: 'Searchable snapshot',
@ -71,13 +59,15 @@ export const SearchableSnapshotField: FunctionComponent<Props> = ({
services: { cloud },
} = useKibana();
const { getUrlForApp, policy, license, isNewPolicy } = useEditPolicyContext();
const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues();
const { isUsingSearchableSnapshotInHotPhase } = useConfiguration();
const searchableSnapshotRepoPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`;
const [formData] = useFormData({ watch: searchableSnapshotRepoPath });
const searchableSnapshotRepo = get(formData, searchableSnapshotRepoPath);
const [formData] = useFormData({
watch: globalFields.searchableSnapshotRepo.path,
});
const searchableSnapshotGlobalRepo = get(formData, globalFields.searchableSnapshotRepo.path);
const isColdPhase = phase === 'cold';
const isFrozenPhase = phase === 'frozen';
const isColdOrFrozenPhase = isColdPhase || isFrozenPhase;
@ -164,7 +154,10 @@ export const SearchableSnapshotField: FunctionComponent<Props> = ({
/>
</EuiCallOut>
);
} else if (searchableSnapshotRepo && !repos.includes(searchableSnapshotRepo)) {
} else if (
searchableSnapshotGlobalRepo &&
!repos.includes(searchableSnapshotGlobalRepo)
) {
calloutContent = (
<EuiCallOut
title={i18n.translate(
@ -201,49 +194,17 @@ export const SearchableSnapshotField: FunctionComponent<Props> = ({
return (
<div className="ilmSearchableSnapshotField">
<UseField<string>
config={{
...searchableSnapshotFields.snapshot_repository,
defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined,
}}
<UseField
path={searchableSnapshotRepoPath}
>
{(field) => {
const singleSelectionArray: [selectedSnapshot?: string] = field.value
? [field.value]
: [];
return (
<ComboBoxField
field={
{
...field,
value: singleSelectionArray,
} as any
}
label={field.label}
fullWidth={false}
euiFieldProps={{
'data-test-subj': 'searchableSnapshotCombobox',
options: repos.map((repo) => ({ label: repo, value: repo })),
singleSelection: { asPlainText: true },
isLoading,
noSuggestions: !!(error || repos.length === 0),
onCreateOption: (newOption: string) => {
field.setValue(newOption);
},
onChange: (options: EuiComboBoxOptionOption[]) => {
if (options.length > 0) {
field.setValue(options[0].label);
} else {
field.setValue('');
}
},
}}
/>
);
defaultValue={!!searchableSnapshotGlobalRepo ? [searchableSnapshotGlobalRepo] : []}
component={RepositoryComboBoxField}
componentProps={{
globalRepository: searchableSnapshotGlobalRepo,
isLoading,
repos,
noSuggestions: !!(error || repos.length === 0),
}}
</UseField>
/>
{calloutContent && (
<>
<EuiSpacer size="s" />

View file

@ -8,7 +8,7 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { useConfigurationIssues } from '../../../form';
import { useConfiguration } from '../../../form';
import {
ForcemergeField,
@ -30,7 +30,7 @@ const i18nTexts = {
};
export const WarmPhase: FunctionComponent = () => {
const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues();
const { isUsingSearchableSnapshotInHotPhase } = useConfiguration();
return (
<Phase phase="warm">

View file

@ -11,7 +11,7 @@ import { useFormData } from '../../../../../shared_imports';
import { formDataToAbsoluteTimings } from '../../lib';
import { useConfigurationIssues } from '../../form';
import { useConfiguration } from '../../form';
import { FormInternal } from '../../types';
@ -20,7 +20,7 @@ import { Timeline as ViewComponent } from './timeline';
export const Timeline: FunctionComponent = () => {
const [formData] = useFormData<FormInternal>();
const timings = formDataToAbsoluteTimings(formData);
const { isUsingRollover } = useConfigurationIssues();
const { isUsingRollover } = useConfiguration();
return (
<ViewComponent
hotPhaseMinAge={timings.hot.min_age}

View file

@ -18,3 +18,9 @@ export const ROLLOVER_FORM_PATHS = {
maxAge: 'phases.hot.actions.rollover.max_age',
maxSize: 'phases.hot.actions.rollover.max_size',
};
/**
* This repository is provisioned by Elastic Cloud and will always
* exist as a "managed" repository.
*/
export const CLOUD_DEFAULT_REPO = 'found-snapshots';

View file

@ -30,15 +30,11 @@ import {
EuiTitle,
} from '@elastic/eui';
import { TextField, useForm, useFormData } from '../../../shared_imports';
import { TextField, useForm, useFormData, useKibana } from '../../../shared_imports';
import { toasts } from '../../services/notification';
import { createDocLink } from '../../services/documentation';
import { UseField } from './form';
import { savePolicy } from './save_policy';
import {
ColdPhase,
DeletePhase,
@ -49,11 +45,14 @@ import {
Timeline,
FormErrorsCallout,
} from './components';
import { createPolicyNameValidations, createSerializer, deserializer, Form, schema } from './form';
import {
createPolicyNameValidations,
createSerializer,
createDeserializer,
Form,
getSchema,
} from './form';
import { useEditPolicyContext } from './edit_policy_context';
import { FormInternal } from './types';
export interface Props {
@ -76,20 +75,38 @@ export const EditPolicy: React.FunctionComponent<Props> = ({ history }) => {
license,
} = useEditPolicyContext();
const serializer = useMemo(() => {
return createSerializer(isNewPolicy ? undefined : currentPolicy);
}, [isNewPolicy, currentPolicy]);
const {
services: { cloud },
} = useKibana();
const [saveAsNew, setSaveAsNew] = useState(false);
const originalPolicyName: string = isNewPolicy ? '' : policyName!;
const isAllowedByLicense = license.canUseSearchableSnapshot();
const isCloudEnabled = Boolean(cloud?.isCloudEnabled);
const serializer = useMemo(() => {
return createSerializer(isNewPolicy ? undefined : currentPolicy);
}, [isNewPolicy, currentPolicy]);
const deserializer = useMemo(() => {
return createDeserializer(isCloudEnabled);
}, [isCloudEnabled]);
const defaultValue = useMemo(
() => ({
...currentPolicy,
name: originalPolicyName,
}),
[currentPolicy, originalPolicyName]
);
const schema = useMemo(() => {
return getSchema(isCloudEnabled);
}, [isCloudEnabled]);
const { form } = useForm({
schema,
defaultValue: {
...currentPolicy,
name: originalPolicyName,
},
defaultValue,
deserializer,
serializer,
});

View file

@ -9,20 +9,25 @@ import React, { FunctionComponent } from 'react';
import { Form as LibForm, FormHook } from '../../../../../shared_imports';
import { ConfigurationIssuesProvider } from '../configuration_issues_context';
import { ConfigurationProvider } from '../configuration_context';
import { FormErrorsProvider } from '../form_errors_context';
import { PhaseTimingsProvider } from '../phase_timings_context';
import { GlobalFieldsProvider } from '../global_fields_context';
interface Props {
form: FormHook;
}
export const Form: FunctionComponent<Props> = ({ form, children }) => (
<LibForm form={form}>
<ConfigurationIssuesProvider>
<FormErrorsProvider>
<PhaseTimingsProvider>{children}</PhaseTimingsProvider>
</FormErrorsProvider>
</ConfigurationIssuesProvider>
</LibForm>
);
export const Form: FunctionComponent<Props> = ({ form, children }) => {
return (
<LibForm form={form}>
<ConfigurationProvider>
<FormErrorsProvider>
<GlobalFieldsProvider>
<PhaseTimingsProvider>{children}</PhaseTimingsProvider>
</GlobalFieldsProvider>
</FormErrorsProvider>
</ConfigurationProvider>
</LibForm>
);
};

View file

@ -12,7 +12,7 @@ import { useFormData } from '../../../../shared_imports';
import { isUsingDefaultRolloverPath, isUsingCustomRolloverPath } from '../constants';
export interface ConfigurationIssues {
export interface Configuration {
/**
* Whether the serialized policy will use rollover. This blocks certain actions in
* the form such as hot phase (forcemerge, shrink) and cold phase (searchable snapshot).
@ -28,7 +28,7 @@ export interface ConfigurationIssues {
isUsingSearchableSnapshotInColdPhase: boolean;
}
const ConfigurationIssuesContext = createContext<ConfigurationIssues>(null as any);
const ConfigurationContext = createContext<Configuration>(null as any);
const pathToHotPhaseSearchableSnapshot =
'phases.hot.actions.searchable_snapshot.snapshot_repository';
@ -36,7 +36,7 @@ const pathToHotPhaseSearchableSnapshot =
const pathToColdPhaseSearchableSnapshot =
'phases.cold.actions.searchable_snapshot.snapshot_repository';
export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => {
export const ConfigurationProvider: FunctionComponent = ({ children }) => {
const [formData] = useFormData({
watch: [
pathToHotPhaseSearchableSnapshot,
@ -49,25 +49,18 @@ export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) =>
// Provide default value, as path may become undefined if removed from the DOM
const isUsingCustomRollover = get(formData, isUsingCustomRolloverPath, true);
return (
<ConfigurationIssuesContext.Provider
value={{
isUsingRollover: isUsingDefaultRollover === false ? isUsingCustomRollover : true,
isUsingSearchableSnapshotInHotPhase:
get(formData, pathToHotPhaseSearchableSnapshot) != null,
isUsingSearchableSnapshotInColdPhase:
get(formData, pathToColdPhaseSearchableSnapshot) != null,
}}
>
{children}
</ConfigurationIssuesContext.Provider>
);
const context: Configuration = {
isUsingRollover: isUsingDefaultRollover === false ? isUsingCustomRollover : true,
isUsingSearchableSnapshotInHotPhase: get(formData, pathToHotPhaseSearchableSnapshot) != null,
isUsingSearchableSnapshotInColdPhase: get(formData, pathToColdPhaseSearchableSnapshot) != null,
};
return <ConfigurationContext.Provider value={context}>{children}</ConfigurationContext.Provider>;
};
export const useConfigurationIssues = () => {
const ctx = useContext(ConfigurationIssuesContext);
if (!ctx)
throw new Error('Cannot use configuration issues outside of configuration issues context');
export const useConfiguration = () => {
const ctx = useContext(ConfigurationContext);
if (!ctx) throw new Error('Cannot use configuration outside of configuration context');
return ctx;
};

View file

@ -8,18 +8,29 @@
import { produce } from 'immer';
import { SerializedPolicy } from '../../../../../common/types';
import { splitSizeAndUnits } from '../../../lib/policies';
import { determineDataTierAllocationType, isUsingDefaultRollover } from '../../../lib';
import { getDefaultRepository } from '../lib';
import { FormInternal } from '../types';
import { CLOUD_DEFAULT_REPO } from '../constants';
export const deserializer = (policy: SerializedPolicy): FormInternal => {
export const createDeserializer = (isCloudEnabled: boolean) => (
policy: SerializedPolicy
): FormInternal => {
const {
phases: { hot, warm, cold, frozen, delete: deletePhase },
} = policy;
let defaultRepository = getDefaultRepository([
hot?.actions.searchable_snapshot,
cold?.actions.searchable_snapshot,
frozen?.actions.searchable_snapshot,
]);
if (!defaultRepository && isCloudEnabled) {
defaultRepository = CLOUD_DEFAULT_REPO;
}
const _meta: FormInternal['_meta'] = {
hot: {
isUsingDefaultRollover: isUsingDefaultRollover(policy),
@ -49,6 +60,9 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => {
delete: {
enabled: Boolean(deletePhase),
},
searchableSnapshot: {
repository: defaultRepository,
},
};
return produce<FormInternal>(

View file

@ -9,7 +9,7 @@ import { setAutoFreeze } from 'immer';
import { cloneDeep } from 'lodash';
import { SerializedPolicy } from '../../../../../common/types';
import { defaultRolloverAction } from '../../../constants';
import { deserializer } from './deserializer';
import { createDeserializer } from './deserializer';
import { createSerializer } from './serializer';
import { FormInternal } from '../types';
@ -18,6 +18,8 @@ const isObject = (v: unknown): v is { [key: string]: any } =>
const unknownValue = { some: 'value' };
const deserializer = createDeserializer(false);
const populateWithUnknownEntries = (v: unknown) => {
if (isObject(v)) {
for (const key of Object.keys(v)) {

View file

@ -0,0 +1,54 @@
/*
* 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, { createContext, FunctionComponent, useContext } from 'react';
import { UseMultiFields, FieldHook, FieldConfig } from '../../../../shared_imports';
/**
* Those are the fields that we always want present in our form.
*/
interface GlobalFieldsTypes {
deleteEnabled: boolean;
searchableSnapshotRepo: string;
}
type GlobalFields = {
[K in keyof GlobalFieldsTypes]: FieldHook<GlobalFieldsTypes[K]>;
};
const GlobalFieldsContext = createContext<GlobalFields | null>(null);
export const globalFields: Record<
keyof GlobalFields,
{ path: string; config?: FieldConfig<any> }
> = {
deleteEnabled: {
path: '_meta.delete.enabled',
},
searchableSnapshotRepo: {
path: '_meta.searchableSnapshot.repository',
},
};
export const GlobalFieldsProvider: FunctionComponent = ({ children }) => {
return (
<UseMultiFields<GlobalFieldsTypes> fields={globalFields}>
{(fields) => {
return (
<GlobalFieldsContext.Provider value={fields}>{children}</GlobalFieldsContext.Provider>
);
}}
</UseMultiFields>
);
};
export const useGlobalFields = () => {
const ctx = useContext(GlobalFieldsContext);
if (!ctx) throw new Error('Cannot use global fields outside of global fields context');
return ctx;
};

View file

@ -5,20 +5,17 @@
* 2.0.
*/
export { deserializer } from './deserializer';
export { createDeserializer } from './deserializer';
export { createSerializer } from './serializer';
export { schema, searchableSnapshotFields } from './schema';
export { getSchema } from './schema';
export * from './validations';
export { Form, EnhancedUseField as UseField } from './components';
export {
ConfigurationIssuesProvider,
useConfigurationIssues,
} from './configuration_issues_context';
export { ConfigurationProvider, useConfiguration } from './configuration_context';
export { FormErrorsProvider, useFormErrorsContext } from './form_errors_context';
@ -27,3 +24,5 @@ export {
usePhaseTimings,
PhaseTimingConfiguration,
} from './phase_timings_context';
export { useGlobalFields, globalFields } from './global_fields_context';

View file

@ -8,7 +8,7 @@
import React, { createContext, FunctionComponent, useContext } from 'react';
import { useFormData } from '../../../../shared_imports';
import { FormInternal } from '../types';
import { UseField } from './index';
import { useGlobalFields } from './index';
export interface PhaseTimingConfiguration {
/**
@ -48,6 +48,7 @@ export interface PhaseTimings {
const PhaseTimingsContext = createContext<PhaseTimings>(null as any);
export const PhaseTimingsProvider: FunctionComponent = ({ children }) => {
const { deleteEnabled } = useGlobalFields();
const [formData] = useFormData<FormInternal>({
watch: [
'_meta.warm.enabled',
@ -58,21 +59,15 @@ export const PhaseTimingsProvider: FunctionComponent = ({ children }) => {
});
return (
<UseField path="_meta.delete.enabled">
{(field) => {
return (
<PhaseTimingsContext.Provider
value={{
...getPhaseTimingConfiguration(formData),
isDeletePhaseEnabled: formData?._meta?.delete?.enabled,
setDeletePhaseEnabled: field.setValue,
}}
>
{children}
</PhaseTimingsContext.Provider>
);
<PhaseTimingsContext.Provider
value={{
...getPhaseTimingConfiguration(formData),
isDeletePhaseEnabled: deleteEnabled.value,
setDeletePhaseEnabled: deleteEnabled.setValue,
}}
</UseField>
>
{children}
</PhaseTimingsContext.Provider>
);
};

View file

@ -9,12 +9,8 @@ import { i18n } from '@kbn/i18n';
import { FormSchema, fieldValidators } from '../../../../shared_imports';
import { defaultIndexPriority } from '../../../constants';
import { ROLLOVER_FORM_PATHS } from '../constants';
import { FormInternal } from '../types';
const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS);
import { ROLLOVER_FORM_PATHS, CLOUD_DEFAULT_REPO } from '../constants';
import { i18nTexts } from '../i18n_texts';
import {
ifExistsNumberGreaterThanZero,
ifExistsNumberNonNegative,
@ -22,7 +18,7 @@ import {
minAgeValidator,
} from './validations';
import { i18nTexts } from '../i18n_texts';
const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS);
const { emptyField, numberGreaterThanField } = fieldValidators;
@ -54,6 +50,13 @@ export const searchableSnapshotFields = {
validations: [
{ validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) },
],
// TODO: update text copy
helpText: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.repositoryHelpText',
{
defaultMessage: 'Each phase uses the same snapshot repository.',
}
),
},
storage: {
label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageLabel', {
@ -114,7 +117,7 @@ const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({
serializer: serializers.stringToNumber,
});
export const schema: FormSchema<FormInternal> = {
export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
_meta: {
hot: {
isUsingDefaultRollover: {
@ -230,6 +233,11 @@ export const schema: FormSchema<FormInternal> = {
defaultValue: 'd',
},
},
searchableSnapshot: {
repository: {
defaultValue: isCloudEnabled ? CLOUD_DEFAULT_REPO : '',
},
},
},
phases: {
hot: {
@ -288,6 +296,7 @@ export const schema: FormSchema<FormInternal> = {
set_priority: {
priority: getPriorityField('hot'),
},
searchable_snapshot: searchableSnapshotFields,
},
},
warm: {
@ -375,4 +384,4 @@ export const schema: FormSchema<FormInternal> = {
},
},
},
};
});

View file

@ -124,7 +124,12 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
/**
* HOT PHASE SEARCHABLE SNAPSHOT
*/
if (!updatedPolicy.phases.hot!.actions?.searchable_snapshot) {
if (updatedPolicy.phases.hot!.actions?.searchable_snapshot) {
hotPhaseActions.searchable_snapshot = {
...hotPhaseActions.searchable_snapshot,
snapshot_repository: _meta.searchableSnapshot.repository,
};
} else {
delete hotPhaseActions.searchable_snapshot;
}
}
@ -234,7 +239,12 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
/**
* COLD PHASE SEARCHABLE SNAPSHOT
*/
if (!updatedPolicy.phases.cold?.actions?.searchable_snapshot) {
if (updatedPolicy.phases.cold?.actions?.searchable_snapshot) {
coldPhase.actions.searchable_snapshot = {
...coldPhase.actions.searchable_snapshot,
snapshot_repository: _meta.searchableSnapshot.repository,
};
} else {
delete coldPhase.actions.searchable_snapshot;
}
} else {
@ -251,7 +261,12 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
/**
* FROZEN PHASE SEARCHABLE SNAPSHOT
*/
if (!updatedPolicy.phases.frozen?.actions?.searchable_snapshot) {
if (updatedPolicy.phases.frozen?.actions?.searchable_snapshot) {
frozenPhase.actions.searchable_snapshot = {
...frozenPhase.actions.searchable_snapshot,
snapshot_repository: _meta.searchableSnapshot.repository,
};
} else {
delete frozenPhase.actions.searchable_snapshot;
}
} else {
@ -271,7 +286,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
deletePhase.actions.delete = deletePhase.actions.delete ?? {};
/**
* DELETE PHASE SEARCHABLE SNAPSHOT
* DELETE PHASE MIN AGE
*/
if (updatedPolicy.phases.delete?.min_age) {
deletePhase.min_age = `${updatedPolicy.phases.delete!.min_age}${_meta.delete.minAgeUnit}`;

View file

@ -6,13 +6,15 @@
*/
import { flow } from 'fp-ts/function';
import { deserializer } from '../form';
import { createDeserializer } from '../form';
import {
formDataToAbsoluteTimings,
calculateRelativeFromAbsoluteMilliseconds,
} from './absolute_timing_to_relative_timing';
const deserializer = createDeserializer(false);
export const calculateRelativeTimingMs = flow(
formDataToAbsoluteTimings,
calculateRelativeFromAbsoluteMilliseconds

View file

@ -0,0 +1,20 @@
/*
* 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 { SearchableSnapshotAction } from '../../../../../common/types';
export const getDefaultRepository = (
configs: Array<SearchableSnapshotAction | undefined>
): string => {
if (configs.length === 0) {
return '';
}
if (Boolean(configs[0]?.snapshot_repository)) {
return configs[0]!.snapshot_repository;
}
return getDefaultRepository(configs.slice(1));
};

View file

@ -12,3 +12,5 @@ export {
PhaseAgeInMilliseconds,
RelativePhaseTimingInMs,
} from './absolute_timing_to_relative_timing';
export { getDefaultRepository } from './get_default_repository';

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SerializedPolicy } from '../../../../common/types';
export type DataTierAllocationType = 'node_roles' | 'node_attrs' | 'none';
@ -76,5 +75,8 @@ export interface FormInternal extends SerializedPolicy {
cold: ColdPhaseMetaFields;
frozen: FrozenPhaseMetaFields;
delete: DeletePhaseMetaFields;
searchableSnapshot: {
repository: string;
};
};
}

View file

@ -23,6 +23,7 @@ export {
useFormContext,
FormSchema,
ValidationConfig,
UseMultiFields,
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers';