From e38da055ede2171b9a494d7542365247a9f39c31 Mon Sep 17 00:00:00 2001 From: suzhou Date: Fri, 30 Dec 2022 09:07:12 +0800 Subject: [PATCH 01/33] feat: update Signed-off-by: suzhou --- .../built_in_components/index.tsx | 9 +- public/components/FormGenerator/index.tsx | 10 +- .../Aliases/containers/AliasActions/index.tsx | 12 +- .../Aliases/containers/Aliases/Aliases.tsx | 1 + .../containers/IndexForm/index.tsx | 23 +- public/pages/Main/Main.tsx | 11 +- .../containers/Rollover/Rollover.test.tsx | 90 ++++ .../Rollover/containers/Rollover/Rollover.tsx | 409 ++++++++++++++++++ .../__snapshots__/IndexDetail.test.tsx.snap | 358 +++++++++++++++ .../Rollover/containers/Rollover/index.ts | 3 + public/pages/Rollover/index.ts | 8 + public/utils/constants.ts | 2 + 12 files changed, 923 insertions(+), 13 deletions(-) create mode 100644 public/pages/Rollover/containers/Rollover/Rollover.test.tsx create mode 100644 public/pages/Rollover/containers/Rollover/Rollover.tsx create mode 100644 public/pages/Rollover/containers/Rollover/__snapshots__/IndexDetail.test.tsx.snap create mode 100644 public/pages/Rollover/containers/Rollover/index.ts create mode 100644 public/pages/Rollover/index.ts diff --git a/public/components/FormGenerator/built_in_components/index.tsx b/public/components/FormGenerator/built_in_components/index.tsx index 2a1affb66..561b41e5f 100644 --- a/public/components/FormGenerator/built_in_components/index.tsx +++ b/public/components/FormGenerator/built_in_components/index.tsx @@ -44,7 +44,14 @@ const componentMap: Record { - const findItem = options.find((item: { label: string }) => item.label === searchValue); + const allOptions = (options as { label: string; options?: { label: string }[] }[]).reduce((total, current) => { + if (current.options) { + return [...total, ...current.options]; + } else { + return [...total, current]; + } + }, [] as { label: string }[]); + const findItem = allOptions.find((item: { label: string }) => item.label === searchValue); if (findItem) { onChange(searchValue); } diff --git a/public/components/FormGenerator/index.tsx b/public/components/FormGenerator/index.tsx index 29b2278bf..9ca7d7241 100644 --- a/public/components/FormGenerator/index.tsx +++ b/public/components/FormGenerator/index.tsx @@ -2,7 +2,7 @@ import React, { forwardRef, useRef, useImperativeHandle, useEffect, useMemo } fr import { EuiForm, EuiFormProps, EuiSpacer } from "@elastic/eui"; import { isEqual, omit, pick } from "lodash"; import AllBuiltInComponents, { IFieldComponentProps } from "./built_in_components"; -import useField, { InitOption, FieldOption, Rule, FieldInstance, FieldName } from "../../lib/field"; +import useField, { InitOption, FieldOption, Rule, FieldInstance, FieldName, transformNameToString } from "../../lib/field"; import AdvancedSettings, { IAdvancedSettingsProps, IAdvancedSettingsRef } from "../AdvancedSettings"; import CustomFormRow, { CustomFormRowProps } from "../CustomFormRow"; @@ -23,7 +23,7 @@ interface IFormGeneratorAdvancedSettings extends IAdvancedSettingsProps { export interface IField { rowProps: Pick; - name: string; + name: FieldName; type?: keyof typeof AllBuiltInComponents; component?: React.ComponentType; options?: Omit; @@ -129,10 +129,10 @@ function FormGenerator(props: IFormGeneratorProps, ref: React.Ref void; onUpdateAlias: () => void; + history: RouteComponentProps["history"]; } export default function AliasesActions(props: AliasesActionsProps) { - const { selectedItems, onDelete, onUpdateAlias } = props; + const { selectedItems, onDelete, onUpdateAlias, history } = props; const [deleteIndexModalVisible, setDeleteIndexModalVisible] = useState(false); const onDeleteIndexModalClose = () => { @@ -51,6 +53,12 @@ export default function AliasesActions(props: AliasesActionsProps) { "data-test-subj": "editAction", onClick: onUpdateAlias, }, + { + name: "Rollover", + disabled: !selectedItems.length, + "data-test-subj": "rolloverAction", + onClick: () => history.push(`${ROUTES.ROLLOVER}/${selectedItems[0].alias}`), + }, { name: "Delete", disabled: !selectedItems.length, diff --git a/public/pages/Aliases/containers/Aliases/Aliases.tsx b/public/pages/Aliases/containers/Aliases/Aliases.tsx index 6e50c16fd..cd2f37855 100644 --- a/public/pages/Aliases/containers/Aliases/Aliases.tsx +++ b/public/pages/Aliases/containers/Aliases/Aliases.tsx @@ -280,6 +280,7 @@ class Aliases extends Component { }} selectedItems={this.state.selectedItems} onDelete={this.getAliases} + history={this.props.history} /> ), }, diff --git a/public/pages/CreateIndex/containers/IndexForm/index.tsx b/public/pages/CreateIndex/containers/IndexForm/index.tsx index 25500fa93..d702460a4 100644 --- a/public/pages/CreateIndex/containers/IndexForm/index.tsx +++ b/public/pages/CreateIndex/containers/IndexForm/index.tsx @@ -47,6 +47,7 @@ export const getAliasActionsByDiffArray = ( export interface IndexFormProps extends Pick { index?: string; + value?: Partial; mode?: IndicesUpdateMode; onCancel?: () => void; onSubmitSuccess?: (indexName: string) => void; @@ -73,12 +74,26 @@ const findLineNumber = (regexp: RegExp, str: string): number => { export class IndexForm extends Component { static contextType = CoreServicesContext; + /** + * convert the mappings.properies to array + * @param payload index detail with the mappings.properties is a map + */ + static transformIndexDetailToLocal(payload?: Partial): Partial { + const newPayload = { ...payload }; + set(newPayload, "mappings.properties", transformObjectToArray(get(newPayload, "mappings.properties", {}))); + return newPayload as IndexItem; + } + static transformIndexDetailToRemote(payload?: Partial): Partial { + const newPayload = { ...payload }; + set(newPayload, "mappings.properties", transformArrayToObject(get(newPayload, "mappings.properties", {}))); + return newPayload as IndexItemRemote; + } constructor(props: IndexFormProps & { services: BrowserServices }) { super(props); const isEdit = this.isEdit; this.state = { isSubmitting: false, - indexDetail: merge({}, defaultIndexSettings), + indexDetail: merge({}, defaultIndexSettings, IndexForm.transformIndexDetailToLocal(props.value)), oldIndexDetail: undefined, loading: isEdit, }; @@ -110,6 +125,7 @@ export class IndexForm extends Component this.indexDetailRef?.hasUnsavedChanges(mode); + getValue = () => IndexForm.transformIndexDetailToRemote(this.state.indexDetail); getIndexDetail = async (indexName: string): Promise => { const response = await this.commonService.apiCaller>({ @@ -133,11 +149,10 @@ export class IndexForm extends Component { )} /> + ( +
+ +
+ )} + /> diff --git a/public/pages/Rollover/containers/Rollover/Rollover.test.tsx b/public/pages/Rollover/containers/Rollover/Rollover.test.tsx new file mode 100644 index 000000000..d6691c00d --- /dev/null +++ b/public/pages/Rollover/containers/Rollover/Rollover.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { MemoryRouter as Router } from "react-router"; +import { Route, RouteComponentProps, Switch } from "react-router-dom"; +import { browserServicesMock, coreServicesMock, apiCallerMock } from "../../../../../test/mocks"; +import Rollover, { RolloverProps } from "./Rollover"; +import { ModalProvider } from "../../../../components/Modal"; +import { ServicesContext } from "../../../../services"; +import { CoreServicesContext } from "../../../../components/core_services"; + +function renderWithRouter(props: Omit, initialEntries: string[]) { + return { + ...render( + + + + + + } /> + + + + + + ), + }; +} + +describe("container spec", () => { + beforeEach(() => { + apiCallerMock(browserServicesMock); + }); + it("render the component", async () => { + browserServicesMock.indexService.getIndices = jest.fn(() => { + return { + ok: true, + response: { + indices: [ + { + "docs.count": "5", + "docs.deleted": "2", + health: "green", + index: "test_index", + pri: "1", + "pri.store.size": "100KB", + rep: "0", + status: "open", + "store.size": "100KB", + uuid: "some_uuid", + managed: "", + managedPolicy: "", + data_stream: "", + }, + ], + }, + } as any; + }) as typeof browserServicesMock.indexService.getIndices; + const { container, getByTestId, queryByText } = renderWithRouter({}, [`/test_index`]); + + await waitFor(() => { + expect(container.firstChild).toMatchSnapshot(); + expect(document.querySelector("#RolloverModalOverview")).not.toBeNull(); + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(1); + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "indices.get", + data: { + index: "test_index", + }, + }); + }); + + userEvent.click(document.getElementById("RolloverModalAlias") as Element); + await waitFor(() => { + expect(queryByText("Index alias")).not.toBeNull(); + }); + userEvent.click(getByTestId("detailModalEdit")); + await waitFor(() => {}); + userEvent.click(getByTestId("createIndexCreateButton")); + await waitFor(() => { + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(4); + }); + }); +}); diff --git a/public/pages/Rollover/containers/Rollover/Rollover.tsx b/public/pages/Rollover/containers/Rollover/Rollover.tsx new file mode 100644 index 000000000..721fc6b90 --- /dev/null +++ b/public/pages/Rollover/containers/Rollover/Rollover.tsx @@ -0,0 +1,409 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { RouteComponentProps } from "react-router-dom"; +import { isEmpty, merge } from "lodash"; +import { BREADCRUMBS, ROUTES } from "../../../../utils/constants"; +import { ServicesContext } from "../../../../services"; +import { BrowserServices } from "../../../../models/interfaces"; +import IndexFormWrapper, { IndexForm } from "../../../CreateIndex/containers/IndexForm"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, +} from "@elastic/eui"; +import CustomFormRow from "../../../../components/CustomFormRow"; +import { ContentPanel } from "../../../../components/ContentPanel"; +import FormGenerator, { AllBuiltInComponents, IFormGeneratorRef } from "../../../../components/FormGenerator"; +import { Alias } from "../../../../../server/models/interfaces"; +import { IndexItemRemote } from "../../../../../models/interfaces"; + +export interface RolloverProps extends RouteComponentProps<{ source: string }> {} + +export interface IRolloverRequestBody { + source?: string; + targetIndex?: IndexItemRemote; + conditions?: { + max_age?: string; + max_docs?: number; + max_size?: string; + max_primary_shard_size?: string; + }; +} + +export default function IndexDetail(props: RolloverProps) { + const [options, setOptions] = useState<{ + alias: { label: string; aliases: Alias[] }[]; + dataStreams: { label: string }[]; + }>({ + alias: [], + dataStreams: [], + }); + const coreService = useContext(CoreServicesContext); + const services = useContext(ServicesContext) as BrowserServices; + const sourceRef = useRef(null); + const conditionsRef = useRef(null); + const indexFormRef = useRef(null); + const [tempValue, setValue] = useState({}); + const [flyoutVisible, setFlyoutVisible] = useState(false); + const [loading, setIsLoading] = useState(false); + const [writeIndexValue, setWriteIndexValue] = useState(undefined); + + const onChange = (val?: Record) => { + const finalResult = merge({}, tempValue, val); + setValue(finalResult); + }; + + const refreshOptions = () => + Promise.all([ + services.commonService.apiCaller({ + endpoint: "cat.aliases", + data: { + format: "json", + }, + }), + services.indexService.getDataStreams({}), + ]).then(([aliases, dataStreams]) => { + if (aliases.ok && dataStreams.ok) { + const allAlias: { label: string; aliases: Alias[] }[] = []; + aliases.response.forEach((item) => { + let findIndex; + if (allAlias.find((alias) => alias.label === item.alias)) { + findIndex = allAlias.findIndex((alias) => alias.label === item.alias); + } else { + findIndex = allAlias.length; + allAlias.push({ + label: item.alias, + aliases: [], + }); + } + allAlias[findIndex].aliases.push(item); + }); + setOptions({ + alias: allAlias, + dataStreams: dataStreams.response.dataStreams.map((item) => ({ label: item.name })), + }); + } + }); + + const submitWriteIndex = async () => { + const result = await services.commonService.apiCaller({ + endpoint: "indices.updateAliases", + data: { + body: { + actions: [ + { + add: { + index: writeIndexValue, + alias: sourceRef.current?.getValue("source"), + is_write_index: true, + }, + }, + ], + }, + }, + }); + if (result.ok) { + coreService?.notifications.toasts.addSuccess(`Set ${writeIndexValue} as write index successfully.`); + refreshOptions(); + } else { + coreService?.notifications.toasts.addDanger(result.error); + } + }; + + const onSubmit = async () => { + const formGeneratersRes = await Promise.all([sourceRef.current?.validatePromise(), conditionsRef.current?.validatePromise()]); + const hasError = formGeneratersRes.some((item) => item?.errors); + if (hasError) { + return; + } + const finalValues: IRolloverRequestBody = merge({}, tempValue); + formGeneratersRes.forEach((item) => merge(finalValues, item?.values)); + + const payload: { + alias: string; + newIndex?: string; + body: Omit & { conditions?: IRolloverRequestBody["conditions"] }; + } = { + alias: finalValues.source || "", + body: {}, + }; + if (sourceType === "alias" && !isEmpty(finalValues.targetIndex || {})) { + const { index, ...others } = finalValues.targetIndex || {}; + payload.newIndex = index; + payload.body = { + ...others, + conditions: finalValues.conditions, + }; + } else { + payload.body.conditions = finalValues.conditions; + } + setIsLoading(true); + + const result = await services.commonService.apiCaller({ + endpoint: "indices.rollover", + data: payload, + }); + + setIsLoading(false); + + if (result.ok) { + coreService?.notifications.toasts.addSuccess(`${payload.alias} has been rollovered successfully.`); + props.history.replace(sourceType === "alias" ? ROUTES.ALIASES : ROUTES.INDICES); + } else { + coreService?.notifications.toasts.addDanger(result.error); + } + }; + + useEffect(() => { + coreService?.chrome.setBreadcrumbs([ + BREADCRUMBS.INDEX_MANAGEMENT, + { + ...BREADCRUMBS.ROLLOVER, + href: `#${props.location.pathname}`, + }, + ]); + refreshOptions(); + }, []); + + const sourceOptions = useMemo(() => { + return [ + { + label: "aliases", + options: options.alias, + }, + { + label: "data streams", + options: options.dataStreams, + }, + ]; + }, [options]); + + const initialValue = useMemo(() => { + return { + source: props.match.params.source, + }; + }, [props.match.params.source]); + + const sourceType: "dataStreams" | "alias" | undefined = useMemo(() => { + const sourceValue = sourceRef.current?.getValue("source"); + if (options.alias.find((item) => item.label === sourceValue)) { + return "alias"; + } + + if (options.dataStreams.find((item) => item.label === sourceValue)) { + return "dataStreams"; + } + + return; + }, [sourceRef.current?.getValue("source")]); + + const reasons = useMemo(() => { + let result: React.ReactChild[] = []; + if (sourceType === "alias") { + const findItem = options.alias.find((item) => item.label === sourceRef.current?.getValue("source")); + if (findItem && findItem.aliases.length > 1) { + // has to check if it has write_index + if (findItem.aliases.every((item) => item.is_write_index !== "true")) { + result.push( + <> + + + Please select an index to be the write index of this alias. + + setWriteIndexValue(val)} + options={findItem.aliases.map((item) => ({ label: item.index }))} + /> + + + + Set index as write index + + + + + ); + } + } + } + + return result; + }, [sourceType, options, writeIndexValue]); + + return ( +
+ +

Rollover

+
+ Creates a new index for a data stream or index alias.} + style={{ + marginBottom: 20, + }} + > + <> + + + + {reasons.length ? ( + <> + + +
    + {reasons.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ + ) : null} +
+ + {sourceType === "alias" ? ( + <> + + setFlyoutVisible(true)}>Define target index + + + + ) : null} + + + + + {flyoutVisible ? ( + null}> + + +

Define target index

+
+
+ + + + + { + setFlyoutVisible(false); + }} + > + Cancel + + { + onChange({ + targetIndex: indexFormRef.current?.getValue(), + }); + setFlyoutVisible(false); + }} + > + Save + + +
+ ) : null} + + + { + if (sourceType === "alias") { + props.history.push(ROUTES.ALIASES); + } else { + props.history.push(ROUTES.INDICES); + } + }} + data-test-subj="rolloverCancelButton" + > + Cancel + + + + + Rollover + + + +
+ ); +} diff --git a/public/pages/Rollover/containers/Rollover/__snapshots__/IndexDetail.test.tsx.snap b/public/pages/Rollover/containers/Rollover/__snapshots__/IndexDetail.test.tsx.snap new file mode 100644 index 000000000..bf8b6b363 --- /dev/null +++ b/public/pages/Rollover/containers/Rollover/__snapshots__/IndexDetail.test.tsx.snap @@ -0,0 +1,358 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`container spec render the component 1`] = `null`; + +exports[`container spec render the component 2`] = ` +
+
+
+
+ + test_index + +
+
+ +
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ Index name +
+
+ + test_index + +
+
+
+
+ Health +
+
+ + green + +
+
+
+
+ Status +
+
+
+
+
+ EuiIconMock +
+
+ green +
+
+
+
+
+
+
+ Creation date +
+
+ + - + +
+
+
+
+ Total size +
+
+ + 100KB + +
+
+
+
+ Size of primaries +
+
+ + 100KB + +
+
+
+
+ Total documents +
+
+ + 5 + +
+
+
+
+ Deleted documents +
+
+ + 2 + +
+
+
+
+ Primaries +
+
+ + 1 + +
+
+
+
+ Replicas +
+
+ + 0 + +
+
+
+
+ Index blocks +
+
+ + - + +
+
+
+
+ Managed by policy +
+
+ + - + +
+
+
+
+
+
+
+
+`; diff --git a/public/pages/Rollover/containers/Rollover/index.ts b/public/pages/Rollover/containers/Rollover/index.ts new file mode 100644 index 000000000..c6d13070d --- /dev/null +++ b/public/pages/Rollover/containers/Rollover/index.ts @@ -0,0 +1,3 @@ +import IndexDetail from "./Rollover"; + +export default IndexDetail; diff --git a/public/pages/Rollover/index.ts b/public/pages/Rollover/index.ts new file mode 100644 index 000000000..928fa46d3 --- /dev/null +++ b/public/pages/Rollover/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import Rollover from "./containers/Rollover"; + +export default Rollover; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index d0a919883..921538e9a 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -61,6 +61,7 @@ export const ROUTES = Object.freeze({ CREATE_TEMPLATE: "/create-template", SPLIT_INDEX: "/split-index", SHRINK_INDEX: "/shrink-index", + ROLLOVER: "/rollover", }); export const BREADCRUMBS = Object.freeze({ @@ -106,6 +107,7 @@ export const BREADCRUMBS = Object.freeze({ EDIT_TEMPLATE: { text: "Edit template", href: `#${ROUTES.CREATE_TEMPLATE}` }, SPLIT_INDEX: { text: "Split Index", href: `#${ROUTES.SPLIT_INDEX}` }, SHRINK_INDEX: { text: "Shrink index", href: `#${ROUTES.SHRINK_INDEX}` }, + ROLLOVER: { text: "Rollover", href: `#${ROUTES.ROLLOVER}` }, }); // TODO: EUI has a SortDirection already From 88e95b2ad888ab383b3bf77544567b8ef567acab Mon Sep 17 00:00:00 2001 From: suzhou Date: Fri, 30 Dec 2022 16:59:06 +0800 Subject: [PATCH 02/33] feat: update Signed-off-by: suzhou --- .../containers/Rollover/Rollover.test.tsx | 47 +-- .../__snapshots__/IndexDetail.test.tsx.snap | 358 ------------------ 2 files changed, 4 insertions(+), 401 deletions(-) delete mode 100644 public/pages/Rollover/containers/Rollover/__snapshots__/IndexDetail.test.tsx.snap diff --git a/public/pages/Rollover/containers/Rollover/Rollover.test.tsx b/public/pages/Rollover/containers/Rollover/Rollover.test.tsx index d6691c00d..5339a1493 100644 --- a/public/pages/Rollover/containers/Rollover/Rollover.test.tsx +++ b/public/pages/Rollover/containers/Rollover/Rollover.test.tsx @@ -6,7 +6,6 @@ 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 { MemoryRouter as Router } from "react-router"; import { Route, RouteComponentProps, Switch } from "react-router-dom"; import { browserServicesMock, coreServicesMock, apiCallerMock } from "../../../../../test/mocks"; @@ -23,7 +22,7 @@ function renderWithRouter(props: Omit, - } /> + } /> @@ -38,53 +37,15 @@ describe("container spec", () => { apiCallerMock(browserServicesMock); }); it("render the component", async () => { - browserServicesMock.indexService.getIndices = jest.fn(() => { + browserServicesMock.indexService.getDataStreams = jest.fn(() => { return { ok: true, - response: { - indices: [ - { - "docs.count": "5", - "docs.deleted": "2", - health: "green", - index: "test_index", - pri: "1", - "pri.store.size": "100KB", - rep: "0", - status: "open", - "store.size": "100KB", - uuid: "some_uuid", - managed: "", - managedPolicy: "", - data_stream: "", - }, - ], - }, } as any; - }) as typeof browserServicesMock.indexService.getIndices; - const { container, getByTestId, queryByText } = renderWithRouter({}, [`/test_index`]); + }) as typeof browserServicesMock.indexService.getDataStreams; + const { container } = renderWithRouter({}, [`/test_alias`]); await waitFor(() => { expect(container.firstChild).toMatchSnapshot(); - expect(document.querySelector("#RolloverModalOverview")).not.toBeNull(); - expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(1); - expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ - endpoint: "indices.get", - data: { - index: "test_index", - }, - }); - }); - - userEvent.click(document.getElementById("RolloverModalAlias") as Element); - await waitFor(() => { - expect(queryByText("Index alias")).not.toBeNull(); - }); - userEvent.click(getByTestId("detailModalEdit")); - await waitFor(() => {}); - userEvent.click(getByTestId("createIndexCreateButton")); - await waitFor(() => { - expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(4); }); }); }); diff --git a/public/pages/Rollover/containers/Rollover/__snapshots__/IndexDetail.test.tsx.snap b/public/pages/Rollover/containers/Rollover/__snapshots__/IndexDetail.test.tsx.snap deleted file mode 100644 index bf8b6b363..000000000 --- a/public/pages/Rollover/containers/Rollover/__snapshots__/IndexDetail.test.tsx.snap +++ /dev/null @@ -1,358 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`container spec render the component 1`] = `null`; - -exports[`container spec render the component 2`] = ` -
-
-
-
- - test_index - -
-
- -
-
-
-
-
-
-
-
-
- - - - -
-
-
-
-
-
-
- Index name -
-
- - test_index - -
-
-
-
- Health -
-
- - green - -
-
-
-
- Status -
-
-
-
-
- EuiIconMock -
-
- green -
-
-
-
-
-
-
- Creation date -
-
- - - - -
-
-
-
- Total size -
-
- - 100KB - -
-
-
-
- Size of primaries -
-
- - 100KB - -
-
-
-
- Total documents -
-
- - 5 - -
-
-
-
- Deleted documents -
-
- - 2 - -
-
-
-
- Primaries -
-
- - 1 - -
-
-
-
- Replicas -
-
- - 0 - -
-
-
-
- Index blocks -
-
- - - - -
-
-
-
- Managed by policy -
-
- - - - -
-
-
-
-
-
-
-
-`; From 411e2aad4cb40e57b17fae818ea1cc2cfd89aa20 Mon Sep 17 00:00:00 2001 From: suzhou Date: Fri, 30 Dec 2022 17:00:02 +0800 Subject: [PATCH 03/33] feat: update Signed-off-by: suzhou --- .../__snapshots__/Rollover.test.tsx.snap | 405 ++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 public/pages/Rollover/containers/Rollover/__snapshots__/Rollover.test.tsx.snap diff --git a/public/pages/Rollover/containers/Rollover/__snapshots__/Rollover.test.tsx.snap b/public/pages/Rollover/containers/Rollover/__snapshots__/Rollover.test.tsx.snap new file mode 100644 index 000000000..c94829b0d --- /dev/null +++ b/public/pages/Rollover/containers/Rollover/__snapshots__/Rollover.test.tsx.snap @@ -0,0 +1,405 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`container spec render the component 1`] = ` +
+

+ Rollover +

+
+
+
+ Creates a new index for a data stream or index alias. +
+
+
+
+
+
+

+ Configure source +

+
+
+
+
+
+
+
+ +
+
+
+ Specify one alias or data stream you want to rollover from. +
+ + +
+
+
+
+
+
+
+
+

+ Conditions - optional +

+
+
+
+
+
+
+
+ +
+
+
+ The minimum age required to roll over the index. Accepts time units, e.g. "5h" or "1d". +
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ The minimum number of documents required to roll over the index. +
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ The minimum size of the total primary shard storage required to roll over the index. Accepts byte units, e.g. "500mb" or "50gb". +
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ The minimum size of a single primary shard required to roll over the index. Accepts byte units, e.g. "500mb" or "50gb". +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+`; From 9c97e52d7764d5d1defb1546f9af55708ef5950b Mon Sep 17 00:00:00 2001 From: suzhou Date: Fri, 10 Feb 2023 00:23:32 +0800 Subject: [PATCH 04/33] feat: add write index in aliases Signed-off-by: suzhou --- .../Aliases/containers/Aliases/Aliases.tsx | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/public/pages/Aliases/containers/Aliases/Aliases.tsx b/public/pages/Aliases/containers/Aliases/Aliases.tsx index cd2f37855..e4f7eb833 100644 --- a/public/pages/Aliases/containers/Aliases/Aliases.tsx +++ b/public/pages/Aliases/containers/Aliases/Aliases.tsx @@ -29,7 +29,7 @@ import { ContentPanel, ContentPanelActions } from "../../../../components/Conten import { DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_QUERY_PARAMS } from "../../utils/constants"; import CommonService from "../../../../services/CommonService"; import { IAlias } from "../../interface"; -import { BREADCRUMBS } from "../../../../utils/constants"; +import { BREADCRUMBS, ROUTES } from "../../../../utils/constants"; import { CoreServicesContext } from "../../../../components/core_services"; import { ServicesContext } from "../../../../services"; import IndexControls, { SearchControlsProps } from "../../components/IndexControls"; @@ -165,7 +165,7 @@ class Aliases extends Component { }; groupResponse = (array: IAlias[]) => { - const groupedMap: Record = {}; + const groupedMap: Record = {}; array.forEach((item, index) => { groupedMap[item.alias] = groupedMap[item.alias] || { ...item, @@ -173,10 +173,24 @@ class Aliases extends Component { indexArray: [], }; groupedMap[item.alias].indexArray.push(item.index); + if (item.is_write_index === "true") { + groupedMap[item.alias].writeIndex = item.index; + } else if (!groupedMap[item.alias].writeIndex) { + groupedMap[item.alias].writeIndex = ""; + } + }); + const result = Object.values(groupedMap).map((item) => { + if (item.indexArray.length === 1) { + return { + ...item, + writeIndex: item.indexArray[0], + }; + } + + return item; }); - const result = Object.values(groupedMap); result.sort((a, b) => a.order - b.order); - return Object.values(groupedMap).sort(); + return result; }; getAliases = async (): Promise => { @@ -354,6 +368,17 @@ class Aliases extends Component { ); }, }, + { + field: "writeIndex", + name: "Writing index", + render: (value: string) => { + if (value) { + return {value}; + } + + return "-"; + }, + }, { field: "indexArray", name: "Index name", From 1aa06dc5194ded4b1d6990c503fb05e04bca77f5 Mon Sep 17 00:00:00 2001 From: suzhou Date: Fri, 10 Feb 2023 12:24:22 +0800 Subject: [PATCH 05/33] feat: remove conditions section Signed-off-by: suzhou --- .../Rollover/containers/Rollover/Rollover.tsx | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/public/pages/Rollover/containers/Rollover/Rollover.tsx b/public/pages/Rollover/containers/Rollover/Rollover.tsx index 721fc6b90..10db68d86 100644 --- a/public/pages/Rollover/containers/Rollover/Rollover.tsx +++ b/public/pages/Rollover/containers/Rollover/Rollover.tsx @@ -124,7 +124,7 @@ export default function IndexDetail(props: RolloverProps) { }; const onSubmit = async () => { - const formGeneratersRes = await Promise.all([sourceRef.current?.validatePromise(), conditionsRef.current?.validatePromise()]); + const formGeneratersRes = await Promise.all([sourceRef.current?.validatePromise()]); const hasError = formGeneratersRes.some((item) => item?.errors); if (hasError) { return; @@ -145,10 +145,7 @@ export default function IndexDetail(props: RolloverProps) { payload.newIndex = index; payload.body = { ...others, - conditions: finalValues.conditions, }; - } else { - payload.body.conditions = finalValues.conditions; } setIsLoading(true); @@ -306,48 +303,6 @@ export default function IndexDetail(props: RolloverProps) { ) : null} - - - - {flyoutVisible ? ( null}> From 0b74b6b6159c93a9fa1f35cb1d51d7396d761497 Mon Sep 17 00:00:00 2001 From: suzhou Date: Fri, 10 Feb 2023 19:55:01 +0800 Subject: [PATCH 06/33] feat: update Signed-off-by: suzhou --- public/pages/Aliases/containers/Aliases/Aliases.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/pages/Aliases/containers/Aliases/Aliases.tsx b/public/pages/Aliases/containers/Aliases/Aliases.tsx index e4f7eb833..ebf82f12e 100644 --- a/public/pages/Aliases/containers/Aliases/Aliases.tsx +++ b/public/pages/Aliases/containers/Aliases/Aliases.tsx @@ -66,7 +66,7 @@ function IndexNameDisplay(props: { indices: string[]; alias: string }) { }; return ( - <> +
{props.indices.slice(0, 3).join(", ")} {props.indices.length <= 3 ? null : ( setHide(!hide)}> @@ -102,7 +102,7 @@ function IndexNameDisplay(props: { indices: string[]; alias: string }) { )} - +
); } From 426aae76a33a00e8f005b7f77de4d0b47f4d0a96 Mon Sep 17 00:00:00 2001 From: suzhou Date: Sun, 12 Feb 2023 11:34:20 +0800 Subject: [PATCH 07/33] feat: update Signed-off-by: suzhou --- .../Rollover/containers/Rollover/Rollover.tsx | 85 +++++++++++++------ 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/public/pages/Rollover/containers/Rollover/Rollover.tsx b/public/pages/Rollover/containers/Rollover/Rollover.tsx index 10db68d86..3dc387a28 100644 --- a/public/pages/Rollover/containers/Rollover/Rollover.tsx +++ b/public/pages/Rollover/containers/Rollover/Rollover.tsx @@ -54,7 +54,6 @@ export default function IndexDetail(props: RolloverProps) { const coreService = useContext(CoreServicesContext); const services = useContext(ServicesContext) as BrowserServices; const sourceRef = useRef(null); - const conditionsRef = useRef(null); const indexFormRef = useRef(null); const [tempValue, setValue] = useState({}); const [flyoutVisible, setFlyoutVisible] = useState(false); @@ -211,29 +210,34 @@ export default function IndexDetail(props: RolloverProps) { let result: React.ReactChild[] = []; if (sourceType === "alias") { const findItem = options.alias.find((item) => item.label === sourceRef.current?.getValue("source")); - if (findItem && findItem.aliases.length > 1) { - // has to check if it has write_index - if (findItem.aliases.every((item) => item.is_write_index !== "true")) { - result.push( - <> - - - Please select an index to be the write index of this alias. - - setWriteIndexValue(val)} - options={findItem.aliases.map((item) => ({ label: item.index }))} - /> - - - - Set index as write index - - - - - ); + if (findItem) { + if (findItem.aliases.length > 1) { + // has to check if it has write_index + if (findItem.aliases.every((item) => item.is_write_index !== "true")) { + result.push( + <> + + + Assign a write index from this alias before performing rollover. + + + setWriteIndexValue(val)} + options={findItem.aliases.map((item) => ({ label: item.index }))} + /> + + + + + Assign as write index + + + + + ); + } } } } @@ -284,7 +288,7 @@ export default function IndexDetail(props: RolloverProps) { {reasons.length ? ( <> - +
    {reasons.map((item, index) => (
  • {item}
  • @@ -293,11 +297,40 @@ export default function IndexDetail(props: RolloverProps) { ) : null} + {(() => { + if (sourceType === "alias") { + const findItem = options.alias.find((item) => item.label === sourceRef.current?.getValue("source")); + let writeIndex = ""; + if (findItem) { + if (findItem.aliases.length > 1) { + // has to check if it has write_index + if (findItem.aliases.some((item) => item.is_write_index === "true")) { + const indexItem = findItem.aliases.find((item) => item.is_write_index === "true"); + writeIndex = indexItem?.index || ""; + } + } else { + writeIndex = findItem.aliases[0].index; + } + } + if (writeIndex) { + return ( + <> + + + {writeIndex} + + + ); + } + } + + return null; + })()} {sourceType === "alias" ? ( <> - + setFlyoutVisible(true)}>Define target index From 6b0ea135cb0274f6a1b0c8582684c4fd1e4fb6aa Mon Sep 17 00:00:00 2001 From: suzhou Date: Sun, 12 Feb 2023 11:44:38 +0800 Subject: [PATCH 08/33] feat: merge data streams Signed-off-by: suzhou --- cypress/integration/data_streams.js | 90 ++++ opensearch_dashboards.json | 2 +- .../DescriptionListHoz/DescriptionListHoz.tsx | 10 +- .../DescriptionListHoz.test.tsx.snap | 2 +- .../components/IndexMapping/IndexMapping.scss | 0 .../IndexMapping/IndexMapping.test.tsx | 2 +- .../components/IndexMapping/IndexMapping.tsx | 8 +- .../__snapshots__/IndexMapping.test.tsx.snap | 0 .../components/IndexMapping/helper.ts | 2 +- .../components/IndexMapping/index.ts | 0 .../components/IndexMapping/interfaces.ts | 2 +- .../MappingLabel/MappingLabel.test.tsx | 0 .../components/MappingLabel/MappingLabel.tsx | 10 +- .../__snapshots__/MappingLabel.test.tsx.snap | 0 .../components/MappingLabel/index.ts | 0 .../IndexSettings/IndexSettings.tsx | 158 ++++++ .../components/IndexSettings/index.ts | 3 + .../BackingIndices/BackingIndices.tsx | 167 +++++++ .../containers/BackingIndices/index.ts | 3 + .../CreateDataStream.test.tsx | 50 ++ .../CreateDataStream/CreateDataStream.tsx | 62 +++ .../CreateDataStream.test.tsx.snap | 235 +++++++++ .../containers/CreateDataStream/index.ts | 8 + .../DataStreamDetail.test.tsx | 69 +++ .../DataStreamDetail/DataStreamDetail.tsx | 207 ++++++++ .../DataStreamDetail.test.tsx.snap | 231 +++++++++ .../containers/DataStreamDetail/hooks.tsx | 71 +++ .../containers/DataStreamDetail/index.ts | 9 + .../DefineDataStream/DefineDataStream.tsx | 131 +++++ .../containers/DefineDataStream/index.ts | 3 + .../containers/IndexAlias/IndexAlias.tsx | 75 +++ .../containers/IndexAlias/index.ts | 3 + .../TemplateMappings/TemplateMappings.tsx | 65 +++ .../containers/TemplateMappings/index.ts | 3 + public/pages/CreateDataStream/hooks.tsx | 10 + public/pages/CreateDataStream/index.ts | 8 + public/pages/CreateDataStream/interface.ts | 20 + .../components/IndexDetail/IndexDetail.tsx | 4 +- .../containers/IndexForm/index.tsx | 2 +- .../CreateIndexTemplate.test.tsx.snap | 6 +- .../TemplateDetail/TemplateDetail.tsx | 2 +- .../containers/TemplateDetail/hooks.tsx | 2 +- .../TemplateMappings/TemplateMappings.tsx | 4 +- .../IndexControls/IndexControls.test.tsx | 32 ++ .../IndexControls/IndexControls.tsx | 36 ++ .../__snapshots__/IndexControls.test.tsx.snap | 47 ++ .../components/IndexControls/index.ts | 9 + .../DataStreams/DataStreams.test.tsx | 102 ++++ .../containers/DataStreams/DataStreams.tsx | 414 ++++++++++++++++ .../__snapshots__/DataStreams.test.tsx.snap | 448 ++++++++++++++++++ .../containers/DataStreams/index.ts | 3 + .../DataStreamsActions.test.tsx | 105 ++++ .../DataStreamsActions.test.tsx.snap | 55 +++ .../containers/DataStreamsActions/index.tsx | 69 +++ .../DeleteDataStreamsModal.test.tsx | 17 + .../DeleteDataStreamsModal.tsx | 52 ++ .../DeleteDataStreamsModal.test.tsx.snap | 143 ++++++ .../DeleteDataStreamsModal/index.ts | 8 + public/pages/DataStreams/index.ts | 8 + public/pages/DataStreams/interface.ts | 11 + public/pages/DataStreams/utils/constants.tsx | 27 ++ public/pages/Indices/utils/constants.tsx | 6 +- public/pages/Main/Main.tsx | 37 +- .../__snapshots__/ShrinkIndex.test.tsx.snap | 2 +- public/utils/constants.ts | 4 + 65 files changed, 3340 insertions(+), 34 deletions(-) create mode 100644 cypress/integration/data_streams.js rename public/{pages/CreateIndex => }/components/IndexMapping/IndexMapping.scss (100%) rename public/{pages/CreateIndex => }/components/IndexMapping/IndexMapping.test.tsx (98%) rename public/{pages/CreateIndex => }/components/IndexMapping/IndexMapping.tsx (97%) rename public/{pages/CreateIndex => }/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap (100%) rename public/{pages/CreateIndex => }/components/IndexMapping/helper.ts (97%) rename public/{pages/CreateIndex => }/components/IndexMapping/index.ts (100%) rename public/{pages/CreateIndex => }/components/IndexMapping/interfaces.ts (95%) rename public/{pages/CreateIndex => }/components/MappingLabel/MappingLabel.test.tsx (100%) rename public/{pages/CreateIndex => }/components/MappingLabel/MappingLabel.tsx (96%) rename public/{pages/CreateIndex => }/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap (100%) rename public/{pages/CreateIndex => }/components/MappingLabel/index.ts (100%) create mode 100644 public/pages/CreateDataStream/components/IndexSettings/IndexSettings.tsx create mode 100644 public/pages/CreateDataStream/components/IndexSettings/index.ts create mode 100644 public/pages/CreateDataStream/containers/BackingIndices/BackingIndices.tsx create mode 100644 public/pages/CreateDataStream/containers/BackingIndices/index.ts create mode 100644 public/pages/CreateDataStream/containers/CreateDataStream/CreateDataStream.test.tsx create mode 100644 public/pages/CreateDataStream/containers/CreateDataStream/CreateDataStream.tsx create mode 100644 public/pages/CreateDataStream/containers/CreateDataStream/__snapshots__/CreateDataStream.test.tsx.snap create mode 100644 public/pages/CreateDataStream/containers/CreateDataStream/index.ts create mode 100644 public/pages/CreateDataStream/containers/DataStreamDetail/DataStreamDetail.test.tsx create mode 100644 public/pages/CreateDataStream/containers/DataStreamDetail/DataStreamDetail.tsx create mode 100644 public/pages/CreateDataStream/containers/DataStreamDetail/__snapshots__/DataStreamDetail.test.tsx.snap create mode 100644 public/pages/CreateDataStream/containers/DataStreamDetail/hooks.tsx create mode 100644 public/pages/CreateDataStream/containers/DataStreamDetail/index.ts create mode 100644 public/pages/CreateDataStream/containers/DefineDataStream/DefineDataStream.tsx create mode 100644 public/pages/CreateDataStream/containers/DefineDataStream/index.ts create mode 100644 public/pages/CreateDataStream/containers/IndexAlias/IndexAlias.tsx create mode 100644 public/pages/CreateDataStream/containers/IndexAlias/index.ts create mode 100644 public/pages/CreateDataStream/containers/TemplateMappings/TemplateMappings.tsx create mode 100644 public/pages/CreateDataStream/containers/TemplateMappings/index.ts create mode 100644 public/pages/CreateDataStream/hooks.tsx create mode 100644 public/pages/CreateDataStream/index.ts create mode 100644 public/pages/CreateDataStream/interface.ts 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__/DataStreams.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__/DataStreamsActions.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__/DeleteDataStreamsModal.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/cypress/integration/data_streams.js b/cypress/integration/data_streams.js new file mode 100644 index 000000000..ac5f6fe75 --- /dev/null +++ b/cypress/integration/data_streams.js @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { PLUGIN_NAME } from "../support/constants"; + +describe("Data stream", () => { + before(() => { + // Set welcome screen tracking to false + localStorage.setItem("home:welcome:show", "false"); + cy.deleteTemplate("index-common-template"); + cy.createIndexTemplate("index-common-template", { + index_patterns: ["ds-*"], + data_stream: {}, + template: { + aliases: { + alias_for_common_1: {}, + alias_for_common_2: {}, + }, + settings: { + number_of_shards: 2, + number_of_replicas: 1, + }, + }, + }); + cy.request({ + url: `${Cypress.env("opensearch")}/_data_stream/*`, + method: "DELETE", + failOnStatusCode: false, + }); + }); + + beforeEach(() => { + // Visit ISM OSD + cy.visit(`${Cypress.env("opensearch_dashboards")}/app/${PLUGIN_NAME}#/data-streams`); + + // Common text to wait for to confirm page loaded, give up to 60 seconds for initial load + cy.contains("Data streams", { timeout: 60000 }); + }); + + describe("can create a data stream", () => { + it("successfully", () => { + cy.get('[data-test-subj="Create data streamButton"]').click(); + cy.get('[data-test-subj="form-row-name"]').type(`ds-{enter}`); + cy.get('[data-test-subj="CreateDataStreamCreateButton"]').click(); + cy.contains("ds- has been successfully created."); + }); + }); + + describe("can be searched / sorted / paginated", () => { + it("successfully", () => { + cy.contains("ds-"); + cy.contains("index-common-template"); + }); + }); + + describe("can delete a data stream", () => { + it("successfully", () => { + cy.get('[data-test-subj="moreAction"] button') + .click() + .get('[data-test-subj="deleteAction"]') + .should("be.disabled") + .get(`#_selection_column_ds--checkbox`) + .click() + .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_ds--checkbox`).should("not.exist"); + }); + }); + + after(() => { + cy.request({ + url: `${Cypress.env("opensearch")}/_data_stream`, + method: "DELETE", + failOnStatusCode: false, + }); + cy.deleteTemplate("index-common-template"); + }); +}); diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 25abc640e..ba809f725 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -1,7 +1,7 @@ { "id": "indexManagementDashboards", "version": "2.5.0.0", - "opensearchDashboardsVersion": "2.5.0", + "opensearchDashboardsVersion": "2.5.1", "configPath": ["opensearch_index_management"], "requiredPlugins": ["navigation", "opensearchDashboardsReact"], "server": true, diff --git a/public/components/DescriptionListHoz/DescriptionListHoz.tsx b/public/components/DescriptionListHoz/DescriptionListHoz.tsx index 02f24d025..70725850b 100644 --- a/public/components/DescriptionListHoz/DescriptionListHoz.tsx +++ b/public/components/DescriptionListHoz/DescriptionListHoz.tsx @@ -1,4 +1,4 @@ -import { EuiDescriptionList, EuiDescriptionListProps, EuiFlexGroup, EuiFlexItem } from "@elastic/eui"; +import { EuiDescriptionList, EuiDescriptionListProps, EuiFlexGrid, EuiFlexGridProps, EuiFlexGroup, EuiFlexItem } from "@elastic/eui"; import React from "react"; const DisplayItem = ( @@ -20,15 +20,15 @@ const DisplayItem = ( ); }; -export default function DescriptionListHoz(props: EuiDescriptionListProps) { - const { listItems, ...others } = props; +export default function DescriptionListHoz(props: EuiDescriptionListProps & Pick) { + const { listItems, columns = 4, ...others } = props; return ( - + {listItems?.map((item) => ( ))} - + ); } diff --git a/public/components/DescriptionListHoz/__snapshots__/DescriptionListHoz.test.tsx.snap b/public/components/DescriptionListHoz/__snapshots__/DescriptionListHoz.test.tsx.snap index 46662e1b7..5c890d139 100644 --- a/public/components/DescriptionListHoz/__snapshots__/DescriptionListHoz.test.tsx.snap +++ b/public/components/DescriptionListHoz/__snapshots__/DescriptionListHoz.test.tsx.snap @@ -3,7 +3,7 @@ exports[` spec renders the component 1`] = `
    , ref: Ref) => { diff --git a/public/pages/CreateIndex/components/IndexMapping/IndexMapping.tsx b/public/components/IndexMapping/IndexMapping.tsx similarity index 97% rename from public/pages/CreateIndex/components/IndexMapping/IndexMapping.tsx rename to public/components/IndexMapping/IndexMapping.tsx index d4cfeed79..621c9b6dd 100644 --- a/public/pages/CreateIndex/components/IndexMapping/IndexMapping.tsx +++ b/public/components/IndexMapping/IndexMapping.tsx @@ -6,10 +6,10 @@ import React, { forwardRef, useCallback, useState, Ref, useRef, useMemo, useImperativeHandle } from "react"; import { EuiTreeView, EuiIcon, EuiTreeViewProps, EuiButton, EuiSpacer, EuiButtonGroup, EuiLink } from "@elastic/eui"; import { set, get, isEmpty } from "lodash"; -import JSONEditor, { IJSONEditorRef } from "../../../../components/JSONEditor"; -import { Modal } from "../../../../components/Modal"; -import { MappingsProperties } from "../../../../../models/interfaces"; -import CustomFormRow from "../../../../components/CustomFormRow"; +import JSONEditor, { IJSONEditorRef } from "../JSONEditor"; +import { Modal } from "../Modal"; +import { MappingsProperties } from "../../../models/interfaces"; +import CustomFormRow from "../CustomFormRow"; import MappingLabel, { IMappingLabelRef } from "../MappingLabel"; import { transformObjectToArray, transformArrayToObject, countNodesInTree } from "./helper"; import { IndexMappingsObjectAll, IndexMappingProps, EDITOR_MODE, IIndexMappingsRef } from "./interfaces"; diff --git a/public/pages/CreateIndex/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap b/public/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap similarity index 100% rename from public/pages/CreateIndex/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap rename to public/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap diff --git a/public/pages/CreateIndex/components/IndexMapping/helper.ts b/public/components/IndexMapping/helper.ts similarity index 97% rename from public/pages/CreateIndex/components/IndexMapping/helper.ts rename to public/components/IndexMapping/helper.ts index e1c73612f..11ec3c81f 100644 --- a/public/pages/CreateIndex/components/IndexMapping/helper.ts +++ b/public/components/IndexMapping/helper.ts @@ -1,4 +1,4 @@ -import { MappingsProperties, MappingsPropertiesObject } from "../../../../../models/interfaces"; +import { MappingsProperties, MappingsPropertiesObject } from "../../../models/interfaces"; export const transformObjectToArray = (obj: MappingsPropertiesObject): MappingsProperties => { return Object.entries(obj).map(([fieldName, fieldSettings]) => { diff --git a/public/pages/CreateIndex/components/IndexMapping/index.ts b/public/components/IndexMapping/index.ts similarity index 100% rename from public/pages/CreateIndex/components/IndexMapping/index.ts rename to public/components/IndexMapping/index.ts diff --git a/public/pages/CreateIndex/components/IndexMapping/interfaces.ts b/public/components/IndexMapping/interfaces.ts similarity index 95% rename from public/pages/CreateIndex/components/IndexMapping/interfaces.ts rename to public/components/IndexMapping/interfaces.ts index fb943262e..d830f59dd 100644 --- a/public/pages/CreateIndex/components/IndexMapping/interfaces.ts +++ b/public/components/IndexMapping/interfaces.ts @@ -1,4 +1,4 @@ -import { MappingsProperties, MappingsPropertiesObject } from "../../../../../models/interfaces"; +import { MappingsProperties, MappingsPropertiesObject } from "../../../models/interfaces"; export type IndexMappingsAll = { properties?: MappingsProperties; diff --git a/public/pages/CreateIndex/components/MappingLabel/MappingLabel.test.tsx b/public/components/MappingLabel/MappingLabel.test.tsx similarity index 100% rename from public/pages/CreateIndex/components/MappingLabel/MappingLabel.test.tsx rename to public/components/MappingLabel/MappingLabel.test.tsx diff --git a/public/pages/CreateIndex/components/MappingLabel/MappingLabel.tsx b/public/components/MappingLabel/MappingLabel.tsx similarity index 96% rename from public/pages/CreateIndex/components/MappingLabel/MappingLabel.tsx rename to public/components/MappingLabel/MappingLabel.tsx index b68c1cf0a..3599cb093 100644 --- a/public/pages/CreateIndex/components/MappingLabel/MappingLabel.tsx +++ b/public/components/MappingLabel/MappingLabel.tsx @@ -12,11 +12,11 @@ import { EuiFormRowProps, } from "@elastic/eui"; import { set, pick } from "lodash"; -import { MappingsProperties } from "../../../../../models/interfaces"; -import { AllBuiltInComponents } from "../../../../components/FormGenerator"; -import useField, { transformNameToString } from "../../../../lib/field"; -import { INDEX_MAPPING_TYPES, INDEX_MAPPING_TYPES_WITH_CHILDREN } from "../../../../utils/constants"; -import SimplePopover from "../../../../components/SimplePopover"; +import { MappingsProperties } from "../../../models/interfaces"; +import { AllBuiltInComponents } from "../FormGenerator"; +import useField, { transformNameToString } from "../../lib/field"; +import { INDEX_MAPPING_TYPES, INDEX_MAPPING_TYPES_WITH_CHILDREN } from "../../utils/constants"; +import SimplePopover from "../SimplePopover"; const OLD_VALUE_DISABLED_REASON = "Old mappings cannot be modified"; diff --git a/public/pages/CreateIndex/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap b/public/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap similarity index 100% rename from public/pages/CreateIndex/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap rename to public/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap diff --git a/public/pages/CreateIndex/components/MappingLabel/index.ts b/public/components/MappingLabel/index.ts similarity index 100% rename from public/pages/CreateIndex/components/MappingLabel/index.ts rename to public/components/MappingLabel/index.ts diff --git a/public/pages/CreateDataStream/components/IndexSettings/IndexSettings.tsx b/public/pages/CreateDataStream/components/IndexSettings/IndexSettings.tsx new file mode 100644 index 000000000..3e5285784 --- /dev/null +++ b/public/pages/CreateDataStream/components/IndexSettings/IndexSettings.tsx @@ -0,0 +1,158 @@ +import React from "react"; +import { EuiLink, EuiSpacer, EuiTitle } from "@elastic/eui"; +import flat from "flat"; +import CustomFormRow from "../../../../components/CustomFormRow"; +import { AllBuiltInComponents } from "../../../../components/FormGenerator"; +import AdvancedSettings from "../../../../components/AdvancedSettings"; +import DescriptionListHoz from "../../../../components/DescriptionListHoz"; +import { INDEX_SETTINGS_URL } from "../../../../utils/constants"; +import { SubDetailProps } from "../../interface"; +import { getCommonFormRowProps } from "../../hooks"; + +export default function IndexSettings(props: SubDetailProps) { + const { readonly, field } = props; + const values = field.getValues(); + return ( + <> + + Index settings + + + {readonly ? ( + + ) : ( + <> + + + + + + + + + + + + + )} + + { + field.setValue(["template", "settings"], totalValue); + field.validatePromise(); + }} + accordionProps={{ + initialIsOpen: false, + id: "accordionForCreateDataStreamSettings", + buttonContent:

    Advanced settings

    , + }} + editorProps={{ + disabled: true, + width: "100%", + formatValue: flat, + }} + rowProps={{ + fullWidth: true, + label: "Specify advanced index settings", + helpText: ( + <> +

    + Specify a comma-delimited list of settings.{" "} + + View index settings + +

    +

    + All the settings will be handled in flat structure.{" "} + + Learn more. + +

    + + ), + }} + /> + + ); +} diff --git a/public/pages/CreateDataStream/components/IndexSettings/index.ts b/public/pages/CreateDataStream/components/IndexSettings/index.ts new file mode 100644 index 000000000..640205ae7 --- /dev/null +++ b/public/pages/CreateDataStream/components/IndexSettings/index.ts @@ -0,0 +1,3 @@ +import IndexSettings from "./IndexSettings"; + +export default IndexSettings; diff --git a/public/pages/CreateDataStream/containers/BackingIndices/BackingIndices.tsx b/public/pages/CreateDataStream/containers/BackingIndices/BackingIndices.tsx new file mode 100644 index 000000000..60ac29dce --- /dev/null +++ b/public/pages/CreateDataStream/containers/BackingIndices/BackingIndices.tsx @@ -0,0 +1,167 @@ +import React, { useContext, useEffect, useState } from "react"; +import { EuiBasicTable, EuiHealth, EuiLink, EuiSpacer } from "@elastic/eui"; +import { ServicesContext } from "../../../../services"; +import { BrowserServices } from "../../../../models/interfaces"; +import { ManagedCatIndex } from "../../../../../server/models/interfaces"; +import { DataStreamInEdit, SubDetailProps } from "../../interface"; +import { ROUTES } from "../../../../utils/constants"; +import { ContentPanel } from "../../../../components/ContentPanel"; + +const renderNumber = (value: string) => { + return value || "-"; +}; + +export const HEALTH_TO_COLOR: { + [health: string]: string; + green: string; + yellow: string; + red: string; +} = { + green: "success", + yellow: "warning", + red: "danger", +}; + +export default function BackingIndices(props: SubDetailProps) { + const { field } = props; + const values: DataStreamInEdit = field.getValues(); + const services = useContext(ServicesContext) as BrowserServices; + const [indexes, setIndexes] = useState([]); + useEffect(() => { + if (values.name) { + services.indexService + .getIndices({ + from: 0, + size: 999, + search: values.name, + terms: values.name, + sortField: "index", + sortDirection: "desc", + showDataStreams: true, + }) + .then((result) => { + if (result && result.ok) { + setIndexes(result.response.indices.filter((item) => item.data_stream === values.name)); + } + }); + } + }, [values.name]); + const writingIndex = (values.indices || [])[(values.indices?.length || 0) - 1]?.index_name; + return ( + + + { + return {index}; + }, + }, + { + field: "health", + name: "Health", + sortable: true, + truncateText: true, + textOnly: true, + render: (health: string, item: ManagedCatIndex) => { + const color = health ? HEALTH_TO_COLOR[health] : "subdued"; + const text = health || item.status; + return ( + + {text} + + ); + }, + }, + { + field: "managed", + name: "Managed by policy", + sortable: false, + truncateText: true, + textOnly: true, + width: "140px", + render: renderNumber, + }, + { + field: "status", + name: "Status", + sortable: true, + truncateText: true, + textOnly: true, + render: (status: string, item: ManagedCatIndex) => { + return {item.extraStatus || status}; + }, + }, + { + field: "store.size", + name: "Total size", + sortable: true, + truncateText: true, + textOnly: true, + dataType: "number", + render: renderNumber, + }, + { + field: "pri.store.size", + name: "Size of primaries", + sortable: true, + truncateText: true, + textOnly: true, + dataType: "number", + render: renderNumber, + }, + { + field: "docs.count", + name: "Total documents", + sortable: true, + truncateText: true, + textOnly: true, + dataType: "number", + render: (count: string) => {count || "-"}, + }, + { + field: "docs.deleted", + name: "Deleted documents", + sortable: true, + truncateText: true, + textOnly: true, + dataType: "number", + render: (deleted: string) => {deleted || "-"}, + }, + { + field: "pri", + name: "Primaries", + sortable: true, + truncateText: true, + textOnly: true, + dataType: "number", + }, + { + field: "rep", + name: "Replicas", + sortable: true, + truncateText: true, + textOnly: true, + dataType: "number", + }, + { + field: "rep", + name: "Writing index", + textOnly: true, + render: (value: string, record: ManagedCatIndex) => { + return record.index === writingIndex ? "Yes" : "No"; + }, + }, + ]} + /> + + ); +} diff --git a/public/pages/CreateDataStream/containers/BackingIndices/index.ts b/public/pages/CreateDataStream/containers/BackingIndices/index.ts new file mode 100644 index 000000000..33d4f13e0 --- /dev/null +++ b/public/pages/CreateDataStream/containers/BackingIndices/index.ts @@ -0,0 +1,3 @@ +import BackingIndices from "./BackingIndices"; + +export default BackingIndices; diff --git a/public/pages/CreateDataStream/containers/CreateDataStream/CreateDataStream.test.tsx b/public/pages/CreateDataStream/containers/CreateDataStream/CreateDataStream.test.tsx new file mode 100644 index 000000000..5c7be025b --- /dev/null +++ b/public/pages/CreateDataStream/containers/CreateDataStream/CreateDataStream.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 CreateDataStream from "./CreateDataStream"; +import { ServicesContext } from "../../../../services"; +import { browserServicesMock, coreServicesMock, apiCallerMock } from "../../../../../test/mocks"; +import { ROUTES } from "../../../../utils/constants"; +import { CoreServicesContext } from "../../../../components/core_services"; + +function renderCreateDataStreamWithRouter(initialEntries = [ROUTES.DATA_STREAMS] as string[]) { + return { + ...render( + + + + + } + /> + } /> +

    location is: {ROUTES.DATA_STREAMS}

    } /> +
    +
    +
    +
    + ), + }; +} + +describe(" spec", () => { + beforeEach(() => { + apiCallerMock(browserServicesMock); + }); + it("it goes to data streams page when click cancel", async () => { + const { getByTestId, getByText, container, findByText } = renderCreateDataStreamWithRouter([ROUTES.CREATE_DATA_STREAM]); + await findByText("Define data stream"); + expect(container).toMatchSnapshot(); + userEvent.click(getByTestId("CreateDataStreamCancelButton")); + await waitFor(() => { + expect(getByText(`location is: ${ROUTES.DATA_STREAMS}`)).toBeInTheDocument(); + }); + }); +}); diff --git a/public/pages/CreateDataStream/containers/CreateDataStream/CreateDataStream.tsx b/public/pages/CreateDataStream/containers/CreateDataStream/CreateDataStream.tsx new file mode 100644 index 000000000..4faa1978c --- /dev/null +++ b/public/pages/CreateDataStream/containers/CreateDataStream/CreateDataStream.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from "react"; +import { RouteComponentProps } from "react-router-dom"; +import DataStreamDetail from "../DataStreamDetail"; +import { BREADCRUMBS, ROUTES } from "../../../../utils/constants"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { isEqual } from "lodash"; + +interface CreateDataStreamProps extends RouteComponentProps<{ dataStream?: string }> {} + +export default class CreateDataStream extends Component { + static contextType = CoreServicesContext; + + get dataStream() { + return this.props.match.params.dataStream; + } + + setBreadCrumb() { + const isEdit = this.dataStream; + let lastBread: typeof BREADCRUMBS.TEMPLATES; + if (isEdit) { + lastBread = { + text: this.dataStream || "", + href: `#${this.props.location.pathname}`, + }; + } else { + lastBread = BREADCRUMBS.CREATE_DATA_STREAM; + } + this.context.chrome.setBreadcrumbs([BREADCRUMBS.INDEX_MANAGEMENT, BREADCRUMBS.DATA_STREAMS, lastBread]); + } + + componentDidUpdate(prevProps: Readonly): void { + if (!isEqual(prevProps, this.props)) { + this.setBreadCrumb(); + } + } + + componentDidMount = async (): Promise => { + this.setBreadCrumb(); + }; + + onCancel = (): void => { + this.props.history.push(ROUTES.DATA_STREAMS); + }; + + render() { + return ( +
    + this.props.history.push(ROUTES.DATA_STREAMS)} + /> +
    + ); + } +} diff --git a/public/pages/CreateDataStream/containers/CreateDataStream/__snapshots__/CreateDataStream.test.tsx.snap b/public/pages/CreateDataStream/containers/CreateDataStream/__snapshots__/CreateDataStream.test.tsx.snap new file mode 100644 index 000000000..19e46190f --- /dev/null +++ b/public/pages/CreateDataStream/containers/CreateDataStream/__snapshots__/CreateDataStream.test.tsx.snap @@ -0,0 +1,235 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec it goes to data streams page when click cancel 1`] = ` +
    +
    +
    +
    +

    + Create data stream +

    +
    +
    +
    +
    + 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. + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Define data stream + + + + +

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    +
    + No index template matched +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +`; diff --git a/public/pages/CreateDataStream/containers/CreateDataStream/index.ts b/public/pages/CreateDataStream/containers/CreateDataStream/index.ts new file mode 100644 index 000000000..e34c2a2e0 --- /dev/null +++ b/public/pages/CreateDataStream/containers/CreateDataStream/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import CreateDataStream from "./CreateDataStream"; + +export default CreateDataStream; diff --git a/public/pages/CreateDataStream/containers/DataStreamDetail/DataStreamDetail.test.tsx b/public/pages/CreateDataStream/containers/DataStreamDetail/DataStreamDetail.test.tsx new file mode 100644 index 000000000..594295ac1 --- /dev/null +++ b/public/pages/CreateDataStream/containers/DataStreamDetail/DataStreamDetail.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import { render, waitFor } from "@testing-library/react"; +import DataStreamDetail, { DataStreamDetailProps } from "./DataStreamDetail"; +import { ServicesContext } from "../../../../services"; +import { browserServicesMock, coreServicesMock } from "../../../../../test/mocks"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { HashRouter, Route } from "react-router-dom"; +import { ROUTES } from "../../../../utils/constants"; +import userEvent from "@testing-library/user-event"; + +function renderCreateDataStream(props: Omit) { + return { + ...render( + + + + } /> + <>This is {ROUTES.DATA_STREAMS}} /> + + + + ), + }; +} + +describe(" spec", () => { + // main unit test case is in CreateDataStream.test.tsx + it("render component", async () => { + const { container } = renderCreateDataStream({}); + await waitFor(() => {}, { + timeout: 3000, + }); + expect(container).toMatchSnapshot(); + }); + + it("show the json", async () => { + browserServicesMock.commonService.apiCaller = jest.fn(async () => { + return { + ok: true, + response: { + data_streams: [ + { + name: "good_data_stream", + indices: [], + }, + ], + }, + }; + }) as any; + const { getByText, getByTestId, findAllByText } = renderCreateDataStream({ + dataStream: "good_data_stream", + }); + await findAllByText("good_data_stream"); + userEvent.click(getByText("View JSON")); + await waitFor(() => + expect( + JSON.parse(getByTestId("dataStreamJSONDetailModal").querySelector('[data-test-subj="jsonEditor-valueDisplay"]')?.innerHTML || "{}") + ).toEqual({ + name: "good_data_stream", + indices: [], + }) + ); + }); +}); diff --git a/public/pages/CreateDataStream/containers/DataStreamDetail/DataStreamDetail.tsx b/public/pages/CreateDataStream/containers/DataStreamDetail/DataStreamDetail.tsx new file mode 100644 index 000000000..f033e9e09 --- /dev/null +++ b/public/pages/CreateDataStream/containers/DataStreamDetail/DataStreamDetail.tsx @@ -0,0 +1,207 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { forwardRef, useContext, useEffect, useImperativeHandle, useRef, Ref, useState } from "react"; +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiTitle } from "@elastic/eui"; +import { TemplateItem, TemplateItemRemote } from "../../../../../models/interfaces"; +import useField, { FieldInstance } from "../../../../lib/field"; +import CustomFormRow from "../../../../components/CustomFormRow"; +import { ServicesContext } from "../../../../services"; +import { BrowserServices } from "../../../../models/interfaces"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { CoreStart } from "opensearch-dashboards/public"; +import { getAllDataStreamTemplate, createDataStream, getDataStream } from "./hooks"; +import { Modal } from "../../../../components/Modal"; +import JSONEditor from "../../../../components/JSONEditor"; +import { RouteComponentProps } from "react-router-dom"; +import { ROUTES } from "../../../../utils/constants"; +import DeleteDataStreamsModal from "../../../DataStreams/containers/DeleteDataStreamsModal"; +import DefineDataStream from "../DefineDataStream"; +import IndexSettings from "../../components/IndexSettings"; +import IndexAlias from "../IndexAlias"; +import TemplateMappings from "../TemplateMappings"; +import { ContentPanel } from "../../../../components/ContentPanel"; +import { DataStreamInEdit } from "../../interface"; +import BackingIndices from "../BackingIndices"; +import DataStreamsActions from "../../../DataStreams/containers/DataStreamsActions"; + +export interface DataStreamDetailProps { + dataStream?: string; + onCancel?: () => void; + onSubmitSuccess?: (templateName: string) => void; + readonly?: boolean; + history: RouteComponentProps["history"]; +} + +const DataStreamDetail = (props: DataStreamDetailProps, ref: Ref) => { + const { dataStream, onCancel, onSubmitSuccess, history } = props; + const isEdit = !!dataStream; + const services = useContext(ServicesContext) as BrowserServices; + const coreServices = useContext(CoreServicesContext) as CoreStart; + const [visible, setVisible] = useState(false); + const [templates, setTemplates] = useState< + { + name: string; + index_template: TemplateItemRemote; + }[] + >([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const field = useField({ + values: {} as Partial, + }); + const destroyRef = useRef(false); + const onSubmit = async () => { + const { errors, values: dataStream } = (await field.validatePromise()) || {}; + if (errors) { + return; + } + setIsSubmitting(true); + const result = await createDataStream({ + value: dataStream.name, + commonService: services.commonService, + isEdit, + }); + if (result && result.ok) { + coreServices.notifications.toasts.addSuccess(`${dataStream.name} has been successfully ${isEdit ? "updated" : "created"}.`); + onSubmitSuccess && onSubmitSuccess(dataStream.name); + } else { + coreServices.notifications.toasts.addDanger(result.error); + } + if (destroyRef.current) { + return; + } + setIsSubmitting(false); + }; + useImperativeHandle(ref, () => field); + useEffect(() => { + if (isEdit) { + getDataStream({ + dataStream, + coreService: coreServices, + commonService: services.commonService, + }) + .then((dataStreamDetail) => { + field.resetValues(dataStreamDetail); + }) + .catch(() => { + props.history.replace(ROUTES.DATA_STREAMS); + }); + } else { + getAllDataStreamTemplate({ + commonService: services.commonService, + }).then((result) => setTemplates(result)); + } + return () => { + destroyRef.current = true; + }; + }, []); + const values: DataStreamInEdit & { matchedTemplate?: string } = field.getValues(); + const subCompontentProps = { + ...props, + isEdit, + field, + }; + + return ( + <> + + + {isEdit ?

    {values.name}

    :

    Create data stream

    }
    + {isEdit ? null : ( + + 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. + +
    + } + > + <> + + )} + + {isEdit ? ( + + { + Modal.show({ + "data-test-subj": "dataStreamJSONDetailModal", + title: values.name, + content: , + }); + }} + > + View JSON + + props.history.replace(ROUTES.DATA_STREAMS)} + /> + { + setVisible(false); + }} + onDelete={() => { + setVisible(false); + history.replace(ROUTES.DATA_STREAMS); + }} + /> + + ) : null} + + + + {values.matchedTemplate ? ( + <> + + + + + + + + + + + ) : null} + {isEdit ? ( + <> + + + + ) : null} + {isEdit ? null : ( + <> + + + + + + Cancel + + + + + {isEdit ? "Save changes" : "Create data stream"} + + + + + )} + + ); +}; + +// @ts-ignore +export default forwardRef(DataStreamDetail); diff --git a/public/pages/CreateDataStream/containers/DataStreamDetail/__snapshots__/DataStreamDetail.test.tsx.snap b/public/pages/CreateDataStream/containers/DataStreamDetail/__snapshots__/DataStreamDetail.test.tsx.snap new file mode 100644 index 000000000..b3ad86f59 --- /dev/null +++ b/public/pages/CreateDataStream/containers/DataStreamDetail/__snapshots__/DataStreamDetail.test.tsx.snap @@ -0,0 +1,231 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec render component 1`] = ` +
    +
    +
    +

    + Create data stream +

    +
    +
    +
    +
    + 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. + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    + Define data stream + + + + +

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    +
    + No index template matched +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +`; diff --git a/public/pages/CreateDataStream/containers/DataStreamDetail/hooks.tsx b/public/pages/CreateDataStream/containers/DataStreamDetail/hooks.tsx new file mode 100644 index 000000000..222cc5ffb --- /dev/null +++ b/public/pages/CreateDataStream/containers/DataStreamDetail/hooks.tsx @@ -0,0 +1,71 @@ +import { CoreStart } from "opensearch-dashboards/public"; +import { CommonService } from "../../../../services"; +import { TemplateItemRemote } from "../../../../../models/interfaces"; +import { DataStream } from "../../../../../server/models/interfaces"; + +export const createDataStream = async (props: { value: string; isEdit: boolean; commonService: CommonService }) => { + return await props.commonService.apiCaller({ + endpoint: "transport.request", + data: { + method: "PUT", + path: `_data_stream/${props.value}`, + }, + }); +}; + +export const getDataStream = async (props: { dataStream: string; commonService: CommonService; coreService: CoreStart }) => { + const response = await props.commonService.apiCaller<{ + data_streams: DataStream[]; + }>({ + endpoint: "transport.request", + data: { + method: "GET", + path: `_data_stream/${props.dataStream}`, + }, + }); + let error: string = ""; + if (response.ok) { + const findItem = response.response?.data_streams?.find((item) => item.name === props.dataStream); + if (findItem) { + const dataStreamDetail = findItem; + + return JSON.parse(JSON.stringify(dataStreamDetail)); + } + error = `The data stream ${props.dataStream} does not exist.`; + } else { + error = response.error || ""; + } + + props.coreService.notifications.toasts.addDanger(error); + throw new Error(error); +}; + +export const getAllDataStreamTemplate = (props: { + commonService: CommonService; +}): Promise< + { + name: string; + index_template: TemplateItemRemote; + }[] +> => { + return props.commonService + .apiCaller<{ + index_templates?: { + name: string; + index_template: TemplateItemRemote; + }[]; + }>({ + data: { + method: "GET", + path: "_index_template/*", + }, + endpoint: "transport.request", + }) + .then((result) => { + if (result && result.ok) { + return (result.response.index_templates || []).filter((item) => item.index_template.data_stream); + } + + return []; + }); +}; diff --git a/public/pages/CreateDataStream/containers/DataStreamDetail/index.ts b/public/pages/CreateDataStream/containers/DataStreamDetail/index.ts new file mode 100644 index 000000000..e0d2d6147 --- /dev/null +++ b/public/pages/CreateDataStream/containers/DataStreamDetail/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import DataStreamDetail from "./DataStreamDetail"; + +export default DataStreamDetail; +export * from "./DataStreamDetail"; diff --git a/public/pages/CreateDataStream/containers/DefineDataStream/DefineDataStream.tsx b/public/pages/CreateDataStream/containers/DefineDataStream/DefineDataStream.tsx new file mode 100644 index 000000000..ffe70eaa9 --- /dev/null +++ b/public/pages/CreateDataStream/containers/DefineDataStream/DefineDataStream.tsx @@ -0,0 +1,131 @@ +import React, { useState } from "react"; +import { EuiBadge, EuiLink, EuiSpacer } from "@elastic/eui"; +import { flatten } from "flat"; +import { get, set } from "lodash"; +import { transformObjectToArray } from "../../../../components/IndexMapping"; +import { DataStreamInEdit, SubDetailProps } from "../../interface"; +import { ContentPanel } from "../../../../components/ContentPanel"; +import CustomFormRow from "../../../../components/CustomFormRow"; +import { AllBuiltInComponents } from "../../../../components/FormGenerator"; +import DescriptionListHoz from "../../../../components/DescriptionListHoz"; +import { INDEX_NAMING_MESSAGE, INDEX_NAMING_PATTERN, ROUTES } from "../../../../utils/constants"; +import { getCommonFormRowProps } from "../../hooks"; +import { filterByMinimatch } from "../../../../../utils/helper"; +import { TemplateItemRemote } from "../../../../../models/interfaces"; + +export default function DefineTemplate( + props: SubDetailProps & { + allDataStreamTemplates: { + name: string; + index_template: TemplateItemRemote; + }[]; + } +) { + const { field, isEdit, allDataStreamTemplates } = props; + const [searchValue, setSearchValue] = useState(""); + const values: DataStreamInEdit = field.getValues(); + const Component = isEdit ? AllBuiltInComponents.Text : AllBuiltInComponents.ComboBoxSingle; + const matchedList = allDataStreamTemplates.filter((item) => { + if (!searchValue) { + return false; + } + + return filterByMinimatch(searchValue || "", item.index_template.index_patterns); + }); + matchedList.sort((a, b) => (a.index_template.priority > b.index_template.priority ? -1 : 1)); + const suggestionRegister = field.registerField({ + name: "name", + rules: [ + { + pattern: INDEX_NAMING_PATTERN, + message: "Invalid data stream name.", + }, + ], + props: {}, + }); + return isEdit ? ( + + + {values.template}, + }, + { + title: "Backing indexes", + description: (values.indices || []).length, + }, + { + title: "Timefield", + description: values.timestamp_field?.name || "", + }, + ]} + /> + + ) : ( + + + + ({ + label: searchValue, + value: item.name, + }))} + renderOption={(option: { value: string }) => { + return ( +

    + Matched template: {option.value} +

    + ); + }} + async + {...suggestionRegister} + onCreateOption={undefined} + onSearchChange={(dataStreamName: string) => { + setSearchValue(dataStreamName); + }} + onChange={(value) => { + if (!value) { + field.resetValues({ + name: "", + }); + } else { + suggestionRegister.onChange(value); + const template = matchedList[0]?.index_template?.template; + const payload = { + matchedTemplate: matchedList[0]?.name, + template: { + ...template, + settings: flatten(template.settings || {}), + }, + }; + + set(payload, "template.mappings.properties", transformObjectToArray(get(payload, "template.mappings.properties", {}))); + + field.setValues(payload); + } + }} + /> +
    + + + {values.matchedTemplate ? ( + + {values.matchedTemplate} + + ) : ( + <>No index template matched + )} + +
    + ); +} diff --git a/public/pages/CreateDataStream/containers/DefineDataStream/index.ts b/public/pages/CreateDataStream/containers/DefineDataStream/index.ts new file mode 100644 index 000000000..5d157003d --- /dev/null +++ b/public/pages/CreateDataStream/containers/DefineDataStream/index.ts @@ -0,0 +1,3 @@ +import DefineDataStream from "./DefineDataStream"; + +export default DefineDataStream; diff --git a/public/pages/CreateDataStream/containers/IndexAlias/IndexAlias.tsx b/public/pages/CreateDataStream/containers/IndexAlias/IndexAlias.tsx new file mode 100644 index 000000000..8e3090d5a --- /dev/null +++ b/public/pages/CreateDataStream/containers/IndexAlias/IndexAlias.tsx @@ -0,0 +1,75 @@ +import React, { useContext } from "react"; +import { EuiSpacer, EuiTitle } from "@elastic/eui"; +import AliasSelect from "../../../CreateIndex/components/AliasSelect"; +import CustomFormRow from "../../../../components/CustomFormRow"; +import { ServicesContext } from "../../../../services"; +import { BrowserServices } from "../../../../models/interfaces"; +import DescriptionListHoz from "../../../../components/DescriptionListHoz"; +import { ALIAS_SELECT_RULE } from "../../../../utils/constants"; +import { getCommonFormRowProps } from "../../hooks"; +import { SubDetailProps } from "../../interface"; + +export default function IndexAlias(props: SubDetailProps) { + const { readonly, field } = props; + const values = field.getValues(); + const services = useContext(ServicesContext) as BrowserServices; + return ( + <> + + +
    Index alias
    +
    + } + helpText="Allow the new indexes to be referenced by existing aliases or specify a new alias." + > + <> + + + {readonly ? ( + <> + + + + ) : ( + <> + + + + services?.commonService.apiCaller({ + endpoint: "cat.aliases", + method: "GET", + data: { + format: "json", + name: `*${aliasName || ""}*`, + s: "alias:desc", + }, + }) + } + /> + + + )} + + ); +} diff --git a/public/pages/CreateDataStream/containers/IndexAlias/index.ts b/public/pages/CreateDataStream/containers/IndexAlias/index.ts new file mode 100644 index 000000000..04b13591d --- /dev/null +++ b/public/pages/CreateDataStream/containers/IndexAlias/index.ts @@ -0,0 +1,3 @@ +import IndexAlias from "./IndexAlias"; + +export default IndexAlias; diff --git a/public/pages/CreateDataStream/containers/TemplateMappings/TemplateMappings.tsx b/public/pages/CreateDataStream/containers/TemplateMappings/TemplateMappings.tsx new file mode 100644 index 000000000..e68f71c3e --- /dev/null +++ b/public/pages/CreateDataStream/containers/TemplateMappings/TemplateMappings.tsx @@ -0,0 +1,65 @@ +import React, { useContext, useRef } from "react"; +import { EuiFormRow, EuiLink, EuiSpacer, EuiTitle } from "@elastic/eui"; +import { SubDetailProps } from "../../interface"; +import IndexMapping, { IIndexMappingsRef } from "../../../../components/IndexMapping"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { CoreStart } from "opensearch-dashboards/public"; + +export default function TemplateMappings(props: SubDetailProps) { + const { readonly, field, isEdit } = props; + const mappingsRef = useRef(null); + const coreServices = useContext(CoreServicesContext) as CoreStart; + return ( + <> + + +
    Index mapping
    +
    + } + helpText={ +
    + Define how documents and their fields are stored and indexed.{" "} + + Learn more. + +
    + } + > + <> + + + + + { + if (result) { + return Promise.reject(result); + } + + return Promise.resolve(""); + }); + }, + }, + ], + })} + readonly + isEdit={isEdit} + ref={mappingsRef} + docVersion={coreServices.docLinks.DOC_LINK_VERSION} + /> + + + ); +} diff --git a/public/pages/CreateDataStream/containers/TemplateMappings/index.ts b/public/pages/CreateDataStream/containers/TemplateMappings/index.ts new file mode 100644 index 000000000..d32142ff3 --- /dev/null +++ b/public/pages/CreateDataStream/containers/TemplateMappings/index.ts @@ -0,0 +1,3 @@ +import TemplateMappings from "./TemplateMappings"; + +export default TemplateMappings; diff --git a/public/pages/CreateDataStream/hooks.tsx b/public/pages/CreateDataStream/hooks.tsx new file mode 100644 index 000000000..8560a5e1b --- /dev/null +++ b/public/pages/CreateDataStream/hooks.tsx @@ -0,0 +1,10 @@ +import { EuiFormRowProps } from "@elastic/eui"; +import { FieldInstance, transformNameToString } from "../../lib/field"; + +export const getCommonFormRowProps = (name: string | string[], field: FieldInstance): Partial => { + return { + isInvalid: !!field.getError(name), + error: field.getError(name), + "data-test-subj": `form-row-${transformNameToString(name)}`, + }; +}; diff --git a/public/pages/CreateDataStream/index.ts b/public/pages/CreateDataStream/index.ts new file mode 100644 index 000000000..5d9d0d13f --- /dev/null +++ b/public/pages/CreateDataStream/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import CreateDataStream from "./containers/CreateDataStream"; + +export default CreateDataStream; diff --git a/public/pages/CreateDataStream/interface.ts b/public/pages/CreateDataStream/interface.ts new file mode 100644 index 000000000..237f690d8 --- /dev/null +++ b/public/pages/CreateDataStream/interface.ts @@ -0,0 +1,20 @@ +import { RouteComponentProps } from "react-router-dom"; +import { FieldInstance } from "../../lib/field"; +import { DataStream } from "../../../server/models/interfaces"; + +export interface DataStreamDetailProps { + templateName?: string; + onCancel?: () => void; + onSubmitSuccess?: (templateName: string) => void; + readonly?: boolean; + history: RouteComponentProps["history"]; +} + +export interface SubDetailProps extends DataStreamDetailProps { + field: FieldInstance; + isEdit: boolean; +} + +export interface DataStreamInEdit extends DataStream { + matchedTemplate?: string; +} diff --git a/public/pages/CreateIndex/components/IndexDetail/IndexDetail.tsx b/public/pages/CreateIndex/components/IndexDetail/IndexDetail.tsx index f718fdcee..2aed15ff1 100644 --- a/public/pages/CreateIndex/components/IndexDetail/IndexDetail.tsx +++ b/public/pages/CreateIndex/components/IndexDetail/IndexDetail.tsx @@ -20,7 +20,7 @@ import flat from "flat"; import { ContentPanel } from "../../../../components/ContentPanel"; import UnsavedChangesBottomBar from "../../../../components/UnsavedChangesBottomBar"; import AliasSelect, { AliasSelectProps } from "../AliasSelect"; -import IndexMapping from "../IndexMapping"; +import IndexMapping from "../../../../components/IndexMapping"; import { IndexItem, IndexItemRemote } from "../../../../../models/interfaces"; import { ServerResponse } from "../../../../../server/models/types"; import { @@ -36,7 +36,7 @@ import { import { Modal } from "../../../../components/Modal"; import FormGenerator, { IField, IFormGeneratorRef } from "../../../../components/FormGenerator"; import EuiToolTipWrapper from "../../../../components/EuiToolTipWrapper"; -import { IIndexMappingsRef, transformArrayToObject, transformObjectToArray } from "../IndexMapping"; +import { IIndexMappingsRef, transformArrayToObject, transformObjectToArray } from "../../../../components/IndexMapping"; import { IFieldComponentProps } from "../../../../components/FormGenerator"; import SimplePopover from "../../../../components/SimplePopover"; import { SimpleEuiToast } from "../../../../components/Toast"; diff --git a/public/pages/CreateIndex/containers/IndexForm/index.tsx b/public/pages/CreateIndex/containers/IndexForm/index.tsx index d702460a4..0b73a8072 100644 --- a/public/pages/CreateIndex/containers/IndexForm/index.tsx +++ b/public/pages/CreateIndex/containers/IndexForm/index.tsx @@ -13,7 +13,7 @@ import IndexDetail, { IndexDetailProps, IIndexDetailRef, defaultIndexSettings } import { IAliasAction, IndexItem, IndexItemRemote, MappingsProperties } from "../../../../../models/interfaces"; import { IndicesUpdateMode } from "../../../../utils/constants"; import { CoreServicesContext } from "../../../../components/core_services"; -import { transformArrayToObject, transformObjectToArray } from "../../components/IndexMapping/IndexMapping"; +import { transformArrayToObject, transformObjectToArray } from "../../../../components/IndexMapping/IndexMapping"; import { ServerResponse } from "../../../../../server/models/types"; import { BrowserServices } from "../../../../models/interfaces"; import { ServicesContext } from "../../../../services"; diff --git a/public/pages/CreateIndexTemplate/containers/CreateIndexTemplate/__snapshots__/CreateIndexTemplate.test.tsx.snap b/public/pages/CreateIndexTemplate/containers/CreateIndexTemplate/__snapshots__/CreateIndexTemplate.test.tsx.snap index 78334092d..0bc437ac7 100644 --- a/public/pages/CreateIndexTemplate/containers/CreateIndexTemplate/__snapshots__/CreateIndexTemplate.test.tsx.snap +++ b/public/pages/CreateIndexTemplate/containers/CreateIndexTemplate/__snapshots__/CreateIndexTemplate.test.tsx.snap @@ -106,7 +106,7 @@ exports[` spec it goes to templates page when click cance class="euiSpacer euiSpacer--s" />
    spec it goes to templates page when click cance class="euiSpacer euiSpacer--s" />
    spec it goes to templates page when click cance class="euiSpacer euiSpacer--s" />
    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..a3dff2001 --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreams/DataStreams.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 { DataStreamStats, DataStreamWithStats } from "../../interface"; +import { DataStream } from "../../../../../server/models/interfaces"; + +function renderWithRouter() { + return { + ...render( + + + ( + + + + + + )} + /> + + + + ), + }; +} + +const testTemplateId = "test"; + +describe(" spec", () => { + beforeEach(() => { + browserServicesMock.commonService.apiCaller = jest.fn(async (payload) => { + const path: string = payload?.data?.path || ""; + if (path.startsWith("_data_stream/*")) { + if (path.includes("/_stats?human=true")) { + return { + ok: true, + response: { + data_streams: [ + { + data_stream: "1", + }, + ] as DataStreamStats[], + }, + }; + } else { + return { + ok: true, + response: { + data_streams: [ + { + name: "1", + }, + ] as DataStream[], + }, + }; + } + } + + return { + ok: true, + response: {}, + }; + }) as any; + window.location.hash = "/"; + }); + it("renders the component", async () => { + const { container } = renderWithRouter(); + + expect(container.firstChild).toMatchSnapshot(); + await waitFor(() => { + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(2); + }); + }); + + it("with some actions", async () => { + const { getByPlaceholderText } = renderWithRouter(); + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(2); + userEvent.type(getByPlaceholderText("Search..."), `${testTemplateId}{enter}`); + await waitFor(() => { + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(4); + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + data: { method: "GET", path: "_data_stream/**" }, + endpoint: "transport.request", + }); + }); + }); +}); diff --git a/public/pages/DataStreams/containers/DataStreams/DataStreams.tsx b/public/pages/DataStreams/containers/DataStreams/DataStreams.tsx new file mode 100644 index 000000000..fc30c914d --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreams/DataStreams.tsx @@ -0,0 +1,414 @@ +/* + * 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, + EuiHealth, +} from "@elastic/eui"; +import { ContentPanel, ContentPanelActions } from "../../../../components/ContentPanel"; +import { DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_QUERY_PARAMS, HEALTH_TO_COLOR } from "../../utils/constants"; +import CommonService from "../../../../services/CommonService"; +import { DataStreamStats, DataStreamWithStats } 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"; + +interface DataStreamsProps extends RouteComponentProps { + commonService: CommonService; +} + +type DataStreamsState = { + totalDataStreams: number; + from: string; + size: string; + sortField: keyof DataStreamWithStats; + sortDirection: Direction; + selectedItems: DataStreamWithStats[]; + 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 DataStreamWithStats; + 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.DATA_STREAMS]); + 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, sortDirection, sortField } = 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 }); + + let allDataStreams: DataStreamWithStats[] = []; + let error = ""; + let totalDataStreams = 0; + let dataStreams: DataStreamWithStats[] = []; + + const [allDataStreamsResponse, dataStreamDetailInfoResponse] = await Promise.all([ + commonService.apiCaller<{ + data_streams: DataStream[]; + }>({ + endpoint: "transport.request", + data: { + method: "GET", + path: `_data_stream/*${queryObject.search}*`, + }, + }), + await commonService.apiCaller<{ + data_streams: DataStreamStats[]; + }>({ + endpoint: "transport.request", + data: { + method: "GET", + path: `_data_stream/*${queryObject.search}*/_stats?human=true`, + }, + }), + ]); + + if (allDataStreamsResponse.ok && dataStreamDetailInfoResponse.ok) { + allDataStreams = (allDataStreamsResponse.response?.data_streams || []) + .map((item) => { + const findItem = (dataStreamDetailInfoResponse.response?.data_streams || []).find( + (statsItem) => statsItem.data_stream === item.name + ); + if (!findItem) { + return undefined; + } + + return { + ...findItem, + ...item, + }; + }) + .filter((item) => item) as DataStreamWithStats[]; + totalDataStreams = allDataStreams.length; + allDataStreams.sort((a, b) => { + if (sortDirection === "asc") { + if (a[sortField] < b[sortField]) { + return -1; + } + + return 1; + } else { + if (a[sortField] > b[sortField]) { + return -1; + } + + return 1; + } + }); + dataStreams = allDataStreams.slice(fromNumber * sizeNumber, (fromNumber + 1) * sizeNumber); + } else { + error = allDataStreamsResponse.error || dataStreamDetailInfoResponse.error || ""; + } + + if (!error) { + const payload = { + dataStreams, + totalDataStreams, + selectedItems: this.state.selectedItems + .map((item) => allDataStreams.find((remoteItem) => remoteItem.name === item.name)) + .filter((item) => item) as DataStreamWithStats[], + }; + 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: DataStreamWithStats[]): 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 ( + item.name)} + onDelete={this.getDataStreams} + history={this.props.history} + /> + ), + }, + { + text: "Create data stream", + buttonProps: { + fill: true, + onClick: () => { + this.props.history.push(ROUTES.CREATE_DATA_STREAM); + }, + }, + }, + ]} + /> + } + 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: "status", + name: "Status", + sortable: true, + render: (health: string, item) => { + const color = health ? HEALTH_TO_COLOR[health.toLowerCase()] : "subdued"; + const text = (health || item.status || "").toLowerCase(); + return ( + + {text} + + ); + }, + }, + { + field: "backing_indices", + name: "Backing indices count", + sortable: true, + align: "right", + }, + { + field: "store_size_bytes", + name: "Total size", + sortable: true, + align: "right", + render: (value, record) => { + return <>{record.store_size || ""}; + }, + }, + { + field: "template", + name: "Template", + sortable: true, + render: (value: unknown) => { + return ( + + {value} + + ); + }, + }, + ]} + 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_DATA_STREAM); + }} + > + 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__/DataStreams.test.tsx.snap b/public/pages/DataStreams/containers/DataStreams/__snapshots__/DataStreams.test.tsx.snap new file mode 100644 index 000000000..aa299fb27 --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreams/__snapshots__/DataStreams.test.tsx.snap @@ -0,0 +1,448 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +
    +
    +
    + + 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. + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + EuiIconMock + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + +
    + +
    +
    +

    + You have no data streams. +

    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +`; 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..366cb8d05 --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreamsActions/DataStreamsActions.test.tsx @@ -0,0 +1,105 @@ +/* + * 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(); + browserServicesMock.commonService.apiCaller = jest.fn( + async (): Promise => { + return { ok: true, response: {} }; + } + ); + const { container, getByTestId, getByPlaceholderText } = renderWithRouter({ + selectedItems: ["test_data_stream"], + 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: `/_data_stream/test_data_stream`, + method: "DELETE", + }, + }); + expect(onDelete).toHaveBeenCalledTimes(1); + expect(coreServicesMock.notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(coreServicesMock.notifications.toasts.addSuccess).toHaveBeenCalledWith("Delete [test_data_stream] successfully"); + }); + }, 30000); +}); diff --git a/public/pages/DataStreams/containers/DataStreamsActions/__snapshots__/DataStreamsActions.test.tsx.snap b/public/pages/DataStreams/containers/DataStreamsActions/__snapshots__/DataStreamsActions.test.tsx.snap new file mode 100644 index 000000000..e7beed4bc --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreamsActions/__snapshots__/DataStreamsActions.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec delete data streams 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..fc316995e --- /dev/null +++ b/public/pages/DataStreams/containers/DataStreamsActions/index.tsx @@ -0,0 +1,69 @@ +/* + * 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"; + +export interface DataStreamsActionsProps { + selectedItems: string[]; + 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 + + } + > + setDeleteIndexModalVisible(true), + }, + ], + }, + ]} + /> + + { + 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..b211fd810 --- /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 { CoreStart } from "opensearch-dashboards/public"; +import { ServicesContext } from "../../../../services"; +import { CoreServicesContext } from "../../../../components/core_services"; +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: `/_data_stream/${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__/DeleteDataStreamsModal.test.tsx.snap b/public/pages/DataStreams/containers/DeleteDataStreamsModal/__snapshots__/DeleteDataStreamsModal.test.tsx.snap new file mode 100644 index 000000000..b8f17204c --- /dev/null +++ b/public/pages/DataStreams/containers/DeleteDataStreamsModal/__snapshots__/DeleteDataStreamsModal.test.tsx.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +HTMLCollection [ +