diff --git a/cypress/fixtures/plugins/index-management-dashboards-plugin/sample_convert_index_to_remote_policy.json b/cypress/fixtures/plugins/index-management-dashboards-plugin/sample_convert_index_to_remote_policy.json new file mode 100644 index 000000000..768b2dda1 --- /dev/null +++ b/cypress/fixtures/plugins/index-management-dashboards-plugin/sample_convert_index_to_remote_policy.json @@ -0,0 +1,42 @@ +{ + "policy": { + "description": "Convert old indexes to searchable snapshots", + "default_state": "active", + "states": [ + { + "name": "active", + "actions": [], + "transitions": [ + { + "state_name": "archive", + "conditions": { + "min_index_age": "30d" + } + } + ] + }, + { + "name": "archive", + "actions": [ + { + "snapshot": { + "repository": "remote-repo", + "snapshot": "{{ctx.index}}" + } + }, + { + "convert_index_to_remote": { + "repository": "remote-repo", + "snapshot": "{{ctx.index}}", + "include_aliases": true, + "ignore_index_settings": "index.refresh_interval,index.number_of_replicas", + "number_of_replicas": 0 + } + } + ], + "transitions": [] + } + ] + } +} + diff --git a/models/interfaces.ts b/models/interfaces.ts index 062dd49e8..3e84f1505 100644 --- a/models/interfaces.ts +++ b/models/interfaces.ts @@ -428,6 +428,16 @@ export interface SnapshotAction extends Action { }; } +export interface ConvertIndexToRemoteAction extends Action { + convert_index_to_remote: { + repository: string; + snapshot: string; + include_aliases?: boolean; + ignore_index_settings?: string; + number_of_replicas?: number; + }; +} + export interface IndexPriorityAction extends Action { index_priority: { priority?: number; diff --git a/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.test.tsx b/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.test.tsx new file mode 100644 index 000000000..8e8c7662b --- /dev/null +++ b/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.test.tsx @@ -0,0 +1,186 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import "@testing-library/jest-dom"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; +import userEventModule from "@testing-library/user-event"; +import { DEFAULT_CONVERT_INDEX_TO_REMOTE } from "../../utils/constants"; +import { ConvertIndexToRemoteAction, UIAction } from "../../../../../models/interfaces"; +import { actionRepoSingleton } from "../../utils/helpers"; + +const TEST_PROPS: UIAction = { + action: DEFAULT_CONVERT_INDEX_TO_REMOTE, +} as UIAction; + +const renderComponent = (uiAction: UIAction = TEST_PROPS) => { + render(actionRepoSingleton.getUIAction("convert_index_to_remote").render(uiAction, mockOnChangeAction)); +}; +const mockOnChangeAction = (uiAction: UIAction = TEST_PROPS) => { + cleanup(); + renderComponent(uiAction); +}; + +afterEach(() => cleanup()); + +describe("ConvertIndexToRemoteUIAction component", () => { + const userEvent = userEventModule.setup(); + + it("renders with default values", () => { + const { container } = render(actionRepoSingleton.getUIAction("convert_index_to_remote").render(TEST_PROPS, mockOnChangeAction)); + + // Check that repository field exists with default value + const repositoryInput = screen.getByTestId("action-render-convert-index-to-remote-repository"); + expect(repositoryInput).toBeInTheDocument(); + expect(repositoryInput).toHaveValue("example-repository"); + + // Check that snapshot field exists with default value + const snapshotInput = screen.getByTestId("action-render-convert-index-to-remote-snapshot"); + expect(snapshotInput).toBeInTheDocument(); + expect(snapshotInput).toHaveValue("example-snapshot"); + + // Check that include_aliases switch exists + const includeAliasesSwitch = screen.getByTestId("action-render-convert-index-to-remote-include-aliases"); + expect(includeAliasesSwitch).toBeInTheDocument(); + expect(includeAliasesSwitch).not.toBeChecked(); + + // Check that ignore_index_settings field exists + const ignoreIndexSettingsInput = screen.getByTestId("action-render-convert-index-to-remote-ignore-index-settings"); + expect(ignoreIndexSettingsInput).toBeInTheDocument(); + expect(ignoreIndexSettingsInput).toHaveValue(""); + + // Check that number_of_replicas field exists + const numberOfReplicasInput = screen.getByTestId("action-render-convert-index-to-remote-number-of-replicas"); + expect(numberOfReplicasInput).toBeInTheDocument(); + expect(numberOfReplicasInput).toHaveValue(0); + + expect(container).toMatchSnapshot(); + }); + + it("updates repository value on change", async () => { + renderComponent(); + + const repositoryInput = screen.getByTestId("action-render-convert-index-to-remote-repository"); + await userEvent.clear(repositoryInput); + await userEvent.type(repositoryInput, "my-remote-repo"); + + expect(repositoryInput).toHaveValue("my-remote-repo"); + }); + + it("updates snapshot value on change", async () => { + renderComponent(); + + const snapshotInput = screen.getByTestId("action-render-convert-index-to-remote-snapshot"); + await userEvent.clear(snapshotInput); + await userEvent.type(snapshotInput, "{{ctx.index}}"); + + expect(snapshotInput).toHaveValue("{{ctx.index}}"); + }); + + it("toggles include_aliases switch", async () => { + renderComponent(); + + const includeAliasesSwitch = screen.getByTestId("action-render-convert-index-to-remote-include-aliases"); + expect(includeAliasesSwitch).not.toBeChecked(); + + await userEvent.click(includeAliasesSwitch); + expect(includeAliasesSwitch).toBeChecked(); + }); + + it("updates ignore_index_settings value on change", async () => { + renderComponent(); + + const ignoreIndexSettingsInput = screen.getByTestId("action-render-convert-index-to-remote-ignore-index-settings"); + await userEvent.type(ignoreIndexSettingsInput, "index.refresh_interval,index.number_of_replicas"); + + expect(ignoreIndexSettingsInput).toHaveValue("index.refresh_interval,index.number_of_replicas"); + }); + + it("updates number_of_replicas value on change", async () => { + renderComponent(); + + const numberOfReplicasInput = screen.getByTestId("action-render-convert-index-to-remote-number-of-replicas"); + await userEvent.clear(numberOfReplicasInput); + await userEvent.type(numberOfReplicasInput, "2"); + + expect(numberOfReplicasInput).toHaveValue(2); + }); + + it("validates required fields", () => { + const uiAction = actionRepoSingleton.getUIAction("convert_index_to_remote"); + + // Valid action + expect(uiAction.isValid()).toBe(true); + + // Invalid action - missing repository + const invalidAction1 = { + ...TEST_PROPS, + action: { + convert_index_to_remote: { + ...DEFAULT_CONVERT_INDEX_TO_REMOTE.convert_index_to_remote, + repository: "", + }, + }, + }; + const invalidUIAction1 = actionRepoSingleton.getUIActionFromData(invalidAction1.action); + expect(invalidUIAction1.isValid()).toBe(false); + + // Invalid action - missing snapshot + const invalidAction2 = { + ...TEST_PROPS, + action: { + convert_index_to_remote: { + ...DEFAULT_CONVERT_INDEX_TO_REMOTE.convert_index_to_remote, + snapshot: "", + }, + }, + }; + const invalidUIAction2 = actionRepoSingleton.getUIActionFromData(invalidAction2.action); + expect(invalidUIAction2.isValid()).toBe(false); + }); + + it("renders with custom values", () => { + const customAction: ConvertIndexToRemoteAction = { + convert_index_to_remote: { + repository: "s3-repo", + snapshot: "{{ctx.index}}-snapshot", + include_aliases: true, + ignore_index_settings: "index.refresh_interval", + number_of_replicas: 2, + }, + }; + + const customProps = { ...TEST_PROPS, action: customAction }; + const { container } = render( + actionRepoSingleton.getUIAction("convert_index_to_remote").render(customProps, mockOnChangeAction) + ); + + expect(screen.getByTestId("action-render-convert-index-to-remote-repository")).toHaveValue("s3-repo"); + expect(screen.getByTestId("action-render-convert-index-to-remote-snapshot")).toHaveValue("{{ctx.index}}-snapshot"); + expect(screen.getByTestId("action-render-convert-index-to-remote-include-aliases")).toBeChecked(); + expect(screen.getByTestId("action-render-convert-index-to-remote-ignore-index-settings")).toHaveValue("index.refresh_interval"); + expect(screen.getByTestId("action-render-convert-index-to-remote-number-of-replicas")).toHaveValue(2); + + expect(container).toMatchSnapshot(); + }); + + it("returns correct content", () => { + const uiAction = actionRepoSingleton.getUIAction("convert_index_to_remote"); + expect(uiAction.content()).toBe("Convert index to remote"); + }); + + it("converts to action correctly", () => { + const uiAction = actionRepoSingleton.getUIAction("convert_index_to_remote"); + const action = uiAction.toAction(); + + expect(action).toHaveProperty("convert_index_to_remote"); + expect(action.convert_index_to_remote).toHaveProperty("repository"); + expect(action.convert_index_to_remote).toHaveProperty("snapshot"); + expect(action.convert_index_to_remote).toHaveProperty("include_aliases"); + expect(action.convert_index_to_remote).toHaveProperty("ignore_index_settings"); + expect(action.convert_index_to_remote).toHaveProperty("number_of_replicas"); + }); +}); + diff --git a/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.tsx b/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.tsx new file mode 100644 index 000000000..1319a2c57 --- /dev/null +++ b/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.tsx @@ -0,0 +1,160 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ChangeEvent } from "react"; +import { EuiCompressedFormRow, EuiCompressedFieldText, EuiSpacer, EuiCompressedFieldNumber, EuiCompressedSwitch } from "@elastic/eui"; +import { ConvertIndexToRemoteAction, UIAction } from "../../../../../models/interfaces"; +import { makeId } from "../../../../utils/helpers"; +import { ActionType } from "../../utils/constants"; +import EuiFormCustomLabel from "../EuiFormCustomLabel"; + +export default class ConvertIndexToRemoteUIAction implements UIAction { + id: string; + action: ConvertIndexToRemoteAction; + type = ActionType.ConvertIndexToRemote; + + constructor(action: ConvertIndexToRemoteAction, id: string = makeId()) { + this.action = action; + this.id = id; + } + + content = () => `Convert index to remote`; + + clone = (action: ConvertIndexToRemoteAction) => new ConvertIndexToRemoteUIAction(action, this.id); + + isValid = () => { + return !!this.action.convert_index_to_remote.snapshot && !!this.action.convert_index_to_remote.repository; + }; + + render = (action: UIAction, onChangeAction: (action: UIAction) => void) => { + return ( + <> + + + ) => { + const repository = e.target.value; + onChangeAction( + this.clone({ + convert_index_to_remote: { + ...action.action.convert_index_to_remote, + repository, + }, + }) + ); + }} + data-test-subj="action-render-convert-index-to-remote-repository" + /> + + + + + ) => { + const snapshot = e.target.value; + onChangeAction( + this.clone({ + convert_index_to_remote: { + ...action.action.convert_index_to_remote, + snapshot, + }, + }) + ); + }} + data-test-subj="action-render-convert-index-to-remote-snapshot" + /> + + + + + ) => { + const include_aliases = e.target.checked; + onChangeAction( + this.clone({ + convert_index_to_remote: { + ...action.action.convert_index_to_remote, + include_aliases, + }, + }) + ); + }} + data-test-subj="action-render-convert-index-to-remote-include-aliases" + /> + + + + + ) => { + const ignore_index_settings = e.target.value; + onChangeAction( + this.clone({ + convert_index_to_remote: { + ...action.action.convert_index_to_remote, + ignore_index_settings, + }, + }) + ); + }} + data-test-subj="action-render-convert-index-to-remote-ignore-index-settings" + /> + + + + + ) => { + const number_of_replicas = parseInt(e.target.value); + onChangeAction( + this.clone({ + convert_index_to_remote: { + ...action.action.convert_index_to_remote, + number_of_replicas, + }, + }) + ); + }} + min={0} + data-test-subj="action-render-convert-index-to-remote-number-of-replicas" + /> + + + ); + }; + + toAction = () => this.action; +} + diff --git a/public/pages/VisualCreatePolicy/components/UIActions/__snapshots__/ConvertIndexToRemoteUIAction.test.tsx.snap b/public/pages/VisualCreatePolicy/components/UIActions/__snapshots__/ConvertIndexToRemoteUIAction.test.tsx.snap new file mode 100644 index 000000000..234193571 --- /dev/null +++ b/public/pages/VisualCreatePolicy/components/UIActions/__snapshots__/ConvertIndexToRemoteUIAction.test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConvertIndexToRemoteUIAction component renders with custom values 1`] = ` + +
+

+ Repository +

+
+
+`; + +exports[`ConvertIndexToRemoteUIAction component renders with default values 1`] = ` + +
+

+ Repository +

+
+
+`; + diff --git a/public/pages/VisualCreatePolicy/components/UIActions/index.ts b/public/pages/VisualCreatePolicy/components/UIActions/index.ts index 53db9ec57..c9095bf6e 100644 --- a/public/pages/VisualCreatePolicy/components/UIActions/index.ts +++ b/public/pages/VisualCreatePolicy/components/UIActions/index.ts @@ -6,6 +6,7 @@ import AliasUIAction from "./AliasUIAction/AliasUIAction"; import AllocationUIAction from "./AllocationUIAction"; import CloseUIAction from "./CloseUIAction"; +import ConvertIndexToRemoteUIAction from "./ConvertIndexToRemoteUIAction"; import DeleteUIAction from "./DeleteUIAction"; import ForceMergeUIAction from "./ForceMergeUIAction"; import IndexPriorityUIAction from "./IndexPriorityUIAction"; @@ -23,6 +24,7 @@ export { AliasUIAction, AllocationUIAction, CloseUIAction, + ConvertIndexToRemoteUIAction, DeleteUIAction, ForceMergeUIAction, IndexPriorityUIAction, diff --git a/public/pages/VisualCreatePolicy/utils/constants.ts b/public/pages/VisualCreatePolicy/utils/constants.ts index 3390bec13..2a3a5a106 100644 --- a/public/pages/VisualCreatePolicy/utils/constants.ts +++ b/public/pages/VisualCreatePolicy/utils/constants.ts @@ -9,6 +9,7 @@ export enum ActionType { Alias = "alias", Allocation = "allocation", Close = "close", + ConvertIndexToRemote = "convert_index_to_remote", Delete = "delete", ForceMerge = "force_merge", IndexPriority = "index_priority", @@ -195,10 +196,21 @@ export const DEFAULT_SNAPSHOT = { }, }; +export const DEFAULT_CONVERT_INDEX_TO_REMOTE = { + convert_index_to_remote: { + repository: "example-repository", + snapshot: "example-snapshot", + include_aliases: false, + ignore_index_settings: "", + number_of_replicas: 0, + }, +}; + export const actions = [ DEFAULT_ALIAS, DEFAULT_ALLOCATION, DEFAULT_CLOSE, + DEFAULT_CONVERT_INDEX_TO_REMOTE, DEFAULT_DELETE, DEFAULT_FORCE_MERGE, DEFAULT_INDEX_PRIORITY, diff --git a/public/pages/VisualCreatePolicy/utils/helpers.test.ts b/public/pages/VisualCreatePolicy/utils/helpers.test.ts index bb2dd3ad2..c9d0ba1a0 100644 --- a/public/pages/VisualCreatePolicy/utils/helpers.test.ts +++ b/public/pages/VisualCreatePolicy/utils/helpers.test.ts @@ -75,13 +75,21 @@ class DummyUIAction implements UIAction { } test("action repository usage", () => { - expect(actionRepoSingleton.getAllActionTypes().length).toBe(15); - actionRepoSingleton.registerAction("dummy", DummyUIAction, DEFAULT_DUMMY); expect(actionRepoSingleton.getAllActionTypes().length).toBe(16); + actionRepoSingleton.registerAction("dummy", DummyUIAction, DEFAULT_DUMMY); + expect(actionRepoSingleton.getAllActionTypes().length).toBe(17); expect(actionRepoSingleton.getUIAction("dummy") instanceof DummyUIAction).toBe(true); expect(actionRepoSingleton.getUIActionFromData(DEFAULT_DUMMY) instanceof DummyUIAction).toBe(true); }); +test("convert_index_to_remote action is registered in repository", () => { + expect(actionRepoSingleton.getAllActionTypes()).toContain("convert_index_to_remote"); + const uiAction = actionRepoSingleton.getUIAction("convert_index_to_remote"); + expect(uiAction).toBeDefined(); + expect(uiAction.type).toBe("convert_index_to_remote"); + expect(uiAction.isValid).toBeDefined(); +}); + test("changing the default state name correctly updates default_state", () => { const policy = DEFAULT_POLICY; const updatedState = { ...policy.states[0], name: "new_hot" }; diff --git a/public/pages/VisualCreatePolicy/utils/helpers.ts b/public/pages/VisualCreatePolicy/utils/helpers.ts index 37bfba4d2..958cce8d0 100644 --- a/public/pages/VisualCreatePolicy/utils/helpers.ts +++ b/public/pages/VisualCreatePolicy/utils/helpers.ts @@ -9,6 +9,7 @@ import { DEFAULT_ALIAS, DEFAULT_ALLOCATION, DEFAULT_CLOSE, + DEFAULT_CONVERT_INDEX_TO_REMOTE, DEFAULT_DELETE, DEFAULT_FORCE_MERGE, DEFAULT_INDEX_PRIORITY, @@ -26,6 +27,7 @@ import { AliasUIAction, AllocationUIAction, CloseUIAction, + ConvertIndexToRemoteUIAction, DeleteUIAction, ForceMergeUIAction, IndexPriorityUIAction, @@ -75,6 +77,8 @@ export const getUIAction = (actionType: string): UIAction => { return new AllocationUIAction(DEFAULT_ALLOCATION); case ActionType.Close: return new CloseUIAction(DEFAULT_CLOSE); + case ActionType.ConvertIndexToRemote: + return new ConvertIndexToRemoteUIAction(DEFAULT_CONVERT_INDEX_TO_REMOTE); case ActionType.Delete: return new DeleteUIAction(DEFAULT_DELETE); case ActionType.ForceMerge: @@ -127,6 +131,7 @@ class ActionRepository { alias: [AliasUIAction, DEFAULT_ALIAS], allocation: [AllocationUIAction, DEFAULT_ALLOCATION], close: [CloseUIAction, DEFAULT_CLOSE], + convert_index_to_remote: [ConvertIndexToRemoteUIAction, DEFAULT_CONVERT_INDEX_TO_REMOTE], delete: [DeleteUIAction, DEFAULT_DELETE], force_merge: [ForceMergeUIAction, DEFAULT_FORCE_MERGE], index_priority: [IndexPriorityUIAction, DEFAULT_INDEX_PRIORITY],