From 80d7fb6e2c0869528265bf00aef14cf7d1945d38 Mon Sep 17 00:00:00 2001 From: suzhou Date: Thu, 2 Feb 2023 16:00:29 +0800 Subject: [PATCH 01/41] feat: temp commit Signed-off-by: suzhou --- .../IndexControls/IndexControls.test.tsx | 32 ++ .../IndexControls/IndexControls.tsx | 36 ++ .../__snapshots__/IndexControls.test.tsx.snap | 47 ++ .../components/IndexControls/index.ts | 9 + .../DataStreams/DataStreams.test.tsx | 96 ++++ .../containers/DataStreams/DataStreams.tsx | 382 ++++++++++++++++ .../__snapshots__/Templates.test.tsx.snap | 423 ++++++++++++++++++ .../containers/DataStreams/index.ts | 3 + .../DataStreamsActions.test.tsx | 142 ++++++ .../TemplatesActions.test.tsx.snap | 55 +++ .../containers/DataStreamsActions/index.tsx | 77 ++++ .../DeleteDataStreamsModal.test.tsx | 17 + .../DeleteDataStreamsModal.tsx | 52 +++ .../DeleteTemplateModal.test.tsx.snap | 143 ++++++ .../DeleteDataStreamsModal/index.ts | 8 + public/pages/DataStreams/index.ts | 8 + public/pages/DataStreams/interface.ts | 7 + public/pages/DataStreams/utils/constants.tsx | 16 + public/pages/Main/Main.tsx | 16 + public/utils/constants.ts | 1 + 20 files changed, 1570 insertions(+) create mode 100644 public/pages/DataStreams/components/IndexControls/IndexControls.test.tsx create mode 100644 public/pages/DataStreams/components/IndexControls/IndexControls.tsx create mode 100644 public/pages/DataStreams/components/IndexControls/__snapshots__/IndexControls.test.tsx.snap create mode 100644 public/pages/DataStreams/components/IndexControls/index.ts create mode 100644 public/pages/DataStreams/containers/DataStreams/DataStreams.test.tsx create mode 100644 public/pages/DataStreams/containers/DataStreams/DataStreams.tsx create mode 100644 public/pages/DataStreams/containers/DataStreams/__snapshots__/Templates.test.tsx.snap create mode 100644 public/pages/DataStreams/containers/DataStreams/index.ts create mode 100644 public/pages/DataStreams/containers/DataStreamsActions/DataStreamsActions.test.tsx create mode 100644 public/pages/DataStreams/containers/DataStreamsActions/__snapshots__/TemplatesActions.test.tsx.snap create mode 100644 public/pages/DataStreams/containers/DataStreamsActions/index.tsx create mode 100644 public/pages/DataStreams/containers/DeleteDataStreamsModal/DeleteDataStreamsModal.test.tsx create mode 100644 public/pages/DataStreams/containers/DeleteDataStreamsModal/DeleteDataStreamsModal.tsx create mode 100644 public/pages/DataStreams/containers/DeleteDataStreamsModal/__snapshots__/DeleteTemplateModal.test.tsx.snap create mode 100644 public/pages/DataStreams/containers/DeleteDataStreamsModal/index.ts create mode 100644 public/pages/DataStreams/index.ts create mode 100644 public/pages/DataStreams/interface.ts create mode 100644 public/pages/DataStreams/utils/constants.tsx diff --git a/public/pages/DataStreams/components/IndexControls/IndexControls.test.tsx b/public/pages/DataStreams/components/IndexControls/IndexControls.test.tsx new file mode 100644 index 000000000..9e343056c --- /dev/null +++ b/public/pages/DataStreams/components/IndexControls/IndexControls.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { render, waitFor } from "@testing-library/react"; +// @ts-ignore +import userEvent from "@testing-library/user-event"; +import IndexControls from "./IndexControls"; + +describe(" spec", () => { + it("renders the component", async () => { + const { container } = render( {}} />); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it("onChange with right data", async () => { + const onSearchChangeMock = jest.fn(); + const { getByPlaceholderText } = render(); + + userEvent.type(getByPlaceholderText("Search..."), "test"); + await waitFor(() => { + expect(onSearchChangeMock).toBeCalledTimes(4); + expect(onSearchChangeMock).toBeCalledWith({ + search: "test", + }); + }); + }); +}); diff --git a/public/pages/DataStreams/components/IndexControls/IndexControls.tsx b/public/pages/DataStreams/components/IndexControls/IndexControls.tsx new file mode 100644 index 000000000..6864a1e3b --- /dev/null +++ b/public/pages/DataStreams/components/IndexControls/IndexControls.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from "react"; +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from "@elastic/eui"; + +export interface SearchControlsProps { + value: { + search: string; + }; + onSearchChange: (args: SearchControlsProps["value"]) => void; +} + +export default function SearchControls(props: SearchControlsProps) { + const [state, setState] = useState(props.value); + const onChange = (field: T, value: SearchControlsProps["value"][T]) => { + const payload = { + ...state, + [field]: value, + }; + setState(payload); + props.onSearchChange(payload); + }; + useEffect(() => { + setState(props.value); + }, [props.value]); + return ( + + + onChange("search", e.target.value)} /> + + + ); +} diff --git a/public/pages/DataStreams/components/IndexControls/__snapshots__/IndexControls.test.tsx.snap b/public/pages/DataStreams/components/IndexControls/__snapshots__/IndexControls.test.tsx.snap new file mode 100644 index 000000000..1c3d7a474 --- /dev/null +++ b/public/pages/DataStreams/components/IndexControls/__snapshots__/IndexControls.test.tsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +
+
+
+
+ +
+ + EuiIconMock + +
+
+ +
+
+
+
+
+`; diff --git a/public/pages/DataStreams/components/IndexControls/index.ts b/public/pages/DataStreams/components/IndexControls/index.ts new file mode 100644 index 000000000..993ddb52b --- /dev/null +++ b/public/pages/DataStreams/components/IndexControls/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import IndexControls from "./IndexControls"; + +export * from "./IndexControls"; +export default IndexControls; diff --git a/public/pages/DataStreams/containers/DataStreams/DataStreams.test.tsx b/public/pages/DataStreams/containers/DataStreams/DataStreams.test.tsx new file mode 100644 index 000000000..62145c8b2 --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreams/DataStreams.test.tsx @@ -0,0 +1,96 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { render, waitFor } from "@testing-library/react"; +import { Redirect, Route, Switch } from "react-router-dom"; +import { HashRouter as Router } from "react-router-dom"; +import { browserServicesMock, coreServicesMock } from "../../../../../test/mocks"; +import DataStreams from "./index"; +import { ServicesContext } from "../../../../services"; +import { ROUTES } from "../../../../utils/constants"; +import { CoreServicesContext } from "../../../../components/core_services"; +import userEvent from "@testing-library/user-event"; +import { ITemplate } from "../../interface"; + +function renderWithRouter() { + return { + ...render( + + + ( + + + + + + )} + /> + + + + ), + }; +} + +const testTemplateId = "test"; + +describe(" spec", () => { + beforeEach(() => { + browserServicesMock.commonService.apiCaller = jest.fn(async (payload) => { + if (payload.endpoint === "cat.dataStreams") { + return { + ok: true, + response: [ + { + name: testTemplateId, + index_patterns: "[1]", + version: "", + order: 1, + }, + ] as ITemplate[], + }; + } + + return { + ok: true, + response: {}, + }; + }) as any; + window.location.hash = "/"; + }); + it("renders the component", async () => { + const { container, getByTestId } = renderWithRouter(); + + expect(container.firstChild).toMatchSnapshot(); + await waitFor(() => { + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(1); + }); + userEvent.click(getByTestId("tableHeaderCell_name_0").querySelector("button") as Element); + await waitFor(() => { + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(4); + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + data: { format: "json", name: `**`, s: "name:asc" }, + endpoint: "cat.dataStreams", + }); + }); + }); + + it("with some actions", async () => { + const { getByPlaceholderText } = renderWithRouter(); + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(1); + userEvent.type(getByPlaceholderText("Search..."), `${testTemplateId}{enter}`); + await waitFor(() => { + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(4); + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + data: { format: "json", name: `*${testTemplateId}*`, s: "name:desc" }, + endpoint: "cat.dataStreams", + }); + }); + }); +}); diff --git a/public/pages/DataStreams/containers/DataStreams/DataStreams.tsx b/public/pages/DataStreams/containers/DataStreams/DataStreams.tsx new file mode 100644 index 000000000..3a14a9c82 --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreams/DataStreams.tsx @@ -0,0 +1,382 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component, useContext } from "react"; +import { debounce, isEqual } from "lodash"; +import { Link, RouteComponentProps } from "react-router-dom"; +import queryString from "query-string"; +import { + EuiHorizontalRule, + EuiBasicTable, + Criteria, + EuiTableSortingType, + Direction, + Pagination, + EuiTableSelectionType, + EuiButton, + EuiLink, + EuiTitle, + EuiFormRow, + EuiEmptyPrompt, + EuiText, +} from "@elastic/eui"; +import { ContentPanel, ContentPanelActions } from "../../../../components/ContentPanel"; +import { DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_QUERY_PARAMS } from "../../utils/constants"; +import CommonService from "../../../../services/CommonService"; +import { DataStreamStats } from "../../interface"; +import { BREADCRUMBS, ROUTES } from "../../../../utils/constants"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { ServicesContext } from "../../../../services"; +import IndexControls, { SearchControlsProps } from "../../components/IndexControls"; +import DataStreamsActions from "../DataStreamsActions"; +import { CoreStart } from "opensearch-dashboards/public"; +import { DataStream } from "../../../../../server/models/interfaces"; +import { TemplateConvert } from "../../../CreateIndexTemplate/components/TemplateType"; + +interface DataStreamsProps extends RouteComponentProps { + commonService: CommonService; +} + +type DataStreamWithStats = DataStream & DataStreamStats; + +type DataStreamsState = { + totalDataStreams: number; + from: string; + size: string; + sortField: keyof DataStreamWithStats; + sortDirection: Direction; + selectedItems: DataStream[]; + dataStreams: DataStreamWithStats[]; + loading: boolean; +} & SearchControlsProps["value"]; + +const defaultFilter = { + search: DEFAULT_QUERY_PARAMS.search, +}; + +class DataStreams extends Component { + static contextType = CoreServicesContext; + constructor(props: DataStreamsProps) { + super(props); + const { + from = DEFAULT_QUERY_PARAMS.from, + size = DEFAULT_QUERY_PARAMS.size, + search = DEFAULT_QUERY_PARAMS.search, + sortField = DEFAULT_QUERY_PARAMS.sortField, + sortDirection = DEFAULT_QUERY_PARAMS.sortDirection, + } = queryString.parse(props.history.location.search) as { + from: string; + size: string; + search: string; + sortField: keyof DataStream; + sortDirection: Direction; + }; + this.state = { + ...defaultFilter, + totalDataStreams: 0, + from, + size, + search, + sortField, + sortDirection, + selectedItems: [], + dataStreams: [], + loading: false, + }; + + this.getDataStreams = debounce(this.getDataStreams, 500, { leading: true }); + } + + componentDidMount() { + this.context.chrome.setBreadcrumbs([BREADCRUMBS.INDEX_MANAGEMENT, BREADCRUMBS.TEMPLATES]); + this.getDataStreams(); + } + + getQueryState = (state: DataStreamsState) => { + return Object.keys(DEFAULT_QUERY_PARAMS).reduce((total, key) => { + return { + ...total, + [key]: state[key as keyof typeof DEFAULT_QUERY_PARAMS], + }; + }, {} as DataStreamsState); + }; + + getDataStreams = async (): Promise => { + this.setState({ loading: true }); + const { from, size } = this.state; + const fromNumber = Number(from); + const sizeNumber = Number(size); + const { history, commonService } = this.props; + const queryObject = this.getQueryState(this.state); + const queryParamsString = queryString.stringify(queryObject); + history.replace({ ...this.props.location, search: queryParamsString }); + + const payload: any = { + format: "json", + name: `*${queryObject.search}*`, + s: `${queryObject.sortField}:${queryObject.sortDirection}`, + }; + + let allDataStreams: DataStreamWithStats[] = []; + let error = ""; + let totalDataStreams = 0; + + let allDataStreamsResponse = await commonService.apiCaller<{ + data_streams: DataStream[]; + }>({ + endpoint: "transport.request", + data: { + method: "GET", + path: "_data_stream/*", + }, + }); + let dataStreams: DataStream[] = []; + + if (allDataStreamsResponse.ok) { + const dataStreamsResponse = allDataStreamsResponse?.response?.data_streams || []; + const dataStreams = dataStreamsResponse.slice(fromNumber * sizeNumber, (fromNumber + 1) * sizeNumber); + const dataStreamDetailInfoResponse = await commonService.apiCaller<{ + data_streams: DataStreamStats[]; + }>({ + endpoint: "transport.request", + data: { + method: "GET", + path: `_data_stream/${dataStreams.map((item) => item.name).join(",")}/_stats?human=true`, + }, + }); + totalDataStreams = dataStreamsResponse.length; + if (dataStreamDetailInfoResponse.ok) { + allDataStreams = dataStreamsResponse.map((item) => { + const findItem = + (dataStreamDetailInfoResponse.response?.data_streams || []).find((detailItem) => detailItem.data_stream === item.name) || + ({} as DataStreamStats); + return { + ...findItem, + ...item, + }; + }); + } else { + error = dataStreamDetailInfoResponse.error; + } + } else { + error = allDataStreamsResponse.error; + } + + if (!error) { + const payload = { + dataStreams: allDataStreams, + totalDataStreams, + selectedItems: this.state.selectedItems + .map((item) => dataStreams.find((remoteItem) => remoteItem.name === item.name)) + .filter((item) => item), + } as DataStreamsState; + this.setState(payload); + } else { + this.context.notifications.toasts.addDanger(error); + } + + // Avoiding flicker by showing/hiding the "Data stream" column only after the results are loaded. + this.setState({ loading: false }); + }; + + onTableChange = ({ page: tablePage, sort }: Criteria): void => { + const { index: page, size } = tablePage || {}; + const { field: sortField, direction: sortDirection } = sort || {}; + this.setState( + { + from: "" + page, + size: "" + size, + sortField: sortField || DEFAULT_QUERY_PARAMS.sortField, + sortDirection: sortDirection as Direction, + }, + () => this.getDataStreams() + ); + }; + + onSelectionChange = (selectedItems: DataStream[]): void => { + this.setState({ selectedItems }); + }; + + onSearchChange = (params: Parameters[0]): void => { + this.setState({ from: "0", ...params }, () => this.getDataStreams()); + }; + + render() { + const { totalDataStreams, from, size, sortField, sortDirection, dataStreams } = this.state; + + const pagination: Pagination = { + pageIndex: Number(from), + pageSize: Number(size), + pageSizeOptions: DEFAULT_PAGE_SIZE_OPTIONS, + totalItemCount: Number(totalDataStreams), + }; + + const sorting: EuiTableSortingType = { + sort: { + direction: sortDirection, + field: sortField, + }, + }; + + const selection: EuiTableSelectionType = { + onSelectionChange: this.onSelectionChange, + }; + return ( + + ), + }, + { + text: "Create template", + buttonProps: { + fill: true, + onClick: () => { + this.props.history.push(ROUTES.CREATE_TEMPLATE); + }, + }, + }, + ]} + /> + } + bodyStyles={{ padding: "initial" }} + title={ + <> + + Data streams + + + A data stream is internally composed of multiple backing indices. Search requests are routed to all the backing indices, + while indexing requests are routed to the latest write index.{" "} + + Learn more. + + + } + > + <> + + + } + > + + + + { + return ( + + {value} + + ); + }, + }, + { + field: "backing_indices", + name: "backing indices count", + align: "right", + }, + { + field: "store_size_bytes", + name: "Total size", + sortable: true, + render: (value, record: DataStreamWithStats) => { + return <>{record.store_size || ""}; + }, + }, + { + field: "order", + name: "Priority", + sortable: true, + align: "right", + }, + ]} + isSelectable={true} + itemId="name" + items={dataStreams} + onChange={this.onTableChange} + pagination={pagination} + selection={selection} + sorting={sorting} + noItemsMessage={ + isEqual( + { + search: this.state.search, + }, + defaultFilter + ) ? ( + +

You have no data streams.

+ + } + actions={[ + { + this.props.history.push(ROUTES.CREATE_TEMPLATE); + }} + > + Create data stream + , + ]} + /> + ) : ( + +

There are no data streams matching your applied filters. Reset your filters to view your data streams.

+ + } + actions={[ + { + this.setState(defaultFilter, () => { + this.getDataStreams(); + }); + }} + > + Reset filters + , + ]} + /> + ) + } + /> +
+ ); + } +} + +export default function DataStreamsContainer(props: Omit) { + const context = useContext(ServicesContext); + return ; +} diff --git a/public/pages/DataStreams/containers/DataStreams/__snapshots__/Templates.test.tsx.snap b/public/pages/DataStreams/containers/DataStreams/__snapshots__/Templates.test.tsx.snap new file mode 100644 index 000000000..2cf6b1d99 --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreams/__snapshots__/Templates.test.tsx.snap @@ -0,0 +1,423 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +
+
+
+ + Templates + +
+
+
+
+ Index templates let you initialize new indexes or data streams with predefined mappings and settings. + + + Learn more. + EuiIconMock + + (opens in a new tab or window) + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + EuiIconMock + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+ + + + + Template type + + + + + + +
+
+ +
+ +
+
+

+ You have no templates. +

+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+`; diff --git a/public/pages/DataStreams/containers/DataStreams/index.ts b/public/pages/DataStreams/containers/DataStreams/index.ts new file mode 100644 index 000000000..8f30e12ce --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreams/index.ts @@ -0,0 +1,3 @@ +import DataStreams from "./DataStreams"; + +export default DataStreams; diff --git a/public/pages/DataStreams/containers/DataStreamsActions/DataStreamsActions.test.tsx b/public/pages/DataStreams/containers/DataStreamsActions/DataStreamsActions.test.tsx new file mode 100644 index 000000000..5ada2c739 --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreamsActions/DataStreamsActions.test.tsx @@ -0,0 +1,142 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { browserServicesMock, coreServicesMock } from "../../../../../test/mocks"; +import DataStreamsActions, { DataStreamsActionsProps } from "./index"; +import { ServicesContext } from "../../../../services"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { Route, HashRouter as Router, Switch, Redirect } from "react-router-dom"; +import { ROUTES } from "../../../../utils/constants"; +const historyPushMock = jest.fn(); + +function renderWithRouter(props: Omit) { + return { + ...render( + + + + + ( + + + { + routeProps.history.push(...args); + historyPushMock(...args); + }, + }} + /> + + + )} + /> + + + + + + ), + }; +} + +describe(" spec", () => { + it("renders the component and all the actions should be disabled when no items selected", async () => { + const { container, getByTestId } = renderWithRouter({ + selectedItems: [], + onDelete: () => null, + }); + + await waitFor(() => { + expect(container.firstChild).toMatchSnapshot(); + }); + + userEvent.click(document.querySelector('[data-test-subj="moreAction"] button') as Element); + await waitFor(() => { + expect(getByTestId("deleteAction")).toBeDisabled(); + }); + }); + + it("delete data streams by calling commonService", async () => { + const onDelete = jest.fn(); + let times = 0; + browserServicesMock.commonService.apiCaller = jest.fn( + async (payload): Promise => { + if (payload.data?.path?.startsWith("/_index_template/")) { + if (times >= 1) { + return { + ok: true, + response: {}, + }; + } else { + times++; + return { + ok: false, + error: "test error", + }; + } + } + return { ok: true, response: {} }; + } + ); + const { container, getByTestId, getByPlaceholderText } = renderWithRouter({ + selectedItems: [ + { + name: "test_template", + index_patterns: "1", + version: "1", + order: 0, + }, + ], + onDelete, + }); + + await waitFor(() => { + expect(container.firstChild).toMatchSnapshot(); + }); + + userEvent.click(document.querySelector('[data-test-subj="moreAction"] button') as Element); + userEvent.click(getByTestId("deleteAction")); + userEvent.type(getByPlaceholderText("delete"), "delete"); + userEvent.click(getByTestId("deleteConfirmButton")); + + await waitFor(() => { + expect(browserServicesMock.commonService.apiCaller).toHaveBeenCalledTimes(1); + expect(browserServicesMock.commonService.apiCaller).toHaveBeenCalledWith({ + endpoint: "transport.request", + data: { + path: `/_index_template/test_template`, + method: "DELETE", + }, + }); + expect(coreServicesMock.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(coreServicesMock.notifications.toasts.addDanger).toHaveBeenCalledWith("test error"); + expect(onDelete).toHaveBeenCalledTimes(0); + }); + + userEvent.click(getByTestId("deleteConfirmButton")); + + await waitFor(() => { + expect(browserServicesMock.commonService.apiCaller).toHaveBeenCalledTimes(2); + expect(coreServicesMock.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(coreServicesMock.notifications.toasts.addSuccess).toHaveBeenCalledWith("Delete [test_template] successfully"); + expect(onDelete).toHaveBeenCalledTimes(1); + }); + + userEvent.click(document.querySelector('[data-test-subj="moreAction"] button') as Element); + userEvent.click(getByTestId("editAction")); + await waitFor(() => expect(historyPushMock).toBeCalledTimes(1), { + timeout: 3000, + }); + }, 30000); +}); diff --git a/public/pages/DataStreams/containers/DataStreamsActions/__snapshots__/TemplatesActions.test.tsx.snap b/public/pages/DataStreams/containers/DataStreamsActions/__snapshots__/TemplatesActions.test.tsx.snap new file mode 100644 index 000000000..d12e4fb9f --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreamsActions/__snapshots__/TemplatesActions.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec delete templates by calling commonService 1`] = ` +
+
+ +
+
+`; + +exports[` spec renders the component and all the actions should be disabled when no items selected 1`] = ` +
+
+ +
+
+`; diff --git a/public/pages/DataStreams/containers/DataStreamsActions/index.tsx b/public/pages/DataStreams/containers/DataStreamsActions/index.tsx new file mode 100644 index 000000000..54815f275 --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreamsActions/index.tsx @@ -0,0 +1,77 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React, { useMemo, useState } from "react"; +import { RouteComponentProps } from "react-router-dom"; +import { EuiButton, EuiContextMenu } from "@elastic/eui"; +import SimplePopover from "../../../../components/SimplePopover"; +import DeleteIndexModal from "../DeleteDataStreamsModal"; +import { ITemplate } from "../../interface"; +import { ROUTES } from "../../../../utils/constants"; + +export interface DataStreamsActionsProps { + selectedItems: ITemplate[]; + onDelete: () => void; + history: RouteComponentProps["history"]; +} + +export default function DataStreamsActions(props: DataStreamsActionsProps) { + const { selectedItems, onDelete } = props; + const [deleteIndexModalVisible, setDeleteIndexModalVisible] = useState(false); + + const onDeleteIndexModalClose = () => { + setDeleteIndexModalVisible(false); + }; + + const renderKey = useMemo(() => Date.now(), [selectedItems]); + + return ( + <> + + Actions + + } + > + props.history.push(`${ROUTES.CREATE_TEMPLATE}/${selectedItems[0].name}`), + }, + { + name: "Delete", + disabled: selectedItems.length !== 1, + "data-test-subj": "deleteAction", + onClick: () => setDeleteIndexModalVisible(true), + }, + ], + }, + ]} + /> + + item.name)} + visible={deleteIndexModalVisible} + onClose={onDeleteIndexModalClose} + onDelete={() => { + onDeleteIndexModalClose(); + onDelete(); + }} + /> + + ); +} diff --git a/public/pages/DataStreams/containers/DeleteDataStreamsModal/DeleteDataStreamsModal.test.tsx b/public/pages/DataStreams/containers/DeleteDataStreamsModal/DeleteDataStreamsModal.test.tsx new file mode 100644 index 000000000..f35d94cf1 --- /dev/null +++ b/public/pages/DataStreams/containers/DeleteDataStreamsModal/DeleteDataStreamsModal.test.tsx @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { render } from "@testing-library/react"; +import DeleteTemplateModal from "./DeleteDataStreamsModal"; + +describe(" spec", () => { + it("renders the component", async () => { + // the main unit test case is in TemplateActions.test.tsx + render( {}} onClose={() => {}} />); + expect(document.body.children).toMatchSnapshot(); + }); +}); diff --git a/public/pages/DataStreams/containers/DeleteDataStreamsModal/DeleteDataStreamsModal.tsx b/public/pages/DataStreams/containers/DeleteDataStreamsModal/DeleteDataStreamsModal.tsx new file mode 100644 index 000000000..ace88fb2f --- /dev/null +++ b/public/pages/DataStreams/containers/DeleteDataStreamsModal/DeleteDataStreamsModal.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useContext } from "react"; +import { ServicesContext } from "../../../../services"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { CoreStart } from "opensearch-dashboards/public"; +import DeleteModal from "../../../../components/DeleteModal"; + +interface DeleteTemplateModalProps { + selectedItems: string[]; + visible: boolean; + onClose: () => void; + onDelete: () => void; +} + +export default function DeleteTemplateModal(props: DeleteTemplateModalProps) { + const { onClose, visible, selectedItems, onDelete } = props; + const services = useContext(ServicesContext); + const coreServices = useContext(CoreServicesContext) as CoreStart; + + const onConfirm = useCallback(async () => { + if (services) { + const result = await services.commonService.apiCaller({ + endpoint: "transport.request", + data: { + path: `/_index_template/${selectedItems.join(",")}`, + method: "DELETE", + }, + }); + if (result && result.ok) { + coreServices.notifications.toasts.addSuccess(`Delete [${selectedItems.join(",")}] successfully`); + onDelete(); + } else { + coreServices.notifications.toasts.addDanger(result?.error || ""); + } + } + }, [selectedItems, services, coreServices, onDelete]); + + return ( + + ); +} diff --git a/public/pages/DataStreams/containers/DeleteDataStreamsModal/__snapshots__/DeleteTemplateModal.test.tsx.snap b/public/pages/DataStreams/containers/DeleteDataStreamsModal/__snapshots__/DeleteTemplateModal.test.tsx.snap new file mode 100644 index 000000000..b379e55a1 --- /dev/null +++ b/public/pages/DataStreams/containers/DeleteDataStreamsModal/__snapshots__/DeleteTemplateModal.test.tsx.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +HTMLCollection [ +