diff --git a/cypress/integration/aliases.js b/cypress/integration/aliases.js
index 7d77a0f06..f2a9b235c 100644
--- a/cypress/integration/aliases.js
+++ b/cypress/integration/aliases.js
@@ -45,7 +45,7 @@ describe("Aliases", () => {
});
});
- describe("shows more flyout", () => {
+ describe("shows more modal", () => {
it("successfully", () => {
cy.get('[placeholder="Search..."]').type("alias-for-test-0{enter}");
cy.contains("alias-for-test-0");
@@ -64,7 +64,7 @@ describe("Aliases", () => {
cy.get('[data-test-subj="form-name-indexArray"] [data-test-subj="comboBoxSearchInput"]').type(
`${EDIT_INDEX}{enter}${SAMPLE_INDEX_PREFIX}-*{enter}`
);
- cy.get(".euiFlyoutFooter .euiButton--fill").click().get('[data-test-subj="9 more"]').should("exist");
+ cy.get(".euiModalFooter .euiButton--fill").click().get('[data-test-subj="9 more"]').should("exist");
});
});
@@ -89,7 +89,7 @@ describe("Aliases", () => {
.click()
.get(`[title="${SAMPLE_INDEX_PREFIX}-1"] button`)
.click()
- .get(".euiFlyoutFooter .euiButton--fill")
+ .get(".euiModalFooter .euiButton--fill")
.click()
.end();
diff --git a/cypress/integration/indices_spec.js b/cypress/integration/indices_spec.js
index 7962b853b..08ba14862 100644
--- a/cypress/integration/indices_spec.js
+++ b/cypress/integration/indices_spec.js
@@ -330,6 +330,47 @@ describe("Indices", () => {
});
});
+ describe("can shrink an index", () => {
+ before(() => {
+ cy.deleteAllIndices();
+ cy.createIndex(SAMPLE_INDEX, null, {
+ settings: { "index.blocks.write": true, "index.number_of_shards": 2, "index.number_of_replicas": 0 },
+ });
+ });
+
+ it("successfully shrink an index", () => {
+ // Type in SAMPLE_INDEX in search input
+ cy.get(`input[type="search"]`).focus().type(SAMPLE_INDEX);
+
+ cy.wait(1000).get(".euiTableRow").should("have.length", 1);
+ // Confirm we have our initial index
+ cy.contains(SAMPLE_INDEX);
+
+ cy.get('[data-test-subj="moreAction"]').click();
+ // Shrink btn should be disabled if no items selected
+ cy.get('[data-test-subj="Shrink Action"]').should("have.class", "euiContextMenuItem-isDisabled");
+
+ // Select an index
+ cy.get(`[data-test-subj="checkboxSelectRow-${SAMPLE_INDEX}"]`).check({ force: true });
+
+ cy.get('[data-test-subj="moreAction"]').click();
+ // Shrink btn should be enabled
+ cy.get('[data-test-subj="Shrink Action"]').should("exist").should("not.have.class", "euiContextMenuItem-isDisabled").click();
+
+ // Check for Shrink page
+ cy.contains("Shrink index");
+
+ // Enter target index name
+ cy.get(`input[data-test-subj="targetIndexNameInput"]`).type(`${SAMPLE_INDEX}_shrunken`);
+
+ // Click shrink index button
+ cy.get("button").contains("Shrink").click({ force: true });
+
+ // Check for success toast
+ cy.contains(`Successfully started shrinking ${SAMPLE_INDEX}. The shrunken index will be named ${SAMPLE_INDEX}_shrunken.`);
+ });
+ });
+
describe("can close and open an index", () => {
before(() => {
cy.deleteAllIndices();
diff --git a/cypress/integration/split_index.js b/cypress/integration/split_index.js
new file mode 100644
index 000000000..1a07aca54
--- /dev/null
+++ b/cypress/integration/split_index.js
@@ -0,0 +1,184 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { PLUGIN_NAME } from "../support/constants";
+
+const sampleIndex = "index-split";
+const sampleAlias = "alias-split";
+
+describe("Split Index", () => {
+ before(() => {
+ // Set welcome screen tracking to false
+ localStorage.setItem("home:welcome:show", "false");
+ cy.deleteAllIndices();
+ });
+
+ describe("can be created and updated", () => {
+ beforeEach(() => {
+ // Visit ISM OSD
+ cy.visit(`${Cypress.env("opensearch_dashboards")}/app/${PLUGIN_NAME}#/indices`);
+ cy.contains("Rows per page", { timeout: 60000 });
+ });
+
+ let splitNumber = 2;
+ let replicaNumber = 1;
+ it("Create an index successfully", () => {
+ // enter create page
+ cy.get('[data-test-subj="Create IndexButton"]').click();
+ cy.contains("Create index");
+
+ // type field name
+ cy.get('[placeholder="Specify a name for the new index."]').type(sampleIndex).end();
+
+ cy.get('[data-test-subj="comboBoxSearchInput"]').focus().type(`${sampleAlias}`).end();
+
+ // click create
+ cy.get('[data-test-subj="createIndexCreateButton"]').click({ force: true }).end();
+
+ // The index should exist
+ cy.get(`#_selection_column_${sampleIndex}-checkbox`).should("have.exist").end();
+
+ cy.get(`[data-test-subj="viewIndexDetailButton-${sampleIndex}"]`).click().end();
+ cy.get("#indexDetailModalSettings").click().end();
+
+ cy.get('[data-test-subj="form-name-index.number_of_shards"] .euiText').then(($shardNumber) => {
+ splitNumber = $shardNumber.attr("title") * 2;
+ });
+
+ cy.get("#indexDetailModalAlias").click().end();
+ cy.get(`[title="${sampleAlias}"]`).should("exist").end();
+
+ // Update Index status to blocks write otherwise we can't apply split operation on it
+ cy.updateIndexSettings(sampleIndex, { "index.blocks.write": "true" }).end();
+ }); // create index
+
+ it("Split successfully", () => {
+ const targetIndex = `${sampleIndex}` + "-target";
+ cy.get(`[data-test-subj="checkboxSelectRow-${sampleIndex}"]`)
+ .click()
+ .end()
+ .get('[data-test-subj="moreAction"]')
+ .click()
+ .end()
+ .get('[data-test-subj="Split Action"]')
+ .click()
+ .end()
+ // Target Index Name is required
+ .get('[data-test-subj="targetIndexNameInput"]')
+ .type(`${targetIndex}`)
+ .end()
+ // Number of shards after split is required
+ .get('[data-test-subj="numberOfShardsInput"]')
+ .type(`${splitNumber}{downArrow}{enter}`)
+ .end()
+ .get('[data-test-subj="numberOfReplicasInput"]')
+ .clear()
+ .type(`${replicaNumber}`)
+ .end()
+ .get('[data-test-subj="splitButton"]')
+ .click()
+ .end();
+
+ cy.get(`[data-test-subj="viewIndexDetailButton-${targetIndex}"]`).click().end();
+ cy.get("#indexDetailModalSettings").click().end();
+ cy.get('[data-test-subj="form-name-index.number_of_shards"] .euiText').should("have.text", `${splitNumber}`).end();
+ cy.get('[data-test-subj="form-name-index.number_of_replicas"] .euiText').should("have.text", `${replicaNumber}`).end();
+ }); // Split
+
+ it("Split successfully with advanced setting", () => {
+ const targetIndex = `${sampleIndex}` + "-setting";
+ cy.get(`[data-test-subj="checkboxSelectRow-${sampleIndex}"]`)
+ .click()
+ .end()
+ .get('[data-test-subj="moreAction"]')
+ .click()
+ .end()
+ .get('[data-test-subj="Split Action"]')
+ .click()
+ .end()
+ .get("[data-test-subj=targetIndexNameInput]")
+ .type(`${targetIndex}`)
+ .end()
+ // Instead of input shard number at shard field, another option is to populate it in advanced setting
+ .get('[aria-controls="accordionForCreateIndexSettings"]')
+ .click()
+ .end()
+ .get('[data-test-subj="codeEditorContainer"] textarea')
+ .focus()
+ // Need to remove the default {} in advanced setting
+ .clear()
+ .type(`{"index.number_of_shards": "${splitNumber}", "index.number_of_replicas": "${replicaNumber}"}`, {
+ parseSpecialCharSequences: false,
+ })
+ .end()
+ .get('[data-test-subj="splitButton"]')
+ .click()
+ .end();
+
+ cy.get(`[data-test-subj="viewIndexDetailButton-${targetIndex}"]`).click().end();
+ cy.get("#indexDetailModalSettings").click().end();
+ cy.get('[data-test-subj="form-name-index.number_of_shards"] .euiText').should("have.text", `${splitNumber}`).end();
+ cy.get('[data-test-subj="form-name-index.number_of_replicas"] .euiText').should("have.text", `${replicaNumber}`).end();
+ }); // advanced setting
+
+ it("Split successfully with alias", () => {
+ const targetIndex = `${sampleIndex}` + "-alias";
+ const newAlias = "alias-new";
+ cy.get(`[data-test-subj="checkboxSelectRow-${sampleIndex}"]`)
+ .click()
+ .end()
+ .get('[data-test-subj="moreAction"]')
+ .click()
+ .end()
+ .get('[data-test-subj="Split Action"]')
+ .click()
+ .end()
+ .get("[data-test-subj=targetIndexNameInput]")
+ .type(`${targetIndex}`)
+ .end()
+ .get('[data-test-subj="numberOfShardsInput"]')
+ .type(`${splitNumber}{downArrow}{enter}`)
+ .end()
+ // Assign to an existing alias and a new alias
+ .get('[data-test-subj="form-name-aliases"] [data-test-subj="comboBoxSearchInput"]')
+ .type(`${sampleAlias}{enter}${newAlias}{enter}`)
+ .end()
+ .get('[data-test-subj="splitButton"]')
+ .click()
+ .end();
+
+ cy.get(`[data-test-subj="viewIndexDetailButton-${targetIndex}"]`).click().end();
+ // Verify alias associated with the new index
+ cy.get("#indexDetailModalAlias").click().end();
+ cy.get(`[title="${newAlias}"]`).should("exist").end();
+ cy.get(`[title="${sampleAlias}"]`).should("exist").end();
+ }); // Create with alias
+
+ it("Update blocks write to true", () => {
+ // Set index to not blocks write
+ cy.updateIndexSettings(sampleIndex, { "index.blocks.write": "false" }).end();
+ cy.get(`[data-test-subj="checkboxSelectRow-${sampleIndex}"]`)
+ .click()
+ .end()
+ .get('[data-test-subj="moreAction"]')
+ .click()
+ .end()
+ .get('[data-test-subj="Split Action"]')
+ .click()
+ .end()
+ // Index can't be split if it's blocks write status is not true
+ .get('[data-test-subj="splitButton"]')
+ .should("have.class", "euiButton-isDisabled")
+ .end()
+ .wait(1000)
+ // Set index to blocks write
+ .get('[data-test-subj="set-indexsetting-button"]')
+ .click()
+ .end()
+ .get('[data-test-subj="splitButton"]')
+ .click()
+ .end();
+ }); // Blocks write
+ });
+});
diff --git a/public/JobHandler/index.tsx b/public/JobHandler/index.tsx
index 2035a8342..3e974d3bc 100644
--- a/public/JobHandler/index.tsx
+++ b/public/JobHandler/index.tsx
@@ -77,8 +77,7 @@ export function JobHandlerRegister(core: CoreSetup) {
{
title: ((
<>
- Source index
+ The state of the shards in the source index are abnormal, you must check the index's health status before shrinking, and it is + recommended to shrink an index when its health status is Green. +
+In order to shrink an existing index, you must first set the index to block write operations.
++ You must first open the index before shrinking it. Depending on the size of the source index, this may take additional time + to complete. The index will be in the Red state while the index is opening. +
++ It's recommended to shrink an index when its health status is Green, because if the index's health status is Yellow, it + may cause problems when initializing the new shrunken index's shards. +
++ When the source index's setting [index.blocks.read_only] is [true], it will be copied to the new shrunken index and then + the new shrunken index's metadata write will be blocked, this will cause the new shrunken index's shards to be unassigned, + you can set the setting to [null] or [false] in the advanced settings bellow. +
++ When shrinking an index, a copy of every shard in the index must reside on the same node, you can use the index setting + `index.routing.allocation.require._*` to relocate the copy of every shard to one node. +
++ Ignore this warning if the copy of every shard in the source index just reside on the same node in some cases, like the + OpenSearch cluster has only one node or the cluster has two nodes and each primary shard in the source index has one + replia. +
+Specify the number of shards for the new shrunken index.
+The number must be a factor of the primary shard count of the source index.
+ > + ), + }, + name: "index.number_of_shards", + type: "Select", + options: { + rules: [ + { + validator: (_, value) => { + if (!value || !value.toString().trim() || Number(value) <= 0 || Number(sourceIndex.pri) % value != 0) { + return Promise.reject( + "The number of primary shards in the new shrunken index " + + " must be a positive factor of the number of primary shards in the source index." + ); + } + return Promise.resolve(); + }, + }, + ], + props: { + "data-test-subj": "numberOfShardsInput", + options: numberOfShardsSelectOptions, + placeholder: "Select primary shard count", + }, + }, + }, + { + rowProps: { + label: "Number of replicas", + helpText:
+ Shrink an existing index into a new index with fewer primary shards.{" "}
+
+ Shrink an existing index into a new index with fewer primary shards. + + + Learn more. + EuiIconMock + + (opens in a new tab or window) + + +
++ Specify the number of shards for the new shrunken index. +
++ The number must be a factor of the primary shard count of the source index. +
++ Select aliases or input new aliases. +
++ Press Enter to start editing. +
++ When you're done, press Escape to stop editing. +
+Specify the number of primary shards for the new split index.
+{shardMessage}
+ > + ), + }, + name: "index.number_of_shards", + type: "ComboBoxSingle", + options: { + rules: [ + { + trigger: "onBlur", + validator: (rule, value) => { + if (!value || value === "") { + // do not pass the validation + // return a rejected promise with error message + return Promise.reject("Number of shards is required"); + } else if (!this.props.shardsSelectOptions.find((item) => "" + item.label === "" + value)) { + return Promise.reject(`Number of shards ${value} is invalid`); + } + // pass the validation, return a resolved promise + return Promise.resolve(); + }, + }, + ], + props: { + "data-test-subj": "numberOfShardsInput", + options: this.props.shardsSelectOptions, + placeholder: "Specify primary shard count", + onCreateOption: undefined, + }, + }, + }, + { + rowProps: { + label: "Number of replicas", + helpText: REPLICA_NUMBER_MESSAGE, + }, + name: "index.number_of_replicas", + type: "Number", + options: { + props: { + "data-test-subj": "numberOfReplicasInput", + placeholder: "Specify number of replica", + min: 0, + }, + }, + }, + { + name: "aliases", + rowProps: { + label: "Index alias - optional", + helpText: "Allow this index to be referenced by existing aliases or specify a new alias.", + }, + options: { + props: { + refreshOptions: getAlias, + }, + }, + component: WrappedAliasSelect as React.ComponentType+ You must first open the index before splitting it. Depending on the size of the source index, this may take additional time to + complete. The index will be in the Red state while the index is opening. +
+
+
In order to split an existing index, you must first set the index to block write operations.
+
+ Split an existing read-only index into a new index with more primary shards .
+
+ Split an existing read-only index into a new index with more primary shards . + + Learn more. + EuiIconMock + + (opens in a new tab or window) + + +
+