From 5f5a172beaab96183c611558fc9deecad2038421 Mon Sep 17 00:00:00 2001
From: AWSHurneyt
Date: Tue, 30 May 2023 19:25:42 -0700
Subject: [PATCH] Implemented alias action UX. (#754)
* Implemented UX component for configuring and editing alias actions.
Signed-off-by: AWSHurneyt
* Implemented unit and integration tests for alias action UX.
Signed-off-by: AWSHurneyt
* Refactored existing test to accommodate addition of alias action UX.
Signed-off-by: AWSHurneyt
* Fixed a bug preventing validation messages from appearing under both combo boxes at once.
Signed-off-by: AWSHurneyt
* Removed redundant 'popout' icons from 'Learn more' links. Having 'target="_blank"' in the link attributes provides the icon. Updated snapshots.
Signed-off-by: AWSHurneyt
* Added unit tests for AliasUiAction.
Signed-off-by: AWSHurneyt
* Refactored alias action UX to include toggles to hide/display the add/remove combo boxes. Implemented a modal to confirm clearing combo box entries when toggling display.
Signed-off-by: AWSHurneyt
* Refactored UX to remove the alias combo box clear confirmation modal based on PR feedback.
Signed-off-by: AWSHurneyt
* Updated the 2.8 release notes.
Signed-off-by: AWSHurneyt
---------
Signed-off-by: AWSHurneyt
---
.../sample_policy_alias_action.json | 41 ++
.../policies_spec.js | 167 +++++
models/interfaces.ts | 19 +
.../ErrorNotification/ErrorNotification.tsx | 2 +-
.../ErrorNotification.test.tsx.snap | 3 +-
.../components/NewPolicy/NewPolicy.tsx | 2 +-
.../__snapshots__/NewPolicy.test.tsx.snap | 3 +-
.../__snapshots__/ChangePolicy.test.tsx.snap | 3 +-
.../components/DefinePolicy/DefinePolicy.tsx | 2 +-
.../__snapshots__/DefinePolicy.test.tsx.snap | 3 +-
.../containers/CreatePolicy/CreatePolicy.tsx | 2 +-
.../__snapshots__/CreatePolicy.test.tsx.snap | 3 +-
.../utils/__snapshots__/helpers.test.tsx.snap | 37 +
.../pages/CreatePolicy/utils/helpers.test.tsx | 35 +
public/pages/CreatePolicy/utils/helpers.tsx | 21 +
.../ApplyPolicyModal/ApplyPolicyModal.tsx | 2 +-
.../components/ISMTemplates/ISMTemplates.tsx | 2 +-
.../__snapshots__/ISMTemplates.test.tsx.snap | 3 +-
.../components/States/States.tsx | 2 +-
.../States/__snapshots__/States.test.tsx.snap | 3 +-
.../components/Transition/Transition.tsx | 2 +-
.../AliasUIAction/AliasUIAction.test.tsx | 83 +++
.../UIActions/AliasUIAction/AliasUIAction.tsx | 109 +++
.../AliasUIAction/AliasUIActionComponent.tsx | 172 +++++
.../__snapshots__/AliasUIAction.test.tsx.snap | 665 ++++++++++++++++++
.../components/UIActions/index.ts | 2 +
.../containers/CreateAction/CreateAction.tsx | 16 +-
.../__snapshots__/CreateAction.test.tsx.snap | 8 +-
.../CreateTransition/CreateTransition.tsx | 2 +-
.../CreateTransition.test.tsx.snap | 3 +-
.../ErrorNotification/ErrorNotification.tsx | 2 +-
.../ErrorNotification.test.tsx.snap | 3 +-
.../VisualCreatePolicy/VisualCreatePolicy.tsx | 2 +-
.../VisualCreatePolicy/utils/constants.ts | 10 +-
.../VisualCreatePolicy/utils/helpers.test.ts | 4 +-
.../pages/VisualCreatePolicy/utils/helpers.ts | 25 +-
public/utils/constants.ts | 9 +
...dashboards-plugin.release-notes-2.8.0.0.md | 1 +
38 files changed, 1426 insertions(+), 47 deletions(-)
create mode 100644 cypress/fixtures/plugins/index-management-dashboards-plugin/sample_policy_alias_action.json
create mode 100644 public/pages/CreatePolicy/utils/__snapshots__/helpers.test.tsx.snap
create mode 100644 public/pages/CreatePolicy/utils/helpers.test.tsx
create mode 100644 public/pages/CreatePolicy/utils/helpers.tsx
create mode 100644 public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIAction.test.tsx
create mode 100644 public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIAction.tsx
create mode 100644 public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIActionComponent.tsx
create mode 100644 public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/__snapshots__/AliasUIAction.test.tsx.snap
diff --git a/cypress/fixtures/plugins/index-management-dashboards-plugin/sample_policy_alias_action.json b/cypress/fixtures/plugins/index-management-dashboards-plugin/sample_policy_alias_action.json
new file mode 100644
index 000000000..6f62047de
--- /dev/null
+++ b/cypress/fixtures/plugins/index-management-dashboards-plugin/sample_policy_alias_action.json
@@ -0,0 +1,41 @@
+{
+ "policy": {
+ "policy_id": "test_alias_policy_id",
+ "description": "Example policy with an alias action.",
+ "default_state": "alias-state",
+ "states": [
+ {
+ "name": "alias-state",
+ "actions": [
+ {
+ "alias": {
+ "actions": [
+ {
+ "remove": {
+ "aliases": ["alias1", "alias2"]
+ }
+ },
+ {
+ "remove": {
+ "alias": "alias3"
+ }
+ },
+ {
+ "add": {
+ "aliases": ["alias4", "alias5"]
+ }
+ },
+ {
+ "add": {
+ "aliases": "alias6"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "transitions": []
+ }
+ ]
+ }
+}
diff --git a/cypress/integration/plugins/index-management-dashboards-plugin/policies_spec.js b/cypress/integration/plugins/index-management-dashboards-plugin/policies_spec.js
index 7f4ed8b88..2a9f6d384 100644
--- a/cypress/integration/plugins/index-management-dashboards-plugin/policies_spec.js
+++ b/cypress/integration/plugins/index-management-dashboards-plugin/policies_spec.js
@@ -5,6 +5,7 @@
import { BASE_PATH, IM_PLUGIN_NAME } from "../../../utils/constants";
import samplePolicy from "../../../fixtures/plugins/index-management-dashboards-plugin/sample_policy";
+import sampleAliasPolicy from "../../../fixtures/plugins/index-management-dashboards-plugin/sample_policy_alias_action.json";
const POLICY_ID = "test_policy_id";
@@ -64,6 +65,98 @@ describe("Policies", () => {
// Confirm we can see the created policy's description in table
cy.contains("A simple description");
});
+
+ it("with an alias action using the visual editor", () => {
+ /* Create a policy with an alias action */
+ const aliasPolicyId = "visual-editor-alias-policy";
+ const testInputs = {
+ add: ["alias1", "alias2"],
+ remove: ["alias3", "alias5", "alias6"],
+ };
+
+ // Route us to create policy page
+ cy.contains("Create policy").click({ force: true });
+
+ // Use the visual editor
+ cy.contains("Visual editor").click({ force: true });
+ cy.contains("Continue").click({ force: true });
+
+ // Wait for input to load and then type in the policy ID
+ cy.get(`input[placeholder="hot_cold_workflow"]`).type(aliasPolicyId, {
+ force: true,
+ });
+
+ // Type in the policy description
+ cy.get(`[data-test-subj="create-policy-description"]`).type("{selectall}{backspace}" + sampleAliasPolicy.policy.description);
+
+ // Add a state
+ cy.get("button").contains("Add state").click({ force: true });
+
+ // Enter a state name
+ cy.get(`[data-test-subj="create-state-state-name"]`).type(sampleAliasPolicy.policy.states[0].name);
+
+ // Add a new action
+ cy.get("button").contains("+ Add action").click({ force: true });
+
+ // Select 'Alias' type
+ cy.get(`[data-test-subj="create-state-action-type"]`).select("Add / remove aliases");
+
+ // Confirm 'Add action' button is disabled
+ cy.get(`[data-test-subj="flyout-footer-action-button"]`).should("be.disabled");
+
+ // Toggle the add alias combo box
+ cy.get(`[data-test-subj="add-alias-toggle"]`).click({ force: true });
+
+ // Enter aliases to add
+ cy.get(`[data-test-subj="add-alias-combo-box"]`).click({ force: true }).type(testInputs.add.join("{enter}"));
+
+ // Toggle the add alias combo box
+ cy.get(`[data-test-subj="remove-alias-toggle"]`).click({ force: true });
+
+ // Enter aliases to remove
+ cy.get(`[data-test-subj="remove-alias-combo-box"]`).click({ force: true }).type(testInputs.remove.join("{enter}"));
+
+ // Click the 'Add action' button
+ cy.get(`[data-test-subj="flyout-footer-action-button"]`).click({ force: true });
+
+ // Click the 'Save action' button
+ cy.get("button").contains("Save state").click({ force: true });
+
+ // Click the 'Create' button
+ cy.get("button").contains("Create").click({ force: true });
+
+ /* Confirm policy was created as expected */
+
+ // Wait for the 'State management' dashboard to load
+ cy.contains("State management policies (", { timeout: 60000 });
+
+ // Click on the test alias to navigate to the details page
+ cy.contains(aliasPolicyId).click({ force: true });
+
+ // Wait for the details page to load, and click the 'Edit' button
+ cy.url({ timeout: 60000 }).should("include", "policy-details");
+ cy.contains("Edit").click({ force: true });
+
+ // Use the visual editor
+ cy.contains("Visual editor").click({ force: true });
+ cy.contains("Continue").click({ force: true });
+
+ // Click the state edit icon
+ cy.get(`[aria-label="Edit"]`).click({ force: true });
+ cy.get(`[data-test-subj="draggable"]`).within(() => {
+ cy.get(`[aria-label="Edit"]`).click({ force: true });
+ });
+
+ // Confirm all of the expected inputs are in the 'Add' combo box
+ testInputs.add.forEach((alias) => {
+ cy.get(`[data-test-subj="add-alias-combo-box"]`).contains(alias);
+ });
+
+ // Confirm all of the expected inputs are in the 'Remove' combo box
+ testInputs.remove.forEach((alias) => {
+ cy.get(`[data-test-subj="remove-alias-combo-box"]`).contains(alias);
+ });
+ });
});
describe("can be edited", () => {
@@ -71,6 +164,7 @@ describe("Policies", () => {
cy.deleteAllIndices();
cy.deleteIMJobs();
cy.createPolicy(POLICY_ID, samplePolicy);
+ cy.createPolicy(sampleAliasPolicy.policy.policy_id, sampleAliasPolicy);
});
it("successfully", () => {
@@ -116,6 +210,79 @@ describe("Policies", () => {
// Confirm new description shows in table
cy.contains("A new description");
});
+
+ it("with more aliases", () => {
+ // Click on the test alias to navigate to the details page
+ const testInputs = {
+ add: ["alias4", "alias6"],
+ remove: ["alias1", "alias2", "alias3", "alias7"],
+ };
+ /* Edit the policy */
+
+ // Click on the test alias to navigate to the details page
+ cy.contains(sampleAliasPolicy.policy.policy_id).click({ force: true });
+
+ // Wait for the details page to load, and click the 'Edit' button
+ cy.url({ timeout: 60000 }).should("include", "policy-details");
+ cy.contains("Edit").click({ force: true });
+
+ // Use the visual editor
+ cy.contains("Visual editor").click({ force: true });
+ cy.contains("Continue").click({ force: true });
+
+ // Click the 'Edit state' icon
+ cy.get(`[aria-label="Edit"]`).click({ force: true });
+
+ // Click the 'Edit action' icon
+ cy.get(`[data-test-subj="draggable"]`).within(() => {
+ cy.get(`[aria-label="Edit"]`).click({ force: true });
+ });
+
+ // Remove an alias from the 'Add' combo box
+ cy.get(`[aria-label="Remove alias5 from selection in this group"]`).click({ force: true });
+
+ // Add a new alias to the 'Remove' combo box
+ cy.get(`[data-test-subj="remove-alias-combo-box"]`).click({ force: true }).type("alias7{enter}");
+
+ // Save the edits
+ cy.get(`[data-test-subj="flyout-footer-action-button"]`).click({ force: true });
+ cy.get("button").contains("Update state").click({ force: true });
+ cy.get("button").contains("Update").click({ force: true });
+
+ /* Confirm policy was edited as expected */
+
+ // Wait for the 'State management' dashboard to load
+ cy.contains("State management policies (", { timeout: 60000 });
+
+ // Click on the test alias to navigate to the details page
+ cy.contains(sampleAliasPolicy.policy.policy_id).click({ force: true });
+
+ // Wait for the details page to load, and click the 'Edit' button
+ cy.url({ timeout: 60000 }).should("include", "policy-details");
+ cy.contains("Edit").click({ force: true });
+
+ // Use the visual editor
+ cy.contains("Visual editor").click({ force: true });
+ cy.contains("Continue").click({ force: true });
+
+ // Click the 'Edit state' icon
+ cy.get(`[aria-label="Edit"]`).click({ force: true });
+
+ // Click the 'Edit action' icon
+ cy.get(`[data-test-subj="draggable"]`).within(() => {
+ cy.get(`[aria-label="Edit"]`).click({ force: true });
+ });
+
+ // Confirm all of the expected inputs are in the 'Add' combo box
+ testInputs.add.forEach((alias) => {
+ cy.get(`[data-test-subj="add-alias-combo-box"]`).contains(alias);
+ });
+
+ // Confirm all of the expected inputs are in the 'Remove' combo box
+ testInputs.remove.forEach((alias) => {
+ cy.get(`[data-test-subj="remove-alias-combo-box"]`).contains(alias);
+ });
+ });
});
describe("can be deleted", () => {
diff --git a/models/interfaces.ts b/models/interfaces.ts
index 9cfac1414..062dd49e8 100644
--- a/models/interfaces.ts
+++ b/models/interfaces.ts
@@ -325,6 +325,7 @@ export interface UIAction {
clone: (action: Data) => UIAction;
content: () => JSX.Element | string | null;
toAction: () => Action;
+ customDisplayText?: string;
}
export interface ForceMergeAction extends Action {
@@ -433,6 +434,24 @@ export interface IndexPriorityAction extends Action {
};
}
+export enum AliasActions {
+ ADD = "add",
+ REMOVE = "remove",
+}
+
+export type AliasActionItem = {
+ [key in AliasActions]: {
+ alias?: string;
+ aliases?: string[];
+ };
+};
+
+export interface AliasAction extends Action {
+ alias: {
+ actions: AliasActionItem[];
+ };
+}
+
export interface AllocationAction extends Action {
allocation: {
// TODO: These require a complete UI and we are only supporting JSON editor for allocation for now
diff --git a/public/containers/ErrorNotification/ErrorNotification.tsx b/public/containers/ErrorNotification/ErrorNotification.tsx
index e68d6f40f..6d7333a39 100644
--- a/public/containers/ErrorNotification/ErrorNotification.tsx
+++ b/public/containers/ErrorNotification/ErrorNotification.tsx
@@ -143,7 +143,7 @@ class ErrorNotification extends Component
You can set up an error notification for when a policy execution fails.{" "}
- Learn more
+ Learn more
diff --git a/public/containers/ErrorNotification/__snapshots__/ErrorNotification.test.tsx.snap b/public/containers/ErrorNotification/__snapshots__/ErrorNotification.test.tsx.snap
index 06e91c673..c7c4d12f9 100644
--- a/public/containers/ErrorNotification/__snapshots__/ErrorNotification.test.tsx.snap
+++ b/public/containers/ErrorNotification/__snapshots__/ErrorNotification.test.tsx.snap
@@ -60,8 +60,7 @@ exports[` spec renders the component 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
- EuiIconMock
+ Learn more
EuiIconMock
When the new policy will take effect depends on the current state of indices and the states of the new policy.{" "}
- Learn more
+ Learn more
diff --git a/public/pages/ChangePolicy/components/NewPolicy/__snapshots__/NewPolicy.test.tsx.snap b/public/pages/ChangePolicy/components/NewPolicy/__snapshots__/NewPolicy.test.tsx.snap
index 94100bb2f..58003e5d6 100644
--- a/public/pages/ChangePolicy/components/NewPolicy/__snapshots__/NewPolicy.test.tsx.snap
+++ b/public/pages/ChangePolicy/components/NewPolicy/__snapshots__/NewPolicy.test.tsx.snap
@@ -46,8 +46,7 @@ exports[` spec renders the component 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
- EuiIconMock
+ Learn more
EuiIconMock
spec renders the component 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
- EuiIconMock
+ Learn more
EuiIconMock
- Learn more
+ Learn more
diff --git a/public/pages/CreatePolicy/components/DefinePolicy/__snapshots__/DefinePolicy.test.tsx.snap b/public/pages/CreatePolicy/components/DefinePolicy/__snapshots__/DefinePolicy.test.tsx.snap
index 02aca8681..2475ca9dc 100644
--- a/public/pages/CreatePolicy/components/DefinePolicy/__snapshots__/DefinePolicy.test.tsx.snap
+++ b/public/pages/CreatePolicy/components/DefinePolicy/__snapshots__/DefinePolicy.test.tsx.snap
@@ -96,8 +96,7 @@ exports[` spec renders the component 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
- EuiIconMock
+ Learn more
EuiIconMock
- Learn more
+ Learn more
diff --git a/public/pages/CreatePolicy/containers/CreatePolicy/__snapshots__/CreatePolicy.test.tsx.snap b/public/pages/CreatePolicy/containers/CreatePolicy/__snapshots__/CreatePolicy.test.tsx.snap
index 42fd980de..082b6e881 100644
--- a/public/pages/CreatePolicy/containers/CreatePolicy/__snapshots__/CreatePolicy.test.tsx.snap
+++ b/public/pages/CreatePolicy/containers/CreatePolicy/__snapshots__/CreatePolicy.test.tsx.snap
@@ -341,8 +341,7 @@ exports[` spec renders the edit component 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
- EuiIconMock
+ Learn more
EuiIconMock
+
+ You can add up to 10 more aliases.
+
+
+`;
+
+exports[`inputLimitText renders the component with 0 inputs remaining 1`] = `
+
+
+ You have reached the limit of 10 aliases.
+
+
+`;
+
+exports[`inputLimitText renders the component with 1 input remaining 1`] = `
+
+
+ You can add up to 1 more alias.
+
+
+`;
diff --git a/public/pages/CreatePolicy/utils/helpers.test.tsx b/public/pages/CreatePolicy/utils/helpers.test.tsx
new file mode 100644
index 000000000..3a6c774b2
--- /dev/null
+++ b/public/pages/CreatePolicy/utils/helpers.test.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { inputLimitText } from "./helpers";
+
+function renderInputLimitText(currCount?, limit?, singularKeyword?, pluralKeyword?, styleProps?) {
+ return { ...render(inputLimitText(currCount, limit, singularKeyword, pluralKeyword, styleProps)) };
+}
+
+describe("inputLimitText", () => {
+ it("renders the component with 0 inputs", () => {
+ const expected = `You can add up to 10 more aliases.`;
+ const { container } = renderInputLimitText(0, 10, "alias", "aliases");
+ expect(screen.getByText(expected)).toBeInTheDocument();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it("renders the component with 1 input remaining", () => {
+ const expected = "You can add up to 1 more alias.";
+ const { container } = renderInputLimitText(9, 10, "alias", "aliases");
+ expect(screen.getByText(expected)).toBeInTheDocument();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it("renders the component with 0 inputs remaining", () => {
+ const expected = "You have reached the limit of 10 aliases.";
+ const { container } = renderInputLimitText(10, 10, "alias", "aliases");
+ expect(screen.getByText(expected)).toBeInTheDocument();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/public/pages/CreatePolicy/utils/helpers.tsx b/public/pages/CreatePolicy/utils/helpers.tsx
new file mode 100644
index 000000000..75380ba05
--- /dev/null
+++ b/public/pages/CreatePolicy/utils/helpers.tsx
@@ -0,0 +1,21 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from "react";
+import { EuiText } from "@elastic/eui";
+
+// A helper function to generate a simple string explaining how many elements a user can add to a list.
+export const inputLimitText = (currCount = 0, limit = 0, singularKeyword = "", pluralKeyword = "", styleProps = {}) => {
+ const difference = limit - currCount;
+ const remainingLimit = `You can add up to ${difference} ${limit === 1 ? "" : "more"} ${
+ difference === 1 ? singularKeyword : pluralKeyword
+ }.`;
+ const reachedLimit = `You have reached the limit of ${limit} ${limit === 1 ? singularKeyword : pluralKeyword}.`;
+ return (
+
+ {difference > 0 ? remainingLimit : reachedLimit}
+
+ );
+};
diff --git a/public/pages/Indices/components/ApplyPolicyModal/ApplyPolicyModal.tsx b/public/pages/Indices/components/ApplyPolicyModal/ApplyPolicyModal.tsx
index 0b9072112..1c4106669 100644
--- a/public/pages/Indices/components/ApplyPolicyModal/ApplyPolicyModal.tsx
+++ b/public/pages/Indices/components/ApplyPolicyModal/ApplyPolicyModal.tsx
@@ -207,7 +207,7 @@ export default class ApplyPolicyModal extends Component
This policy includes a rollover action. Specify a rollover alias.{" "}
- Learn more
+ Learn more
diff --git a/public/pages/VisualCreatePolicy/components/ISMTemplates/ISMTemplates.tsx b/public/pages/VisualCreatePolicy/components/ISMTemplates/ISMTemplates.tsx
index f3bbd53d3..223ea6e63 100644
--- a/public/pages/VisualCreatePolicy/components/ISMTemplates/ISMTemplates.tsx
+++ b/public/pages/VisualCreatePolicy/components/ISMTemplates/ISMTemplates.tsx
@@ -55,7 +55,7 @@ const ISMTemplates = ({ policy, onChangePolicy }: ISMTemplatesProps) => {
Specify ISM template patterns that match the index to apply the policy.{" "}
- Learn more
+ Learn more
diff --git a/public/pages/VisualCreatePolicy/components/ISMTemplates/__snapshots__/ISMTemplates.test.tsx.snap b/public/pages/VisualCreatePolicy/components/ISMTemplates/__snapshots__/ISMTemplates.test.tsx.snap
index 1929feb30..9b109b00b 100644
--- a/public/pages/VisualCreatePolicy/components/ISMTemplates/__snapshots__/ISMTemplates.test.tsx.snap
+++ b/public/pages/VisualCreatePolicy/components/ISMTemplates/__snapshots__/ISMTemplates.test.tsx.snap
@@ -60,8 +60,7 @@ exports[` spec renders the component 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
- EuiIconMock
+ Learn more
EuiIconMock
"Transitions" define when to move from one state to another.{" "}
- Learn more
+ Learn more
diff --git a/public/pages/VisualCreatePolicy/components/States/__snapshots__/States.test.tsx.snap b/public/pages/VisualCreatePolicy/components/States/__snapshots__/States.test.tsx.snap
index 27704ea90..2df7c2b78 100644
--- a/public/pages/VisualCreatePolicy/components/States/__snapshots__/States.test.tsx.snap
+++ b/public/pages/VisualCreatePolicy/components/States/__snapshots__/States.test.tsx.snap
@@ -43,8 +43,7 @@ exports[` spec renders the component 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
- EuiIconMock
+ Learn more
EuiIconMock
{
helpText="The matching cron expression required to transition to the next state."
learnMore={
- Learn more
+ Learn more
}
/>
diff --git a/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIAction.test.tsx b/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIAction.test.tsx
new file mode 100644
index 000000000..0198771f1
--- /dev/null
+++ b/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIAction.test.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from "react";
+import "@testing-library/jest-dom";
+import { render, screen, cleanup } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { DEFAULT_ALIAS } from "../../../utils/constants";
+import { AliasAction, AliasActions, UIAction } from "../../../../../../models/interfaces";
+import { actionRepoSingleton } from "../../../utils/helpers";
+
+const TEST_PROPS: UIAction = { action: DEFAULT_ALIAS } as UIAction;
+
+const renderComponent = (uiAction: UIAction = TEST_PROPS) => {
+ render(actionRepoSingleton.getUIAction("alias").render(uiAction, mockOnChangeAction));
+};
+const mockOnChangeAction = (uiAction: UIAction = TEST_PROPS) => {
+ cleanup();
+ renderComponent(uiAction);
+};
+
+afterEach(() => cleanup());
+
+describe("AliasUIAction component", () => {
+ it("renders with blank action", () => {
+ const { container } = render(actionRepoSingleton.getUIAction("alias").render(TEST_PROPS, mockOnChangeAction));
+
+ userEvent.click(screen.getByTestId("add-alias-toggle"));
+ userEvent.click(screen.getByTestId("remove-alias-toggle"));
+
+ const addAliasRow = screen.getByTestId("add-alias-row");
+ const removeAliasRow = screen.getByTestId("remove-alias-row");
+
+ expect(addAliasRow).toHaveTextContent("Select aliases to add to indexes");
+ expect(addAliasRow).toHaveTextContent("You can add up to 10 more aliases.");
+
+ expect(removeAliasRow).toHaveTextContent("Select aliases to remove from indexes");
+ expect(removeAliasRow).toHaveTextContent("You can add up to 10 more aliases.");
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it("renders with pre-defined actions", () => {
+ const actions = [
+ { [AliasActions.ADD]: { alias: "alias1" } },
+ { [AliasActions.ADD]: { alias: "alias3" } },
+ { [AliasActions.REMOVE]: { alias: "alias2" } },
+ { [AliasActions.ADD]: { aliases: ["alias5", "alias7", "alias9"] } },
+ { [AliasActions.REMOVE]: { aliases: ["alias4", "alias6", "alias8"] } },
+ ];
+ const testProps = { ...TEST_PROPS };
+ testProps.action.alias.actions = actions;
+
+ const { container } = render(actionRepoSingleton.getUIAction("alias").render(testProps, mockOnChangeAction));
+ expect(screen.getByTestId("add-alias-row")).toHaveTextContent("You can add up to 5 more aliases.");
+ expect(screen.getByTestId("remove-alias-row")).toHaveTextContent("You can add up to 6 more aliases.");
+
+ const addAliasComboBox = screen.getByTestId("add-alias-combo-box");
+ const removeAliasComboBox = screen.getByTestId("remove-alias-combo-box");
+ actions.forEach((action) => {
+ switch (Object.keys(action)[0]) {
+ case AliasActions.ADD:
+ if (action[AliasActions.ADD].alias) expect(addAliasComboBox).toHaveTextContent(action[AliasActions.ADD].alias);
+ if (action[AliasActions.ADD].aliases)
+ action[AliasActions.ADD].aliases.forEach((alias) => {
+ expect(addAliasComboBox).toHaveTextContent(alias);
+ });
+ break;
+ case AliasActions.REMOVE:
+ if (action[AliasActions.REMOVE].alias) expect(removeAliasComboBox).toHaveTextContent(action[AliasActions.REMOVE].alias);
+ if (action[AliasActions.REMOVE].aliases)
+ action[AliasActions.REMOVE].aliases.forEach((alias) => {
+ expect(removeAliasComboBox).toHaveTextContent(alias);
+ });
+ break;
+ }
+ });
+
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIAction.tsx b/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIAction.tsx
new file mode 100644
index 000000000..a6dc67750
--- /dev/null
+++ b/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIAction.tsx
@@ -0,0 +1,109 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from "react";
+import { EuiComboBoxOptionOption } from "@elastic/eui";
+import { AliasAction, AliasActionItem, AliasActions, UIAction } from "../../../../../../models/interfaces";
+import { ActionType } from "../../../utils/constants";
+import { makeId } from "../../../../../utils/helpers";
+import { ALIAS_NAMING_MESSAGE, ALIAS_NAMING_PATTERN } from "../../../../../utils/constants";
+import AliasUIActionComponent from "./AliasUIActionComponent";
+
+export const MAX_ALIAS_ACTIONS = 10;
+export const DUPLICATED_ALIAS_TEXT = "An alias cannot be added and removed in the same action.";
+
+export default class AliasUIAction implements UIAction {
+ customDisplayText = "Add / remove aliases";
+ id: string;
+ action: AliasAction;
+ type = ActionType.Alias;
+ errors: { [key in AliasActions]: string | undefined } = {};
+ selectedItems: { [key in AliasActions]: EuiComboBoxOptionOption[] } = {};
+
+ constructor(action: AliasAction, id: string = makeId()) {
+ this.action = action;
+ this.id = id;
+ this.selectedItems = this.parseToComboBoxOptions(action);
+ this.errors = this.getAliasActionErrorText(this.selectedItems);
+ }
+
+ content = () => this.customDisplayText;
+
+ clone = (action: AliasAction = this.action) => new AliasUIAction(action, this.id);
+
+ isValid = () => {
+ // Either add/remove has at least 1 action
+ if (this.action.alias.actions.length === 0) return false;
+
+ // No errors for any alias actions
+ return !Object.entries(this.errors).some(([_, error]) => error);
+ };
+
+ getAliasActionErrorText = (selectedItems: { [key in AliasActions]: EuiComboBoxOptionOption[] }) => {
+ const errors: { [key in AliasActions]: string | undefined } = {};
+
+ // Each alias is valid
+ let aliasError: string | undefined;
+ this.action.alias.actions.forEach((action) => {
+ const aliasActionType = Object.keys(action)[0];
+ aliasError = this.getAliasErrorText(action, selectedItems);
+ errors[aliasActionType] = aliasError;
+ });
+ return errors;
+ };
+
+ getAliasErrorText = (
+ action: AliasActionItem,
+ selectedItems: { [key in AliasActions]: EuiComboBoxOptionOption[] }
+ ): string | undefined => {
+ const aliasActionType = Object.keys(action)[0] as AliasActions;
+
+ // Validate alias string.
+ const alias = action[aliasActionType].alias;
+ if (alias && !ALIAS_NAMING_PATTERN.test(alias)) return ALIAS_NAMING_MESSAGE;
+
+ // No duplicate aliases between add and remove actions
+ switch (aliasActionType) {
+ case AliasActions.ADD:
+ if ((selectedItems[AliasActions.REMOVE] || []).some((option) => option?.label === alias)) return DUPLICATED_ALIAS_TEXT;
+ break;
+ case AliasActions.REMOVE:
+ if ((selectedItems[AliasActions.ADD] || []).some((option) => option?.label === alias)) return DUPLICATED_ALIAS_TEXT;
+ break;
+ }
+ };
+
+ parseToComboBoxOptions = (aliasAction: AliasAction) => {
+ const allOptions: { [key in AliasActions]: EuiComboBoxOptionOption[] } = {};
+ aliasAction.alias.actions?.forEach((action) => {
+ const aliasActionType = Object.keys(action)[0] as AliasActions;
+ if (!allOptions[aliasActionType]) allOptions[aliasActionType] = [];
+ if (action[aliasActionType].alias) allOptions[aliasActionType].push({ label: action[aliasActionType]?.alias });
+
+ // When retrieving an existing policy from the backend, the GetPolicy API returns an AliasActionItem with a string[]
+ // called "aliases" for each of the aliases configured in the action. Each string[] contains 1 alias.
+ // This IF block checks for that as it indicates a policy is being edited.
+ if (action[aliasActionType].aliases)
+ allOptions[aliasActionType] = allOptions[aliasActionType].concat(
+ action[aliasActionType].aliases.map((alias) => ({ label: alias }))
+ );
+ });
+ return allOptions;
+ };
+
+ render = (uiAction: UIAction, onChangeAction: (uiAction: UIAction) => void) => {
+ return (
+
+ );
+ };
+
+ toAction = () => this.action;
+}
diff --git a/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIActionComponent.tsx b/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIActionComponent.tsx
new file mode 100644
index 000000000..11bfde084
--- /dev/null
+++ b/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/AliasUIActionComponent.tsx
@@ -0,0 +1,172 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Component } from "react";
+import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiSpacer, EuiSwitch } from "@elastic/eui";
+import { AliasAction, AliasActionItem, AliasActions, UIAction } from "../../../../../../models/interfaces";
+import AliasUIAction, { MAX_ALIAS_ACTIONS } from "./AliasUIAction";
+import { inputLimitText } from "../../../../CreatePolicy/utils/helpers";
+
+export interface AliasUIActionComponentProps {
+ action: AliasAction;
+ clone: (action: AliasAction) => AliasUIAction;
+ errors: { [key in AliasActions]: string | undefined };
+ onChangeAction: (uiAction: UIAction) => void;
+ selectedItems: { [key in AliasActions]: EuiComboBoxOptionOption[] };
+}
+
+export interface AliasUIActionComponentState {
+ addAliasToggle: boolean;
+ removeAliasToggle: boolean;
+}
+
+export default class AliasUIActionComponent extends Component {
+ constructor(props: AliasUIActionComponentProps) {
+ super(props);
+
+ const { selectedItems } = props;
+ this.state = {
+ addAliasToggle: selectedItems.add?.length > 0,
+ removeAliasToggle: selectedItems.remove?.length > 0,
+ };
+ }
+
+ componentDidMount() {
+ // TODO: Implement functionality to retrieve, and populate the combo boxes with any pre-existing aliases.
+ }
+
+ onCreateOption = (value: string, options: EuiComboBoxOptionOption[], aliasAction: AliasActions) => {
+ const { action, clone, onChangeAction } = this.props;
+ options.push({ label: value });
+ const aliasActions = action.alias.actions.concat(this.parseToAliasActionItems(options, aliasAction));
+ onChangeAction(clone({ ...action, alias: { actions: aliasActions } }));
+ };
+
+ parseToAliasActionItems = (options: EuiComboBoxOptionOption[], aliasActionType = AliasActions.ADD) => {
+ return options.map((option) => ({ [aliasActionType]: { alias: option.label } })) as AliasActionItem[];
+ };
+
+ onAddAliasChange = (options = []) => {
+ const { action, clone, selectedItems, onChangeAction } = this.props;
+ const parsedOptions = this.parseToAliasActionItems(options, AliasActions.ADD);
+ const parseSelectedItems = this.parseToAliasActionItems(selectedItems.remove || [], AliasActions.REMOVE);
+ onChangeAction(
+ clone({
+ ...action,
+ alias: {
+ // Consolidating the changed options with the existing options in the other combo box
+ actions: parsedOptions.concat(parseSelectedItems),
+ },
+ })
+ );
+ };
+
+ onRemoveAliasChange = (options) => {
+ const { action, clone, selectedItems, onChangeAction } = this.props;
+ const parsedOptions = this.parseToAliasActionItems(options, AliasActions.REMOVE);
+ const parseSelectedItems = this.parseToAliasActionItems(selectedItems.add || [], AliasActions.ADD);
+ onChangeAction(
+ clone({
+ ...action,
+ alias: {
+ // Consolidating the changed options with the existing options in the other combo box
+ actions: parsedOptions.concat(parseSelectedItems),
+ },
+ })
+ );
+ };
+
+ render() {
+ const { errors, selectedItems } = this.props;
+ const { addAliasToggle, removeAliasToggle } = this.state;
+ return (
+ <>
+ {
+ // If the user disables the combo box while there are entries in it, clear the inputs
+ if (addAliasToggle && selectedItems.add?.length > 0) this.onAddAliasChange([]);
+ this.setState({ ...this.state, addAliasToggle: e.target.checked });
+ }}
+ data-test-subj={"add-alias-toggle"}
+ />
+ {addAliasToggle && (
+ <>
+
+
+ <>
+ this.onCreateOption(searchValue, options, AliasActions.ADD))
+ }
+ onChange={(options) => this.onAddAliasChange(options)}
+ isInvalid={errors.add !== undefined}
+ data-test-subj={"add-alias-combo-box"}
+ />
+ {inputLimitText(selectedItems.add?.length, MAX_ALIAS_ACTIONS, "alias", "aliases")}
+ >
+
+ >
+ )}
+
+
+
+ {
+ // If the user disables the combo box while there are entries in it, clear the inputs
+ if (removeAliasToggle && selectedItems.remove?.length > 0) this.onRemoveAliasChange([]);
+ this.setState({ ...this.state, removeAliasToggle: e.target.checked });
+ }}
+ data-test-subj={"remove-alias-toggle"}
+ />
+ {removeAliasToggle && (
+ <>
+
+
+ <>
+ this.onCreateOption(searchValue, options, AliasActions.REMOVE))
+ }
+ onChange={(options) => this.onRemoveAliasChange(options)}
+ isInvalid={errors.remove !== undefined}
+ data-test-subj={"remove-alias-combo-box"}
+ />
+ {inputLimitText(selectedItems.remove?.length, MAX_ALIAS_ACTIONS, "alias", "aliases")}
+ >
+
+ >
+ )}
+ >
+ );
+ }
+}
diff --git a/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/__snapshots__/AliasUIAction.test.tsx.snap b/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/__snapshots__/AliasUIAction.test.tsx.snap
new file mode 100644
index 000000000..64846aae6
--- /dev/null
+++ b/public/pages/VisualCreatePolicy/components/UIActions/AliasUIAction/__snapshots__/AliasUIAction.test.tsx.snap
@@ -0,0 +1,665 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AliasUIAction component renders with blank action 1`] = `
+