From c5b21640775ba450f1ae69b5103da7c9a71de91b Mon Sep 17 00:00:00 2001 From: Seung Yeon Joo Date: Wed, 5 Nov 2025 12:51:34 -0800 Subject: [PATCH 1/2] Add Convert index to remote support for dashboard ism plugin Signed-off-by: Seung Yeon Joo --- ...sample_convert_index_to_remote_policy.json | 42 ++++ models/interfaces.ts | 10 + .../ConvertIndexToRemoteUIAction.test.tsx | 186 ++++++++++++++++++ .../ConvertIndexToRemoteUIAction.tsx | 160 +++++++++++++++ ...ConvertIndexToRemoteUIAction.test.tsx.snap | 34 ++++ .../components/UIActions/index.ts | 2 + .../VisualCreatePolicy/utils/constants.ts | 12 ++ .../VisualCreatePolicy/utils/helpers.test.ts | 12 +- .../pages/VisualCreatePolicy/utils/helpers.ts | 5 + 9 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 cypress/fixtures/plugins/index-management-dashboards-plugin/sample_convert_index_to_remote_policy.json create mode 100644 public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.test.tsx create mode 100644 public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.tsx create mode 100644 public/pages/VisualCreatePolicy/components/UIActions/__snapshots__/ConvertIndexToRemoteUIAction.test.tsx.snap 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], From fa6d274aca957c606848a95585a2f1249b9cb9fd Mon Sep 17 00:00:00 2001 From: Seung Yeon Joo Date: Thu, 4 Jun 2026 15:57:51 -0700 Subject: [PATCH 2/2] Add convert index to remote restore options Signed-off-by: Seung Yeon Joo --- ...sample_convert_index_to_remote_policy.json | 5 +- models/interfaces.ts | 2 + .../ConvertIndexToRemoteUIAction.test.tsx | 120 +++++++++++---- .../ConvertIndexToRemoteUIAction.tsx | 144 ++++++++++++++---- ...ConvertIndexToRemoteUIAction.test.tsx.snap | 34 ----- .../__snapshots__/CreateAction.test.tsx.snap | 5 + .../VisualCreatePolicy/utils/constants.ts | 7 +- 7 files changed, 222 insertions(+), 95 deletions(-) delete mode 100644 public/pages/VisualCreatePolicy/components/UIActions/__snapshots__/ConvertIndexToRemoteUIAction.test.tsx.snap 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 index 768b2dda1..f0dfdd31c 100644 --- 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 @@ -30,7 +30,9 @@ "snapshot": "{{ctx.index}}", "include_aliases": true, "ignore_index_settings": "index.refresh_interval,index.number_of_replicas", - "number_of_replicas": 0 + "number_of_replicas": 0, + "delete_original_index": false, + "rename_pattern": "$1_remote" } } ], @@ -39,4 +41,3 @@ ] } } - diff --git a/models/interfaces.ts b/models/interfaces.ts index 3e84f1505..921879304 100644 --- a/models/interfaces.ts +++ b/models/interfaces.ts @@ -435,6 +435,8 @@ export interface ConvertIndexToRemoteAction extends Action { include_aliases?: boolean; ignore_index_settings?: string; number_of_replicas?: number; + delete_original_index?: boolean; + rename_pattern?: string; }; } diff --git a/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.test.tsx b/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.test.tsx index 8e8c7662b..43880c321 100644 --- a/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.test.tsx +++ b/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.test.tsx @@ -6,7 +6,6 @@ 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"; @@ -26,10 +25,8 @@ const mockOnChangeAction = (uiAction: UIAction = TES 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)); + 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"); @@ -46,6 +43,16 @@ describe("ConvertIndexToRemoteUIAction component", () => { expect(includeAliasesSwitch).toBeInTheDocument(); expect(includeAliasesSwitch).not.toBeChecked(); + // Check that delete_original_index switch exists + const deleteOriginalIndexSwitch = screen.getByTestId("action-render-convert-index-to-remote-delete-original-index"); + expect(deleteOriginalIndexSwitch).toBeInTheDocument(); + expect(deleteOriginalIndexSwitch).not.toBeChecked(); + + // Check that rename_pattern field exists + const renamePatternInput = screen.getByTestId("action-render-convert-index-to-remote-rename-pattern"); + expect(renamePatternInput).toBeInTheDocument(); + expect(renamePatternInput).toHaveValue("$1_remote"); + // Check that ignore_index_settings field exists const ignoreIndexSettingsInput = screen.getByTestId("action-render-convert-index-to-remote-ignore-index-settings"); expect(ignoreIndexSettingsInput).toBeInTheDocument(); @@ -54,29 +61,25 @@ describe("ConvertIndexToRemoteUIAction component", () => { // 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(); + expect(numberOfReplicasInput).toHaveValue(null); }); 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"); + fireEvent.change(repositoryInput, { target: { value: "my-remote-repo" } }); - expect(repositoryInput).toHaveValue("my-remote-repo"); + expect(screen.getByTestId("action-render-convert-index-to-remote-repository")).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}}"); + fireEvent.change(snapshotInput, { target: { value: "{{ctx.index}}" } }); - expect(snapshotInput).toHaveValue("{{ctx.index}}"); + expect(screen.getByTestId("action-render-convert-index-to-remote-snapshot")).toHaveValue("{{ctx.index}}"); }); it("toggles include_aliases switch", async () => { @@ -85,32 +88,52 @@ describe("ConvertIndexToRemoteUIAction component", () => { const includeAliasesSwitch = screen.getByTestId("action-render-convert-index-to-remote-include-aliases"); expect(includeAliasesSwitch).not.toBeChecked(); - await userEvent.click(includeAliasesSwitch); - expect(includeAliasesSwitch).toBeChecked(); + fireEvent.click(includeAliasesSwitch); + expect(screen.getByTestId("action-render-convert-index-to-remote-include-aliases")).toBeChecked(); + }); + + it("toggles delete_original_index switch", async () => { + renderComponent(); + + const deleteOriginalIndexSwitch = screen.getByTestId("action-render-convert-index-to-remote-delete-original-index"); + expect(deleteOriginalIndexSwitch).not.toBeChecked(); + + fireEvent.click(deleteOriginalIndexSwitch); + expect(screen.getByTestId("action-render-convert-index-to-remote-delete-original-index")).toBeChecked(); + }); + + it("updates rename_pattern value on change", async () => { + renderComponent(); + + const renamePatternInput = screen.getByTestId("action-render-convert-index-to-remote-rename-pattern"); + fireEvent.change(renamePatternInput, { target: { value: "remote_$1" } }); + + expect(screen.getByTestId("action-render-convert-index-to-remote-rename-pattern")).toHaveValue("remote_$1"); }); 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"); + fireEvent.change(ignoreIndexSettingsInput, { target: { value: "index.refresh_interval,index.number_of_replicas" } }); - expect(ignoreIndexSettingsInput).toHaveValue("index.refresh_interval,index.number_of_replicas"); + expect(screen.getByTestId("action-render-convert-index-to-remote-ignore-index-settings")).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"); + fireEvent.change(numberOfReplicasInput, { target: { value: "2" } }); - expect(numberOfReplicasInput).toHaveValue(2); + expect(screen.getByTestId("action-render-convert-index-to-remote-number-of-replicas")).toHaveValue(2); }); it("validates required fields", () => { const uiAction = actionRepoSingleton.getUIAction("convert_index_to_remote"); - + // Valid action expect(uiAction.isValid()).toBe(true); @@ -139,6 +162,32 @@ describe("ConvertIndexToRemoteUIAction component", () => { }; const invalidUIAction2 = actionRepoSingleton.getUIActionFromData(invalidAction2.action); expect(invalidUIAction2.isValid()).toBe(false); + + // Invalid action - rename pattern missing $1 + const invalidAction3 = { + ...TEST_PROPS, + action: { + convert_index_to_remote: { + ...DEFAULT_CONVERT_INDEX_TO_REMOTE.convert_index_to_remote, + rename_pattern: "remote_index", + }, + }, + }; + const invalidUIAction3 = actionRepoSingleton.getUIActionFromData(invalidAction3.action); + expect(invalidUIAction3.isValid()).toBe(false); + + // Invalid action - negative number_of_replicas + const invalidAction4 = { + ...TEST_PROPS, + action: { + convert_index_to_remote: { + ...DEFAULT_CONVERT_INDEX_TO_REMOTE.convert_index_to_remote, + number_of_replicas: -1, + }, + }, + }; + const invalidUIAction4 = actionRepoSingleton.getUIActionFromData(invalidAction4.action); + expect(invalidUIAction4.isValid()).toBe(false); }); it("renders with custom values", () => { @@ -149,21 +198,21 @@ describe("ConvertIndexToRemoteUIAction component", () => { include_aliases: true, ignore_index_settings: "index.refresh_interval", number_of_replicas: 2, + delete_original_index: true, + rename_pattern: "remote_$1", }, }; const customProps = { ...TEST_PROPS, action: customAction }; - const { container } = render( - actionRepoSingleton.getUIAction("convert_index_to_remote").render(customProps, mockOnChangeAction) - ); + 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-delete-original-index")).toBeChecked(); + expect(screen.getByTestId("action-render-convert-index-to-remote-rename-pattern")).toHaveValue("remote_$1"); 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", () => { @@ -174,13 +223,26 @@ describe("ConvertIndexToRemoteUIAction component", () => { 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"); + expect(action.convert_index_to_remote).toHaveProperty("delete_original_index"); + expect(action.convert_index_to_remote).toHaveProperty("rename_pattern"); + expect(action.convert_index_to_remote).not.toHaveProperty("number_of_replicas"); }); -}); + it("keeps number_of_replicas when explicitly configured", () => { + const uiAction = actionRepoSingleton.getUIActionFromData({ + convert_index_to_remote: { + ...DEFAULT_CONVERT_INDEX_TO_REMOTE.convert_index_to_remote, + number_of_replicas: 2, + }, + }); + + const action = uiAction.toAction() as ConvertIndexToRemoteAction; + expect(action.convert_index_to_remote.number_of_replicas).toBe(2); + }); +}); diff --git a/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.tsx b/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.tsx index 1319a2c57..965c025a3 100644 --- a/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.tsx +++ b/public/pages/VisualCreatePolicy/components/UIActions/ConvertIndexToRemoteUIAction.tsx @@ -10,6 +10,8 @@ import { makeId } from "../../../../utils/helpers"; import { ActionType } from "../../utils/constants"; import EuiFormCustomLabel from "../EuiFormCustomLabel"; +const DEFAULT_RENAME_PATTERN = "$1_remote"; + export default class ConvertIndexToRemoteUIAction implements UIAction { id: string; action: ConvertIndexToRemoteAction; @@ -25,21 +27,46 @@ export default class ConvertIndexToRemoteUIAction implements UIAction new ConvertIndexToRemoteUIAction(action, this.id); isValid = () => { - return !!this.action.convert_index_to_remote.snapshot && !!this.action.convert_index_to_remote.repository; + return this.isValidAction(this.action); + }; + + isValidAction = (action: ConvertIndexToRemoteAction) => { + const convertAction = action.convert_index_to_remote; + return ( + !!convertAction.snapshot && + !!convertAction.repository && + this.isValidRenamePattern(convertAction.rename_pattern) && + this.isValidNumberOfReplicas(convertAction.number_of_replicas) + ); + }; + + isValidRenamePattern = (renamePattern: string = DEFAULT_RENAME_PATTERN) => { + return renamePattern.trim().length > 0 && renamePattern.includes("$1"); + }; + + isValidNumberOfReplicas = (numberOfReplicas?: number) => { + return typeof numberOfReplicas === "undefined" || numberOfReplicas >= 0; }; render = (action: UIAction, onChangeAction: (action: UIAction) => void) => { + const convertAction = action.action as ConvertIndexToRemoteAction; + const convertIndexToRemote = convertAction.convert_index_to_remote; + const isRepositoryValid = !!convertIndexToRemote.repository; + const isSnapshotValid = !!convertIndexToRemote.snapshot; + const isRenamePatternValid = this.isValidRenamePattern(convertIndexToRemote.rename_pattern); + const isNumberOfReplicasValid = this.isValidNumberOfReplicas(convertIndexToRemote.number_of_replicas); + return ( <> - + ) => { const repository = e.target.value; onChangeAction( @@ -55,15 +82,15 @@ export default class ConvertIndexToRemoteUIAction implements UIAction - - + ) => { const snapshot = e.target.value; onChangeAction( @@ -79,6 +106,32 @@ export default class ConvertIndexToRemoteUIAction implements UIAction + + + ) => { + const renamePattern = e.target.value; + onChangeAction( + this.clone({ + convert_index_to_remote: { + ...action.action.convert_index_to_remote, + rename_pattern: renamePattern, + }, + }) + ); + }} + data-test-subj="action-render-convert-index-to-remote-rename-pattern" + /> + + ) => { - const include_aliases = e.target.checked; + const includeAliases = e.target.checked; onChangeAction( this.clone({ convert_index_to_remote: { ...action.action.convert_index_to_remote, - include_aliases, + include_aliases: includeAliases, }, }) ); @@ -103,6 +156,30 @@ export default class ConvertIndexToRemoteUIAction implements UIAction + + + ) => { + const deleteOriginalIndex = e.target.checked; + onChangeAction( + this.clone({ + convert_index_to_remote: { + ...action.action.convert_index_to_remote, + delete_original_index: deleteOriginalIndex, + }, + }) + ); + }} + data-test-subj="action-render-convert-index-to-remote-delete-original-index" + /> + + ) => { - const ignore_index_settings = e.target.value; + const ignoreIndexSettings = e.target.value; onChangeAction( this.clone({ convert_index_to_remote: { ...action.action.convert_index_to_remote, - ignore_index_settings, + ignore_index_settings: ignoreIndexSettings, }, }) ); @@ -129,21 +206,24 @@ export default class ConvertIndexToRemoteUIAction implements UIAction - + ) => { - const number_of_replicas = parseInt(e.target.value); + const numberOfReplicas = e.target.valueAsNumber; + const updatedConvertIndexToRemote: ConvertIndexToRemoteAction["convert_index_to_remote"] = { + ...action.action.convert_index_to_remote, + number_of_replicas: numberOfReplicas, + }; + if (isNaN(numberOfReplicas)) delete updatedConvertIndexToRemote.number_of_replicas; onChangeAction( this.clone({ - convert_index_to_remote: { - ...action.action.convert_index_to_remote, - number_of_replicas, - }, + convert_index_to_remote: updatedConvertIndexToRemote, }) ); }} @@ -155,6 +235,16 @@ export default class ConvertIndexToRemoteUIAction implements UIAction this.action; + toAction = () => { + const action: ConvertIndexToRemoteAction = { + ...this.action, + convert_index_to_remote: { + ...this.action.convert_index_to_remote, + }, + }; + if (typeof action.convert_index_to_remote.number_of_replicas === "undefined") { + delete action.convert_index_to_remote.number_of_replicas; + } + return 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 deleted file mode 100644 index 234193571..000000000 --- a/public/pages/VisualCreatePolicy/components/UIActions/__snapshots__/ConvertIndexToRemoteUIAction.test.tsx.snap +++ /dev/null @@ -1,34 +0,0 @@ -// 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/containers/CreateAction/__snapshots__/CreateAction.test.tsx.snap b/public/pages/VisualCreatePolicy/containers/CreateAction/__snapshots__/CreateAction.test.tsx.snap index 5931d370c..37ec3f971 100644 --- a/public/pages/VisualCreatePolicy/containers/CreateAction/__snapshots__/CreateAction.test.tsx.snap +++ b/public/pages/VisualCreatePolicy/containers/CreateAction/__snapshots__/CreateAction.test.tsx.snap @@ -110,6 +110,11 @@ exports[` spec renders the component 1`] = ` > Close +