Skip to content

Commit

Permalink
Temp/template management 2.5 (#523)
Browse files Browse the repository at this point in the history
* Feature/common 2.5 (#506)

* feat: split to common change

Signed-off-by: suzhou <[email protected]>

* feat: update

Signed-off-by: suzhou <[email protected]>

Signed-off-by: suzhou <[email protected]>

* feat: update

Signed-off-by: suzhou <[email protected]>

* feat: update

Signed-off-by: suzhou <[email protected]>

* feat: fix template error

Signed-off-by: suzhou <[email protected]>

* feat: update

Signed-off-by: suzhou <[email protected]>

Signed-off-by: suzhou <[email protected]>
  • Loading branch information
SuZhou-Joe committed Jan 6, 2023
1 parent c5dce67 commit 6b1738d
Show file tree
Hide file tree
Showing 33 changed files with 4,438 additions and 8 deletions.
91 changes: 91 additions & 0 deletions cypress/integration/templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import { PLUGIN_NAME } from "../support/constants";

const SAMPLE_TEMPLATE_PREFIX = "index-for-alias-test";
const MAX_TEMPLATE_NUMBER = 30;

describe("Templates", () => {
before(() => {
// Set welcome screen tracking to false
localStorage.setItem("home:welcome:show", "false");
cy.deleteTemplate(`${SAMPLE_TEMPLATE_PREFIX}-${MAX_TEMPLATE_NUMBER}`);
for (let i = 0; i < MAX_TEMPLATE_NUMBER; i++) {
cy.deleteTemplate(`${SAMPLE_TEMPLATE_PREFIX}-${i}`);
cy.createIndexTemplate(`${SAMPLE_TEMPLATE_PREFIX}-${i}`, {
index_patterns: ["template-test-*"],
priority: i,
template: {
aliases: {},
settings: {
number_of_shards: 2,
number_of_replicas: 1,
},
},
});
}
});

beforeEach(() => {
// Visit ISM OSD
cy.visit(`${Cypress.env("opensearch_dashboards")}/app/${PLUGIN_NAME}#/templates`);

// Common text to wait for to confirm page loaded, give up to 60 seconds for initial load
cy.contains("Rows per page", { timeout: 60000 });
});

describe("can be searched / sorted / paginated", () => {
it("successfully", () => {
cy.get('[data-test-subj="pagination-button-1"]').should("exist");
cy.get('[placeholder="Search..."]').type(`${SAMPLE_TEMPLATE_PREFIX}-0`);
cy.contains(`${SAMPLE_TEMPLATE_PREFIX}-0`);
cy.get(".euiTableRow").should("have.length", 1);
});
});

describe("can create a template", () => {
it("successfully", () => {
cy.get('[data-test-subj="Create templateButton"]').click();
cy.contains("Define template");

cy.get('[data-test-subj="form-row-name"] input').type(`${SAMPLE_TEMPLATE_PREFIX}-${MAX_TEMPLATE_NUMBER}`);
cy.get('[data-test-subj="form-row-index_patterns"] [data-test-subj="comboBoxSearchInput"]').type("test{enter}");
cy.get('[data-test-subj="CreateIndexTemplateCreateButton"]').click();

cy.contains(`${SAMPLE_TEMPLATE_PREFIX}-${MAX_TEMPLATE_NUMBER} has been successfully created.`);

cy.get('[placeholder="Search..."]').type(`${SAMPLE_TEMPLATE_PREFIX}-${MAX_TEMPLATE_NUMBER}`);
cy.contains(`${SAMPLE_TEMPLATE_PREFIX}-${MAX_TEMPLATE_NUMBER}`);
cy.get(".euiTableRow").should("have.length", 1);
});
});

describe("can delete a template", () => {
it("successfully", () => {
cy.get('[placeholder="Search..."]').type(`${SAMPLE_TEMPLATE_PREFIX}-0`);
cy.contains(`${SAMPLE_TEMPLATE_PREFIX}-0`);
cy.get(`#_selection_column_${SAMPLE_TEMPLATE_PREFIX}-0-checkbox`).click();

cy.get('[data-test-subj="moreAction"] button').click().get('[data-test-subj="deleteAction"]').click();
// The confirm button should be disabled
cy.get('[data-test-subj="deleteConfirmButton"]').should("be.disabled");
// type delete
cy.wait(500).get('[data-test-subj="deleteInput"]').type("delete");
cy.get('[data-test-subj="deleteConfirmButton"]').should("not.be.disabled");
// click to delete
cy.get('[data-test-subj="deleteConfirmButton"]').click();
// the alias should not exist
cy.wait(500);
cy.get(`#_selection_column_${SAMPLE_TEMPLATE_PREFIX}-0-checkbox`).should("not.exist");
});
});

after(() => {
cy.deleteTemplate(`${SAMPLE_TEMPLATE_PREFIX}-${MAX_TEMPLATE_NUMBER}`);
for (let i = 0; i < MAX_TEMPLATE_NUMBER; i++) {
cy.deleteTemplate(`${SAMPLE_TEMPLATE_PREFIX}-${i}`);
}
});
});
32 changes: 24 additions & 8 deletions models/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,7 @@ export interface TemplateItemRemote extends ITemplateExtras {
/**
* ManagedIndex item shown in the Managed Indices table
*/
export interface ManagedIndexItem {
index: string;
indexUuid: string;
export interface ManagedIndexItem extends IndexItem {
dataStream: string | null;
policyId: string;
policySeqNo: number;
Expand All @@ -98,10 +96,6 @@ export interface ManagedIndexItem {
managedIndexMetaData: ManagedIndexMetaData | null;
}

export interface IndexItem {
index: string;
}

/**
* Interface what the Policy Opensearch Document
*/
Expand Down Expand Up @@ -228,7 +222,7 @@ export interface SMDeleteCondition {
export interface ErrorNotification {
destination?: Destination;
channel?: Channel;
message_template: MessageTemplate;
message_template?: MessageTemplate;
}

export interface Notification {
Expand Down Expand Up @@ -624,3 +618,25 @@ export enum TRANSFORM_AGG_TYPE {
histogram = "histogram",
date_histogram = "date_histogram",
}
export interface IAPICaller {
endpoint: string;
method?: string;
data?: any;
}

export interface IRecoveryItem {
index: string;
stage: "done" | "translog";
}

export interface ITaskItem {
action: string;
description: string;
}

export interface IReindexItem extends ITaskItem {
fromIndex: string;
toIndex: string;
}

export type IAliasAction = Record<string, { index: string; alias: string }>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { EuiRadio } from "@elastic/eui";
import { TEMPLATE_TYPE } from "../../../../utils/constants";
import React from "react";

export interface ITemplateTypeProps {
value?: {};
onChange: (val: ITemplateTypeProps["value"]) => void;
}

export default function TemplateType(props: ITemplateTypeProps) {
const { value, onChange } = props;
return (
<>
<EuiRadio
id={TEMPLATE_TYPE.INDEX_TEMPLATE}
onChange={(e) => e.target.checked && onChange(undefined)}
label={TEMPLATE_TYPE.INDEX_TEMPLATE}
checked={value === undefined}
/>
<EuiRadio
id={TEMPLATE_TYPE.DATA_STREAM}
onChange={(e) => e.target.checked && onChange({})}
label={TEMPLATE_TYPE.DATA_STREAM}
checked={value !== undefined}
/>
</>
);
}

export const TemplateConvert = (props: Pick<ITemplateTypeProps, "value">) =>
props.value === undefined ? TEMPLATE_TYPE.INDEX_TEMPLATE : TEMPLATE_TYPE.DATA_STREAM;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import TemplateType from "./TemplateType";
export { TemplateConvert } from "./TemplateType";

export default TemplateType;
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from "react";
import { MemoryRouter as Router, Route, RouteComponentProps, Switch } from "react-router-dom";
import { render, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import CreateIndexTemplate from "./CreateIndexTemplate";
import { ServicesContext } from "../../../../services";
import { browserServicesMock, coreServicesMock, apiCallerMock } from "../../../../../test/mocks";
import { ROUTES } from "../../../../utils/constants";
import { CoreServicesContext } from "../../../../components/core_services";

function renderCreateIndexTemplateWithRouter(initialEntries = [ROUTES.CREATE_TEMPLATE] as string[]) {
return {
...render(
<Router initialEntries={initialEntries}>
<CoreServicesContext.Provider value={coreServicesMock}>
<ServicesContext.Provider value={browserServicesMock}>
<Switch>
<Route
path={`${ROUTES.CREATE_TEMPLATE}/:template/:mode`}
render={(props: RouteComponentProps) => <CreateIndexTemplate {...props} />}
/>
<Route
path={`${ROUTES.CREATE_TEMPLATE}/:template`}
render={(props: RouteComponentProps) => <CreateIndexTemplate {...props} />}
/>
<Route path={ROUTES.CREATE_TEMPLATE} render={(props: RouteComponentProps) => <CreateIndexTemplate {...props} />} />
<Route path={ROUTES.TEMPLATES} render={(props: RouteComponentProps) => <h1>location is: {ROUTES.TEMPLATES}</h1>} />
</Switch>
</ServicesContext.Provider>
</CoreServicesContext.Provider>
</Router>
),
};
}

describe("<CreateIndexTemplate /> spec", () => {
beforeEach(() => {
apiCallerMock(browserServicesMock);
});
it("it goes to templates page when click cancel", async () => {
const { getByTestId, getByText, findByTitle, container } = renderCreateIndexTemplateWithRouter([
`${ROUTES.CREATE_TEMPLATE}/good_template/readonly`,
]);
await findByTitle("good_template");
expect(container).toMatchSnapshot();
userEvent.click(getByText("Edit"));
await waitFor(() => expect(document.querySelector('[data-test-subj="form-row-name"] [title="good_template"]')).toBeInTheDocument(), {
timeout: 3000,
});
userEvent.click(getByTestId("CreateIndexTemplateCancelButton"));
await waitFor(() => {
expect(getByText(`location is: ${ROUTES.TEMPLATES}`)).toBeInTheDocument();
});
});

it("it goes to indices page when click create successfully in happy path", async () => {
const { getByText, getByTestId } = renderCreateIndexTemplateWithRouter([`${ROUTES.CREATE_TEMPLATE}`]);

const templateNameInput = getByTestId("form-row-name").querySelector("input") as HTMLInputElement;
const submitButton = getByTestId("CreateIndexTemplateCreateButton");
const shardsInput = getByTestId("form-row-template.settings.index.number_of_shards").querySelector("input") as HTMLInputElement;
const replicaInput = getByTestId("form-row-template.settings.index.number_of_replicas").querySelector("input") as HTMLInputElement;
userEvent.type(templateNameInput, `bad_template`);

userEvent.click(submitButton);
await waitFor(() => expect(getByText("Index patterns must be defined")).not.toBeNull(), {
timeout: 3000,
});

const patternInput = getByTestId("form-row-index_patterns").querySelector('[data-test-subj="comboBoxSearchInput"]') as HTMLInputElement;
userEvent.type(patternInput, `test_patterns{enter}`);

userEvent.click(submitButton);
await waitFor(() => expect(coreServicesMock.notifications.toasts.addDanger).toBeCalledWith("bad template"));
userEvent.clear(templateNameInput);
userEvent.type(templateNameInput, "good_template");

userEvent.clear(shardsInput);
userEvent.type(shardsInput, "1.5");
await waitFor(() => expect(getByText("Number of primary shards must be an integer")).toBeInTheDocument(), { timeout: 3000 });
userEvent.clear(shardsInput);
userEvent.type(shardsInput, "1");

userEvent.clear(replicaInput);
userEvent.type(replicaInput, "1.5");
await waitFor(() => expect(getByText("Number of replicas must be an integer")).toBeInTheDocument(), { timeout: 3000 });
userEvent.clear(replicaInput);
userEvent.type(replicaInput, "1");

userEvent.click(getByTestId("createIndexAddFieldButton"));
userEvent.click(submitButton);
await waitFor(() => expect(getByText("Field name is required, please input")).not.toBeNull());
userEvent.click(getByTestId("mapping-visual-editor-0-delete-field"));

userEvent.click(submitButton);
await waitFor(
() => {
expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({
endpoint: "transport.request",
data: {
method: "PUT",
path: "_index_template/good_template",
body: {
priority: 0,
template: {
settings: {
"index.number_of_replicas": "1",
"index.number_of_shards": "1",
"index.refresh_interval": "1s",
},
mappings: { properties: {} },
},
index_patterns: ["test_patterns"],
},
},
});
expect(getByText(`location is: ${ROUTES.TEMPLATES}`)).toBeInTheDocument();
},
{
timeout: 3000,
}
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { Component } from "react";
import { RouteComponentProps } from "react-router-dom";
import TemplateDetail from "../TemplateDetail";
import { BREADCRUMBS, ROUTES } from "../../../../utils/constants";
import { CoreServicesContext } from "../../../../components/core_services";
import { isEqual } from "lodash";

interface CreateIndexTemplateProps extends RouteComponentProps<{ template?: string; mode?: string }> {}

export default class CreateIndexTemplate extends Component<CreateIndexTemplateProps> {
static contextType = CoreServicesContext;

get template() {
return this.props.match.params.template;
}

get readonly() {
return this.props.match.params.mode === "readonly";
}

setBreadCrumb() {
const isEdit = this.template;
const readonly = this.readonly;
let lastBread: typeof BREADCRUMBS.TEMPLATES;
if (readonly && this.template) {
lastBread = {
text: this.template,
href: `#${this.props.location.pathname}`,
};
} else if (isEdit) {
lastBread = {
...BREADCRUMBS.EDIT_TEMPLATE,
href: `#${this.props.location.pathname}`,
};
} else {
lastBread = BREADCRUMBS.CREATE_TEMPLATE;
}
this.context.chrome.setBreadcrumbs([BREADCRUMBS.INDEX_MANAGEMENT, BREADCRUMBS.TEMPLATES, lastBread]);
}

componentDidUpdate(prevProps: Readonly<CreateIndexTemplateProps>): void {
if (!isEqual(prevProps, this.props)) {
this.setBreadCrumb();
}
}

componentDidMount = async (): Promise<void> => {
this.setBreadCrumb();
};

onCancel = (): void => {
this.props.history.push(ROUTES.TEMPLATES);
};

render() {
return (
<div style={{ padding: "0px 50px" }}>
<TemplateDetail
history={this.props.history}
readonly={this.readonly}
templateName={this.template}
onCancel={this.onCancel}
onSubmitSuccess={() => this.props.history.push(ROUTES.TEMPLATES)}
/>
</div>
);
}
}
Loading

0 comments on commit 6b1738d

Please sign in to comment.