[ILM] Read only view (#186955)

## Summary
Fixes https://github.com/elastic/kibana/issues/184805

This PR adds a flyout to view an ILM policy. With this change, the ILM
plugin can be also accessed by users with "read" permission for ILM.
To test this PR, create a new role with `read_ilm` Elasticsearch
privileges and all Kibana privileges.

### Screenshots 

<img width="545" alt="Screenshot 2024-09-06 at 17 40 46"
src="https://github.com/user-attachments/assets/74d3beb2-857c-4803-b308-80a2c1509696">

<img width="540" alt="Screenshot 2024-09-06 at 17 40 59"
src="https://github.com/user-attachments/assets/82046408-cbef-4de3-aa7d-7b69193ad6b7">




https://github.com/user-attachments/assets/01fb445a-120a-489e-8f8d-26375ce391b1



### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2024-09-20 12:17:17 +02:00 committed by GitHub
parent 9a9c0f1afe
commit 1e572cfad9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 2766 additions and 369 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
/*
* 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 type { PolicyFromES } from '../common/types';
export const policyAllPhases: PolicyFromES = {
name: 'test',
modifiedDate: '2024-08-12T12:17:06.271Z',
version: 1,
policy: {
name: 'test',
phases: {
hot: {
actions: {
rollover: {
max_age: '30d',
max_primary_shard_size: '50gb',
max_primary_shard_docs: 25,
max_docs: 235,
max_size: '2gb',
},
set_priority: {
priority: 100,
},
forcemerge: {
max_num_segments: 3,
index_codec: 'best_compression',
},
shrink: {
number_of_shards: 1,
},
readonly: {},
},
min_age: '0ms',
},
warm: {
min_age: '3d',
actions: {
set_priority: {
priority: 50,
},
shrink: {
max_primary_shard_size: '4gb',
},
forcemerge: {
max_num_segments: 44,
index_codec: 'best_compression',
},
allocate: {
number_of_replicas: 3,
},
downsample: {
fixed_interval: '1d',
},
},
},
cold: {
min_age: '55d',
actions: {
searchable_snapshot: {
snapshot_repository: 'found-snapshots',
},
set_priority: {
priority: 0,
},
allocate: {
number_of_replicas: 3,
},
downsample: {
fixed_interval: '4d',
},
},
},
frozen: {
min_age: '555d',
actions: {
searchable_snapshot: {
snapshot_repository: 'found-snapshots',
},
},
},
delete: {
min_age: '7365d',
actions: {
wait_for_snapshot: {
policy: 'cloud-snapshot-policy',
},
delete: {},
},
},
},
},
};

View file

@ -0,0 +1,57 @@
/*
* 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, { ReactElement } from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test';
import { docLinksServiceMock } from '@kbn/core/public/mocks';
import type { PolicyFromES } from '../common/types';
import { KibanaContextProvider } from '../public/shared_imports';
import { PolicyListContextProvider } from '../public/application/sections/policy_list/policy_list_context';
import { ViewPolicyFlyout } from '../public/application/sections/policy_list/policy_flyout';
import * as readOnlyHook from '../public/application/lib/use_is_read_only';
import { policyAllPhases } from './mocks';
let component: ReactElement;
const TestComponent = ({ policy }: { policy: PolicyFromES }) => {
return (
<KibanaContextProvider
services={{ getUrlForApp: () => '', docLinks: docLinksServiceMock.createStartContract() }}
>
<PolicyListContextProvider>
<ViewPolicyFlyout policy={policy} />
</PolicyListContextProvider>
</KibanaContextProvider>
);
};
describe('View policy flyout', () => {
beforeAll(() => {
jest.spyOn(readOnlyHook, 'useIsReadOnly').mockReturnValue(false);
component = <TestComponent policy={policyAllPhases} />;
});
it('shows all phases', () => {
const rendered = mountWithIntl(component);
expect(takeMountedSnapshot(rendered)).toMatchSnapshot();
});
it('renders manage button', () => {
const rendered = mountWithIntl(component);
const button = findTestSubject(rendered, 'managePolicyButton');
expect(button.exists()).toBeTruthy();
});
it(`doesn't render manage button in read only view`, () => {
jest.spyOn(readOnlyHook, 'useIsReadOnly').mockReturnValue(true);
component = <TestComponent policy={policyAllPhases} />;
const rendered = mountWithIntl(component);
const button = findTestSubject(rendered, 'managePolicyButton');
expect(button.exists()).toBeFalsy();
});
});

View file

@ -21,6 +21,7 @@ import { init as initHttp } from '../public/application/services/http';
import { init as initUiMetric } from '../public/application/services/ui_metric';
import { KibanaContextProvider } from '../public/shared_imports';
import { PolicyListContextProvider } from '../public/application/sections/policy_list/policy_list_context';
import * as readOnlyHook from '../public/application/lib/use_is_read_only';
initHttp(httpServiceMock.createSetupContract());
initUiMetric(usageCollectionPluginMock.createSetupContract());
@ -72,9 +73,16 @@ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
createHref: jest.fn(),
location: {
search: '',
},
}),
}));
const mockReactRouterNavigate = jest.fn();
jest.mock('@kbn/kibana-react-plugin/public', () => ({
...jest.requireActual('@kbn/kibana-react-plugin/public'),
reactRouterNavigate: () => mockReactRouterNavigate(),
}));
let component: ReactElement;
const snapshot = (rendered: string[]) => {
@ -129,6 +137,7 @@ const TestComponent = ({ testPolicies }: { testPolicies: PolicyFromES[] }) => {
};
describe('policy table', () => {
beforeEach(() => {
jest.spyOn(readOnlyHook, 'useIsReadOnly').mockReturnValue(false);
component = <TestComponent testPolicies={policies} />;
window.localStorage.removeItem('ILM_SHOW_MANAGED_POLICIES_BY_DEFAULT');
});
@ -296,7 +305,9 @@ describe('policy table', () => {
test('add index template modal shows when add policy to index template button is pressed', () => {
const rendered = mountWithIntl(component);
const policyRow = findTestSubject(rendered, `policyTableRow-${testPolicy.name}`);
const addPolicyToTemplateButton = findTestSubject(policyRow, 'addPolicyToTemplate');
const actionsButton = findTestSubject(policyRow, 'euiCollapsedItemActionsButton');
actionsButton.simulate('click');
const addPolicyToTemplateButton = findTestSubject(rendered, 'addPolicyToTemplate');
addPolicyToTemplateButton.simulate('click');
rendered.update();
expect(findTestSubject(rendered, 'addPolicyToTemplateModal').exists()).toBeTruthy();
@ -312,6 +323,10 @@ describe('policy table', () => {
expect(policyIndices).toBe(`${testPolicy.indices.length}`);
const policyModifiedDate = findTestSubject(firstRow, 'policy-modifiedDate').text();
expect(policyModifiedDate).toBe(`${testDateFormatted}`);
const cells = firstRow.find('td');
// columns are name, linked index templates, linked indices, modified date, actions
expect(cells.length).toBe(5);
});
test('opens a flyout with index templates', () => {
const rendered = mountWithIntl(component);
@ -323,4 +338,25 @@ describe('policy table', () => {
const indexTemplatesLinks = findTestSubject(rendered, 'indexTemplateLink');
expect(indexTemplatesLinks.length).toBe(testPolicy.indexTemplates.length);
});
test('opens a flyout to view policy by calling reactRouterNavigate', async () => {
const rendered = mountWithIntl(component);
const policyNameLink = findTestSubject(rendered, 'policyTablePolicyNameLink').at(0);
policyNameLink.simulate('click');
rendered.update();
expect(mockReactRouterNavigate).toHaveBeenCalled();
});
describe('read only view', () => {
beforeEach(() => {
jest.spyOn(readOnlyHook, 'useIsReadOnly').mockReturnValue(true);
component = <TestComponent testPolicies={policies} />;
});
it(`doesn't show actions column in the table`, () => {
const rendered = mountWithIntl(component);
const policyRow = findTestSubject(rendered, `policyTableRow-testy0`);
const cells = policyRow.find('td');
// columns are name, linked index templates, linked indices, modified date
expect(cells.length).toBe(4);
});
});
});

View file

@ -60,8 +60,6 @@ export interface SerializedActionWithAllocation {
migrate?: MigrateAction;
}
export type SearchableSnapshotStorage = 'full_copy' | 'shared_cache';
export interface SearchableSnapshotAction {
snapshot_repository: string;
/**
@ -69,12 +67,6 @@ export interface SearchableSnapshotAction {
* not suit the vast majority of cases.
*/
force_merge_index?: boolean;
/**
* This configuration lets the user create full or partial searchable snapshots.
* Full searchable snapshots store primary data locally and store replica data in the snapshot.
* Partial searchable snapshots store no data locally.
*/
storage?: SearchableSnapshotStorage;
}
export interface RolloverAction {
@ -96,9 +88,7 @@ export interface SerializedHotPhase extends SerializedPhase {
shrink?: ShrinkAction;
downsample?: DownsampleAction;
set_priority?: {
priority: number | null;
};
set_priority?: SetPriorityAction;
/**
* Only available on enterprise license
*/
@ -113,9 +103,7 @@ export interface SerializedWarmPhase extends SerializedPhase {
forcemerge?: ForcemergeAction;
readonly?: {};
downsample?: DownsampleAction;
set_priority?: {
priority: number | null;
};
set_priority?: SetPriorityAction;
migrate?: MigrateAction;
};
}
@ -126,9 +114,7 @@ export interface SerializedColdPhase extends SerializedPhase {
readonly?: {};
downsample?: DownsampleAction;
allocate?: AllocateAction;
set_priority?: {
priority: number | null;
};
set_priority?: SetPriorityAction;
migrate?: MigrateAction;
/**
* Only available on enterprise license
@ -139,11 +125,6 @@ export interface SerializedColdPhase extends SerializedPhase {
export interface SerializedFrozenPhase extends SerializedPhase {
actions: {
allocate?: AllocateAction;
set_priority?: {
priority: number | null;
};
migrate?: MigrateAction;
/**
* Only available on enterprise license
*/
@ -187,11 +168,8 @@ export interface DownsampleAction {
fixed_interval: string;
}
export interface LegacyPolicy {
name: string;
phases: {
delete: DeletePhase;
};
export interface SetPriorityAction {
priority: number | null;
}
export interface CommonPhaseSettings {
@ -203,44 +181,6 @@ export interface PhaseWithMinAge {
selectedMinimumAgeUnits: string;
}
export interface PhaseWithIndexPriority {
phaseIndexPriority: string;
}
export interface PhaseWithForcemergeAction {
forceMergeEnabled: boolean;
selectedForceMergeSegments: string;
bestCompressionEnabled: boolean;
}
export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge {
waitForSnapshotPolicy: string;
}
export interface IndexLifecyclePolicy {
index: string;
managed: boolean;
action?: string;
action_time_millis?: number;
age?: string;
failed_step?: string;
failed_step_retry_count?: number;
is_auto_retryable_error?: boolean;
lifecycle_date_millis?: number;
phase?: string;
phase_execution?: {
policy: string;
modified_date_in_millis: number;
version: number;
phase_definition: SerializedPhase;
};
phase_time_millis?: number;
policy?: string;
step?: string;
step_info?: {
reason?: string;
type?: string;
message?: string;
};
step_time_millis?: number;
}

View file

@ -22,8 +22,9 @@ const getTestBedConfig = (initialEntries: string[]): TestBedConfig => ({
export interface AppTestBed extends TestBed {
actions: {
clickPolicyNameLink: () => void;
clickCreatePolicyButton: () => void;
clickPolicyNameLink: () => Promise<void>;
clickCreatePolicyButton: () => Promise<void>;
clickEditPolicyButton: () => Promise<void>;
};
}
@ -53,9 +54,17 @@ export const setup = async (
component.update();
};
const clickEditPolicyButton = async () => {
const { component, find } = testBed;
await act(async () => {
find('editPolicy').simulate('click', { button: 0 });
});
component.update();
};
return {
...testBed,
actions: { clickPolicyNameLink, clickCreatePolicyButton },
actions: { clickPolicyNameLink, clickCreatePolicyButton, clickEditPolicyButton },
};
};

View file

@ -7,6 +7,7 @@
import { act } from 'react-dom/test-utils';
import * as hooks from '../../public/application/lib/use_is_read_only';
import { getDefaultHotPhasePolicy } from '../edit_policy/constants';
import { setupEnvironment } from '../helpers';
@ -41,6 +42,7 @@ jest.mock('@elastic/eui', () => {
describe('<App />', () => {
let testBed: AppTestBed;
const { httpSetup, httpRequestsMockHelpers } = setupEnvironment();
jest.spyOn(hooks, 'useIsReadOnly').mockReturnValue(false);
describe('new policy creation', () => {
test('when there are no policies', async () => {
@ -92,7 +94,7 @@ describe('<App />', () => {
await actions.clickPolicyNameLink();
component.update();
expect(testBed.find('policyTitle').text()).toBe(`${editPolicyTitle} ${SPECIAL_CHARS_NAME}`);
expect(testBed.find('policyFlyoutTitle').text()).toBe(SPECIAL_CHARS_NAME);
});
test('loading edit policy page url works', async () => {
@ -166,9 +168,7 @@ describe('<App />', () => {
await actions.clickPolicyNameLink();
component.update();
expect(testBed.find('policyTitle').text()).toBe(
`${editPolicyTitle} ${PERCENT_SIGN_WITH_OTHER_CHARS_NAME}`
);
expect(testBed.find('policyFlyoutTitle').text()).toBe(PERCENT_SIGN_WITH_OTHER_CHARS_NAME);
});
test("loading edit policy page url doesn't work", async () => {
@ -221,9 +221,7 @@ describe('<App />', () => {
await actions.clickPolicyNameLink();
component.update();
expect(testBed.find('policyTitle').text()).toBe(
`${editPolicyTitle} ${PERCENT_SIGN_25_SEQUENCE}`
);
expect(testBed.find('policyFlyoutTitle').text()).toBe(PERCENT_SIGN_25_SEQUENCE);
});
test("loading edit policy page url doesn't work", async () => {

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { IndexTemplatesFlyout } from './index_templates_flyout';

View file

@ -11,7 +11,6 @@ export const defaultIndexPriority = {
hot: '100',
warm: '50',
cold: '0',
frozen: '0',
};
export const defaultRolloverAction: RolloverAction = {

View file

@ -19,3 +19,4 @@ export const UIM_CONFIG_WARM_PHASE: string = 'config_warm_phase';
export const UIM_CONFIG_SET_PRIORITY: string = 'config_set_priority';
export const UIM_INDEX_RETRY_STEP: string = 'index_retry_step';
export const UIM_EDIT_CLICK: string = 'edit_click';
export const UIM_VIEW_CLICK: string = 'view_click';

View file

@ -32,7 +32,7 @@ export const renderApp = (
executionContext: ExecutionContextStart,
cloud?: CloudSetup
): UnmountCallback => {
const { navigateToUrl, getUrlForApp } = application;
const { navigateToUrl, getUrlForApp, capabilities } = application;
const { overlays, http } = startServices;
render(
@ -55,6 +55,7 @@ export const renderApp = (
overlays,
http,
history,
capabilities,
}}
>
<App history={history} />

View file

@ -0,0 +1,19 @@
/*
* 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 { PLUGIN } from '../../../common/constants';
import { useKibana } from '../../shared_imports';
export const useIsReadOnly = () => {
const {
services: { capabilities },
} = useKibana();
const ilmCaps = capabilities[PLUGIN.ID];
const savePermission = Boolean(ilmCaps.save);
const showPermission = Boolean(ilmCaps.show);
return !savePermission && showPermission;
};

View file

@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useEditPolicyContext } from '../edit_policy_context';
import { getIndicesListPath } from '../../../services/navigation';
import { useKibana } from '../../../../shared_imports';
import { IndexTemplatesFlyout } from '../../../components/index_templates_flyout';
import { IndexTemplatesFlyout } from '../../../components';
export const EditWarning: FunctionComponent = () => {
const { isNewPolicy, indices, indexTemplates, policyName, policy } = useEditPolicyContext();

View file

@ -8,22 +8,16 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiTextColor } from '@elastic/eui';
import { useKibana } from '../../../../../../shared_imports';
import { i18nTexts } from '../../../i18n_texts';
import { LearnMoreLink } from '../../learn_more_link';
import { ToggleFieldWithDescribedFormRow } from '../../described_form_row';
import { useKibana } from '../../../../../../shared_imports';
export const DeleteSearchableSnapshotField: React.FunctionComponent = () => {
const { docLinks } = useKibana().services;
return (
<ToggleFieldWithDescribedFormRow
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.deleteSearchableSnapshotTitle"
defaultMessage="Delete searchable snapshot"
/>
</h3>
}
title={<h3>{i18nTexts.editPolicy.deleteSearchableSnapshotLabel}</h3>}
description={
<EuiTextColor color="subdued">
<FormattedMessage

View file

@ -59,13 +59,7 @@ export const HotPhase: FunctionComponent = () => {
return (
<Phase phase="hot">
<DescribedFormRow
title={
<h3>
{i18n.translate('xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle', {
defaultMessage: 'Rollover',
})}
</h3>
}
title={<h3>{i18nTexts.editPolicy.rolloverLabel}</h3>}
description={
<>
<EuiTextColor color="subdued">

View file

@ -7,13 +7,13 @@
import { get } from 'lodash';
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiDescribedFormGroup, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
import { useKibana, useFormData } from '../../../../../../../shared_imports';
import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../common/types';
import { getAvailableNodeRoleForPhase, isNodeRoleFirstPreference } from '../../../../../../lib';
import { useLoadNodes } from '../../../../../../services/api';
import { i18nTexts } from '../../../../i18n_texts';
import { DataTierAllocationType } from '../../../../types';
import {
@ -30,12 +30,6 @@ import {
import './_data_tier_allocation.scss';
const i18nTexts = {
title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', {
defaultMessage: 'Data allocation',
}),
};
interface Props {
phase: PhaseWithAllocation;
description: React.ReactNode;
@ -188,7 +182,7 @@ export const DataTierAllocationField: FunctionComponent<Props> = ({ phase, descr
return (
<EuiDescribedFormGroup
title={<h3>{i18nTexts.title}</h3>}
title={<h3>{i18nTexts.editPolicy.dataAllocationLabel}</h3>}
description={
<>
{description}

View file

@ -30,14 +30,7 @@ export const DownsampleField: React.FunctionComponent<Props> = ({ phase }) => {
return (
<ToggleFieldWithDescribedFormRow
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.downsampleTitle"
defaultMessage="Downsample"
/>
</h3>
}
title={<h3>{i18nTexts.editPolicy.downsampleLabel}</h3>}
description={
<EuiTextColor color="subdued">
<FormattedMessage

View file

@ -34,14 +34,7 @@ export const ForcemergeField: React.FunctionComponent<Props> = ({ phase }) => {
return (
<DescribedFormRow
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableText"
defaultMessage="Force merge"
/>
</h3>
}
title={<h3>{i18nTexts.editPolicy.forceMergeLabel}</h3>}
description={
<>
<FormattedMessage

View file

@ -10,8 +10,6 @@ import React, { FunctionComponent, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiTextColor } from '@elastic/eui';
import { PhaseExceptDelete } from '../../../../../../../common/types';
import { NumericField } from '../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../edit_policy_context';
@ -20,7 +18,7 @@ import { LearnMoreLink, DescribedFormRow } from '../..';
import { useKibana } from '../../../../../../shared_imports';
interface Props {
phase: PhaseExceptDelete;
phase: 'hot' | 'warm' | 'cold';
}
export const IndexPriorityField: FunctionComponent<Props> = ({ phase }) => {

View file

@ -5,9 +5,7 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { get } from 'lodash';
import {
@ -27,20 +25,7 @@ import { UseField, useConfiguration, useGlobalFields } from '../../../../form';
import { getPhaseMinAgeInMilliseconds } from '../../../../lib';
import { timeUnits } from '../../../../constants';
import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util';
const i18nTexts = {
rolloverToolTipDescription: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.minimumAge.rolloverToolTipDescription',
{
defaultMessage:
'Data age is calculated from rollover. Rollover is configured in the hot phase.',
}
),
minAgeUnitFieldSuffix: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.minimumAge.minimumAgeFieldSuffixLabel',
{ defaultMessage: 'old' }
),
};
import { i18nTexts } from '../../../../i18n_texts';
interface Props {
phase: PhaseWithTiming;
@ -95,10 +80,7 @@ export const MinAgeField: FunctionComponent<Props> = ({ phase }): React.ReactEle
>
<EuiFlexItem grow={false}>
<EuiText className={'eui-textNoWrap'} size={'xs'}>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.minimumAge.minimumAgeFieldLabel"
defaultMessage="Move data into phase when:"
/>
{`${i18nTexts.editPolicy.minAgeLabel}:`}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={true}>
@ -127,15 +109,15 @@ export const MinAgeField: FunctionComponent<Props> = ({ phase }): React.ReactEle
<div data-test-subj={`${phase}-rolloverMinAgeInputIconTip`} />
<EuiIconTip
type="iInCircle"
aria-label={i18nTexts.rolloverToolTipDescription}
content={i18nTexts.rolloverToolTipDescription}
aria-label={i18nTexts.editPolicy.rolloverToolTipDescription}
content={i18nTexts.editPolicy.rolloverToolTipDescription}
/>
</>
);
const selectAppendValue: Array<string | React.ReactElement> =
isUsingRollover
? [i18nTexts.minAgeUnitFieldSuffix, icon]
: [i18nTexts.minAgeUnitFieldSuffix];
? [i18nTexts.editPolicy.minAgeUnitFieldSuffix, icon]
: [i18nTexts.editPolicy.minAgeUnitFieldSuffix];
const unitValue = unitField.value as string;
let unitOptions = timeUnits;

View file

@ -11,6 +11,7 @@ import { EuiTextColor } from '@elastic/eui';
import { LearnMoreLink } from '../../learn_more_link';
import { ToggleFieldWithDescribedFormRow } from '../../described_form_row';
import { useKibana } from '../../../../../../shared_imports';
import { i18nTexts } from '../../../i18n_texts';
interface Props {
phase: 'hot' | 'warm' | 'cold';
}
@ -19,14 +20,7 @@ export const ReadonlyField: React.FunctionComponent<Props> = ({ phase }) => {
const { docLinks } = useKibana().services;
return (
<ToggleFieldWithDescribedFormRow
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.readonlyTitle"
defaultMessage="Read only"
/>
</h3>
}
title={<h3>{i18nTexts.editPolicy.readonlyLabel}</h3>}
description={
<EuiTextColor color="subdued">
<FormattedMessage

View file

@ -12,11 +12,12 @@ import { NumericField } from '../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../edit_policy_context';
import { UseField } from '../../../form';
import { i18nTexts } from '../../../i18n_texts';
import { DescribedFormRow } from '../../described_form_row';
interface Props {
phase: 'warm' | 'cold' | 'frozen';
phase: 'warm' | 'cold';
}
export const ReplicasField: FunctionComponent<Props> = ({ phase }) => {
@ -24,13 +25,7 @@ export const ReplicasField: FunctionComponent<Props> = ({ phase }) => {
const initialValue = policy.phases[phase]?.actions?.allocate?.number_of_replicas != null;
return (
<DescribedFormRow
title={
<h3>
{i18n.translate('xpack.indexLifecycleMgmt.numberOfReplicas.formRowTitle', {
defaultMessage: 'Replicas',
})}
</h3>
}
title={<h3>{i18nTexts.editPolicy.replicasLabel}</h3>}
description={i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.numberOfReplicas.formRowDescription',
{

View file

@ -19,6 +19,7 @@ import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provi
import { RepositoryComboBoxField } from './repository_combobox_field';
import './_searchable_snapshot_field.scss';
import { i18nTexts as i18nTextsEdit } from '../../../../i18n_texts';
export interface Props {
phase: 'hot' | 'cold' | 'frozen';
@ -35,12 +36,7 @@ const geti18nTexts = (
case 'hot':
case 'cold':
return {
title: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.fullyMountedSearchableSnapshotField.title',
{
defaultMessage: 'Searchable snapshot',
}
),
title: i18nTextsEdit.editPolicy.searchableSnapshotLabel,
description: (
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.fullyMountedSearchableSnapshotField.description"

View file

@ -39,14 +39,7 @@ export const ShrinkField: FunctionComponent<Props> = ({ phase }) => {
const { docLinks } = useKibana().services;
return (
<DescribedFormRow
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.shrinkText"
defaultMessage="Shrink"
/>
</h3>
}
title={<h3>{i18nTexts.editPolicy.shrinkActionLabel}</h3>}
description={
<EuiTextColor color="subdued">
<FormattedMessage
@ -59,7 +52,7 @@ export const ShrinkField: FunctionComponent<Props> = ({ phase }) => {
titleSize="xs"
switchProps={{
'data-test-subj': `${phase}-shrinkSwitch`,
label: i18nTexts.editPolicy.shrinkLabel,
label: i18nTexts.editPolicy.shrinkToggleLabel,
initialValue: Boolean(policy.phases[phase]?.actions?.shrink),
}}
fullWidth

View file

@ -24,6 +24,7 @@ import { useLoadSnapshotPolicies } from '../../../../../services/api';
import { UseField } from '../../../form';
import { FieldLoadingError, LearnMoreLink, OptionalLabel } from '../..';
import { i18nTexts } from '../../../i18n_texts';
const waitForSnapshotFormField = 'phases.delete.actions.wait_for_snapshot.policy';
@ -145,14 +146,7 @@ export const SnapshotPoliciesField: React.FunctionComponent = () => {
return (
<EuiDescribedFormGroup
title={
<h3>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.deletePhase.waitForSnapshotTitle"
defaultMessage="Wait for snapshot policy"
/>
</h3>
}
title={<h3>{i18nTexts.editPolicy.waitForSnapshotLabel}</h3>}
description={
<>
<FormattedMessage

View file

@ -114,6 +114,7 @@ interface Props {
coldPhaseMinAge?: string;
frozenPhaseMinAge?: string;
deletePhaseMinAge?: string;
showTitle?: boolean;
}
/**
@ -121,7 +122,7 @@ interface Props {
* and should not rely directly on any application-specific context.
*/
export const Timeline: FunctionComponent<Props> = memo(
({ hasDeletePhase, isUsingRollover, ...phasesMinAge }) => {
({ hasDeletePhase, isUsingRollover, showTitle = true, ...phasesMinAge }) => {
const absoluteTimings: AbsoluteTimings = {
hot: { min_age: phasesMinAge.hotPhaseMinAge },
warm: phasesMinAge.warmPhaseMinAge ? { min_age: phasesMinAge.warmPhaseMinAge } : undefined,
@ -147,24 +148,26 @@ export const Timeline: FunctionComponent<Props> = memo(
return (
<EuiFlexGroup gutterSize="s" direction="column" responsive={false}>
<EuiFlexItem>
<EuiTitle size="s">
<h2>{i18nTexts.title}</h2>
</EuiTitle>
<EuiText size="s" color="subdued">
{i18nTexts.description}
&nbsp;
<LearnMoreLink
docPath={docLinks.links.elasticsearch.ilmPhaseTransitions}
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText"
defaultMessage="Learn about timing"
/>
}
/>
</EuiText>
</EuiFlexItem>
{showTitle && (
<EuiFlexItem>
<EuiTitle size="s">
<h2>{i18nTexts.title}</h2>
</EuiTitle>
<EuiText size="s" color="subdued">
{i18nTexts.description}
&nbsp;
<LearnMoreLink
docPath={docLinks.links.elasticsearch.ilmPhaseTransitions}
text={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText"
defaultMessage="Learn about timing"
/>
}
/>
</EuiText>
</EuiFlexItem>
)}
<EuiFlexItem>
<div
className="ilmTimeline"

View file

@ -34,6 +34,7 @@ import {
useFormIsModified,
} from '../../../shared_imports';
import { toasts } from '../../services/notification';
import { getPoliciesListPath, getPolicyViewPath } from '../../services/navigation';
import { UseField } from './form';
import { savePolicy } from './save_policy';
import {
@ -127,8 +128,9 @@ export const EditPolicy: React.FunctionComponent = () => {
[originalPolicyName, existingPolicies, isClonedPolicy]
);
const backToPolicyList = () => {
history.push('/policies');
const backToPolicyList = (name?: string) => {
const url = name ? getPolicyViewPath(name) : getPoliciesListPath();
history.push(url);
};
const submit = async () => {
@ -141,17 +143,18 @@ export const EditPolicy: React.FunctionComponent = () => {
})
);
} else {
const name = getPolicyName();
setHasSubmittedForm(true);
const success = await savePolicy(
{
...policy,
name: getPolicyName(),
name,
},
isNewPolicy || isClonedPolicy
);
if (success) {
backToPolicyList();
backToPolicyList(name);
}
}
};
@ -305,7 +308,10 @@ export const EditPolicy: React.FunctionComponent = () => {
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelTestPolicy" onClick={backToPolicyList}>
<EuiButtonEmpty
data-test-subj="cancelTestPolicy"
onClick={() => backToPolicyList()}
>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.cancelButton"
defaultMessage="Cancel"

View file

@ -67,7 +67,6 @@ export const createDeserializer =
},
frozen: {
enabled: Boolean(frozen),
dataTierAllocationType: determineDataTierAllocationType(frozen?.actions),
minAgeToMilliSeconds: -1,
},
delete: {

View file

@ -7,11 +7,7 @@
import { i18n } from '@kbn/i18n';
import {
PhaseExceptDelete,
PhaseWithDownsample,
PhaseWithTiming,
} from '../../../../../common/types';
import { PhaseWithDownsample, PhaseWithTiming } from '../../../../../common/types';
import { fieldValidators, FormSchema } from '../../../../shared_imports';
import { defaultIndexPriority } from '../../../constants';
import { CLOUD_DEFAULT_REPO, ROLLOVER_FORM_PATHS } from '../constants';
@ -80,9 +76,7 @@ export const searchableSnapshotFields = {
};
const numberOfReplicasField = {
label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.numberOfReplicasLabel', {
defaultMessage: 'Number of replicas',
}),
label: i18nTexts.editPolicy.numberOfReplicasLabel,
validations: [
{
validator: emptyField(i18nTexts.editPolicy.errors.numberRequired),
@ -95,9 +89,7 @@ const numberOfReplicasField = {
};
const numberOfShardsField = {
label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', {
defaultMessage: 'Number of primary shards',
}),
label: i18nTexts.editPolicy.shrinkNumberOfShardsLabel,
defaultValue: 1,
validations: [
{
@ -133,7 +125,7 @@ const allowWriteAfterShrinkField = {
defaultValue: false,
};
const getPriorityField = (phase: PhaseExceptDelete) => ({
const getPriorityField = (phase: 'hot' | 'warm' | 'cold') => ({
defaultValue: defaultIndexPriority[phase],
label: i18nTexts.editPolicy.indexPriorityFieldLabel,
validations: [
@ -380,9 +372,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
actions: {
rollover: {
max_age: {
label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumAgeLabel', {
defaultMessage: 'Maximum age',
}),
label: i18nTexts.editPolicy.maxAgeLabel,
validations: [
{
validator: rolloverThresholdsValidator,
@ -397,9 +387,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
fieldsToValidateOnChange: rolloverFormPaths,
},
max_docs: {
label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumDocumentsLabel', {
defaultMessage: 'Maximum documents',
}),
label: i18nTexts.editPolicy.maxDocsLabel,
validations: [
{
validator: rolloverThresholdsValidator,
@ -443,9 +431,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
fieldsToValidateOnChange: rolloverFormPaths,
},
max_size: {
label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel', {
defaultMessage: 'Maximum index size',
}),
label: i18nTexts.editPolicy.maxSizeLabel,
validations: [
{
validator: rolloverThresholdsValidator,
@ -505,12 +491,6 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({
frozen: {
min_age: getMinAgeField('frozen'),
actions: {
allocate: {
number_of_replicas: numberOfReplicasField,
},
set_priority: {
priority: getPriorityField('frozen'),
},
searchable_snapshot: searchableSnapshotFields,
},
},

View file

@ -9,7 +9,10 @@ import { i18n } from '@kbn/i18n';
export const i18nTexts = {
editPolicy: {
shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.enableShrinkLabel', {
shrinkActionLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.actionLabel', {
defaultMessage: 'Shrink',
}),
shrinkToggleLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.enableShrinkLabel', {
defaultMessage: 'Shrink index',
}),
shrinkCountLabel: i18n.translate(
@ -18,6 +21,12 @@ export const i18nTexts = {
defaultMessage: 'Configure shard count',
}
),
shrinkNumberOfShardsLabel: i18n.translate(
'xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel',
{
defaultMessage: 'Number of primary shards',
}
),
shrinkSizeLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardSizeLabel',
{
@ -54,12 +63,18 @@ export const i18nTexts = {
),
},
},
forceMergeLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.forceMerge.enableText', {
defaultMessage: 'Force merge',
}),
forceMergeEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.enableLabel', {
defaultMessage: 'Force merge data',
}),
readonlyEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.readonlyFieldLabel', {
defaultMessage: 'Make index read only',
}),
readonlyLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.readonlyTitle', {
defaultMessage: 'Read only',
}),
downsampleEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.downsampleFieldLabel', {
defaultMessage: 'Enable downsampling',
}),
@ -116,12 +131,6 @@ export const i18nTexts = {
defaultMessage: 'Snapshot repository',
}
),
searchableSnapshotsStorageFieldLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel',
{
defaultMessage: 'Searchable snapshot storage',
}
),
maxPrimaryShardSizeLabel: i18n.translate(
'xpack.indexLifecycleMgmt.hotPhase.maximumPrimaryShardSizeLabel',
{
@ -140,6 +149,64 @@ export const i18nTexts = {
defaultMessage: 'Maximum shard size units',
}
),
maxAgeLabel: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumAgeLabel', {
defaultMessage: 'Maximum age',
}),
maxDocsLabel: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumDocumentsLabel', {
defaultMessage: 'Maximum documents',
}),
maxSizeLabel: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel', {
defaultMessage: 'Maximum index size',
}),
downsampleLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.downsampleTitle', {
defaultMessage: 'Downsample',
}),
minAgeLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.minimumAge.minimumAgeFieldLabel',
{ defaultMessage: 'Move data into phase when' }
),
rolloverLabel: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle', {
defaultMessage: 'Rollover',
}),
rolloverToolTipDescription: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.minimumAge.rolloverToolTipDescription',
{
defaultMessage:
'Data age is calculated from rollover. Rollover is configured in the hot phase.',
}
),
minAgeUnitFieldSuffix: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.minimumAge.minimumAgeFieldSuffixLabel',
{ defaultMessage: 'old' }
),
replicasLabel: i18n.translate('xpack.indexLifecycleMgmt.numberOfReplicas.formRowTitle', {
defaultMessage: 'Replicas',
}),
numberOfReplicasLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.numberOfReplicasLabel',
{
defaultMessage: 'Number of replicas',
}
),
dataAllocationLabel: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', {
defaultMessage: 'Data allocation',
}),
searchableSnapshotLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.fullyMountedSearchableSnapshotField.title',
{
defaultMessage: 'Searchable snapshot',
}
),
waitForSnapshotLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.deletePhase.waitForSnapshotTitle',
{ defaultMessage: 'Wait for snapshot policy' }
),
deleteSearchableSnapshotLabel: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.deletePhase.deleteSearchableSnapshotTitle',
{
defaultMessage: 'Delete searchable snapshot',
}
),
errors: {
numberRequired: i18n.translate(
'xpack.indexLifecycleMgmt.editPolicy.errors.numberRequiredErrorMessage',

View file

@ -78,7 +78,7 @@ interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, Dow
readonlyEnabled: boolean;
}
interface FrozenPhaseMetaFields extends DataAllocationMetaFields, MinAgeField {
interface FrozenPhaseMetaFields extends MinAgeField {
enabled: boolean;
}

View file

@ -0,0 +1,32 @@
/*
* 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 from 'react';
import { EuiBadge, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const deprecatedPolicyTooltips = {
badge: i18n.translate('xpack.indexLifecycleMgmt.policyTable.templateBadgeType.deprecatedLabel', {
defaultMessage: 'Deprecated',
}),
badgeTooltip: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.templateBadgeType.deprecatedDescription',
{
defaultMessage:
'This policy is no longer supported and might be removed in a future release. Instead, use one of the other policies available or create a new one.',
}
),
};
export const DeprecatedPolicyBadge = () => {
return (
<EuiToolTip content={deprecatedPolicyTooltips.badgeTooltip}>
<EuiBadge color="warning" data-test-subj="deprecatedPolicyBadge">
{deprecatedPolicyTooltips.badge}
</EuiBadge>
</EuiToolTip>
);
};

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
export { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal';
export { ConfirmDelete } from './confirm_delete';
export { DeprecatedPolicyBadge } from './deprecated_policy_badge';
export { ListActionHandler } from './list_action_handler';
export { ManagedPolicyBadge } from './managed_policy_badge';
export { PolicyTable } from './policy_table';

View file

@ -7,14 +7,14 @@
import React from 'react';
import { usePolicyListContext } from '../policy_list_context';
import { IndexTemplatesFlyout } from '../../../components/index_templates_flyout';
import { ConfirmDelete } from './confirm_delete';
import { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal';
import { IndexTemplatesFlyout } from '../../../components';
import { ViewPolicyFlyout } from '../policy_flyout';
import { ConfirmDelete, AddPolicyToTemplateConfirmModal } from '.';
interface Props {
updatePolicies: () => void;
deletePolicyCallback: () => void;
}
export const ListActionHandler: React.FunctionComponent<Props> = ({ updatePolicies }) => {
export const ListActionHandler: React.FunctionComponent<Props> = ({ deletePolicyCallback }) => {
const { listAction, setListAction } = usePolicyListContext();
if (listAction?.actionType === 'viewIndexTemplates') {
return (
@ -32,7 +32,7 @@ export const ListActionHandler: React.FunctionComponent<Props> = ({ updatePolici
<ConfirmDelete
policyToDelete={listAction.selectedPolicy}
callback={() => {
updatePolicies();
deletePolicyCallback();
setListAction(null);
}}
onCancel={() => {
@ -58,5 +58,10 @@ export const ListActionHandler: React.FunctionComponent<Props> = ({ updatePolici
/>
);
}
if (listAction?.actionType === 'viewPolicy') {
return <ViewPolicyFlyout policy={listAction.selectedPolicy} />;
}
return null;
};

View file

@ -0,0 +1,33 @@
/*
* 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 from 'react';
import { EuiBadge, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const managedPolicyTooltips = {
badge: i18n.translate('xpack.indexLifecycleMgmt.policyTable.templateBadgeType.managedLabel', {
defaultMessage: 'Managed',
}),
badgeTooltip: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.templateBadgeType.managedDescription',
{
defaultMessage:
'This policy is preconfigured and managed by Elastic; editing or deleting this policy might break Kibana.',
}
),
};
export const ManagedPolicyBadge = () => {
return (
<EuiToolTip content={managedPolicyTooltips.badgeTooltip}>
<EuiBadge color="hollow" data-test-subj="managedPolicyBadge">
{managedPolicyTooltips.badge}
</EuiBadge>
</EuiToolTip>
);
};

View file

@ -11,8 +11,6 @@ import {
EuiLink,
EuiInMemoryTable,
EuiToolTip,
EuiButtonIcon,
EuiBadge,
EuiFlexItem,
EuiSwitch,
EuiSearchBarProps,
@ -28,59 +26,30 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { useEuiTablePersist } from '@kbn/shared-ux-table-persist';
import { hasLinkedIndices } from '../../../lib/policies';
import { useStateWithLocalStorage } from '../../../lib/settings_local_storage';
import { PolicyFromES } from '../../../../../common/types';
import { useKibana } from '../../../../shared_imports';
import { getIndicesListPath, getPolicyEditPath } from '../../../services/navigation';
import {
getIndicesListPath,
getPolicyEditPath,
getPolicyViewPath,
} from '../../../services/navigation';
import { trackUiMetric } from '../../../services/ui_metric';
import { UIM_VIEW_CLICK } from '../../../constants';
import { UIM_EDIT_CLICK } from '../../../constants';
import { hasLinkedIndices } from '../../../lib/policies';
import { usePolicyListContext } from '../policy_list_context';
import { ManagedPolicyBadge, DeprecatedPolicyBadge } from '.';
import { useIsReadOnly } from '../../../lib/use_is_read_only';
const actionTooltips = {
deleteEnabled: i18n.translate('xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText', {
defaultMessage: 'Delete policy',
}),
deleteDisabled: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip',
{
defaultMessage: 'You cannot delete a policy that is being used by an index',
}
),
viewIndices: i18n.translate('xpack.indexLifecycleMgmt.policyTable.viewIndicesButtonText', {
defaultMessage: 'View indices linked to policy',
}),
addIndexTemplate: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText',
viewIndexTemplates: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.viewIndexTemplatesButtonText',
{
defaultMessage: 'Add policy to index template',
}
),
};
const managedPolicyTooltips = {
badge: i18n.translate('xpack.indexLifecycleMgmt.policyTable.templateBadgeType.managedLabel', {
defaultMessage: 'Managed',
}),
badgeTooltip: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.templateBadgeType.managedDescription',
{
defaultMessage:
'This policy is preconfigured and managed by Elastic; editing or deleting this policy might break Kibana.',
}
),
};
const deprecatedPolicyTooltips = {
badge: i18n.translate('xpack.indexLifecycleMgmt.policyTable.templateBadgeType.deprecatedLabel', {
defaultMessage: 'Deprecated',
}),
badgeTooltip: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.templateBadgeType.deprecatedDescription',
{
defaultMessage:
'This policy is no longer supported and might be removed in a future release. Instead, use one of the other policies available or create a new one.',
defaultMessage: 'View index templates linked to policy',
}
),
};
@ -94,7 +63,7 @@ const PAGE_SIZE_OPTIONS = [10, 25, 50];
export const PolicyTable: React.FunctionComponent<Props> = ({ policies }) => {
const [query, setQuery] = useState('');
const isReadOnly = useIsReadOnly();
const history = useHistory();
const {
services: { getUrlForApp },
@ -188,8 +157,8 @@ export const PolicyTable: React.FunctionComponent<Props> = ({ policies }) => {
<EuiLink
className="eui-textBreakAll"
data-test-subj="policyTablePolicyNameLink"
{...reactRouterNavigate(history, getPolicyEditPath(value), () =>
trackUiMetric(METRIC_TYPE.CLICK, UIM_EDIT_CLICK)
{...reactRouterNavigate(history, getPolicyViewPath(value), () =>
trackUiMetric(METRIC_TYPE.CLICK, UIM_VIEW_CLICK)
)}
>
{value}
@ -198,22 +167,14 @@ export const PolicyTable: React.FunctionComponent<Props> = ({ policies }) => {
{isDeprecated && (
<>
&nbsp;
<EuiToolTip content={deprecatedPolicyTooltips.badgeTooltip}>
<EuiBadge color="warning" data-test-subj="deprecatedPolicyBadge">
{deprecatedPolicyTooltips.badge}
</EuiBadge>
</EuiToolTip>
<DeprecatedPolicyBadge />
</>
)}
{isManaged && (
<>
&nbsp;
<EuiToolTip content={managedPolicyTooltips.badgeTooltip}>
<EuiBadge color="hollow" data-test-subj="managedPolicyBadge">
{managedPolicyTooltips.badge}
</EuiBadge>
</EuiToolTip>
<ManagedPolicyBadge />
</>
)}
</>
@ -229,7 +190,7 @@ export const PolicyTable: React.FunctionComponent<Props> = ({ policies }) => {
sortable: ({ indexTemplates }) => (indexTemplates ?? []).length,
render: (value: string[], policy: PolicyFromES) => {
return value && value.length > 0 ? (
<EuiToolTip content={actionTooltips.viewIndices} position="left">
<EuiToolTip content={actionTooltips.viewIndexTemplates} position="left">
<EuiButtonEmpty
flush="both"
data-test-subj="viewIndexTemplates"
@ -273,50 +234,77 @@ export const PolicyTable: React.FunctionComponent<Props> = ({ policies }) => {
return value ? moment(value).format('MMM D, YYYY') : value;
},
},
{
];
if (!isReadOnly) {
columns.push({
actions: [
{
render: (policy: PolicyFromES) => {
return (
<EuiToolTip content={actionTooltips.addIndexTemplate}>
<EuiButtonIcon
data-test-subj="addPolicyToTemplate"
onClick={() =>
setListAction({ selectedPolicy: policy, actionType: 'addIndexTemplate' })
}
iconType="plusInCircle"
aria-label={actionTooltips.addIndexTemplate}
/>
</EuiToolTip>
);
},
isPrimary: true,
name: i18n.translate('xpack.indexLifecycleMgmt.policyTable.editActionLabel', {
defaultMessage: 'Edit',
}),
description: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.editActionDescription',
{
defaultMessage: 'Edit this policy',
}
),
type: 'icon',
icon: 'pencil',
onClick: ({ name }) => history.push(getPolicyEditPath(name)),
'data-test-subj': 'editPolicy',
},
{
render: (policy: PolicyFromES, enabled: boolean) => {
return (
<EuiToolTip
content={enabled ? actionTooltips.deleteEnabled : actionTooltips.deleteDisabled}
>
<EuiButtonIcon
data-test-subj="deletePolicy"
onClick={() =>
setListAction({ selectedPolicy: policy, actionType: 'deletePolicy' })
name: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addToIndexTemplateActionLabel',
{
defaultMessage: 'Add to index template',
}
),
description: i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.addToIndexTemplateActionDescription',
{ defaultMessage: 'Add policy to index template' }
),
type: 'icon',
icon: 'plusInCircle',
onClick: (policy) =>
setListAction({ selectedPolicy: policy, actionType: 'addIndexTemplate' }),
'data-test-subj': 'addPolicyToTemplate',
},
{
isPrimary: true,
name: i18n.translate('xpack.indexLifecycleMgmt.policyTable.deleteActionLabel', {
defaultMessage: 'Delete',
}),
description: (policy) => {
return hasLinkedIndices(policy)
? i18n.translate(
'xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip',
{
defaultMessage: 'You cannot delete a policy that is being used by an index',
}
iconType="trash"
aria-label={actionTooltips.deleteEnabled}
disabled={!enabled}
/>
</EuiToolTip>
);
)
: i18n.translate('xpack.indexLifecycleMgmt.policyTable.deleteActionDescription', {
defaultMessage: 'Delete this policy',
});
},
enabled: (policy: PolicyFromES) => !hasLinkedIndices(policy),
type: 'icon',
icon: 'trash',
color: 'danger',
onClick: (policy) =>
setListAction({ selectedPolicy: policy, actionType: 'deletePolicy' }),
enabled: (policy) => !hasLinkedIndices(policy),
'data-test-subj': 'deletePolicy',
},
],
name: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.actionsHeader', {
defaultMessage: 'Actions',
}),
},
];
'data-test-subj': 'policyActionsCollapsedButton',
});
}
return (
<EuiInMemoryTable

View file

@ -0,0 +1,37 @@
/*
* 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 from 'react';
import { PhaseDescription } from './phase_description';
import { Phases } from '../../../../../common/types';
import {
MinAge,
Replicas,
Downsample,
Readonly,
IndexPriority,
DataAllocation,
SearchableSnapshot,
} from './components';
export const ColdPhase = ({ phases }: { phases: Phases }) => {
return (
<PhaseDescription
phase={'cold'}
phases={phases}
components={[
MinAge,
SearchableSnapshot,
Replicas,
Downsample,
Readonly,
DataAllocation,
IndexPriority,
]}
/>
);
};

View file

@ -0,0 +1,39 @@
/*
* 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, { ReactNode } from 'react';
import {
EuiDescriptionListDescription,
EuiDescriptionListTitle,
EuiSpacer,
EuiText,
} from '@elastic/eui';
export const ActionDescription = ({
title,
descriptionItems,
}: {
title: string;
descriptionItems?: string[] | ReactNode[];
}) => {
return (
<>
<EuiDescriptionListTitle>{title}</EuiDescriptionListTitle>
{descriptionItems && (
<EuiDescriptionListDescription>
{descriptionItems.map((descriptionItem, index) => (
<EuiText color="subdued" key={index}>
<EuiSpacer size="s" />
{descriptionItem}
</EuiText>
))}
</EuiDescriptionListDescription>
)}
<EuiSpacer size="m" />
</>
);
};

View file

@ -0,0 +1,85 @@
/*
* 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 from 'react';
import { EuiBadge, EuiCode } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
AllocateAction,
PhaseWithAllocation,
SerializedColdPhase,
SerializedWarmPhase,
} from '../../../../../../common/types';
import { determineDataTierAllocationType } from '../../../../lib';
import type { ActionComponentProps } from './types';
import { ActionDescription } from './action_description';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
const getAllocationDescription = (
type: ReturnType<typeof determineDataTierAllocationType>,
phase: PhaseWithAllocation,
allocate?: AllocateAction
) => {
if (type === 'none') {
return i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.dataAllocationDisabledLabel', {
defaultMessage: 'Disabled',
});
}
if (type === 'node_roles') {
const label =
phase === 'warm'
? i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.dataAllocationWarmNodesLabel', {
defaultMessage: 'Using warm nodes',
})
: i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.dataAllocationColdNodesdLabel', {
defaultMessage: 'Using cold nodes',
});
return (
<>
{label}{' '}
<EuiBadge color="success">
<FormattedMessage
id="xpack.indexLifecycleMgmt.policyFlyout.recommendedDataAllocationLabel"
defaultMessage="Recommended"
/>
</EuiBadge>
</>
);
}
if (type === 'node_attrs') {
return (
<>
{i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.dataAllocationAttributtesLabel', {
defaultMessage: 'Node attributes',
})}
{': '}
<EuiCode>{JSON.stringify(allocate?.require)}</EuiCode>
</>
);
}
};
export const DataAllocation = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const allocate = (phaseConfig as SerializedWarmPhase | SerializedColdPhase)?.actions.allocate;
const migrate = (phaseConfig as SerializedWarmPhase | SerializedColdPhase)?.actions.migrate;
const allocationType = determineDataTierAllocationType({ allocate, migrate });
const allocationDescription = getAllocationDescription(
allocationType,
phase as PhaseWithAllocation,
allocate
);
return (
<ActionDescription
title={i18nTexts.editPolicy.dataAllocationLabel}
descriptionItems={allocationDescription ? [allocationDescription] : []}
/>
);
};

View file

@ -0,0 +1,27 @@
/*
* 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 from 'react';
import { i18nTexts as i18nTextsFlyout } from './i18n_texts';
import { SerializedDeletePhase } from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { ActionDescription } from './action_description';
import type { ActionComponentProps } from './types';
export const DeleteSearchableSnapshot = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const deleteSearchableSnapshot = (phaseConfig as SerializedDeletePhase)?.actions.delete
?.delete_searchable_snapshot;
return (
<ActionDescription
title={i18nTexts.editPolicy.deleteSearchableSnapshotLabel}
descriptionItems={[
Boolean(deleteSearchableSnapshot) ? i18nTextsFlyout.yes : i18nTextsFlyout.no,
]}
/>
);
};

View file

@ -0,0 +1,27 @@
/*
* 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 from 'react';
import { PhaseWithDownsample } from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { ActionDescription } from './action_description';
import type { ActionComponentProps } from './types';
export const Downsample = ({ phase, phases }: ActionComponentProps) => {
const downsample = phases[phase as PhaseWithDownsample]?.actions.downsample;
return downsample ? (
<ActionDescription
title={i18nTexts.editPolicy.downsampleLabel}
descriptionItems={[
<>
{`${i18nTexts.editPolicy.downsampleIntervalFieldLabel}: `}
<strong>{downsample.fixed_interval}</strong>
</>,
]}
/>
) : null;
};

View file

@ -0,0 +1,38 @@
/*
* 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 from 'react';
import { SerializedHotPhase, SerializedWarmPhase } from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { i18nTexts as i18nTextsFlyout } from './i18n_texts';
import type { ActionComponentProps } from './types';
import { ActionDescription } from './action_description';
export const Forcemerge = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const forcemerge = (phaseConfig as SerializedHotPhase | SerializedWarmPhase)?.actions.forcemerge;
return forcemerge ? (
<ActionDescription
title={i18nTexts.editPolicy.forceMergeLabel}
descriptionItems={[
<>
{`${i18nTexts.editPolicy.maxNumSegmentsFieldLabel}: `}
<strong>{forcemerge.max_num_segments}</strong>
</>,
<>
{`${i18nTexts.editPolicy.bestCompressionFieldLabel}: `}
<strong>
{forcemerge.index_codec === 'best_compression'
? i18nTextsFlyout.yes
: i18nTextsFlyout.no}
</strong>
</>,
]}
/>
) : null;
};

View file

@ -0,0 +1,17 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const i18nTexts = {
yes: i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.yesLabel', {
defaultMessage: 'Yes',
}),
no: i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.noLabel', {
defaultMessage: 'No',
}),
};

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
export { Rollover } from './rollover';
export { MinAge } from './min_age';
export { Forcemerge } from './forcemerge';
export { Shrink } from './shrink';
export { SearchableSnapshot } from './searchable_snapshot';
export { Downsample } from './downsample';
export { Readonly } from './readonly';
export { IndexPriority } from './index_priority';
export { Replicas } from './replicas';
export { DataAllocation } from './data_allocation';
export { WaitForSnapshot } from './wait_for_snapshot';
export { DeleteSearchableSnapshot } from './delete_searchable_snapshot';

View file

@ -0,0 +1,29 @@
/*
* 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 from 'react';
import {
SerializedHotPhase,
SerializedWarmPhase,
SerializedColdPhase,
} from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { ActionDescription } from './action_description';
import type { ActionComponentProps } from './types';
export const IndexPriority = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const indexPriority = (
phaseConfig as SerializedHotPhase | SerializedWarmPhase | SerializedColdPhase
)?.actions.set_priority;
return indexPriority ? (
<ActionDescription
title={i18nTexts.editPolicy.indexPriorityFieldLabel}
descriptionItems={[indexPriority.priority]}
/>
) : null;
};

View file

@ -0,0 +1,21 @@
/*
* 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 from 'react';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { ActionDescription } from './action_description';
import type { ActionComponentProps } from './types';
export const MinAge = ({ phase, phases }: ActionComponentProps) => {
const minAge = phases[phase]?.min_age;
return minAge ? (
<ActionDescription
title={i18nTexts.editPolicy.minAgeLabel}
descriptionItems={[`${minAge} ${i18nTexts.editPolicy.minAgeUnitFieldSuffix}`]}
/>
) : null;
};

View file

@ -0,0 +1,23 @@
/*
* 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 from 'react';
import {
SerializedColdPhase,
SerializedHotPhase,
SerializedWarmPhase,
} from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { ActionDescription } from './action_description';
import type { ActionComponentProps } from './types';
export const Readonly = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const readonly = (phaseConfig as SerializedHotPhase | SerializedWarmPhase | SerializedColdPhase)
?.actions.readonly;
return readonly ? <ActionDescription title={i18nTexts.editPolicy.readonlyLabel} /> : null;
};

View file

@ -0,0 +1,28 @@
/*
* 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 from 'react';
import { PhaseWithAllocation } from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import type { ActionComponentProps } from './types';
import { ActionDescription } from './action_description';
export const Replicas = ({ phase, phases }: ActionComponentProps) => {
const allocate = phases[phase as PhaseWithAllocation]?.actions.allocate;
return allocate?.number_of_replicas !== undefined ? (
<ActionDescription
title={i18nTexts.editPolicy.replicasLabel}
descriptionItems={[
<>
{`${i18nTexts.editPolicy.numberOfReplicasLabel}: `}
<strong>{allocate.number_of_replicas}</strong>
</>,
]}
/>
) : null;
};

View file

@ -0,0 +1,76 @@
/*
* 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 from 'react';
import { EuiBadge } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { SerializedHotPhase } from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { ActionDescription } from './action_description';
import type { ActionComponentProps } from './types';
export const Rollover = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const rollover = (phaseConfig as SerializedHotPhase)?.actions.rollover;
const descriptionItems = [];
if (rollover?.max_primary_shard_size) {
descriptionItems.push(
<>
{`${i18nTexts.editPolicy.maxPrimaryShardSizeLabel}: `}
<strong>{rollover.max_primary_shard_size}</strong>
</>
);
}
if (rollover?.max_primary_shard_docs) {
descriptionItems.push(
<>
{`${i18nTexts.editPolicy.maxPrimaryShardDocsLabel}: `}
<strong>{rollover.max_primary_shard_docs}</strong>
</>
);
}
if (rollover?.max_age) {
descriptionItems.push(
<>
{`${i18nTexts.editPolicy.maxAgeLabel}: `}
<strong>{rollover.max_age}</strong>
</>
);
}
if (rollover?.max_docs) {
descriptionItems.push(
<>
{`${i18nTexts.editPolicy.maxDocsLabel}: `}
<strong>{rollover.max_docs}</strong>
</>
);
}
if (rollover?.max_size) {
descriptionItems.push(
<>
{`${i18nTexts.editPolicy.maxSizeLabel}: `}
<strong>{rollover.max_size}</strong>{' '}
<EuiBadge color="warning">
<FormattedMessage
id="xpack.indexLifecycleMgmt.policyFlyout.deprecatedLabel"
defaultMessage="Deprecated"
/>
</EuiBadge>
</>
);
}
return rollover ? (
<ActionDescription
title={i18nTexts.editPolicy.rolloverLabel}
descriptionItems={descriptionItems}
/>
) : null;
};

View file

@ -0,0 +1,35 @@
/*
* 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 from 'react';
import { EuiCode } from '@elastic/eui';
import {
SerializedColdPhase,
SerializedFrozenPhase,
SerializedHotPhase,
} from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { ActionDescription } from './action_description';
import type { ActionComponentProps } from './types';
export const SearchableSnapshot = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const searchableSnapshot = (
phaseConfig as SerializedHotPhase | SerializedColdPhase | SerializedFrozenPhase
).actions?.searchable_snapshot;
return searchableSnapshot ? (
<ActionDescription
title={i18nTexts.editPolicy.searchableSnapshotLabel}
descriptionItems={[
<>
{`${i18nTexts.editPolicy.searchableSnapshotsRepoFieldLabel}: `}
<EuiCode>{searchableSnapshot.snapshot_repository}</EuiCode>
</>,
]}
/>
) : null;
};

View file

@ -0,0 +1,49 @@
/*
* 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 from 'react';
import { SerializedHotPhase, SerializedWarmPhase } from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { i18nTexts as i18nTextsFlyout } from './i18n_texts';
import type { ActionComponentProps } from './types';
import { ActionDescription } from './action_description';
export const Shrink = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const shrink = (phaseConfig as SerializedHotPhase | SerializedWarmPhase)?.actions.shrink;
const descriptionItems = [];
if (shrink?.number_of_shards) {
descriptionItems.push(
<>
{`${i18nTexts.editPolicy.shrinkNumberOfShardsLabel}: `}
<strong>{shrink.number_of_shards}</strong>
</>
);
}
if (shrink?.max_primary_shard_size) {
descriptionItems.push(
<>
{`${i18nTexts.editPolicy.maxPrimaryShardSizeLabel}: `}
<strong>{shrink.max_primary_shard_size}</strong>
</>
);
}
descriptionItems.push(
<>
{`${i18nTexts.editPolicy.allowWriteAfterShrinkLabel}: `}
<strong>{shrink?.allow_write_after_shrink ? i18nTextsFlyout.yes : i18nTextsFlyout.no}</strong>
</>
);
return shrink ? (
<ActionDescription
title={i18nTexts.editPolicy.shrinkActionLabel}
descriptionItems={descriptionItems}
/>
) : null;
};

View file

@ -0,0 +1,13 @@
/*
* 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 type { Phase, Phases } from '../../../../../../common/types';
export interface ActionComponentProps {
phase: Phase;
phases: Phases;
}

View file

@ -0,0 +1,24 @@
/*
* 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 from 'react';
import { EuiCode } from '@elastic/eui';
import { SerializedDeletePhase } from '../../../../../../common/types';
import { i18nTexts } from '../../../edit_policy/i18n_texts';
import { ActionDescription } from './action_description';
import type { ActionComponentProps } from './types';
export const WaitForSnapshot = ({ phase, phases }: ActionComponentProps) => {
const phaseConfig = phases[phase];
const waitForSnapshot = (phaseConfig as SerializedDeletePhase).actions?.wait_for_snapshot;
return waitForSnapshot ? (
<ActionDescription
title={i18nTexts.editPolicy.waitForSnapshotLabel}
descriptionItems={[<EuiCode>{waitForSnapshot.policy}</EuiCode>]}
/>
) : null;
};

View file

@ -0,0 +1,28 @@
/*
* 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.
*/
/*
* 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 from 'react';
import { PhaseDescription } from './phase_description';
import { Phases } from '../../../../../common/types';
import { MinAge, WaitForSnapshot, DeleteSearchableSnapshot } from './components';
export const DeletePhase = ({ phases }: { phases: Phases }) => {
return (
<PhaseDescription
phase={'delete'}
phases={phases}
components={[MinAge, WaitForSnapshot, DeleteSearchableSnapshot]}
/>
);
};

View file

@ -0,0 +1,24 @@
/*
* 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.
*/
/*
* 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 from 'react';
import { PhaseDescription } from './phase_description';
import { Phases } from '../../../../../common/types';
import { MinAge, SearchableSnapshot } from './components';
export const FrozenPhase = ({ phases }: { phases: Phases }) => {
return (
<PhaseDescription phase={'frozen'} phases={phases} components={[MinAge, SearchableSnapshot]} />
);
};

View file

@ -0,0 +1,37 @@
/*
* 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 from 'react';
import { PhaseDescription } from './phase_description';
import { Phases } from '../../../../../common/types';
import {
Rollover,
Forcemerge,
Shrink,
SearchableSnapshot,
Downsample,
Readonly,
IndexPriority,
} from './components';
export const HotPhase = ({ phases }: { phases: Phases }) => {
return (
<PhaseDescription
phase={'hot'}
phases={phases}
components={[
Rollover,
Forcemerge,
Shrink,
SearchableSnapshot,
Downsample,
Readonly,
IndexPriority,
]}
/>
);
};

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { ViewPolicyFlyout } from './view_policy_flyout';

View file

@ -0,0 +1,43 @@
/*
* 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, { ComponentType } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiDescriptionList } from '@elastic/eui';
import { PhaseIndicator } from './phase_indicator';
import { ActionComponentProps } from './components/types';
import { i18nTexts } from '../../edit_policy/i18n_texts';
export const PhaseDescription = ({
phase,
phases,
components,
}: ActionComponentProps & {
components: Array<ComponentType<ActionComponentProps>>;
}) => {
const title = i18nTexts.editPolicy.titles[phase];
return (
<>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<PhaseIndicator phase={phase} />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="s">
<h2>{title}</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiDescriptionList>
{components.map((Component, index) => (
<Component phase={phase} phases={phases} key={index} />
))}
</EuiDescriptionList>
<EuiSpacer size="l" />
</>
);
};

View file

@ -0,0 +1,32 @@
/*
* 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 from 'react';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import type { Phase } from '../../../../../common/types';
const phaseToIndicatorColors = {
hot: euiThemeVars.euiColorVis9,
warm: euiThemeVars.euiColorVis5,
cold: euiThemeVars.euiColorVis1,
frozen: euiThemeVars.euiColorVis4,
delete: euiThemeVars.euiColorLightShade,
};
export const PhaseIndicator = ({ phase }: { phase: Phase }) => {
return (
<div
css={css`
width: 16px;
height: 8px;
display: inline-block;
border-radius: 4px;
background-color: ${phaseToIndicatorColors[phase]};
`}
/>
);
};

View file

@ -0,0 +1,31 @@
/*
* 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 from 'react';
import { PolicyFromES } from '../../../../../common/types';
import { Timeline as ViewComponent } from '../../edit_policy/components/timeline/timeline';
export const Timeline = ({ policy }: { policy: PolicyFromES }) => {
const hasDeletePhase = Boolean(policy.policy.phases.delete);
const isUsingRollover = Boolean(policy.policy.phases.hot?.actions.rollover);
const warmPhaseMinAge = policy.policy.phases.warm?.min_age;
const coldPhaseMinAge = policy.policy.phases.cold?.min_age;
const frozenPhaseMinAge = policy.policy.phases.frozen?.min_age;
const deletePhaseMinAge = policy.policy.phases.delete?.min_age;
return (
<ViewComponent
showTitle={false}
hasDeletePhase={hasDeletePhase}
isUsingRollover={isUsingRollover}
hotPhaseMinAge={undefined}
warmPhaseMinAge={warmPhaseMinAge}
coldPhaseMinAge={coldPhaseMinAge}
frozenPhaseMinAge={frozenPhaseMinAge}
deletePhaseMinAge={deletePhaseMinAge}
/>
);
};

View file

@ -0,0 +1,204 @@
/*
* 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, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
EuiButton,
EuiButtonEmpty,
EuiContextMenu,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiIcon,
EuiPopover,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import { PolicyFromES } from '../../../../../common/types';
import { trackUiMetric } from '../../../services/ui_metric';
import { hasLinkedIndices } from '../../../lib/policies';
import { getPoliciesListPath, getPolicyEditPath } from '../../../services/navigation';
import { UIM_EDIT_CLICK } from '../../../constants';
import { useIsReadOnly } from '../../../lib/use_is_read_only';
import { usePolicyListContext } from '../policy_list_context';
import { DeprecatedPolicyBadge, ManagedPolicyBadge } from '../components';
import { HotPhase } from './hot_phase';
import { WarmPhase } from './warm_phase';
import { Timeline } from './timeline';
import { ColdPhase } from './cold_phase';
import { DeletePhase } from './delete_phase';
import { FrozenPhase } from './frozen_phase';
export const ViewPolicyFlyout = ({ policy }: { policy: PolicyFromES }) => {
const isReadOnly = useIsReadOnly();
const { setListAction } = usePolicyListContext();
const history = useHistory();
const onClose = () => {
history.push(getPoliciesListPath());
};
const onEdit = (policyName: string) => {
trackUiMetric(METRIC_TYPE.CLICK, UIM_EDIT_CLICK);
history.push(getPolicyEditPath(policyName));
};
const [showPopover, setShowPopover] = useState(false);
const actionMenuItems = [
/**
* Edit policy
*/
{
name: i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.editActionLabel', {
defaultMessage: 'Edit',
}),
icon: <EuiIcon type="pencil" />,
onClick: () => onEdit(policy.name),
},
/**
* Add policy to index template
*/
{
name: i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.addToIndexTemplate', {
defaultMessage: 'Add to index template',
}),
icon: <EuiIcon type="plusInCircle" />,
onClick: () => setListAction({ selectedPolicy: policy, actionType: 'addIndexTemplate' }),
},
];
/**
* Delete policy
*/
if (!hasLinkedIndices(policy)) {
actionMenuItems.push({
name: i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.deleteActionLabel', {
defaultMessage: 'Delete',
}),
icon: <EuiIcon type="trash" />,
onClick: () => {
setShowPopover(false);
setListAction({ selectedPolicy: policy, actionType: 'deletePolicy' });
},
});
}
const managePolicyButton = (
<EuiButton
aria-label={i18n.translate(
'xpack.indexLifecycleMgmt.policyFlyout.managePolicyActionsAriaLabel',
{
defaultMessage: 'Manage policy',
}
)}
onClick={() => setShowPopover((previousBool) => !previousBool)}
iconType="arrowUp"
iconSide="right"
fill
data-test-subj="managePolicyButton"
>
{i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.managePolicyButtonLabel', {
defaultMessage: 'Manage',
})}
</EuiButton>
);
return (
<EuiFlyout
onClose={onClose}
closeButtonProps={{
'data-test-subj': 'policyFlyoutCloseButton',
}}
>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle data-test-subj="policyFlyoutTitle">
<h1>{policy.name}</h1>
</EuiTitle>
</EuiFlexItem>
{policy.policy.deprecated ? (
<EuiFlexItem grow={false}>
{' '}
<DeprecatedPolicyBadge />
</EuiFlexItem>
) : null}
{policy.policy?._meta?.managed ? (
<EuiFlexItem grow={false}>
{' '}
<ManagedPolicyBadge />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{/* Timeline */}
<Timeline policy={policy} />
<EuiSpacer size="xxl" />
{/* Hot phase */}
{policy.policy.phases.hot && <HotPhase phases={policy.policy.phases} />}
{/* Warm phase */}
{policy.policy.phases.warm && <WarmPhase phases={policy.policy.phases} />}
{/* Cold phase */}
{policy.policy.phases.cold && <ColdPhase phases={policy.policy.phases} />}
{/* Frozen phase */}
{policy.policy.phases.frozen && <FrozenPhase phases={policy.policy.phases} />}
{/* Delete phase */}
{policy.policy.phases.delete && <DeletePhase phases={policy.policy.phases} />}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
{i18n.translate('xpack.indexLifecycleMgmt.policyFlyout.closeButtonLabel', {
defaultMessage: 'Close',
})}
</EuiButtonEmpty>
</EuiFlexItem>
{!isReadOnly && (
<EuiFlexGroup gutterSize="none" alignItems="center" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiPopover
isOpen={showPopover}
closePopover={() => setShowPopover(false)}
button={managePolicyButton}
panelPaddingSize="none"
repositionOnScroll
>
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: i18n.translate(
'xpack.indexLifecycleMgmt.policyFlyout.managePolicyTitle',
{
defaultMessage: 'Options',
}
),
items: actionMenuItems,
},
]}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -0,0 +1,39 @@
/*
* 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 from 'react';
import { PhaseDescription } from './phase_description';
import { Phases } from '../../../../../common/types';
import {
MinAge,
Replicas,
Forcemerge,
Shrink,
Downsample,
Readonly,
IndexPriority,
DataAllocation,
} from './components';
export const WarmPhase = ({ phases }: { phases: Phases }) => {
return (
<PhaseDescription
phase={'warm'}
phases={phases}
components={[
MinAge,
Replicas,
Shrink,
Forcemerge,
Downsample,
Readonly,
DataAllocation,
IndexPriority,
]}
/>
);
};

View file

@ -5,16 +5,18 @@
* 2.0.
*/
import React, { Fragment } from 'react';
import React, { Fragment, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiSpacer, EuiPageHeader, EuiPageTemplate } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { usePolicyListContext } from './policy_list_context';
import { useIsReadOnly } from '../../lib/use_is_read_only';
import { PolicyFromES } from '../../../../common/types';
import { PolicyTable } from './components/policy_table';
import { getPolicyCreatePath } from '../../services/navigation';
import { ListActionHandler } from './components/list_action_handler';
import { getPoliciesListPath, getPolicyCreatePath } from '../../services/navigation';
import { PolicyTable, ListActionHandler } from './components';
import { ViewPolicyFlyout } from './policy_flyout';
interface Props {
policies: PolicyFromES[];
@ -23,6 +25,19 @@ interface Props {
export const PolicyList: React.FunctionComponent<Props> = ({ policies, updatePolicies }) => {
const history = useHistory();
const isReadOnly = useIsReadOnly();
const { setListAction } = usePolicyListContext();
const [flyoutPolicy, setFlyoutPolicy] = useState<PolicyFromES | null>(null);
useEffect(() => {
const params = new URLSearchParams(history.location.search);
const policyParam = decodeURIComponent(params.get('policy') ?? '');
const policyFromParam = policies.find((policy) => policy.name === policyParam);
if (policyFromParam) {
setFlyoutPolicy(policyFromParam);
} else {
setFlyoutPolicy(null);
}
}, [history.location.search, policies, setListAction]);
const createPolicyButton = (
<EuiButton
@ -65,9 +80,17 @@ export const PolicyList: React.FunctionComponent<Props> = ({ policies, updatePol
);
}
const rightSideItems = isReadOnly ? [] : [createPolicyButton];
return (
<>
<ListActionHandler updatePolicies={updatePolicies} />
<ListActionHandler
deletePolicyCallback={() => {
// if a flyout was open, then close it
history.push(getPoliciesListPath());
// update the policies in the list after 1 was deleted
updatePolicies();
}}
/>
<EuiPageHeader
pageTitle={
@ -86,12 +109,14 @@ export const PolicyList: React.FunctionComponent<Props> = ({ policies, updatePol
/>
}
bottomBorder
rightSideItems={[createPolicyButton]}
rightSideItems={rightSideItems}
/>
<EuiSpacer size="l" />
<PolicyTable policies={policies} />
{flyoutPolicy && <ViewPolicyFlyout policy={flyoutPolicy} />}
</>
);
};

View file

@ -9,7 +9,7 @@ import React, { createContext, ReactChild, useContext, useState } from 'react';
import { PolicyFromES } from '../../../../common/types';
interface ListAction {
actionType: 'viewIndexTemplates' | 'addIndexTemplate' | 'deletePolicy';
actionType: 'viewIndexTemplates' | 'addIndexTemplate' | 'deletePolicy' | 'viewPolicy';
selectedPolicy: PolicyFromES;
}

View file

@ -18,6 +18,10 @@ export const getPolicyEditPath = (policyName: string): string => {
return encodeURI(`/policies/edit/${encodeURIComponent(policyName)}`);
};
export const getPolicyViewPath = (policyName: string): string => {
return encodeURI(`/policies?policy=${encodeURIComponent(policyName)}`);
};
export const getPolicyCreatePath = () => {
return ROUTES.create;
};

View file

@ -46,4 +46,5 @@ export interface AppServicesContext {
overlays: OverlayStart;
http: HttpSetup;
history: ScopedHistory;
capabilities: ApplicationStart['capabilities'];
}

View file

@ -75,7 +75,11 @@ export class IndexLifecycleManagementServerPlugin implements Plugin<void, void,
privileges: [
{
requiredClusterPrivileges: ['manage_ilm'],
ui: [],
ui: ['save'],
},
{
requiredClusterPrivileges: ['read_ilm'],
ui: ['show'],
},
],
});

View file

@ -22610,12 +22610,10 @@
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "Licence Enterprise requise",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel": "Référentiel de snapshot",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "Nom de référentiel de snapshot obligatoire.",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel": "Stockage de snapshots quil est possible de rechercher",
"xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButton": "Afficher la requête",
"xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardCountLabel": "Configurer le nombre de partitions",
"xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardSizeLabel": "Configurer la taille des partitions",
"xpack.indexLifecycleMgmt.editPolicy.shrinkIndexExplanationText": "Réduisez l'index en un nouvel index contenant moins de partitions principales.",
"xpack.indexLifecycleMgmt.editPolicy.shrinkText": "Réduire",
"xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage": "{verb} la politique de cycle de vie \"{lifecycleName}\"",
"xpack.indexLifecycleMgmt.editPolicy.timeUnits.daysLabel": "jours",
"xpack.indexLifecycleMgmt.editPolicy.timeUnits.hoursLabel": "heures",
@ -22726,10 +22724,8 @@
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage": "La politique {policyName} a été ajoutée au modèle d'index {templateName}",
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.templateHasPolicyWarningTitle": "Le modèle a déjà une stratégie",
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "Ajouter la politique \"{name}\" au modèle d'index",
"xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText": "Ajouter la stratégie au modèle d'index",
"xpack.indexLifecycleMgmt.policyTable.captionText": "Le tableau ci-dessous contient {count, plural, one {# politique de cycle de vie des index} other {# politiques de cycle de vie des index}}.",
"xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip": "Vous ne pouvez pas supprimer une stratégie utilisée par un index",
"xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText": "Supprimer la stratégie",
"xpack.indexLifecycleMgmt.policyTable.emptyPrompt.createButtonLabel": "Créer une stratégie",
"xpack.indexLifecycleMgmt.policyTable.emptyPromptDescription": " Une stratégie de cycle de vie des index permet de gérer vos index à mesure qu'ils vieillissent.",
"xpack.indexLifecycleMgmt.policyTable.emptyPromptTitle": "Créez votre première stratégie de cycle de vie des index",

View file

@ -22599,12 +22599,10 @@
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "エンタープライズライセンスが必要です",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel": "スナップショットリポジトリ",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "スナップショットリポジトリ名が必要です。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel": "検索可能スナップショットストレージ",
"xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButton": "リクエストを表示",
"xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardCountLabel": "シャード数を構成",
"xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardSizeLabel": "シャードサイズを構成",
"xpack.indexLifecycleMgmt.editPolicy.shrinkIndexExplanationText": "インデックス情報をプライマリシャードの少ない新規インデックスに縮小します。",
"xpack.indexLifecycleMgmt.editPolicy.shrinkText": "縮小",
"xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage": "ライフサイクルポリシー「{lifecycleName}」を{verb}",
"xpack.indexLifecycleMgmt.editPolicy.timeUnits.daysLabel": "日",
"xpack.indexLifecycleMgmt.editPolicy.timeUnits.hoursLabel": "時間",
@ -22715,10 +22713,8 @@
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage": "インデックステンプレート {templateName} にポリシー {policyName} を追加しました",
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.templateHasPolicyWarningTitle": "テンプレートにすでにポリシーがあります",
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "インデックステンプレートにポリシー「{name}」 を追加",
"xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText": "インデックステンプレートにポリシーを追加",
"xpack.indexLifecycleMgmt.policyTable.captionText": "次の表には{count, plural, other {# 個のインデックスライフサイクルポリシー}}が含まれています。",
"xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip": "インデックスが使用中のポリシーは削除できません",
"xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText": "ポリシーを削除",
"xpack.indexLifecycleMgmt.policyTable.emptyPrompt.createButtonLabel": "ポリシーを作成",
"xpack.indexLifecycleMgmt.policyTable.emptyPromptDescription": " ライフサイクルポリシーは、インデックスが古くなるにつれ管理しやすくなります。",
"xpack.indexLifecycleMgmt.policyTable.emptyPromptTitle": "初めのインデックスライフサイクルポリシーの作成",

View file

@ -22627,12 +22627,10 @@
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "需要企业许可证",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel": "快照存储库",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "快照存储库名称必填。",
"xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel": "可搜索快照存储",
"xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButton": "显示请求",
"xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardCountLabel": "配置分片计数",
"xpack.indexLifecycleMgmt.editPolicy.shrink.configureShardSizeLabel": "配置分片大小",
"xpack.indexLifecycleMgmt.editPolicy.shrinkIndexExplanationText": "将索引缩小成具有较少主分片的新索引。",
"xpack.indexLifecycleMgmt.editPolicy.shrinkText": "缩小",
"xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage": "{verb}生命周期策略“{lifecycleName}”",
"xpack.indexLifecycleMgmt.editPolicy.timeUnits.daysLabel": "天",
"xpack.indexLifecycleMgmt.editPolicy.timeUnits.hoursLabel": "小时",
@ -22743,10 +22741,8 @@
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.successMessage": "已将策略“{policyName}”添加到索引模板“{templateName}”。",
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.templateHasPolicyWarningTitle": "模板已有策略",
"xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "将策略 “{name}” 添加到索引模板",
"xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText": "将策略添加到索引模板",
"xpack.indexLifecycleMgmt.policyTable.captionText": "下表包含 {count, plural, other {# 个索引生命周期策略}}。",
"xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip": "您无法删除索引正在使用的策略",
"xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText": "删除策略",
"xpack.indexLifecycleMgmt.policyTable.emptyPrompt.createButtonLabel": "创建策略",
"xpack.indexLifecycleMgmt.policyTable.emptyPromptDescription": " 索引生命周期策略帮助您管理变旧的索引。",
"xpack.indexLifecycleMgmt.policyTable.emptyPromptTitle": "创建您的首个索引生命周期索引",

View file

@ -132,8 +132,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('Edit policy', async () => {
const link = await findPolicyLinkInListView(POLICY_NAME);
await link.click();
const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`);
const editPolicyButton = await policyRow.findByTestSubject('editPolicy');
await editPolicyButton.click();
await retry.waitFor('ILM edit form', async () => {
return (
(await testSubjects.getVisibleText('policyTitle')) === `Edit policy ${POLICY_NAME}`
@ -143,8 +146,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('Request flyout', async () => {
const link = await findPolicyLinkInListView(POLICY_NAME);
await link.click();
const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`);
const editPolicyButton = await policyRow.findByTestSubject('editPolicy');
await editPolicyButton.click();
await retry.waitFor('ILM request button', async () => {
return testSubjects.exists('requestButton');
});
@ -160,11 +166,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
it('View policy flyout', async () => {
const link = await findPolicyLinkInListView(POLICY_NAME);
await link.click();
await retry.waitFor('View policy flyout to be present', async () => {
return testSubjects.isDisplayed('policyFlyoutTitle');
});
await a11y.testAppSnapshot();
});
it('Add policy to index template modal', async () => {
await filterByPolicyName(POLICY_NAME);
const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`);
const addPolicyButton = await policyRow.findByTestSubject('addPolicyToTemplate');
const actionsButton = await policyRow.findByTestSubject('euiCollapsedItemActionsButton');
await actionsButton.click();
const addPolicyButton = await testSubjects.find('addPolicyToTemplate');
await addPolicyButton.click();
await retry.waitFor('ILM add policy to index template modal to be present', async () => {
@ -177,8 +197,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('Delete policy modal', async () => {
await filterByPolicyName(POLICY_NAME);
const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`);
const deleteButton = await policyRow.findByTestSubject('deletePolicy');
const deleteButton = await policyRow.findByTestSubject('deletePolicy');
await deleteButton.click();
await retry.waitFor('ILM delete policy modal to be present', async () => {
@ -191,10 +211,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('Index templates flyout', async () => {
await filterByPolicyName(POLICY_NAME);
const policyRow = await testSubjects.find(`policyTableRow-${POLICY_NAME}`);
const actionsButton = await policyRow.findByTestSubject('viewIndexTemplates');
const actionsButton = await policyRow.findByTestSubject('euiCollapsedItemActionsButton');
await actionsButton.click();
const templatesButton = await testSubjects.find('viewIndexTemplates');
await templatesButton.click();
const flyoutTitleSelector = 'indexTemplatesFlyoutHeader';
await retry.waitFor('Index templates flyout', async () => {
return testSubjects.isDisplayed(flyoutTitleSelector);

View file

@ -68,6 +68,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
snapshotRepository: repoName,
});
await retry.waitFor('policy flyout', async () => {
return (await pageObjects.indexLifecycleManagement.flyoutHeaderText()) === policyName;
});
await pageObjects.indexLifecycleManagement.closePolicyFlyout();
await retry.waitFor('navigation back to home page.', async () => {
return (
(await pageObjects.indexLifecycleManagement.pageHeaderText()) ===

View file

@ -11,5 +11,6 @@ export default ({ loadTestFile }: FtrProviderContext) => {
describe('Index Lifecycle Management app', function () {
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./home_page'));
loadTestFile(require.resolve('./read_only_view'));
});
};

View file

@ -0,0 +1,43 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'indexLifecycleManagement']);
const log = getService('log');
const retry = getService('retry');
const security = getService('security');
describe('Read only view', function () {
before(async () => {
await security.testUser.setRoles(['read_ilm']);
await pageObjects.common.navigateToApp('indexLifecycleManagement');
});
after(async () => {
await security.testUser.restoreDefaults();
});
it('Loads the app', async () => {
await log.debug('Checking for page header');
const headerText = await pageObjects.indexLifecycleManagement.pageHeaderText();
expect(headerText).to.be('Index Lifecycle Policies');
const createPolicyButtonExists =
await pageObjects.indexLifecycleManagement.createPolicyButtonExists();
expect(createPolicyButtonExists).to.be(false);
await pageObjects.indexLifecycleManagement.clickPolicyNameLink(0);
await retry.waitFor('flyout to be visible', async () => {
const flyoutHeader = await pageObjects.indexLifecycleManagement.flyoutHeader();
return await flyoutHeader.isDisplayed();
});
});
});
};

View file

@ -573,6 +573,20 @@ export default async function ({ readConfigFile }) {
],
},
read_ilm: {
elasticsearch: {
cluster: ['read_ilm'],
},
kibana: [
{
feature: {
advancedSettings: ['read'],
},
spaces: ['default'],
},
],
},
index_management_user: {
elasticsearch: {
cluster: ['monitor', 'manage_index_templates', 'manage_enrich'],

View file

@ -30,6 +30,9 @@ export function IndexLifecycleManagementPageProvider({ getService }: FtrProvider
async clickCreatePolicyButton() {
return await testSubjects.click('createPolicyButton');
},
async createPolicyButtonExists() {
return await testSubjects.exists('createPolicyButton');
},
async fillNewPolicyForm(policy: Policy) {
const {
policyName,
@ -88,5 +91,19 @@ export function IndexLifecycleManagementPageProvider({ getService }: FtrProvider
async getPolicyRow(name: string) {
return await testSubjects.findAll(`policyTableRow-${name}`);
},
async flyoutHeaderText() {
return await testSubjects.getVisibleText('policyFlyoutTitle');
},
async closePolicyFlyout() {
await testSubjects.click('policyFlyoutCloseButton');
},
async flyoutHeader() {
return await testSubjects.find('policyFlyoutTitle');
},
async clickPolicyNameLink(index: number) {
const links = await testSubjects.findAll('policyTablePolicyNameLink');
await links[index].click();
},
};
}