From a676ab73119136c953e1223afe6c6e08610f1738 Mon Sep 17 00:00:00 2001 From: suzhou Date: Tue, 3 Jan 2023 14:36:02 +0800 Subject: [PATCH 01/10] Feature/common 2.5 (#519) * feat: split to common change Signed-off-by: suzhou * feat: update Signed-off-by: suzhou Signed-off-by: suzhou --- cypress/support/commands.js | 54 +- cypress/support/index.d.ts | 16 + opensearch_dashboards.json | 6 +- package.json | 8 +- public/JobHandler/index.tsx | 274 +++++++++ .../AdvancedSettings.test.tsx | 40 ++ .../AdvancedSettings.test.tsx.snap | 177 ++++++ public/components/AdvancedSettings/index.scss | 5 + public/components/AdvancedSettings/index.tsx | 91 +++ public/components/BetterComboBox/index.tsx | 15 + public/components/BetterToolTip/index.tsx | 19 + .../ChannelNotification.test.tsx | 77 +++ .../ChannelNotification.tsx | 87 +++ .../ChannelNotification.test.tsx.snap | 108 ++++ .../components/ChannelNotification/index.ts | 8 + .../components/ContentPanel/ContentPanel.tsx | 2 +- .../ContentPanel/ContentPanelActions.tsx | 7 +- .../CustomFormRow/CustomFormRow.test.tsx | 19 + .../__snapshots__/CustomFormRow.test.tsx.snap | 26 + public/components/CustomFormRow/index.tsx | 22 + .../DeleteModal/DeleteModal.test.tsx | 17 + public/components/DeleteModal/DeleteModal.tsx | 80 +++ .../__snapshots__/DeleteModal.test.tsx.snap | 147 +++++ public/components/DeleteModal/index.ts | 8 + .../DescriptionListHoz.test.tsx | 16 + .../DescriptionListHoz/DescriptionListHoz.tsx | 34 ++ .../DescriptionListHoz.test.tsx.snap | 28 + public/components/DescriptionListHoz/index.ts | 3 + .../EuiToolTipWrapper.test.tsx | 32 ++ .../EuiToolTipWrapper.test.tsx.snap | 13 + public/components/EuiToolTipWrapper/index.tsx | 79 +++ .../FormGenerator/FormGenerator.test.tsx | 304 ++++++++++ .../__snapshots__/FormGenerator.test.tsx.snap | 141 +++++ .../built_in_components/index.tsx | 69 +++ public/components/FormGenerator/index.tsx | 183 ++++++ .../JSONDiffEditor/JSONDiffEditor.scss | 3 + .../JSONDiffEditor/JSONDiffEditor.test.tsx | 16 + .../JSONDiffEditor/JSONDiffEditor.tsx | 151 +++++ .../JSONDiffEditor/JSONTextArea.tsx | 76 +++ .../JSONDiffEditor.test.tsx.snap | 7 + public/components/JSONDiffEditor/index.tsx | 28 + public/components/JSONDiffEditor/interface.ts | 9 + .../components/JSONEditor/JSONEditor.test.tsx | 106 ++++ public/components/JSONEditor/JSONEditor.tsx | 90 +++ .../__snapshots__/JSONEditor.test.tsx.snap | 125 ++++ public/components/JSONEditor/index.ts | 9 + .../LegacyNotification.test.tsx | 19 + .../LegacyNotification/LegacyNotification.tsx | 50 ++ .../LegacyNotification.test.tsx.snap | 43 ++ public/components/LegacyNotification/index.ts | 8 + public/components/Modal/Modal.tsx | 131 ++++- public/components/Modal/index.ts | 4 +- .../RemoteSelect/RemoteSelect.test.tsx | 70 +++ .../__snapshots__/RemoteSelect.test.tsx.snap | 109 ++++ public/components/RemoteSelect/index.tsx | 99 ++++ .../SimplePopover/SimplePopover.test.tsx | 53 ++ .../SimplePopover/SimplePopover.tsx | 104 ++++ .../__snapshots__/SimplePopover.test.tsx.snap | 19 + public/components/SimplePopover/index.ts | 8 + .../SwitchableEditor/SwitchableEditor.scss | 6 + .../SwitchableEditor.test.tsx | 38 ++ .../SwitchableEditor/SwitchableEditor.tsx | 54 ++ .../SwitchableEditor.test.tsx.snap | 415 ++++++++++++++ public/components/SwitchableEditor/index.tsx | 8 + public/components/Toast/Toast.test.tsx | 30 + .../Toast/__snapshots__/Toast.test.tsx.snap | 47 ++ public/components/Toast/index.tsx | 65 +++ .../ErrorNotification.test.tsx | 57 ++ .../ErrorNotification/ErrorNotification.tsx | 161 ++++++ .../ErrorNotification.test.tsx.snap | 218 +++++++ public/containers/ErrorNotification/index.ts | 8 + .../IndexDetail/IndexDetail.test.tsx | 95 ++++ .../__snapshots__/IndexDetail.test.tsx.snap | 38 ++ public/containers/IndexDetail/index.tsx | 70 +++ public/context/JobSchedulerContext.tsx | 12 + public/index_management_app.tsx | 3 + public/lib/JobScheduler/JobScheduler.test.ts | 120 ++++ public/lib/JobScheduler/JobScheduler.ts | 157 +++++ public/lib/JobScheduler/index.ts | 2 + public/lib/JobScheduler/interface.ts | 33 ++ public/lib/JobScheduler/store-localstorage.ts | 44 ++ public/lib/field/index.tsx | 174 ++++++ public/lib/field/interfaces.ts | 181 ++++++ public/lib/field/messages.ts | 30 + public/lib/field/rules/index.ts | 90 +++ public/lib/field/util.ts | 34 ++ public/models/interfaces.ts | 20 +- public/plugin.ts | 2 + public/services/CommonService.test.ts | 24 + public/services/CommonService.ts | 28 + public/services/IndexService.test.ts | 8 + public/services/IndexService.ts | 16 +- public/services/SnapshotManagementService.ts | 14 +- public/services/index.ts | 2 + public/utils/constants.ts | 207 ++++++- server/models/interfaces.ts | 32 +- server/models/types.ts | 2 +- server/plugin.ts | 23 +- server/routes/aliases.ts | 25 + server/routes/common.ts | 27 + server/routes/index.ts | 4 +- server/services/AliasServices.ts | 91 +++ server/services/CommonService.ts | 58 ++ server/services/IndexService.ts | 78 ++- server/services/index.ts | 4 + test/jest.config.js | 4 + test/mocks/browserServicesMock.ts | 6 + test/mocks/coreServicesMock.ts | 22 +- test/mocks/httpClientMock.ts | 1 + test/mocks/index.ts | 116 +++- utils/constants.ts | 28 + utils/helper.ts | 5 + yarn.lock | 538 ++++++++++-------- 113 files changed, 6849 insertions(+), 278 deletions(-) create mode 100644 public/JobHandler/index.tsx create mode 100644 public/components/AdvancedSettings/AdvancedSettings.test.tsx create mode 100644 public/components/AdvancedSettings/__snapshots__/AdvancedSettings.test.tsx.snap create mode 100644 public/components/AdvancedSettings/index.scss create mode 100644 public/components/AdvancedSettings/index.tsx create mode 100644 public/components/BetterComboBox/index.tsx create mode 100644 public/components/BetterToolTip/index.tsx create mode 100644 public/components/ChannelNotification/ChannelNotification.test.tsx create mode 100644 public/components/ChannelNotification/ChannelNotification.tsx create mode 100644 public/components/ChannelNotification/__snapshots__/ChannelNotification.test.tsx.snap create mode 100644 public/components/ChannelNotification/index.ts create mode 100644 public/components/CustomFormRow/CustomFormRow.test.tsx create mode 100644 public/components/CustomFormRow/__snapshots__/CustomFormRow.test.tsx.snap create mode 100644 public/components/CustomFormRow/index.tsx create mode 100644 public/components/DeleteModal/DeleteModal.test.tsx create mode 100644 public/components/DeleteModal/DeleteModal.tsx create mode 100644 public/components/DeleteModal/__snapshots__/DeleteModal.test.tsx.snap create mode 100644 public/components/DeleteModal/index.ts create mode 100644 public/components/DescriptionListHoz/DescriptionListHoz.test.tsx create mode 100644 public/components/DescriptionListHoz/DescriptionListHoz.tsx create mode 100644 public/components/DescriptionListHoz/__snapshots__/DescriptionListHoz.test.tsx.snap create mode 100644 public/components/DescriptionListHoz/index.ts create mode 100644 public/components/EuiToolTipWrapper/EuiToolTipWrapper.test.tsx create mode 100644 public/components/EuiToolTipWrapper/__snapshots__/EuiToolTipWrapper.test.tsx.snap create mode 100644 public/components/EuiToolTipWrapper/index.tsx create mode 100644 public/components/FormGenerator/FormGenerator.test.tsx create mode 100644 public/components/FormGenerator/__snapshots__/FormGenerator.test.tsx.snap create mode 100644 public/components/FormGenerator/built_in_components/index.tsx create mode 100644 public/components/FormGenerator/index.tsx create mode 100644 public/components/JSONDiffEditor/JSONDiffEditor.scss create mode 100644 public/components/JSONDiffEditor/JSONDiffEditor.test.tsx create mode 100644 public/components/JSONDiffEditor/JSONDiffEditor.tsx create mode 100644 public/components/JSONDiffEditor/JSONTextArea.tsx create mode 100644 public/components/JSONDiffEditor/__snapshots__/JSONDiffEditor.test.tsx.snap create mode 100644 public/components/JSONDiffEditor/index.tsx create mode 100644 public/components/JSONDiffEditor/interface.ts create mode 100644 public/components/JSONEditor/JSONEditor.test.tsx create mode 100644 public/components/JSONEditor/JSONEditor.tsx create mode 100644 public/components/JSONEditor/__snapshots__/JSONEditor.test.tsx.snap create mode 100644 public/components/JSONEditor/index.ts create mode 100644 public/components/LegacyNotification/LegacyNotification.test.tsx create mode 100644 public/components/LegacyNotification/LegacyNotification.tsx create mode 100644 public/components/LegacyNotification/__snapshots__/LegacyNotification.test.tsx.snap create mode 100644 public/components/LegacyNotification/index.ts create mode 100644 public/components/RemoteSelect/RemoteSelect.test.tsx create mode 100644 public/components/RemoteSelect/__snapshots__/RemoteSelect.test.tsx.snap create mode 100644 public/components/RemoteSelect/index.tsx create mode 100644 public/components/SimplePopover/SimplePopover.test.tsx create mode 100644 public/components/SimplePopover/SimplePopover.tsx create mode 100644 public/components/SimplePopover/__snapshots__/SimplePopover.test.tsx.snap create mode 100644 public/components/SimplePopover/index.ts create mode 100644 public/components/SwitchableEditor/SwitchableEditor.scss create mode 100644 public/components/SwitchableEditor/SwitchableEditor.test.tsx create mode 100644 public/components/SwitchableEditor/SwitchableEditor.tsx create mode 100644 public/components/SwitchableEditor/__snapshots__/SwitchableEditor.test.tsx.snap create mode 100644 public/components/SwitchableEditor/index.tsx create mode 100644 public/components/Toast/Toast.test.tsx create mode 100644 public/components/Toast/__snapshots__/Toast.test.tsx.snap create mode 100644 public/components/Toast/index.tsx create mode 100644 public/containers/ErrorNotification/ErrorNotification.test.tsx create mode 100644 public/containers/ErrorNotification/ErrorNotification.tsx create mode 100644 public/containers/ErrorNotification/__snapshots__/ErrorNotification.test.tsx.snap create mode 100644 public/containers/ErrorNotification/index.ts create mode 100644 public/containers/IndexDetail/IndexDetail.test.tsx create mode 100644 public/containers/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap create mode 100644 public/containers/IndexDetail/index.tsx create mode 100644 public/context/JobSchedulerContext.tsx create mode 100644 public/lib/JobScheduler/JobScheduler.test.ts create mode 100644 public/lib/JobScheduler/JobScheduler.ts create mode 100644 public/lib/JobScheduler/index.ts create mode 100644 public/lib/JobScheduler/interface.ts create mode 100644 public/lib/JobScheduler/store-localstorage.ts create mode 100644 public/lib/field/index.tsx create mode 100644 public/lib/field/interfaces.ts create mode 100644 public/lib/field/messages.ts create mode 100644 public/lib/field/rules/index.ts create mode 100644 public/lib/field/util.ts create mode 100644 public/services/CommonService.test.ts create mode 100644 public/services/CommonService.ts create mode 100644 server/routes/aliases.ts create mode 100644 server/routes/common.ts create mode 100644 server/services/AliasServices.ts create mode 100644 server/services/CommonService.ts create mode 100644 utils/helper.ts diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 0850da201..5e8cf088f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -4,7 +4,7 @@ */ const { API, INDEX, ADMIN_AUTH } = require("./constants"); -const { NODE_API } = require("../../server/utils/constants") +const { NODE_API } = require("../../server/utils/constants"); // *********************************************** // This example commands.js shows you how to @@ -123,8 +123,8 @@ Cypress.Commands.add("createIndex", (index, policyID = null, settings = {}) => { }); Cypress.Commands.add("deleteSnapshot", (repository, snapshot) => { - cy.request("DELETE", `${Cypress.env("opensearch")}${NODE_API._SNAPSHOTS}/${repository}/${snapshot}`) -}) + cy.request("DELETE", `${Cypress.env("opensearch")}${NODE_API._SNAPSHOTS}/${repository}/${snapshot}`); +}); Cypress.Commands.add("createRollup", (rollupId, rollupJSON) => { cy.request("PUT", `${Cypress.env("opensearch")}${API.ROLLUP_JOBS_BASE}/${rollupId}`, rollupJSON); @@ -134,6 +134,14 @@ Cypress.Commands.add("createIndexTemplate", (name, template) => { cy.request("PUT", `${Cypress.env("opensearch")}${API.INDEX_TEMPLATE_BASE}/${name}`, template); }); +Cypress.Commands.add("deleteTemplate", (name) => { + cy.request({ + url: `${Cypress.env("opensearch")}${API.INDEX_TEMPLATE_BASE}/${name}`, + failOnStatusCode: false, + method: "DELETE", + }); +}); + Cypress.Commands.add("createDataStream", (name) => { cy.request("PUT", `${Cypress.env("opensearch")}${API.DATA_STREAM_BASE}/${name}`); }); @@ -150,6 +158,10 @@ Cypress.Commands.add("createTransform", (transformId, transformJSON) => { cy.request("PUT", `${Cypress.env("opensearch")}${API.TRANSFORM_JOBS_BASE}/${transformId}`, transformJSON); }); +Cypress.Commands.add("createPipeline", (pipelineId, pipelineJSON) => { + cy.request("PUT", `${Cypress.env("opensearch")}/_ingest/pipeline/${pipelineId}`, pipelineJSON); +}); + Cypress.Commands.add("disableJitter", () => { // Sets the jitter to 0 in the ISM plugin cluster settings const jitterJson = { @@ -163,3 +175,39 @@ Cypress.Commands.add("disableJitter", () => { }; cy.request("PUT", `${Cypress.env("opensearch")}/_cluster/settings`, jitterJson); }); + +Cypress.Commands.add("addAlias", (alias, index) => { + cy.request({ + url: `${Cypress.env("opensearch")}/_aliases`, + method: "POST", + body: { + actions: [ + { + add: { + index, + alias, + }, + }, + ], + }, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add("removeAlias", (alias) => { + cy.request({ + url: `${Cypress.env("opensearch")}/_aliases`, + method: "POST", + body: { + actions: [ + { + remove: { + index: "*", + alias, + }, + }, + ], + }, + failOnStatusCode: false, + }); +}); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index b662c58ae..0054853f6 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -104,5 +104,21 @@ declare namespace Cypress { * cy.disableJitter() */ disableJitter(): Chainable; + + /** + * Delete template + * @example + * cy.deleteTemplate("some_template") + */ + deleteTemplate(name: string); + + /** + * Create a ingest pipeline + * @example + * cy.createPipeline("pipelineId", {"description": "sample description", "processors": []}) + */ + createPipeline(pipelineId: string, pipeline: object); + addAlias(alias: string, index: string); + removeAlias(alias: string); } } diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 315ef2b15..02e22c8ce 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -1,9 +1,9 @@ { "id": "indexManagementDashboards", - "version": "2.4.1", - "opensearchDashboardsVersion": "2.4.1", + "version": "2.5.0", + "opensearchDashboardsVersion": "2.4.2", "configPath": ["opensearch_index_management"], - "requiredPlugins": ["navigation"], + "requiredPlugins": ["navigation", "opensearchDashboardsReact"], "server": true, "ui": true } diff --git a/package.json b/package.json index 2f3e07e5d..69c0dfc37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opensearch_index_management_dashboards", - "version": "2.4.1", + "version": "2.5.0", "description": "Opensearch Dashboards plugin for Index Management", "main": "index.js", "license": "Apache-2.0", @@ -52,14 +52,18 @@ "@elastic/elastic-eslint-config-kibana": "link:../../packages/opensearch-eslint-config-opensearch-dashboards", "@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards", "@testing-library/dom": "^8.11.3", + "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.1.9", + "@types/diff": "^5.0.2", + "@types/flat": "^5.0.2", "@types/react-dom": "^16.9.8", "@types/react-router-dom": "^5.3.2", "cypress": "^6.0.0", + "diff": "^4.0.1", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", "husky": "^3.0.0", - "lint-staged": "^9.2.0", + "lint-staged": "^10.2.0", "ts-loader": "^6.2.1" }, "engines": { diff --git a/public/JobHandler/index.tsx b/public/JobHandler/index.tsx new file mode 100644 index 000000000..2035a8342 --- /dev/null +++ b/public/JobHandler/index.tsx @@ -0,0 +1,274 @@ +import React, { ReactChild } from "react"; +import { EuiLink } from "@elastic/eui"; +import { CoreSetup } from "../../../../src/core/public"; +import { jobSchedulerInstance } from "../context/JobSchedulerContext"; +import { CommonService, IndexService } from "../services"; +import { ReindexJobMetaData, RecoveryJobMetaData } from "../models/interfaces"; +import { ROUTES } from "../utils/constants"; + +const DetailLink = (props: { index: string }) => { + return {props.index}; +}; + +type TaskResult = { + found: boolean; + _source: { + completed: boolean; + response: { + failures: { + cause?: { + reason: string; + }; + }[]; + }; + error?: { + type: string; + reason: string; + }; + }; +}; + +export const EVENT_MAP = { + REINDEX_COMPLETE: "REINDEX_COMPLETE", + SPLIT_COMPLETE: "SPLIT_COMPLETE", + SHRINK_COMPLETE: "SHRINK_COMPLETE", +}; + +const triggerEvent = (eventName: string, data?: unknown) => { + const event = new CustomEvent(eventName, { + detail: data, + }); + window.dispatchEvent(event); +}; + +export const listenEvent = (eventName: string, callback: () => void) => { + window.addEventListener(eventName, callback); +}; + +export const destroyListener = (eventName: string, callback: () => void) => { + window.removeEventListener(eventName, callback); +}; + +export function JobHandlerRegister(core: CoreSetup) { + const indexService = new IndexService(core.http); + const commonService = new CommonService(core.http); + jobSchedulerInstance.addCallback({ + callbackName: "callbackForReindex", + callback: async (job: ReindexJobMetaData) => { + const extras = job.extras; + const tasksResult = await commonService.apiCaller({ + endpoint: "transport.request", + data: { + path: `.tasks/_doc/${extras.taskId}`, + method: "GET", + }, + }); + if (tasksResult.ok) { + const { _source, found } = tasksResult.response; + const { completed, response, error } = (_source || {}) as TaskResult["_source"]; + const { failures } = response; + if (completed && found) { + if (!failures.length && !error?.reason) { + if (extras.toastId) { + core.notifications.toasts.remove(extras.toastId); + } + triggerEvent(EVENT_MAP.REINDEX_COMPLETE, job); + core.notifications.toasts.addSuccess( + { + title: (( + <> + Source index has been successfully reindexed as{" "} + + + ) as unknown) as string, + }, + { + toastLifeTimeMs: 1000 * 60 * 60 * 24 * 5, + } + ); + } else { + let errors: ReactChild[] = []; + if (failures.length) { + errors.push( +
    + {Array.from(new Set(failures.map((item) => item.cause?.reason).filter((item) => item))).map((item) => ( +
  • {item}
  • + ))} +
+ ); + } + + if (error?.reason) { + errors.push( +
    +
  • {error.reason}
  • +
+ ); + } + + if (extras.toastId) { + core.notifications.toasts.remove(extras.toastId); + } + core.notifications.toasts.addDanger( + { + title: (( + <> + Reindex from to {extras.destIndex} has some errors, please check the errors + below: + + ) as unknown) as string, + text: ((
{errors}
) as unknown) as string, + }, + { + toastLifeTimeMs: 1000 * 60 * 60 * 24 * 5, + } + ); + } + return true; + } + } + + return false; + }, + timeoutCallback(job: ReindexJobMetaData) { + const extras = job.extras; + if (extras.toastId) { + core.notifications.toasts.remove(extras.toastId); + } + core.notifications.toasts.addDanger( + { + title: (( + <> + Reindex from to {extras.destIndex} does not finish in reasonable time, please check + the task {extras.taskId} manually + + ) as unknown) as string, + }, + { + toastLifeTimeMs: 1000 * 60 * 60 * 24 * 5, + } + ); + }, + listenType: "reindex", + }); + jobSchedulerInstance.addCallback({ + callbackName: "callbackForSplit", + callback: async (job: RecoveryJobMetaData) => { + const extras = job.extras; + const indexResult = await indexService.getIndices({ + from: 0, + size: 10, + search: extras.destIndex, + terms: extras.destIndex, + sortField: "index", + sortDirection: "desc", + showDataStreams: false, + }); + if (indexResult.ok) { + const [firstItem] = indexResult.response.indices || []; + if (firstItem && firstItem.health !== "red") { + if (extras.toastId) { + core.notifications.toasts.remove(extras.toastId); + } + triggerEvent(EVENT_MAP.SPLIT_COMPLETE, job); + core.notifications.toasts.addSuccess( + { + title: (( + <> + Source index has been successfully split as{" "} + . + + ) as unknown) as string, + }, + { + toastLifeTimeMs: 1000 * 60 * 60 * 24 * 5, + } + ); + return true; + } + } + + return false; + }, + timeoutCallback(job: RecoveryJobMetaData) { + const extras = job.extras; + if (extras.toastId) { + core.notifications.toasts.remove(extras.toastId); + } + core.notifications.toasts.addDanger( + { + title: (( + <> + Split to {extras.destIndex} does not finish in reasonable time, please check the + index manually + + ) as unknown) as string, + }, + { + toastLifeTimeMs: 1000 * 60 * 60 * 24 * 5, + } + ); + }, + listenType: "split", + }); + jobSchedulerInstance.addCallback({ + callbackName: "callbackForShrink", + callback: async (job: RecoveryJobMetaData) => { + const extras = job.extras; + const indexResult = await indexService.getIndices({ + from: 0, + size: 10, + search: extras.destIndex, + terms: extras.destIndex, + sortField: "index", + sortDirection: "desc", + showDataStreams: false, + }); + if (indexResult.ok) { + const [firstItem] = indexResult.response.indices || []; + if (firstItem && firstItem.health !== "red") { + if (extras.toastId) { + core.notifications.toasts.remove(extras.toastId); + } + triggerEvent(EVENT_MAP.SHRINK_COMPLETE, job); + core.notifications.toasts.addSuccess( + { + title: (( + <> + Source index has been successfully shrunken as{" "} + . + + ) as unknown) as string, + }, + { + toastLifeTimeMs: 1000 * 60 * 60 * 24 * 5, + } + ); + return true; + } + } + + return false; + }, + timeoutCallback(job: RecoveryJobMetaData) { + const extras = job.extras; + if (extras.toastId) { + core.notifications.toasts.remove(extras.toastId); + } + core.notifications.toasts.addDanger( + { + title: (( + <> + Shrink to {extras.destIndex} does not finish in reasonable time, please check the + index manually. + + ) as unknown) as string, + }, + { + toastLifeTimeMs: 1000 * 60 * 60 * 24 * 5, + } + ); + }, + listenType: "shrink", + }); +} diff --git a/public/components/AdvancedSettings/AdvancedSettings.test.tsx b/public/components/AdvancedSettings/AdvancedSettings.test.tsx new file mode 100644 index 000000000..037111627 --- /dev/null +++ b/public/components/AdvancedSettings/AdvancedSettings.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import { fireEvent, render } from "@testing-library/react"; +import AdvancedSettings from "./index"; +import userEvent from "@testing-library/user-event"; + +describe(" spec", () => { + it("render the component", () => { + render(); + expect(document.body.children).toMatchSnapshot(); + }); + + it("do some actions with render props", async () => { + const onChangeMock = jest.fn(); + render( + ( + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ +
+
+
+
+ , +] +`; diff --git a/public/components/SwitchableEditor/index.tsx b/public/components/SwitchableEditor/index.tsx new file mode 100644 index 000000000..8b0e14658 --- /dev/null +++ b/public/components/SwitchableEditor/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import SwitchableEditor from "./SwitchableEditor"; + +export default SwitchableEditor; +export * from "./SwitchableEditor"; diff --git a/public/components/Toast/Toast.test.tsx b/public/components/Toast/Toast.test.tsx new file mode 100644 index 000000000..543fe391f --- /dev/null +++ b/public/components/Toast/Toast.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { waitFor } from "@testing-library/react"; +import { act } from "react-dom/test-utils"; +import { SimpleEuiToast } from "./index"; + +describe("SimpleEuiToast show", () => { + it("render the component", async () => { + await act(async () => { + SimpleEuiToast.addSuccess("Success information"); + }); + expect(document.body).toMatchSnapshot(); + expect(document.querySelector('[data-test-subj="toast_Success information"]')).not.toBeNull(); + await act(async () => { + SimpleEuiToast.addDanger("Error information"); + }); + expect(document.querySelector('[data-test-subj="toast_Error information"]')).not.toBeNull(); + await act(async () => { + SimpleEuiToast.show({ + toastLifeTimeMs: 10, + title: "Test quick destroy", + }); + }); + await waitFor(() => { + expect(document.querySelector('[data-test-subj="toast_Test quick destroy"]')).toBeNull(); + }); + }); +}); diff --git a/public/components/Toast/__snapshots__/Toast.test.tsx.snap b/public/components/Toast/__snapshots__/Toast.test.tsx.snap new file mode 100644 index 000000000..9678911ce --- /dev/null +++ b/public/components/Toast/__snapshots__/Toast.test.tsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SimpleEuiToast show render the component 1`] = ` + +
+
+
+

+ A new notification appears +

+
+ + Success information + +
+ +
+
+
+ +`; diff --git a/public/components/Toast/index.tsx b/public/components/Toast/index.tsx new file mode 100644 index 000000000..fc0679f6e --- /dev/null +++ b/public/components/Toast/index.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useState } from "react"; +import { render } from "react-dom"; +import { EuiGlobalToastList, EuiGlobalToastListProps } from "@elastic/eui"; + +export type SimpleEuiToastProps = EuiGlobalToastListProps["toasts"][number]; + +const TOAST_MOUNT_ID = "EUI_SIMPLE_TOAST_MOUNT_ID"; + +let addToastHandler: (params: SimpleEuiToastProps) => void; +let removeAllToastsHandler: () => void; +let id = 0; + +const SimpleToast = () => { + const [toasts, setToasts] = useState([]); + + addToastHandler = (toast) => { + setToasts(toasts.concat(toast)); + }; + + const removeToast: (params: SimpleEuiToastProps & { id: string }) => void = (removedToast) => { + setToasts(toasts.filter((toast) => toast.id !== removedToast.id)); + }; + + removeAllToastsHandler = () => { + setToasts([]); + }; + + useEffect(() => { + return () => { + removeAllToastsHandler(); + }; + }, []); + + return ; +}; + +export const SimpleEuiToast = { + show: (props: Partial & { title: SimpleEuiToastProps["title"] }) => { + let dom; + if (!document.getElementById(TOAST_MOUNT_ID)) { + dom = document.createElement("div"); + dom.id = TOAST_MOUNT_ID; + dom.setAttribute("data-role", "SimpleEuiToast"); + document.body.appendChild(dom); + render(, dom); + } else { + dom = document.getElementById(TOAST_MOUNT_ID); + } + addToastHandler({ + ...props, + "data-test-subj": `toast_${props.title}`, + id: `toast_${id++}`, + }); + }, + addSuccess: (message: SimpleEuiToastProps["text"]) => + SimpleEuiToast.show({ + title: message, + color: "success", + }), + addDanger: (message: SimpleEuiToastProps["text"]) => + SimpleEuiToast.show({ + title: message, + color: "danger", + }), +}; diff --git a/public/containers/ErrorNotification/ErrorNotification.test.tsx b/public/containers/ErrorNotification/ErrorNotification.test.tsx new file mode 100644 index 000000000..8d8ae4b0a --- /dev/null +++ b/public/containers/ErrorNotification/ErrorNotification.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { render } from "@testing-library/react"; +import ErrorNotificationContainer, { ErrorNotificationProps } from "./ErrorNotification"; +import { ServicesContext } from "../../services"; +import { browserServicesMock, coreServicesMock } from "../../../test/mocks"; +import { ErrorNotification as IErrorNotification } from "../../../models/interfaces"; +import { CoreServicesContext } from "../../components/core_services"; + +const ErrorNotification = (props: Pick) => { + const [value, onChange] = useState(props.value); + return ( + + + + ); +}; + +function renderErrorNotification(errorNotification: IErrorNotification) { + return { + ...render( + + + + ), + }; +} + +describe(" spec", () => { + it("renders the component", () => { + const { container } = render( + + ); + expect(container.firstChild).toMatchSnapshot(); + }); + + it("renders the channel ui editor for channels", () => { + const errorNotification = { channel: { id: "some_id" }, message_template: { source: "some source message" } }; + const { queryByTestId, queryByText } = renderErrorNotification(errorNotification); + + expect(queryByTestId("channel-notification-refresh")).not.toBeNull(); + expect(queryByText("Switch to using Channel ID")).toBeNull(); + }); + + it("renders the json legacy editor for destinations", () => { + const errorNotification = { destination: { slack: { url: "https://slack.com" } }, message_template: { source: "some source message" } }; + const { queryByTestId, queryByText } = renderErrorNotification(errorNotification); + + expect(queryByTestId("channel-notification-refresh")).toBeNull(); + expect(queryByText("Switch to using Channel ID")).not.toBeNull(); + }); +}); diff --git a/public/containers/ErrorNotification/ErrorNotification.tsx b/public/containers/ErrorNotification/ErrorNotification.tsx new file mode 100644 index 000000000..a02b66475 --- /dev/null +++ b/public/containers/ErrorNotification/ErrorNotification.tsx @@ -0,0 +1,161 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ChangeEvent, Component, useContext } from "react"; +import { EuiLink, EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from "@elastic/eui"; +import { ContentPanel } from "../../components/ContentPanel"; +import "brace/theme/github"; +import "brace/mode/json"; +import { FeatureChannelList } from "../../../server/models/interfaces"; +import { BrowserServices } from "../../models/interfaces"; +import { ErrorNotification as IErrorNotification } from "../../../models/interfaces"; +import { ServicesContext } from "../../services"; +import { getErrorMessage } from "../../utils/helpers"; +import { CoreServicesContext } from "../../components/core_services"; +import ChannelNotification from "../../components/ChannelNotification"; +import LegacyNotification from "../../components/LegacyNotification"; +import { ERROR_NOTIFICATION_DOCUMENTATION_URL } from "../../utils/constants"; + +export interface ErrorNotificationProps { + value?: IErrorNotification; + onChange: (val: Required["value"]) => void; + onChangeChannelId?: (value: string) => void; + onChangeMessage?: (value: string) => void; + browserServices: BrowserServices; +} + +interface ErrorNotificationState { + channels: FeatureChannelList[]; + loadingChannels: boolean; +} + +class ErrorNotification extends Component { + static contextType = CoreServicesContext; + constructor(props: ErrorNotificationProps) { + super(props); + + this.state = { + channels: [], + loadingChannels: true, + }; + } + + componentDidMount = async (): Promise => { + await this.getChannels(); + }; + + getChannels = async (): Promise => { + this.setState({ loadingChannels: true }); + try { + const { notificationService } = this.props.browserServices; + const response = await notificationService.getChannels(); + if (response.ok) { + this.setState({ channels: response.response.channel_list }); + } else { + this.context.notifications.toasts.addDanger(`Could not load notification channels: ${response.error}`); + } + } catch (err) { + this.context.notifications.toasts.addDanger(getErrorMessage(err, "Could not load the notification channels")); + } + this.setState({ loadingChannels: false }); + }; + + onChangeChannelId = (e: ChangeEvent) => { + const { onChange, value, onChangeChannelId } = this.props; + const id = e.target.value; + onChangeChannelId && onChangeChannelId(id); + onChange({ + ...value, + channel: { + id, + }, + }); + }; + + onChangeMessage = (e: ChangeEvent) => { + const { onChange, value, onChangeMessage } = this.props; + const message = e.target.value; + onChangeMessage && onChangeMessage(message); + onChange({ + ...value, + message_template: { + source: message, + }, + }); + }; + + onSwitchToChannels = () => { + const { onChange } = this.props; + onChange({ + channel: { + id: "", + }, + message_template: { + source: "", + }, + }); + }; + + render() { + const { value: errorNotification, onChange } = this.props; + const { channels, loadingChannels } = this.state; + const hasDestination = !!errorNotification?.destination; + + let content = ( + + ); + + // If we have a destination in the error notification then it's either an older policy or they created through the API + if (hasDestination) { + content = ; + } + + return ( + + + +

Error notification

+
+
+ + + - optional + + + + } + titleSize="s" + subTitleText={ + +

+ You can set up an error notification for when a policy execution fails.{" "} + + Learn more + +

+
+ } + > +
{content}
+
+ ); + } +} + +export default function ErrorNotificationContainer(props: Omit) { + const browserServices = useContext(ServicesContext) as BrowserServices; + return ; +} diff --git a/public/containers/ErrorNotification/__snapshots__/ErrorNotification.test.tsx.snap b/public/containers/ErrorNotification/__snapshots__/ErrorNotification.test.tsx.snap new file mode 100644 index 000000000..f24ff5faa --- /dev/null +++ b/public/containers/ErrorNotification/__snapshots__/ErrorNotification.test.tsx.snap @@ -0,0 +1,218 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +
+
+
+
+
+
+

+ Error notification +

+
+
+
+
+
+ + - optional + +
+
+
+
+
+
+

+ You can set up an error notification for when a policy execution fails. + + + Learn more + EuiIconMock + EuiIconMock + + (opens in a new tab or window) + + +

+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+ + EuiIconMock + +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+ Embed variables in your message using Mustache template. +
+
+
+
+
+
+`; diff --git a/public/containers/ErrorNotification/index.ts b/public/containers/ErrorNotification/index.ts new file mode 100644 index 000000000..5e1409536 --- /dev/null +++ b/public/containers/ErrorNotification/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import ErrorNotification from "./ErrorNotification"; + +export default ErrorNotification; diff --git a/public/containers/IndexDetail/IndexDetail.test.tsx b/public/containers/IndexDetail/IndexDetail.test.tsx new file mode 100644 index 000000000..bc87da9a7 --- /dev/null +++ b/public/containers/IndexDetail/IndexDetail.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import { render, waitFor } from "@testing-library/react"; +import IndexDetail, { IIndexDetailProps } from "./index"; +import { browserServicesMock, coreServicesMock } from "../../../test/mocks"; +import { ServicesContext } from "../../services"; +import { CoreServicesContext } from "../../components/core_services"; +import { CatIndex } from "../../../server/models/interfaces"; + +browserServicesMock.commonService.apiCaller = jest.fn( + async (payload): Promise => { + if (payload.data?.index?.includes("error_index")) { + return { + ok: false, + error: "error index", + }; + } + + return { + ok: true, + response: (payload.data.index || []).map( + (index: string): CatIndex => { + return { + index, + "docs.count": "0", + "docs.deleted": "1", + "pri.store.size": "1", + data_stream: "no", + "store.size": "1mb", + rep: "2", + uuid: "1", + health: "green", + pri: "4", + status: "open", + }; + } + ), + }; + } +); + +function renderWithServiceAndCore(props: IIndexDetailProps) { + return { + ...render( + + + + + + ), + }; +} + +describe(" spec", () => { + it("render the component", async () => { + const { container, queryByText } = renderWithServiceAndCore({ + indices: ["test"], + children: <>content underneath the table, + }); + + expect(queryByText("children content here")).toBeNull(); + await waitFor(() => { + expect(container).toMatchSnapshot(); + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(1); + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "cat.indices", + data: { + index: ["test"], + format: "json", + }, + }); + }); + expect(queryByText("content underneath the table")).not.toBeNull(); + }); + + it("render with error", async () => { + const onGetIndicesDetailMock = jest.fn(); + renderWithServiceAndCore({ + indices: ["error_index"], + children: <>content underneath the table, + onGetIndicesDetail: onGetIndicesDetailMock, + }); + + await waitFor(() => { + expect(coreServicesMock.notifications.toasts.addDanger).toBeCalledTimes(1); + expect(coreServicesMock.notifications.toasts.addDanger).toBeCalledWith("error index"); + expect(onGetIndicesDetailMock).toBeCalledTimes(1); + expect(onGetIndicesDetailMock).toBeCalledWith([]); + }); + }); +}); diff --git a/public/containers/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap b/public/containers/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap new file mode 100644 index 000000000..a38a36806 --- /dev/null +++ b/public/containers/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec render the component 1`] = ` +
+
+
+
+

+ Source index details +

+
+
+
+
+
+
+
+
+
+`; diff --git a/public/containers/IndexDetail/index.tsx b/public/containers/IndexDetail/index.tsx new file mode 100644 index 000000000..b353d8573 --- /dev/null +++ b/public/containers/IndexDetail/index.tsx @@ -0,0 +1,70 @@ +import { EuiSpacer } from "@elastic/eui"; +import React, { useContext, useEffect, useState } from "react"; +import { CatIndex } from "../../../server/models/interfaces"; +import { ContentPanel } from "../../components/ContentPanel"; +import { CoreServicesContext } from "../../components/core_services"; +import { ServicesContext } from "../../services"; +import DescriptionListHoz from "../../components/DescriptionListHoz"; + +export interface IIndexDetailProps { + indices: string[]; + onGetIndicesDetail?: (args: CatIndex[]) => void; + children?: React.ReactChild; +} + +export default function IndexDetail(props: IIndexDetailProps) { + const [loading, setLoading] = useState(false); + const [items, setItems] = useState([] as CatIndex[]); + const services = useContext(ServicesContext); + const coreServices = useContext(CoreServicesContext); + useEffect(() => { + (async () => { + setLoading(true); + const result = await services?.commonService.apiCaller({ + endpoint: "cat.indices", + data: { + index: props.indices, + format: "json", + }, + }); + let finalResponse: CatIndex[] = []; + if (result?.ok) { + finalResponse = result.response; + } else { + coreServices?.notifications.toasts.addDanger(result?.error || ""); + } + setItems(finalResponse); + props.onGetIndicesDetail && props.onGetIndicesDetail(finalResponse); + setLoading(false); + })(); + }, [props.indices.join(","), setLoading, setItems, coreServices]); + return ( + + + {items && items.length ? ( + + ) : null} + + {loading ? null : props.children} + + ); +} diff --git a/public/context/JobSchedulerContext.tsx b/public/context/JobSchedulerContext.tsx new file mode 100644 index 000000000..73353dc78 --- /dev/null +++ b/public/context/JobSchedulerContext.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { JobScheduler } from "../lib/JobScheduler"; + +const jobSchedulerInstance = new JobScheduler({ + callbacks: [], +}); + +jobSchedulerInstance.init(); + +export { jobSchedulerInstance }; + +export const JobSchedulerContext = React.createContext(jobSchedulerInstance); diff --git a/public/index_management_app.tsx b/public/index_management_app.tsx index a7f684c43..f668540d2 100644 --- a/public/index_management_app.tsx +++ b/public/index_management_app.tsx @@ -16,6 +16,7 @@ import { NotificationService, ServicesContext, SnapshotManagementService, + CommonService, } from "./services"; import { DarkModeContext } from "./components/DarkMode"; import Main from "./pages/Main"; @@ -32,6 +33,7 @@ export function renderApp(coreStart: CoreStart, params: AppMountParameters, land const transformService = new TransformService(http); const notificationService = new NotificationService(http); const snapshotManagementService = new SnapshotManagementService(http); + const commonService = new CommonService(http); const services = { indexService, managedIndexService, @@ -40,6 +42,7 @@ export function renderApp(coreStart: CoreStart, params: AppMountParameters, land transformService, notificationService, snapshotManagementService, + commonService, }; const isDarkMode = coreStart.uiSettings.get("theme:darkMode") || false; diff --git a/public/lib/JobScheduler/JobScheduler.test.ts b/public/lib/JobScheduler/JobScheduler.test.ts new file mode 100644 index 000000000..7f76efcdd --- /dev/null +++ b/public/lib/JobScheduler/JobScheduler.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { waitFor } from "@testing-library/dom"; +import { JobScheduler } from "./JobScheduler"; + +describe("JobScheduler spec", () => { + it("basic usage", async () => { + const callback = jest.fn(async () => { + return false; + }); + const timeoutCallback = jest.fn(); + // setup job scheduler + const jobScheduler = new JobScheduler({ + callbacks: [ + { + callback, + callbackName: "test", + timeoutCallback, + }, + ], + }); + jobScheduler.init(); + + // add a job + const addedJob = await jobScheduler.addJob({ + interval: 1000, + timeout: 2500, + extras: {}, + type: "reindex", + }); + await jobScheduler.addJob(addedJob); + // if the same job was added, ignore that. + expect(jobScheduler.getAllJobs()).resolves.toHaveLength(1); + + // excute every second + await waitFor( + () => + new Promise(async (resolve, reject) => { + const result = await jobScheduler.getAllJobs(); + try { + expect(result).toHaveLength(0); + resolve(true); + } catch (e) { + reject(e); + } + }), + { + timeout: 10000, + } + ); + expect(callback).toBeCalledTimes(3); + + // setup a long timeout job + const testJob = await jobScheduler.addJob({ + interval: 1000, + type: "reindex", + extras: {}, + id: "test", + }); + expect(testJob.id).toEqual("test"); + await jobScheduler.changeJob(testJob.id, { + timeout: 2000, + }); + await new Promise((resolve) => setTimeout(resolve, 3000)); + // wait for 3s, and the job should be gone + expect(jobScheduler.getAllJobs()).resolves.toHaveLength(0); + + // add a callback + jobScheduler.addCallback({ + callbackName: "test1", + callback: async () => false, + timeoutCallback, + }); + + // delete the callback + jobScheduler.deleteCallback("test1"); + expect(jobScheduler.getAllCallbacks()).toHaveLength(1); + + // add a job + const testDeleteJob = await jobScheduler.addJob({ + interval: 1000, + type: "reindex", + extras: {}, + id: "testDeleteJob", + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + jobScheduler.deleteJob(testDeleteJob.id); + }, 30000); + + it("jobs when resume", async () => { + const callback = jest.fn(() => Promise.reject(false)); + const timeoutCallback = jest.fn(); + // setup job scheduler + const jobScheduler = new JobScheduler({ + callbacks: [ + { + callback, + callbackName: "test", + timeoutCallback, + }, + ], + }); + jobScheduler.addJob({ + createTime: Date.now() - 20 * 1000, + timeout: 2000, + interval: 1000, + type: "reindex", + extras: {}, + }); + jobScheduler.init(); + await new Promise((resolve) => setTimeout(resolve, 3000)); + expect(jobScheduler.getAllJobs()).resolves.toHaveLength(0); + expect(callback).toBeCalledTimes(1); + expect(timeoutCallback).toBeCalledTimes(1); + const result = await jobScheduler.changeJob("1", {}); + expect(result).toBe(false); + }); +}); diff --git a/public/lib/JobScheduler/JobScheduler.ts b/public/lib/JobScheduler/JobScheduler.ts new file mode 100644 index 000000000..945a7a705 --- /dev/null +++ b/public/lib/JobScheduler/JobScheduler.ts @@ -0,0 +1,157 @@ +import { IJobSchedulerOptions, IJobItemMetadata, IStorage, JobItemMetadata, TimeoutId } from "./interface"; +import { StoreLocalStorage } from "./store-localstorage"; + +export class JobScheduler { + private options: IJobSchedulerOptions; + // key: jobId value: timerid + private runningJobMap: Record = {}; + private storage: IStorage; + constructor(options: IJobSchedulerOptions) { + this.options = options; + this.storage = options.storage || new StoreLocalStorage(); + } + async init(): Promise { + this.loopJob(); + return true; + } + private getId() { + return `${Date.now()}_${Math.floor(Math.random() * 10)}`; + } + private formatJob(job: IJobItemMetadata): JobItemMetadata { + const formattedJob = { ...job }; + if (!formattedJob.id) { + formattedJob.id = this.getId(); + } + + if (!formattedJob.createTime) { + formattedJob.createTime = Date.now(); + } + + if (!formattedJob.timeout) { + formattedJob.timeout = 1000 * 60 * 60; + } + + return formattedJob as JobItemMetadata; + } + private isStaledJob(job: JobItemMetadata) { + if (!job.latestRunTime) { + // haven't run once, return false + return false; + } + + return job.timeout + job.createTime < job.latestRunTime; + } + private async loopJob() { + const jobs = await this.storage.getAll(); + // loop all the jobs to see if any job do not exist in runningJobMap + jobs.forEach(async (job) => { + // if a job is staled, remove that + if (this.isStaledJob(job)) { + await this.runStaledJob(job.id); + await this.deleteJob(job.id); + return; + } + + if (!this.runningJobMap[job.id]) { + const timeoutCallback = setTimeout(async () => { + if (!this.isStaledJob(job)) { + this.runJob(job.id); + } else { + await this.runStaledJob(job.id); + await this.deleteJob(job.id); + } + }, job.interval); + this.runningJobMap[job.id] = timeoutCallback; + } + }); + } + private async runStaledJob(jobId: JobItemMetadata["id"]): Promise { + const job = await this.getJob(jobId); + if (!job) { + return undefined; + } + const filteredCallbacks = this.options.callbacks.filter( + (callbackItem) => callbackItem.listenType === job.type || !callbackItem.listenType + ); + await Promise.all( + filteredCallbacks.map(async (callbackItem) => { + try { + return callbackItem.timeoutCallback(job); + } catch (e) { + return false; + } + }) + ); + } + private async runJob(jobId: JobItemMetadata["id"]): Promise { + const job = await this.getJob(jobId); + if (!job) { + return undefined; + } + const filteredCallbacks = this.options.callbacks.filter( + (callbackItem) => callbackItem.listenType === job.type || !callbackItem.listenType + ); + job.latestRunTime = Date.now(); + const result = await Promise.all( + filteredCallbacks.map(async (callbackItem) => { + try { + return await callbackItem.callback(job); + } catch (e) { + return false; + } + }) + ); + const hasFinish = result.some((res) => res === true); + await this.deleteJob(job.id); + if (!hasFinish) { + await this.addJob(job); + } + } + addCallback(callback: IJobSchedulerOptions["callbacks"][number]) { + this.options.callbacks.push(callback); + } + deleteCallback(callbackName: string) { + const findIndex = this.options.callbacks.findIndex((item) => item.callbackName === callbackName); + if (findIndex > -1) { + this.options.callbacks.splice(findIndex, 1); + } + } + getAllCallbacks() { + return this.options.callbacks; + } + async addJob(job: IJobItemMetadata): Promise { + const formattedJob = this.formatJob(job); + if (this.runningJobMap[formattedJob.id]) { + return formattedJob; + } + + await this.storage.set(formattedJob.id, formattedJob); + this.loopJob(); + return formattedJob; + } + async deleteJob(jobId: JobItemMetadata["id"]): Promise { + clearTimeout(this.runningJobMap[jobId]); + delete this.runningJobMap[jobId]; + const storageResult = await this.storage.delete(jobId); + return storageResult || true; + } + getJob(jobId: JobItemMetadata["id"]): Promise { + return this.storage.get(jobId); + } + getAllJobs(): Promise { + return this.storage.getAll(); + } + async changeJob(jobId: JobItemMetadata["id"], jobMeta: Partial>): Promise { + const nowJob = await this.getJob(jobId); + + if (!nowJob) { + return false; + } + + return this.storage.set(jobId, { + ...nowJob, + ...jobMeta, + id: jobId, + }); + } +} diff --git a/public/lib/JobScheduler/index.ts b/public/lib/JobScheduler/index.ts new file mode 100644 index 000000000..86f934b50 --- /dev/null +++ b/public/lib/JobScheduler/index.ts @@ -0,0 +1,2 @@ +export * from "./interface"; +export { JobScheduler } from "./JobScheduler"; diff --git a/public/lib/JobScheduler/interface.ts b/public/lib/JobScheduler/interface.ts new file mode 100644 index 000000000..9e907fee2 --- /dev/null +++ b/public/lib/JobScheduler/interface.ts @@ -0,0 +1,33 @@ +export interface IJobItemMetadata { + interval: number; + extras: any; // extra fields to store job-related info + type: "reindex" | "split" | "shrink"; // enum for job type + id?: string; // a number to indicate the job + createTime?: number; // the time when this job is created + latestRunTime?: number; // the time when the job latest run, will be used to check if the job is staled + // the timeout for job to do, once the time goes beyond the timeout + // a timeout error toast will show. + timeout?: number; +} + +export type JobItemMetadata = IJobItemMetadata & Required>; + +export interface IJobSchedulerOptions { + callbacks: { + listenType?: IJobItemMetadata["type"]; + callback: (params: IJobItemMetadata) => Promise; + timeoutCallback: (params: IJobItemMetadata) => void; + callbackName: string; + }[]; + storage?: IStorage; +} + +export interface IStorage { + setup(): Promise; + getAll(): Promise; + set(key: string, value: JobItemMetadata): Promise; + get(key: string): Promise; + delete(key: string): Promise; +} + +export type TimeoutId = ReturnType; diff --git a/public/lib/JobScheduler/store-localstorage.ts b/public/lib/JobScheduler/store-localstorage.ts new file mode 100644 index 000000000..cdb69a4b7 --- /dev/null +++ b/public/lib/JobScheduler/store-localstorage.ts @@ -0,0 +1,44 @@ +import { IStorage, JobItemMetadata } from "./interface"; + +const JOB_STORAGE_KEY = "ISM_JOBS"; + +export class StoreLocalStorage implements IStorage { + async setup(): Promise { + // do nothing + return true; + } + async getAll(): Promise { + return JSON.parse(localStorage.getItem(JOB_STORAGE_KEY) || "[]"); + } + async set(key: string, value: JobItemMetadata): Promise { + try { + const result = await this.getAll(); + const findIndex = result.findIndex((item) => item.id === key); + if (findIndex > -1) { + result[findIndex] = value; + } else { + result.push(value); + } + this.saveToDisk(result); + return true; + } catch (e) { + return false; + } + } + async get(key: string): Promise { + const all = await this.getAll(); + return all.find((item) => item.id === key); + } + async delete(key: string): Promise { + const result = await this.getAll(); + return this.saveToDisk(result.filter((item) => item.id !== key)); + } + private saveToDisk(payload: JobItemMetadata[]) { + try { + localStorage.setItem(JOB_STORAGE_KEY, JSON.stringify(payload)); + return true; + } catch (e) { + return false; + } + } +} diff --git a/public/lib/field/index.tsx b/public/lib/field/index.tsx new file mode 100644 index 000000000..750fe4eeb --- /dev/null +++ b/public/lib/field/index.tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useRef, useState } from "react"; +import { set, get, unset } from "lodash"; +import { Rule, FieldOption, FieldInstance, InitOption, InitResult, ValidateFunction, FieldName } from "./interfaces"; +import buildInRules from "./rules"; + +export function transformNameToString(name: FieldName) { + if (Array.isArray(name)) { + return name.join("."); + } else { + return name; + } +} + +export default function useField(options?: FieldOption): FieldInstance { + const [, setValuesState] = useState((options?.values || {}) as Record); + const [, setErrorsState] = useState({} as Record); + const destroyRef = useRef(false); + const values = useRef>(options?.values || {}); + const errors = useRef>({}); + const fieldsMapRef = useRef>({}); + const setValues = (obj: Record) => { + if (destroyRef.current) { + return; + } + values.current = { + ...values.current, + ...obj, + }; + setValuesState(values.current); + }; + const resetValues = (obj: Record) => { + if (destroyRef.current) { + return; + } + values.current = obj; + setValuesState(values.current); + }; + const setValue: FieldInstance["setValue"] = (name: FieldName, value) => { + const payload = { ...values.current }; + if (!Array.isArray(name)) { + name = [name]; + } + set(payload, name, value); + setValues(payload); + }; + const setErrors: FieldInstance["setErrors"] = (errs) => { + if (destroyRef.current) { + return; + } + errors.current = errs; + setErrorsState(errors.current); + }; + const setError: FieldInstance["setError"] = (name, error) => { + setErrors({ + ...errors.current, + [transformNameToString(name)]: error, + }); + }; + const validateField = async (name: FieldName) => { + const fieldOptions = fieldsMapRef.current[transformNameToString(name)]; + const rules: Rule[] = fieldOptions.rules || []; + const result = await Promise.all( + rules.map(async (item) => { + let validateFunction: ValidateFunction = () => undefined; + if (item.validator) { + validateFunction = item.validator; + } else if (item.required) { + validateFunction = buildInRules.required; + } else if (item.format) { + validateFunction = buildInRules.format; + } else if (typeof item.min === "number" || typeof item.max === "number") { + validateFunction = buildInRules.size; + } else if (item.pattern) { + validateFunction = buildInRules.pattern; + } + + let errorInfo = null; + try { + const result = validateFunction( + { + ...item, + field: transformNameToString(name), + }, + get(values.current, name) + ); + if (result && (result as Promise).then) { + await result; + } else { + errorInfo = result; + } + } catch (e) { + errorInfo = e || item.message; + } + + return errorInfo; + }) + ); + const fieldErrors = result.filter((item) => item) as string[]; + + return fieldErrors; + }; + useEffect(() => { + return () => { + destroyRef.current = true; + }; + }, []); + const refCallbacks = useRef>>({}); + return { + registerField: (initOptions: InitOption): InitResult => { + const fieldName = transformNameToString(initOptions.name); + fieldsMapRef.current[fieldName] = initOptions; + const payload: InitResult = { + ...initOptions.props, + value: get(values.current, initOptions.name), + onChange: async (val) => { + setValue(initOptions.name, val); + options?.onChange && options?.onChange(initOptions.name, val); + const validateErros = await validateField(initOptions.name); + setError(initOptions.name, validateErros.length ? validateErros : null); + }, + }; + if (options?.unmountComponent) { + if (!refCallbacks.current[fieldName]) { + refCallbacks.current[fieldName] = (ref: any) => { + if (!ref) { + delete fieldsMapRef.current[fieldName]; + delete refCallbacks.current[fieldName]; + } + }; + } + payload.ref = refCallbacks.current[fieldName] as React.RefCallback; + } + return payload; + }, + setValue, + setValues, + getValue: (name) => get(values.current, name), + getValues: () => values.current, + getError: (name) => errors.current[transformNameToString(name)], + getErrors: () => errors.current, + validatePromise: async () => { + const result = await Promise.all( + Object.values(fieldsMapRef.current).map(({ name }) => { + return validateField(name).then((res) => { + if (res.length) { + return { + [transformNameToString(name)]: res, + }; + } + + return null; + }); + }) + ); + const resultArray = result.filter((item) => item) as Record[]; + const resultPayload = resultArray.reduce((total, current) => ({ ...total, ...current }), {} as Record); + setErrors(resultPayload); + return { + errors: resultArray.length ? resultPayload : null, + values: values.current, + }; + }, + setError, + setErrors, + resetValues, + deleteValue: (key) => { + const newValues = { ...values.current }; + unset(newValues, key); + resetValues(newValues); + }, + }; +} + +export * from "./interfaces"; diff --git a/public/lib/field/interfaces.ts b/public/lib/field/interfaces.ts new file mode 100644 index 000000000..d51b964e4 --- /dev/null +++ b/public/lib/field/interfaces.ts @@ -0,0 +1,181 @@ +import React from "react"; + +// if it's a string[], the value will become nested. +// registerField({ name: ['a', 'b', 'c.d'] }) => { a: { b: { c,d: '' } } } +export type FieldName = string | string[]; + +export type FieldOption = { + /** + * All component changes will arrive here [set value will not trigger this function] + */ + onChange?: (name: FieldName, value?: any) => void; + + /** + * Initialization data + */ + values?: {}; + + unmountComponent?: boolean; +}; + +export type ValidateResults = { + errors: Record | null; + values: any; +}; + +export type InitResult = { + value?: T; + onChange(value: T): void; + ref?: React.RefCallback; +}; + +export type Rule = { + /** + * cannot be empty (cannot be used with pattern) + * @default true + */ + required?: boolean; + + /** + * error message + */ + message?: string; + + /** + * Check Regular Expression + */ + pattern?: RegExp; + /** + * Minimum string length /minimum number of arrays + */ + minLength?: number; + /** + * Maximum string length /maximum number of arrays + */ + maxLength?: number; + + /** + * String exact length /array exact number + */ + length?: number; + + /** + * minimum + */ + min?: number; + + /** + * maximum value + */ + max?: number; + /** + * Summary of common patterns + */ + format?: "url" | "email" | "tel" | "number"; + + /** + * Custom verification, (don't forget to execute callback() when the verification is successful, otherwise the verification will not return) + */ + validator?: (rule: Rule, value: string | number | object | boolean | Date | null | any) => string | Promise; + + /** + * The name of the event that triggered the validation + */ + trigger?: "onChange" | "onBlur" | string; +}; + +export type InitOption = { + /** + * The name of the field + */ + name: FieldName; + + /** + * The name of the event that triggered the data change + * @default 'onChange' + */ + trigger?: string | "onChange" | "onBlur"; + + /** + * Check rules + */ + rules?: Rule[]; + + /** + * Component custom events can be written here, others will be transparently transmitted (small package version ^0.3.0 support, large package ^0.7.0 support) + */ + props?: any; +}; + +export type FieldInstance = { + /** + * Initialize each component + */ + registerField(option?: InitOption): InitResult; + + /** + * check + * @param name + */ + validatePromise(name?: FieldName): Promise; + + /** + * Get the value of a single input control + * @param field name + */ + getValue(name: FieldName): any; + + /** + * Get the values ​​of a set of input controls, if no parameters are passed in, get the values ​​of all components + * @param names + */ + getValues(): any; + + /** + * Set the value of a single input control (will trigger render, please follow the timing of react) + */ + setValue(name: FieldName, value: any): void; + + /** + * Set the value of a set of input controls (will trigger render, please follow the timing of react) + */ + setValues(obj: any): void; + + /** + * Reset values + */ + resetValues(obj: any): void; + + /** + * Delete value + */ + deleteValue(key: FieldName): void; + + /** + * Get the Error of a single input control + */ + getError(name: FieldName): null | string[]; + + /** + * Get the Error for a set of input controls + * @param names field name + */ + getErrors(): any; + + /** + * Sets the Error for a single input control + * @param name + * @param errors + */ + setError(name: FieldName, errors: null | string[]): void; + + /** + * Sets the Error for a set of input controls + */ + setErrors(obj: any): void; +}; + +export type ValidateFunction = ( + rule: Rule & { field: string; aliasName?: string }, + value: string | number | object | boolean | Date +) => string | Promise | undefined; diff --git a/public/lib/field/messages.ts b/public/lib/field/messages.ts new file mode 100644 index 000000000..df8e34dc4 --- /dev/null +++ b/public/lib/field/messages.ts @@ -0,0 +1,30 @@ +export default { + default: "%s verification failed", + required: "%s is a required field", + format: { + number: "%s is not a legal number", + email: "%s is not a valid email address", + url: "%s is not a valid URL address", + tel: "%s is not a valid phone number", + }, + number: { + length: "%s length must be %s", + min: "%s field value must not be less than %s", + max: "%s field value must not be greater than %s", + minLength: "%s field character length must be at least %s", + maxLength: "%s field character length cannot exceed %s", + }, + string: { + length: "%s length must be %s", + min: "%s field value must not be less than %s", + max: "%s field value must not be greater than %s", + minLength: "%s field character length must be at least %s", + maxLength: "%s field character length cannot exceed %s", + }, + array: { + length: "%s length must be %s", + minLength: "%s must not be less than %s", + maxLength: "%s must not exceed %s", + }, + pattern: "%s field value %s does not match the regular %s", +}; diff --git a/public/lib/field/rules/index.ts b/public/lib/field/rules/index.ts new file mode 100644 index 000000000..949fc4564 --- /dev/null +++ b/public/lib/field/rules/index.ts @@ -0,0 +1,90 @@ +import { ValidateFunction } from "../interfaces"; +import messages from "../messages"; +import { format as messageFormat } from "../util"; + +const pattern = { + email: /[\w\u4E00-\u9FA5]+([-+.][\w\u4E00-\u9FA5]+)*@[\w\u4E00-\u9FA5]+([-.][\w\u4E00-\u9FA5]+)*\.[\w\u4E00-\u9FA5]+([-.][\w\u4E00-\u9FA5]+)*/, + url: /^(?:(?:http|https|ftp):\/\/|\/\/)(?:(?:(?:[-\w\u00a1-\uffff]+)(?:\.[-\w\u00a1-\uffff]+)+|localhost)(?::\d{2,5})?(?:(?:\/|#)[^\s]*)?)$/, + number: /\d*/, + tel: /^(1\d{10})$|(((400)-(\d{3})-(\d{4}))|^((\d{7,8})|(\d{3,4})-(\d{7,8})|(\d{7,8})-(\d{1,4}))$)$|^([ ]?)$/, +}; + +const types = { + number(value: any) { + if (isNaN(value)) { + return false; + } + return typeof value === "number" || (typeof value === "string" && !!value.match(pattern.number)); + }, + email(value: any) { + return typeof value === "string" && !!value.match(pattern.email) && value.length < 255; + }, + url(value: any) { + return typeof value === "string" && !!value.match(pattern.url); + }, + tel(value: any) { + return typeof value === "string" && !!value.match(pattern.tel); + }, +}; + +const rules = { + required: (rule, value) => { + if (value === undefined || value === null || value === "" || value.length === 0) { + return messageFormat(rule.message || messages.required, rule.aliasName || rule.field); + } + }, + format: (rule, value) => { + const custom = ["email", "number", "url", "tel"]; + const ruleType = rule.format; + if (ruleType && custom.indexOf(ruleType) > -1 && !types[ruleType](value)) { + return messageFormat(rule.message || messages.format[ruleType], rule.aliasName || rule.field, ruleType); + } + }, + size: (rule, value) => { + let key: "number" | "string" | null = null; + const isNum = typeof value === "number"; + const isStr = typeof value === "string"; + + if (isNum) { + key = "number"; + } else if (isStr) { + key = "string"; + } + + if (!key) { + return false; + } + + if (typeof rule.min === "number" || typeof rule.max === "number") { + let val = value; + const max = Number(rule.max); + const min = Number(rule.min); + + if (isStr) { + val = Number(val); + } + + if (val < min) { + return messageFormat(rule.message || messages[key].min, rule.aliasName || rule.field, "" + rule.min); + } else if (val > max) { + return messageFormat(rule.message || messages[key].max, rule.aliasName || rule.field, "" + rule.max); + } + } + }, + pattern: (rule, value: string) => { + if (rule.pattern) { + if (rule.pattern instanceof RegExp) { + if (!rule.pattern.test(value)) { + return messageFormat(rule.message || messages.pattern, rule.aliasName || rule.field, value, rule.pattern.toString()); + } + } else if (typeof rule.pattern === "string") { + const _pattern = new RegExp(rule.pattern); + if (!_pattern.test(value)) { + return messageFormat(rule.message || messages.pattern, rule.aliasName || rule.field, value, rule.pattern); + } + } + } + }, +} as Record; + +export default rules as Record; diff --git a/public/lib/field/util.ts b/public/lib/field/util.ts new file mode 100644 index 000000000..6511e2160 --- /dev/null +++ b/public/lib/field/util.ts @@ -0,0 +1,34 @@ +const formatRegExp = /%[sdj%]/g; + +export function format(...args: string[]) { + let i = 1; + const f = args[0]; + const len = args.length; + if (typeof f === "string") { + const str = String(f).replace(formatRegExp, (x) => { + if (x === "%%") { + return "%"; + } + if (i >= len) { + return x; + } + switch (x) { + case "%s": + return String(args[i++]); + case "%d": + return `${Number(args[i++])}`; + case "%j": + try { + return JSON.stringify(args[i++]); + } catch (_) { + return "[Circular]"; + } + default: + return x; + } + }); + + return str; + } + return f; +} diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index 7a8fb9784..41a60d9ab 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -5,6 +5,7 @@ import { Direction, Query } from "@elastic/eui"; import { SMPolicy } from "../../models/interfaces"; +import { IJobItemMetadata } from "../lib/JobScheduler"; import { IndexService, ManagedIndexService, @@ -13,6 +14,7 @@ import { TransformService, NotificationService, SnapshotManagementService, + CommonService, } from "../services"; export interface BrowserServices { @@ -23,6 +25,7 @@ export interface BrowserServices { transformService: TransformService; notificationService: NotificationService; snapshotManagementService: SnapshotManagementService; + commonService: CommonService; } export interface SMPoliciesQueryParams { @@ -60,7 +63,6 @@ export interface Column { sortable: boolean; } - export interface RestoreError { reason?: string; type?: string; @@ -108,3 +110,19 @@ export interface IndexItem { index: string; restore_status?: string; } +export interface ReindexJobMetaData extends IJobItemMetadata { + extras: { + toastId: string; + sourceIndex: string; + destIndex: string; + taskId: string; + }; +} + +export interface RecoveryJobMetaData extends IJobItemMetadata { + extras: { + toastId: string; + sourceIndex: string; + destIndex: string; + }; +} diff --git a/public/plugin.ts b/public/plugin.ts index ce07e27e6..059190ae1 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -8,6 +8,7 @@ import { IndexManagementPluginSetup } from "."; import { IndexManagementPluginStart } from "."; import { actionRepoSingleton } from "./pages/VisualCreatePolicy/utils/helpers"; import { ROUTES } from "./utils/constants"; +import { JobHandlerRegister } from "./JobHandler"; export class IndexManagementPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) { @@ -15,6 +16,7 @@ export class IndexManagementPlugin implements Plugin { + it("calls api caller nodejs route when calling apiCaller", async () => { + httpClientMock.fetch = jest.fn().mockResolvedValue({ ok: true }); + const queryObject = { + endpoint: "indices.get", + }; + await commonService.apiCaller(queryObject); + + expect(httpClientMock.fetch).toHaveBeenCalledTimes(1); + expect(httpClientMock.fetch).toHaveBeenCalledWith(`${NODE_API.API_CALLER}`, { + method: "POST", + body: JSON.stringify(queryObject), + }); + }); +}); diff --git a/public/services/CommonService.ts b/public/services/CommonService.ts new file mode 100644 index 000000000..2df20b5b6 --- /dev/null +++ b/public/services/CommonService.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { HttpFetchOptions, HttpSetup } from "opensearch-dashboards/public"; +import { ServerResponse } from "../../server/models/types"; +import { NODE_API } from "../../utils/constants"; +import { IAPICaller } from "../../models/interfaces"; + +export default class CommonService { + httpClient: HttpSetup; + + constructor(httpClient: HttpSetup) { + this.httpClient = httpClient; + } + + apiCaller = async (params: IAPICaller): Promise> => { + let url = `${NODE_API.API_CALLER}`; + const payload: HttpFetchOptions = {}; + payload.method = "POST"; + payload.body = JSON.stringify({ + data: params.data, + endpoint: params.endpoint, + }); + return (await this.httpClient.fetch(url, payload)) as ServerResponse; + }; +} diff --git a/public/services/IndexService.test.ts b/public/services/IndexService.test.ts index dd877cd7b..749a12c48 100644 --- a/public/services/IndexService.test.ts +++ b/public/services/IndexService.test.ts @@ -45,4 +45,12 @@ describe("IndexService spec", () => { expect(httpClientMock.get).toHaveBeenCalledTimes(1); }); + + it("calls search nodejs route when calling getAlias", async () => { + httpClientMock.post = jest.fn().mockResolvedValue({ data: {} }); + const queryObject = {}; + await indexService.getAliases(queryObject); + + expect(httpClientMock.get).toHaveBeenCalledTimes(1); + }); }); diff --git a/public/services/IndexService.ts b/public/services/IndexService.ts index da2d058a4..777a32b52 100644 --- a/public/services/IndexService.ts +++ b/public/services/IndexService.ts @@ -8,6 +8,7 @@ import { AcknowledgedResponse, ApplyPolicyResponse, DataStream, + GetAliasesResponse, GetDataStreamsAndIndicesNamesResponse, GetDataStreamsResponse, GetIndicesResponse, @@ -36,9 +37,22 @@ export default class IndexService { return await this.httpClient.get(url, { query: queryObject }); }; + getAliases = async (queryObject: HttpFetchQuery): Promise> => { + const url = `..${NODE_API._ALIASES}`; + return await this.httpClient.get(url, { query: queryObject }); + }; + getDataStreamsAndIndicesNames = async (searchValue: string): Promise> => { const [getIndicesResponse, getDataStreamsResponse] = await Promise.all([ - this.getIndices({ from: 0, size: 100, search: searchValue, sortDirection: "desc", sortField: "index", showDataStreams: true }), + this.getIndices({ + from: 0, + size: 10, + search: searchValue, + terms: [searchValue], + sortDirection: "desc", + sortField: "index", + showDataStreams: true, + }), this.getDataStreams({ search: searchValue }), ]); diff --git a/public/services/SnapshotManagementService.ts b/public/services/SnapshotManagementService.ts index 75fe1fa22..74d35b10d 100644 --- a/public/services/SnapshotManagementService.ts +++ b/public/services/SnapshotManagementService.ts @@ -47,9 +47,10 @@ export default class SnapshotManagementService { createSnapshot = async (snapshotId: string, repository: string, snapshot: Snapshot): Promise> => { let url = `..${NODE_API._SNAPSHOTS}/${snapshotId}`; - const response = (await this.httpClient.put(url, { query: { repository }, body: JSON.stringify(snapshot) })) as ServerResponse< - CreateSnapshotResponse - >; + const response = (await this.httpClient.put(url, { + query: { repository }, + body: JSON.stringify(snapshot), + })) as ServerResponse; return response; }; @@ -81,9 +82,10 @@ export default class SnapshotManagementService { primaryTerm: number ): Promise> => { let url = `..${NODE_API.SMPolicies}/${policyId}`; - const response = (await this.httpClient.put(url, { query: { seqNo, primaryTerm }, body: JSON.stringify(policy) })) as ServerResponse< - DocumentSMPolicy - >; + const response = (await this.httpClient.put(url, { + query: { seqNo, primaryTerm }, + body: JSON.stringify(policy), + })) as ServerResponse; return response; }; diff --git a/public/services/index.ts b/public/services/index.ts index 28eee9f41..2a6397bec 100644 --- a/public/services/index.ts +++ b/public/services/index.ts @@ -11,6 +11,7 @@ import RollupService from "./RollupService"; import TransformService from "./TransformService"; import NotificationService from "./NotificationService"; import SnapshotManagementService from "./SnapshotManagementService"; +import CommonService from "./CommonService"; export { ServicesConsumer, @@ -22,4 +23,5 @@ export { TransformService, NotificationService, SnapshotManagementService, + CommonService, }; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index a45f5a14d..07250614c 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { InitOption } from "../lib/field"; +import { ComponentMapEnum } from "../components/FormGenerator"; + export const PLUGIN_NAME = "opensearch_index_management_dashboards"; export const DEFAULT_EMPTY_DATA = "-"; @@ -13,7 +16,7 @@ export const ACTIONS_DOCUMENTATION_URL = "https://opensearch.org/docs/im-plugin/ export const STATES_DOCUMENTATION_URL = "https://opensearch.org/docs/im-plugin/ism/policies/#states"; export const ERROR_NOTIFICATION_DOCUMENTATION_URL = "https://opensearch.org/docs/im-plugin/ism/policies/#error-notifications"; export const TRANSITION_DOCUMENTATION_URL = "https://opensearch.org/docs/im-plugin/ism/policies/#transitions"; -export const INDEX_SETTINGS_URL = "https://opensearch.org/docs/latest/api-reference/index-apis/create-index/#index-settings"; +export const INDEX_SETTINGS_URL = "https://opensearch.org/docs/latest/api-reference/index-apis/create-index#index-settings"; export const SNAPSHOT_MANAGEMENT_DOCUMENTATION_URL = "https://opensearch.org/docs/latest/opensearch/snapshots/snapshot-management/"; export const CRON_EXPRESSION_DOCUMENTATION_URL = "https://opensearch.org/docs/latest/monitoring-plugins/alerting/cron/"; export const RESTORE_SNAPSHOT_DOCUMENTATION_URL = @@ -22,6 +25,7 @@ export const REPOSITORY_DOCUMENTATION_URL = "https://opensearch.org/docs/latest/ export const FS_REPOSITORY_DOCUMENTATION_URL = "https://opensearch.org/docs/latest/opensearch/snapshots/snapshot-restore/#shared-file-system"; export const S3_REPOSITORY_DOCUMENTATION_URL = "https://opensearch.org/docs/latest/opensearch/snapshots/snapshot-restore/#amazon-s3"; +export const SHRINK_DOCUMENTATION_URL = "https://opensearch.org/docs/latest/api-reference/index-apis/shrink-index"; export const ROUTES = Object.freeze({ CHANGE_POLICY: "/change-policy", @@ -49,6 +53,14 @@ export const ROUTES = Object.freeze({ REPOSITORIES: "/repositories", CREATE_REPOSITORY: "/create-repository", EDIT_REPOSITORY: "/edit-repository", + CREATE_INDEX: "/create-index", + INDEX_DETAIL: "/index-detail", + REINDEX: "/reindex", + ALIASES: "/aliases", + TEMPLATES: "/templates", + CREATE_TEMPLATE: "/create-template", + SPLIT_INDEX: "/split-index", + SHRINK_INDEX: "/shrink-index", }); export const BREADCRUMBS = Object.freeze({ @@ -84,6 +96,16 @@ export const BREADCRUMBS = Object.freeze({ REPOSITORIES: { text: "Repositories", href: `#${ROUTES.REPOSITORIES}` }, CREATE_REPOSITORY: { text: "Create repository", href: `#${ROUTES.CREATE_REPOSITORY}` }, EDIT_REPOSITORY: { text: "Edit repository", href: `#${ROUTES.EDIT_REPOSITORY}` }, + CREATE_INDEX: { text: "Create Index", href: `#${ROUTES.CREATE_INDEX}` }, + EDIT_INDEX: { text: "Edit Index", href: `#${ROUTES.CREATE_INDEX}` }, + INDEX_DETAIL: { text: "Index Detail", href: "#" }, + REINDEX: { text: "Reindex", href: `#${ROUTES.REINDEX}` }, + ALIASES: { text: "Aliases", href: `#${ROUTES.ALIASES}` }, + TEMPLATES: { text: "Templates", href: `#${ROUTES.TEMPLATES}` }, + CREATE_TEMPLATE: { text: "Create template", href: `#${ROUTES.CREATE_TEMPLATE}` }, + 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}` }, }); // TODO: EUI has a SortDirection already @@ -122,7 +144,7 @@ export const browseIndicesCols = [ width: "100%", truncateText: true, sortable: true, - } + }, ]; export const restoreIndicesCols = [ @@ -137,6 +159,181 @@ export const restoreIndicesCols = [ field: "restore_status", name: "Restore status", width: "25%", - sortable: true - } -]; \ No newline at end of file + sortable: true, + }, +]; +export const INDEX_IMPORT_SETTINGS = ["index.number_of_replicas", "index.number_of_shards", "index.refresh_interval"]; + +export const INDEX_DYNAMIC_SETTINGS = [ + "index.number_of_replicas", + "index.auto_expand_replicas", + "index.search.idle.after", + "index.refresh_interval", + "index.max_result_window", + "index.max_inner_result_window", + "index.max_rescore_window", + "index.max_docvalue_fields_search", + "index.max_script_fields", + "index.max_ngram_diff", + "index.max_shingle_diff", + "index.max_refresh_listeners", + "index.analyze.max_token_count", + "index.highlight.max_analyzed_offset", + "index.max_terms_count", + "index.max_regex_length", + "index.query.default_field", + "index.routing.allocation.enable", + "index.gc_deletes", + "index.default_pipeline", + "index.final_pipeline", + "index.hidden", +]; + +export const INDEX_MAPPING_TYPES: { + label?: string; + hasChildren?: boolean; + options?: { + fields?: (InitOption & { label: string; type: ComponentMapEnum; initValue?: any })[]; + }; +}[] = [ + { + label: "alias", + options: { + fields: [ + { + label: "Path", + name: "path", + type: "Input", + rules: [ + { + required: true, + message: "Path is required.", + }, + ], + }, + ], + }, + }, + { + label: "boolean", + }, + { + label: "binary", + }, + { + label: "completion", + }, + { + label: "date", + }, + { + label: "date_range", + }, + { + label: "double", + }, + { + label: "double_range", + }, + { + label: "float", + }, + { + label: "geo_point", + }, + { + label: "geo_shape", + }, + { + label: "half_float", + }, + { + label: "integer", + }, + { + label: "ip", + }, + { + label: "ip_range", + }, + { + label: "keyword", + }, + { + label: "long", + }, + { + label: "long_range", + }, + { + label: "object", + hasChildren: true, + }, + { + label: "percolator", + }, + { + label: "rank_feature", + }, + { + label: "rank_features", + }, + { + label: "search_as_you_type", + }, + { + label: "text", + }, + { + label: "token_count", + options: { + fields: [ + { + label: "Analyzer", + name: "analyzer", + initValue: "standard", + type: "Input", + rules: [ + { + required: true, + message: "Analyzer is required.", + }, + ], + }, + ], + }, + }, +]; + +export enum IndicesUpdateMode { + mappings = "mappings", + settings = "settings", + alias = "alias", +} + +export const INDEX_MAPPING_TYPES_WITH_CHILDREN = INDEX_MAPPING_TYPES.filter((item) => item.hasChildren).map((item) => item.label); + +export const DEFAULT_LEGACY_ERROR_NOTIFICATION = { + destination: { + slack: { + url: "", + }, + }, + message_template: { + source: "The index {{ctx.index}} failed during policy execution.", + }, +}; + +export const ALIAS_STATUS_OPTIONS = ["open", "closed", "hidden", "none", "all"].map((item) => ({ + label: item, + value: item, +})); + +export const INDEX_NAMING_MESSAGE = `Must be in lowercase letters. Cannot begin with underscores or hyphens. Spaces, commas, and characters :, \", *, +, /, \, |, ?, #, > are not allowed.`; + +export const REPLICA_NUMBER_MESSAGE = "Specify the number of replicas each primary shard should have. Default is 1."; + +export const TEMPLATE_TYPE = { + INDEX_TEMPLATE: "Indexes", + DATA_STREAM: "Data streams", +}; diff --git a/server/models/interfaces.ts b/server/models/interfaces.ts index e460d8019..e1417c6d5 100644 --- a/server/models/interfaces.ts +++ b/server/models/interfaces.ts @@ -12,6 +12,7 @@ import { TransformService, NotificationService, SnapshotManagementService, + CommonService, } from "../services"; import { DocumentPolicy, @@ -22,6 +23,7 @@ import { Rollup, Transform, } from "../../models/interfaces"; +import AliasServices from "../services/AliasServices"; export interface NodeServices { indexService: IndexService; @@ -32,6 +34,8 @@ export interface NodeServices { transformService: TransformService; notificationService: NotificationService; snapshotManagementService: SnapshotManagementService; + commonService: CommonService; + aliasService: AliasServices; } export interface SearchResponse { @@ -170,13 +174,13 @@ export interface IndexUpdateResponse { failedIndices: FailedIndex[]; } -export interface ApplyPolicyResponse extends IndexUpdateResponse { } +export interface ApplyPolicyResponse extends IndexUpdateResponse {} -export interface RemovePolicyResponse extends IndexUpdateResponse { } +export interface RemovePolicyResponse extends IndexUpdateResponse {} -export interface ChangePolicyResponse extends IndexUpdateResponse { } +export interface ChangePolicyResponse extends IndexUpdateResponse {} -export interface RetryManagedIndexResponse extends IndexUpdateResponse { } +export interface RetryManagedIndexResponse extends IndexUpdateResponse {} export interface RetryParams { index: string; @@ -322,7 +326,7 @@ export interface QueryStringQuery { export interface CatIndex { "docs.count": string; "docs.deleted": string; - health: string; + health: "red" | "yellow" | "green"; index: string; pri: string; "pri.store.size": string; @@ -331,15 +335,17 @@ export interface CatIndex { "store.size": string; uuid: string; data_stream: string | null; + extraStatus?: "recovery" | "reindex" | "open" | "close"; } export interface CatSnapshotIndex { index?: string; - "restore_status"?: string; + restore_status?: string; } export interface ManagedCatIndex extends CatIndex { managed: string; + managedPolicy: string; } export interface DataStream { @@ -465,3 +471,17 @@ export interface GetSMPoliciesResponse { policies: DocumentSMPolicy[]; totalPolicies: number; } + +export interface Alias { + alias: string; + index: string; + filter: string; + is_write_index: string; + "routing.index": string; + "routing.search": string; +} + +export interface GetAliasesResponse { + aliases: Alias[]; + totalAliases: number; +} diff --git a/server/models/types.ts b/server/models/types.ts index cbb7566b6..45ae6b8d9 100644 --- a/server/models/types.ts +++ b/server/models/types.ts @@ -25,5 +25,5 @@ export type RollupsSort = { "rollup.rollup.last_updated_time": "rollup.last_updated_time"; }; -export type ServerResponse = FailedServerResponse | { ok: true; response: T }; +export type ServerResponse = FailedServerResponse | { ok: true; response: T; error?: string }; export type FailedServerResponse = { ok: false; error: string }; diff --git a/server/plugin.ts b/server/plugin.ts index 27943917d..43f0c5909 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -15,9 +15,22 @@ import { DataStreamService, NotificationService, SnapshotManagementService, + CommonService, + AliasServices, } from "./services"; -import { indices, policies, managedIndices, rollups, transforms, notifications, snapshotManagement } from "../server/routes"; +import { + indices, + policies, + managedIndices, + rollups, + transforms, + notifications, + snapshotManagement, + common, + aliases, +} from "../server/routes"; import dataStreams from "./routes/dataStreams"; +import { NodeServices } from "./models/interfaces"; export class IndexPatternManagementPlugin implements Plugin { public async setup(core: CoreSetup) { @@ -35,7 +48,9 @@ export class IndexPatternManagementPlugin implements Plugin>> => { + try { + const { search } = request.query as { + search?: string; + }; + + const client = this.osDriver.asScoped(request); + const [aliases, apiAccessible, errMsg] = await getAliases(client, search); + + if (!apiAccessible) + return response.custom({ + statusCode: 200, + body: { + ok: false, + error: errMsg, + }, + }); + + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: { + aliases: aliases, + totalAliases: aliases.length, + }, + }, + }); + } catch (err) { + console.error("Index Management - AliasesService - getAliases:", err); + return response.custom({ + statusCode: 200, + body: { + ok: false, + error: err.message, + }, + }); + } + }; +} + +export async function getAliases( + { callAsCurrentUser: callWithRequest }: ILegacyScopedClusterClient, + search?: string +): Promise<[Alias[], boolean, string]> { + const searchPattern = search ? `*${search}*` : "*"; + + let accessible = true; + let errMsg = ""; + const aliasesResponse = await callWithRequest("cat.aliases", { + format: "json", + name: searchPattern, + }).catch((e) => { + if (e.statusCode === 403 && e.message.startsWith(SECURITY_EXCEPTION_PREFIX)) { + accessible = false; + errMsg = e.message; + return { alias: [] }; + } + throw e; + }); + + return [aliasesResponse, accessible, errMsg]; +} diff --git a/server/services/CommonService.ts b/server/services/CommonService.ts new file mode 100644 index 000000000..9ba04cffc --- /dev/null +++ b/server/services/CommonService.ts @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AcknowledgedResponse } from "../models/interfaces"; +import { ServerResponse } from "../models/types"; +import { + OpenSearchDashboardsRequest, + OpenSearchDashboardsResponseFactory, + ILegacyCustomClusterClient, + IOpenSearchDashboardsResponse, + RequestHandlerContext, +} from "../../../../src/core/server"; +import { IAPICaller } from "../../models/interfaces"; + +export interface ICommonCaller { + (arg: any): T; +} + +export default class IndexService { + osDriver: ILegacyCustomClusterClient; + + constructor(osDriver: ILegacyCustomClusterClient) { + this.osDriver = osDriver; + } + + apiCaller = async ( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory + ): Promise>> => { + try { + const { callAsCurrentUser: callWithRequest } = this.osDriver.asScoped(request); + const useQuery = !request.body; + const usedParam = (useQuery ? request.query : request.body) as IAPICaller; + const { endpoint, data } = usedParam || {}; + const payload = useQuery ? JSON.parse(data || "{}") : data; + const commonCallerResponse = await callWithRequest(endpoint, payload || {}); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: commonCallerResponse, + }, + }); + } catch (err) { + console.error("Index Management - CommonService - apiCaller", err); + return response.custom({ + statusCode: 200, + body: { + ok: false, + error: err.message, + }, + }); + } + }; +} diff --git a/server/services/IndexService.ts b/server/services/IndexService.ts index 007cf63c5..952aca459 100644 --- a/server/services/IndexService.ts +++ b/server/services/IndexService.ts @@ -24,6 +24,7 @@ import { } from "../../../../src/core/server"; import { getSearchString } from "../utils/helpers"; import { getIndexToDataStreamMapping } from "./DataStreamService"; +import { IRecoveryItem, IReindexItem, ITaskItem } from "../../models/interfaces"; export default class IndexService { osDriver: ILegacyCustomClusterClient; @@ -58,16 +59,80 @@ export default class IndexService { const { callAsCurrentUser: callWithRequest } = this.osDriver.asScoped(request); - const [indicesResponse, indexToDataStreamMapping]: [CatIndex[], IndexToDataStream] = await Promise.all([ + const [recoverys, tasks, indicesResponse, indexToDataStreamMapping]: [ + IRecoveryItem[], + ITaskItem[], + CatIndex[], + IndexToDataStream + ] = await Promise.all([ + callWithRequest("cat.recovery", { + format: "json", + detailed: true, + }), + callWithRequest("cat.tasks", { + format: "json", + detailed: true, + actions: "indices:data/write/reindex", + }), callWithRequest("cat.indices", params), getIndexToDataStreamMapping({ callAsCurrentUser: callWithRequest }), ]); + const formattedTasks: IReindexItem[] = tasks.map( + (item): IReindexItem => { + const { description } = item; + const regexp = /reindex from \[([^\]]+)\] to \[([^\]]+)\]/i; + const matchResult = description.match(regexp); + if (matchResult) { + const [, fromIndex, toIndex] = matchResult; + return { ...item, fromIndex, toIndex }; + } else { + return { + ...item, + fromIndex: "", + toIndex: "", + }; + } + } + ); + + const onGoingRecovery = recoverys.filter((item) => item.stage !== "done"); + // Augment the indices with their parent data stream name. indicesResponse.forEach((index) => { index.data_stream = indexToDataStreamMapping[index.index] || null; + let extraStatus: CatIndex["extraStatus"] = index.status as "open" | "close"; + if (index.health === "green") { + if (formattedTasks.find((item) => item.toIndex === index.index)) { + extraStatus = "reindex"; + } + } else { + if (onGoingRecovery.find((item) => item.index === index.index)) { + extraStatus = "recovery"; + } + } + + if (extraStatus) { + index.extraStatus = extraStatus; + } }); + if (sortField === "status") { + // add new more status to status field so we need to sort + indicesResponse.sort((a, b) => { + let flag; + const aStatus = a.extraStatus as string; + const bStatus = b.extraStatus as string; + if (sortDirection === "asc") { + flag = aStatus < bStatus; + } else { + flag = aStatus > bStatus; + } + + return flag ? -1 : 1; + }); + } + // Filtering out indices that belong to a data stream. This must be done before pagination. const filteredIndices = showDataStreams ? indicesResponse : indicesResponse.filter((index) => index.data_stream === null); @@ -85,7 +150,11 @@ export default class IndexService { body: { ok: true, response: { - indices: paginatedIndices.map((catIndex: CatIndex) => ({ ...catIndex, managed: managedStatus[catIndex.index] || "N/A" })), + indices: paginatedIndices.map((catIndex: CatIndex) => ({ + ...catIndex, + managed: managedStatus[catIndex.index] ? "Yes" : "No", + managedPolicy: managedStatus[catIndex.index], + })), totalIndices: filteredIndices.length, }, }, @@ -125,7 +194,10 @@ export default class IndexService { for (const indexName in explainResponse) { if (indexName === "total_managed_indices") continue; const explain = explainResponse[indexName] as ExplainAPIManagedIndexMetaData; - managed[indexName] = explain["index.plugins.index_state_management.policy_id"] === null ? "No" : "Yes"; + managed[indexName] = + explain["index.plugins.index_state_management.policy_id"] === null + ? "" + : explain["index.plugins.index_state_management.policy_id"]; } return managed; diff --git a/server/services/index.ts b/server/services/index.ts index 7f74da407..9e5d3cf3e 100644 --- a/server/services/index.ts +++ b/server/services/index.ts @@ -11,6 +11,8 @@ import RollupService from "./RollupService"; import TransformService from "./TransformService"; import NotificationService from "./NotificationService"; import SnapshotManagementService from "./SnapshotManagementService"; +import CommonService from "./CommonService"; +import AliasServices from "./AliasServices"; export { IndexService, @@ -21,4 +23,6 @@ export { TransformService, NotificationService, SnapshotManagementService, + CommonService, + AliasServices, }; diff --git a/test/jest.config.js b/test/jest.config.js index d88355ea1..a5214b1aa 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -34,6 +34,10 @@ module.exports = { "!/build/**", "!/cypress/**", "!**/vendor/**", + "!**/index.d.ts", + "!**/lib/field/**", + // There is a compile error in monaco-editor, ignore related components + "!**/components/JSONDiffEditor/**", ], clearMocks: true, testPathIgnorePatterns: ["/build/", "/node_modules/"], diff --git a/test/mocks/browserServicesMock.ts b/test/mocks/browserServicesMock.ts index fd6aa320f..ec532f136 100644 --- a/test/mocks/browserServicesMock.ts +++ b/test/mocks/browserServicesMock.ts @@ -10,6 +10,8 @@ import { RollupService, TransformService, NotificationService, + CommonService, + SnapshotManagementService, } from "../../public/services"; import httpClientMock from "./httpClientMock"; @@ -19,6 +21,8 @@ const policyService = new PolicyService(httpClientMock); const rollupService = new RollupService(httpClientMock); const transformService = new TransformService(httpClientMock); const notificationService = new NotificationService(httpClientMock); +const snapshotManagementService = new SnapshotManagementService(httpClientMock); +const commonService = new CommonService(httpClientMock); export default { indexService, @@ -27,4 +31,6 @@ export default { rollupService, transformService, notificationService, + snapshotManagementService, + commonService, }; diff --git a/test/mocks/coreServicesMock.ts b/test/mocks/coreServicesMock.ts index 30146ce00..3f2f435d8 100644 --- a/test/mocks/coreServicesMock.ts +++ b/test/mocks/coreServicesMock.ts @@ -14,8 +14,26 @@ const coreServicesMock = { }, notifications: { toasts: { - addDanger: jest.fn().mockName("addDanger"), - addSuccess: jest.fn().mockName("addSuccess"), + addDanger: jest.fn(() => ({})).mockName("addDanger"), + addSuccess: jest.fn(() => ({})).mockName("addSuccess"), + }, + }, + docLinks: { + links: { + opensearch: { + reindexData: { + base: "https://opensearch.org/docs/latest/opensearch/reindex-data/", + }, + queryDSL: { + base: "https://opensearch.org/docs/opensearch/query-dsl/index/", + }, + indexTemplates: { + base: "https://opensearch.org/docs/latest/opensearch/index-templates", + }, + indexAlias: { + base: "https://opensearch.org/docs/latest/opensearch/index-alias/", + }, + }, }, }, }; diff --git a/test/mocks/httpClientMock.ts b/test/mocks/httpClientMock.ts index 25964605e..ed0b31795 100644 --- a/test/mocks/httpClientMock.ts +++ b/test/mocks/httpClientMock.ts @@ -12,5 +12,6 @@ httpClientMock.get = jest.fn(); httpClientMock.head = jest.fn(); httpClientMock.post = jest.fn(); httpClientMock.put = jest.fn(); +httpClientMock.fetch = jest.fn(); export default httpClientMock as HttpSetup; diff --git a/test/mocks/index.ts b/test/mocks/index.ts index df4405a77..a4edfa2a0 100644 --- a/test/mocks/index.ts +++ b/test/mocks/index.ts @@ -9,4 +9,118 @@ import httpClientMock from "./httpClientMock"; import styleMock from "./styleMock"; import coreServicesMock from "./coreServicesMock"; -export { browserServicesMock, historyMock, httpClientMock, styleMock, coreServicesMock }; +const apiCallerMock = (browserServicesMockObject: typeof browserServicesMock) => { + browserServicesMockObject.commonService.apiCaller = jest.fn( + async (payload): Promise => { + switch (payload.endpoint) { + case "transport.request": { + if (payload.data?.path?.startsWith("/_index_template/_simulate_index/bad_index")) { + return { + ok: true, + response: {}, + }; + } else if (payload.data?.path?.startsWith("_index_template/bad_template")) { + return { + ok: false, + error: "bad template", + }; + } else if (payload.data?.path?.startsWith("_index_template/good_template")) { + return { + ok: true, + response: { + index_templates: [ + { + name: "good_template", + index_template: {}, + }, + ], + }, + }; + } else { + return { + ok: true, + response: { + template: { + settings: { + index: { + number_of_replicas: "10", + number_of_shards: "1", + }, + }, + }, + }, + }; + } + } + case "indices.create": + if (payload.data?.index === "bad_index") { + return { + ok: false, + error: "bad_index", + }; + } + + return { + ok: true, + response: {}, + }; + break; + case "cat.aliases": + return { + ok: true, + response: [ + { + alias: ".kibana", + index: ".kibana_1", + filter: "-", + is_write_index: "-", + }, + { + alias: "2", + index: "1234", + filter: "-", + is_write_index: "-", + }, + ], + }; + case "indices.get": + const payloadIndex = payload.data?.index; + if (payloadIndex === "bad_index") { + return { + ok: false, + error: "bad_error", + response: {}, + }; + } + + return { + ok: true, + response: { + [payload.data?.index]: { + aliases: { + update_test_1: {}, + }, + mappings: { + properties: { + test_mapping_1: { + type: "text", + }, + }, + }, + settings: { + "index.number_of_shards": "1", + "index.number_of_replicas": "1", + }, + }, + }, + }; + } + return { + ok: true, + response: {}, + }; + } + ); +}; + +export { browserServicesMock, historyMock, httpClientMock, styleMock, coreServicesMock, apiCallerMock }; diff --git a/utils/constants.ts b/utils/constants.ts index 685b5f99d..0dda4d67a 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -9,6 +9,7 @@ export const NODE_API = Object.freeze({ _SEARCH_SAMPLE_DATA: `${BASE_API_PATH}/_searchSampleData`, _INDICES: `${BASE_API_PATH}/_indices`, _DATA_STREAMS: `${BASE_API_PATH}/_data_streams`, + _ALIASES: `${BASE_API_PATH}/_aliases`, _MAPPINGS: `${BASE_API_PATH}/_mappings`, APPLY_POLICY: `${BASE_API_PATH}/applyPolicy`, EDIT_ROLLOVER_ALIAS: `${BASE_API_PATH}/editRolloverAlias`, @@ -24,6 +25,8 @@ export const NODE_API = Object.freeze({ SMPolicies: `${BASE_API_PATH}/smPolicies`, _SNAPSHOTS: `${BASE_API_PATH}/_snapshots`, _REPOSITORIES: `${BASE_API_PATH}/_repositores`, + PUT_INDEX: `${BASE_API_PATH}/putIndex`, + API_CALLER: `${BASE_API_PATH}/apiCaller`, }); export const REQUEST = Object.freeze({ @@ -33,3 +36,28 @@ export const REQUEST = Object.freeze({ POST: "POST", HEAD: "HEAD", }); + +export const SYSTEM_INDEX = [ + ".plugins-ml-model", + ".plugins-ml-task", + ".opendistro-alerting-config", + ".opendistro-alerting-alert*", + ".opendistro-anomaly-results*", + ".opendistro-anomaly-detector*", + ".opendistro-anomaly-checkpoints", + ".opendistro-anomaly-detection-state", + ".opendistro-reports-*", + ".opensearch-notifications-*", + ".opensearch-notebooks", + ".opensearch-observability", + ".opendistro-asynchronous-search-response*", + ".opendistro_security", + ".opendistro-job-scheduler-lock", + ".opendistro-ism-config", + ".replication-metadata-store", + "kibana*", + ".kibana*", + ".tasks", +]; + +export const SYSTEM_ALIAS = [".plugins*", ".opendistro*", ".opensearch*", ".replication-metadata-store", "kibana*", ".kibana*", ".tasks"]; diff --git a/utils/helper.ts b/utils/helper.ts new file mode 100644 index 000000000..1372474d9 --- /dev/null +++ b/utils/helper.ts @@ -0,0 +1,5 @@ +// minimatch is a peer dependency of glob +import minimatch from "minimatch"; +export const filterByMinimatch = (input: string, rules: string[]): boolean => { + return rules.some((item) => minimatch(input, item)); +}; diff --git a/yarn.lock b/yarn.lock index 31123dffd..890ddb5a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -112,27 +112,6 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@nodelib/fs.scandir@2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" - integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== - dependencies: - "@nodelib/fs.stat" "2.0.4" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" - integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" - integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== - dependencies: - "@nodelib/fs.scandir" "2.1.4" - fastq "^1.6.0" - "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" @@ -154,6 +133,17 @@ lz-string "^1.4.4" pretty-format "^27.0.2" +"@testing-library/react-hooks@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz#3388d07f562d91e7f2431a4a21b5186062ecfee0" + integrity sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/react" ">=16.9.0" + "@types/react-dom" ">=16.9.0" + "@types/react-test-renderer" ">=16.9.0" + react-error-boundary "^3.1.0" + "@testing-library/user-event@^13.1.9": version "13.1.9" resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.1.9.tgz#29e49a42659ac3c1023565ff56819e0153a82e99" @@ -171,13 +161,15 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b" integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== -"@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" +"@types/diff@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.2.tgz#dd565e0086ccf8bc6522c6ebafd8a3125c91c12b" + integrity sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg== + +"@types/flat@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/flat/-/flat-5.0.2.tgz#642a51a037d1f52fda082312b0e4566dc09a9f8f" + integrity sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ== "@types/history@*": version "4.7.8" @@ -216,32 +208,27 @@ dependencies: jest-diff "^24.3.0" -"@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - -"@types/node@*": - version "10.12.27" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.27.tgz#eb3843f15d0ba0986cc7e4d734d2ee8b50709ef8" - integrity sha512-e9wgeY6gaY21on3ve0xAjgBVjGDWq/xUteK0ujsE53bUoxycMkqfnkUgMt6ffZtykZ5X12Mg3T7Pw4TRCObDKg== - "@types/node@12.12.50": version "12.12.50" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.50.tgz#e9b2e85fafc15f2a8aa8fdd41091b983da5fd6ee" integrity sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w== "@types/normalize-package-data@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" - integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + version "2.4.1" + resolved "https://registry.npmmirror.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== -"@types/react-dom@^16.9.8": +"@types/react-dom@>=16.9.0", "@types/react-dom@^16.9.8": version "16.9.10" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f" integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw== @@ -265,7 +252,14 @@ "@types/history" "*" "@types/react" "*" -"@types/react@*", "@types/react@^16", "@types/react@^16.9.8": +"@types/react-test-renderer@>=16.9.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz#7b7f69ca98821ea5501b21ba24ea7b6139da2243" + integrity sha512-C7/5FBJ3g3sqUahguGi03O79b8afNeSD6T8/GU50oQrJCU0bVCCGQHaGKUbg2Ce8VQEEqTw8/HiS6lXHHdgkdQ== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@>=16.9.0", "@types/react@^16", "@types/react@^16.9.8": version "16.14.3" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.3.tgz#f5210f5deecf35d8794845549c93c2c3ad63aa9c" integrity sha512-zPrXn03hmPYqh9DznqSFQsoRtrQ4aHgnZDO+hMGvsE/PORvDTdJCHQ6XvJV31ic+0LzF73huPFXUb++W6Kri0Q== @@ -483,11 +477,23 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^2.0.0, ansi-regex@^3.0.0, ansi-regex@^4.0.0, ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -580,11 +586,6 @@ array-find@^1.0.0: resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8" integrity sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg= -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -625,6 +626,11 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -901,22 +907,27 @@ cachedir@^2.3.0: caller-callsite@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + resolved "https://registry.npmmirror.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== dependencies: callsites "^2.0.0" caller-path@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + resolved "https://registry.npmmirror.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== dependencies: caller-callsite "^2.0.0" callsites@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + resolved "https://registry.npmmirror.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase@^5.0.0: version "5.3.1" @@ -1049,6 +1060,13 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-table3@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" @@ -1067,6 +1085,14 @@ cli-truncate@^0.2.1: slice-ansi "0.0.4" string-width "^1.0.1" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -1113,6 +1139,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^2.0.16: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -1135,6 +1166,11 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -1199,7 +1235,7 @@ core-util-is@1.0.2, core-util-is@~1.0.0: cosmiconfig@^5.2.1: version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + resolved "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== dependencies: import-fresh "^2.0.0" @@ -1207,6 +1243,17 @@ cosmiconfig@^5.2.1: js-yaml "^3.13.1" parse-json "^4.0.0" +cosmiconfig@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -1240,7 +1287,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: cross-spawn@^6.0.0: version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== dependencies: nice-try "^1.0.4" @@ -1369,10 +1416,10 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.1.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== +debug@^4.2.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" @@ -1413,20 +1460,6 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -del@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7" - integrity sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA== - dependencies: - globby "^10.0.1" - graceful-fs "^4.2.2" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.1" - p-map "^3.0.0" - rimraf "^3.0.0" - slash "^3.0.0" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1445,6 +1478,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -1454,13 +1492,6 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - dom-accessibility-api@^0.5.9: version "0.5.13" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz#102ee5f25eacce09bdf1cfa5a298f86da473be4b" @@ -1542,6 +1573,13 @@ enhanced-resolve@~0.9.0: memory-fs "^0.2.0" tapable "^0.1.8" +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -1657,7 +1695,7 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: execa@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + resolved "https://registry.npmmirror.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: cross-spawn "^6.0.0" @@ -1668,22 +1706,7 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" - integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^3.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - -execa@^4.0.2: +execa@^4.0.2, execa@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== @@ -1782,30 +1805,11 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.0.3: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" - merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" - fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fastq@^1.6.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.1.tgz#8b8f2ac8bf3632d67afcd65dac248d5fdc45385e" - integrity sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA== - dependencies: - reusify "^1.0.4" - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -1981,12 +1985,12 @@ get-own-enumerable-property-symbols@^3.0.0: get-stdin@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" + resolved "https://registry.npmmirror.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== get-stream@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + resolved "https://registry.npmmirror.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" @@ -2025,7 +2029,7 @@ glob-all@^3.2.1: glob "^7.1.2" yargs "^15.3.1" -glob-parent@^3.1.0, glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^3.1.0, glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2063,26 +2067,12 @@ global-dirs@^2.0.1: dependencies: ini "1.3.7" -globby@^10.0.1: - version "10.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" - integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== - dependencies: - "@types/glob" "^7.1.1" - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.0.3" - glob "^7.1.3" - ignore "^5.1.1" - merge2 "^1.2.3" - slash "^3.0.0" - graceful-fs@^4.1.11, graceful-fs@^4.1.15: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -2207,7 +2197,7 @@ human-signals@^1.1.1: husky@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/husky/-/husky-3.1.0.tgz#5faad520ab860582ed94f0c1a77f0f04c90b57c0" + resolved "https://registry.npmmirror.com/husky/-/husky-3.1.0.tgz#5faad520ab860582ed94f0c1a77f0f04c90b57c0" integrity sha512-FJkPoHHB+6s4a+jwPqBudBDvYZsoQW5/HBuMSehC8qDiCe50kpcxeqFoDSlow+9I6wg47YxBoT3WxaURlrDIIQ== dependencies: chalk "^2.4.2" @@ -2232,19 +2222,22 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA== -ignore@^5.1.1: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - import-fresh@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + resolved "https://registry.npmmirror.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== dependencies: caller-path "^2.0.0" resolve-from "^3.0.0" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -2379,8 +2372,8 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-directory@^0.3.1: version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + resolved "https://registry.npmmirror.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" @@ -2462,11 +2455,6 @@ is-observable@^1.1.0: dependencies: symbol-observable "^1.1.0" -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - is-path-inside@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" @@ -2645,24 +2633,25 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -lint-staged@^9.2.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-9.5.0.tgz#290ec605252af646d9b74d73a0fa118362b05a33" - integrity sha512-nawMob9cb/G1J98nb8v3VC/E8rcX1rryUYXVZ69aT9kde6YWX+uvNOEHY5yf2gcWcTJGiD0kqXmCnS3oD75GIA== +lint-staged@^10.2.0: + version "10.5.4" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.5.4.tgz#cd153b5f0987d2371fc1d2847a409a2fe705b665" + integrity sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg== dependencies: - chalk "^2.4.2" - commander "^2.20.0" - cosmiconfig "^5.2.1" - debug "^4.1.1" + chalk "^4.1.0" + cli-truncate "^2.1.0" + commander "^6.2.0" + cosmiconfig "^7.0.0" + debug "^4.2.0" dedent "^0.7.0" - del "^5.0.0" - execa "^2.0.3" - listr "^0.14.3" - log-symbols "^3.0.0" + enquirer "^2.3.6" + execa "^4.1.0" + listr2 "^3.2.2" + log-symbols "^4.0.0" micromatch "^4.0.2" normalize-path "^3.0.0" - please-upgrade-node "^3.1.1" - string-argv "^0.3.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" stringify-object "^3.3.0" listr-silent-renderer@^1.1.1: @@ -2694,6 +2683,20 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" +listr2@^3.2.2: + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.16" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.5.1" + through "^2.3.8" + wrap-ansi "^7.0.0" + listr@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" @@ -2755,13 +2758,6 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" -log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== - dependencies: - chalk "^2.4.2" - log-symbols@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" @@ -2779,6 +2775,16 @@ log-update@^2.3.0: cli-cursor "^2.0.0" wrap-ansi "^3.0.1" +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + lru-cache@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -2854,11 +2860,6 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.2.3, merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -3044,7 +3045,7 @@ neo-async@^2.5.0, neo-async@^2.6.1: nice-try@^1.0.4: version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + resolved "https://registry.npmmirror.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== "node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.2.1: @@ -3078,7 +3079,7 @@ nice-try@^1.0.4: normalize-package-data@^2.5.0: version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + resolved "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== dependencies: hosted-git-info "^2.1.4" @@ -3100,18 +3101,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: npm-run-path@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + resolved "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== dependencies: path-key "^2.0.0" -npm-run-path@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" - integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== - dependencies: - path-key "^3.0.0" - npm-run-path@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -3200,13 +3194,8 @@ ospath@^1.2.2: p-finally@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== + resolved "https://registry.npmmirror.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" @@ -3234,10 +3223,10 @@ p-map@^2.0.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== -p-map@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" - integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ== +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== dependencies: aggregate-error "^3.0.0" @@ -3260,6 +3249,13 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + parse-asn1@^5.0.0, parse-asn1@^5.1.5: version "5.1.6" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" @@ -3273,8 +3269,8 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5: parse-json@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + resolved "https://registry.npmmirror.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== dependencies: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" @@ -3316,8 +3312,8 @@ path-is-absolute@^1.0.0: path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + resolved "https://registry.npmmirror.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" @@ -3389,7 +3385,7 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -please-upgrade-node@^3.1.1, please-upgrade-node@^3.2.0: +please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== @@ -3542,6 +3538,13 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -3554,7 +3557,7 @@ react-is@^17.0.1: read-pkg@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + resolved "https://registry.npmmirror.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== dependencies: "@types/normalize-package-data" "^2.4.0" @@ -3647,8 +3650,13 @@ require-main-filename@^2.0.0: resolve-from@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= + resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-url@^0.2.1: version "0.2.1" @@ -3679,15 +3687,23 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" @@ -3713,14 +3729,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: run-node@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e" + resolved "https://registry.npmmirror.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e" integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A== -run-parallel@^1.1.9: - version "1.1.10" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" - integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== - run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -3735,6 +3746,13 @@ rxjs@^6.3.3: dependencies: tslib "^1.9.0" +rxjs@^7.5.1: + version "7.5.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" + integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== + dependencies: + tslib "^2.1.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -3818,8 +3836,8 @@ sha.js@^2.4.0, sha.js@^2.4.8: shebang-command@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== dependencies: shebang-regex "^1.0.0" @@ -3832,15 +3850,20 @@ shebang-command@^2.0.0: shebang-regex@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.0, signal-exit@^3.0.2: +signal-exit@^3.0.0: + version "3.0.7" + resolved "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -3855,6 +3878,24 @@ slice-ansi@0.0.4: resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -3926,7 +3967,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: spdx-correct@^3.0.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + resolved "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== dependencies: spdx-expression-parse "^3.0.0" @@ -3934,21 +3975,21 @@ spdx-correct@^3.0.0: spdx-exceptions@^2.1.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + resolved "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== spdx-expression-parse@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + resolved "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.7" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" - integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== + version "3.0.12" + resolved "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz#69077835abe2710b65f03969898b6637b505a779" + integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -3960,7 +4001,7 @@ split-string@^3.0.1, split-string@^3.0.2: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== sshpk@^1.7.0: version "1.16.1" @@ -4024,7 +4065,7 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== -string-argv@^0.3.0: +string-argv@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== @@ -4101,8 +4142,8 @@ strip-ansi@^6.0.0: strip-eof@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + resolved "https://registry.npmmirror.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== strip-final-newline@^2.0.0: version "2.0.0" @@ -4180,6 +4221,11 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + timers-browserify@^2.0.4: version "2.0.12" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" @@ -4255,6 +4301,11 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -4272,9 +4323,14 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== typedarray@^0.0.6: @@ -4380,7 +4436,7 @@ uuid@^3.3.2: validate-npm-package-license@^3.0.1: version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + resolved "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== dependencies: spdx-correct "^3.0.0" @@ -4462,7 +4518,7 @@ which-module@^2.0.0: which@^1.2.9: version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + resolved "https://registry.npmmirror.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" @@ -4498,6 +4554,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -4523,6 +4588,11 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" From 6b574fe774933e3b30774bc6f31a36838e33994f Mon Sep 17 00:00:00 2001 From: suzhou Date: Tue, 3 Jan 2023 14:37:19 +0800 Subject: [PATCH 02/10] Feature/index management 2.5 (#520) * Feature/common 2.5 (#506) * feat: split to common change Signed-off-by: suzhou * feat: update Signed-off-by: suzhou Signed-off-by: suzhou * feat: update Signed-off-by: suzhou * fix: detail of data stream Signed-off-by: suzhou * feat: update Signed-off-by: suzhou Signed-off-by: suzhou --- cypress/integration/create_index.js | 273 ++++++ cypress/integration/indices_spec.js | 257 +++++- models/interfaces.ts | 60 ++ .../AliasSelect/AliasSelect.test.tsx | 117 +++ .../__snapshots__/AliasSelect.test.tsx.snap | 115 +++ .../components/AliasSelect/index.tsx | 70 ++ .../IndexDetail/IndexDetail.test.tsx | 130 +++ .../components/IndexDetail/IndexDetail.tsx | 548 ++++++++++++ .../__snapshots__/IndexDetail.test.tsx.snap | 177 ++++ .../components/IndexDetail/index.ts | 9 + .../components/IndexMapping/IndexMapping.scss | 19 + .../IndexMapping/IndexMapping.test.tsx | 165 ++++ .../components/IndexMapping/IndexMapping.tsx | 366 ++++++++ .../__snapshots__/IndexMapping.test.tsx.snap | 794 ++++++++++++++++++ .../components/IndexMapping/index.ts | 9 + .../MappingLabel/MappingLabel.test.tsx | 39 + .../components/MappingLabel/MappingLabel.tsx | 268 ++++++ .../__snapshots__/MappingLabel.test.tsx.snap | 240 ++++++ .../components/MappingLabel/index.ts | 4 + .../CreateIndex/CreateIndex.test.tsx | 95 +++ .../containers/CreateIndex/CreateIndex.tsx | 61 ++ .../containers/CreateIndex/index.ts | 8 + .../containers/IndexForm/IndexForm.test.tsx | 289 +++++++ .../containers/IndexForm/index.tsx | 417 +++++++++ public/pages/CreateIndex/index.ts | 8 + .../IndexDetail/IndexDetail.test.tsx | 90 ++ .../containers/IndexDetail/IndexDetail.tsx | 415 +++++++++ .../__snapshots__/IndexDetail.test.tsx.snap | 358 ++++++++ .../containers/IndexDetail/index.ts | 3 + public/pages/IndexDetail/index.ts | 8 + .../CloseIndexModal/CloseIndexModal.test.tsx | 41 + .../CloseIndexModal/CloseIndexModal.tsx | 82 ++ .../CloseIndexModal.test.tsx.snap | 159 ++++ .../components/CloseIndexModal/index.ts | 8 + .../DeleteIndexModal.test.tsx | 28 + .../DeleteIndexModal/DeleteIndexModal.tsx | 80 ++ .../DeleteIndexModal.test.tsx.snap | 164 ++++ .../components/DeleteIndexModal/index.ts | 8 + .../OpenIndexModal/OpenIndexModal.test.tsx | 24 + .../OpenIndexModal/OpenIndexModal.tsx | 59 ++ .../OpenIndexModal.test.tsx.snap | 112 +++ .../components/OpenIndexModal/index.ts | 8 + .../IndexDetail/IndexDetail.test.tsx | 47 ++ .../__snapshots__/IndexDetail.test.tsx.snap | 21 + .../Indices/containers/IndexDetail/index.tsx | 29 + .../containers/Indices/Indices.test.tsx | 10 +- .../Indices/containers/Indices/Indices.tsx | 80 +- .../__snapshots__/Indices.test.tsx.snap | 51 +- .../Indices/containers/Indices/index.scss | 5 + .../IndicesActions/IndicesActions.test.tsx | 499 +++++++++++ .../IndicesActions.test.tsx.snap | 271 ++++++ .../containers/IndicesActions/index.tsx | 242 ++++++ public/pages/Indices/utils/constants.tsx | 250 +++--- public/pages/Indices/utils/helpers.ts | 188 +++++ public/pages/Main/Main.tsx | 63 +- 55 files changed, 7783 insertions(+), 158 deletions(-) create mode 100644 cypress/integration/create_index.js create mode 100644 public/pages/CreateIndex/components/AliasSelect/AliasSelect.test.tsx create mode 100644 public/pages/CreateIndex/components/AliasSelect/__snapshots__/AliasSelect.test.tsx.snap create mode 100644 public/pages/CreateIndex/components/AliasSelect/index.tsx create mode 100644 public/pages/CreateIndex/components/IndexDetail/IndexDetail.test.tsx create mode 100644 public/pages/CreateIndex/components/IndexDetail/IndexDetail.tsx create mode 100644 public/pages/CreateIndex/components/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap create mode 100644 public/pages/CreateIndex/components/IndexDetail/index.ts create mode 100644 public/pages/CreateIndex/components/IndexMapping/IndexMapping.scss create mode 100644 public/pages/CreateIndex/components/IndexMapping/IndexMapping.test.tsx create mode 100644 public/pages/CreateIndex/components/IndexMapping/IndexMapping.tsx create mode 100644 public/pages/CreateIndex/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap create mode 100644 public/pages/CreateIndex/components/IndexMapping/index.ts create mode 100644 public/pages/CreateIndex/components/MappingLabel/MappingLabel.test.tsx create mode 100644 public/pages/CreateIndex/components/MappingLabel/MappingLabel.tsx create mode 100644 public/pages/CreateIndex/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap create mode 100644 public/pages/CreateIndex/components/MappingLabel/index.ts create mode 100644 public/pages/CreateIndex/containers/CreateIndex/CreateIndex.test.tsx create mode 100644 public/pages/CreateIndex/containers/CreateIndex/CreateIndex.tsx create mode 100644 public/pages/CreateIndex/containers/CreateIndex/index.ts create mode 100644 public/pages/CreateIndex/containers/IndexForm/IndexForm.test.tsx create mode 100644 public/pages/CreateIndex/containers/IndexForm/index.tsx create mode 100644 public/pages/CreateIndex/index.ts create mode 100644 public/pages/IndexDetail/containers/IndexDetail/IndexDetail.test.tsx create mode 100644 public/pages/IndexDetail/containers/IndexDetail/IndexDetail.tsx create mode 100644 public/pages/IndexDetail/containers/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap create mode 100644 public/pages/IndexDetail/containers/IndexDetail/index.ts create mode 100644 public/pages/IndexDetail/index.ts create mode 100644 public/pages/Indices/components/CloseIndexModal/CloseIndexModal.test.tsx create mode 100644 public/pages/Indices/components/CloseIndexModal/CloseIndexModal.tsx create mode 100644 public/pages/Indices/components/CloseIndexModal/__snapshots__/CloseIndexModal.test.tsx.snap create mode 100644 public/pages/Indices/components/CloseIndexModal/index.ts create mode 100644 public/pages/Indices/components/DeleteIndexModal/DeleteIndexModal.test.tsx create mode 100644 public/pages/Indices/components/DeleteIndexModal/DeleteIndexModal.tsx create mode 100644 public/pages/Indices/components/DeleteIndexModal/__snapshots__/DeleteIndexModal.test.tsx.snap create mode 100644 public/pages/Indices/components/DeleteIndexModal/index.ts create mode 100644 public/pages/Indices/components/OpenIndexModal/OpenIndexModal.test.tsx create mode 100644 public/pages/Indices/components/OpenIndexModal/OpenIndexModal.tsx create mode 100644 public/pages/Indices/components/OpenIndexModal/__snapshots__/OpenIndexModal.test.tsx.snap create mode 100644 public/pages/Indices/components/OpenIndexModal/index.ts create mode 100644 public/pages/Indices/containers/IndexDetail/IndexDetail.test.tsx create mode 100644 public/pages/Indices/containers/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap create mode 100644 public/pages/Indices/containers/IndexDetail/index.tsx create mode 100644 public/pages/Indices/containers/Indices/index.scss create mode 100644 public/pages/Indices/containers/IndicesActions/IndicesActions.test.tsx create mode 100644 public/pages/Indices/containers/IndicesActions/__snapshots__/IndicesActions.test.tsx.snap create mode 100644 public/pages/Indices/containers/IndicesActions/index.tsx diff --git a/cypress/integration/create_index.js b/cypress/integration/create_index.js new file mode 100644 index 000000000..21720762b --- /dev/null +++ b/cypress/integration/create_index.js @@ -0,0 +1,273 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { PLUGIN_NAME } from "../support/constants"; + +const SAMPLE_INDEX = "index-specific-index"; + +describe("Create Index", () => { + before(() => { + // Set welcome screen tracking to false + localStorage.setItem("home:welcome:show", "false"); + cy.deleteAllIndices(); + cy.deleteTemplate("index-common-template"); + cy.deleteTemplate("index-specific-template"); + cy.createIndexTemplate("index-common-template", { + index_patterns: ["index-*"], + template: { + aliases: { + alias_for_common_1: {}, + alias_for_common_2: {}, + }, + settings: { + number_of_shards: 2, + number_of_replicas: 1, + }, + }, + }); + cy.createIndexTemplate("index-specific-template", { + index_patterns: ["index-specific-*"], + priority: 1, + template: { + aliases: { + alias_for_specific_1: {}, + }, + settings: { + number_of_shards: 3, + number_of_replicas: 2, + }, + mappings: { + properties: { + text: { + type: "text", + }, + }, + }, + }, + }); + }); + + describe("can be created and updated", () => { + beforeEach(() => { + // Visit ISM OSD + cy.visit(`${Cypress.env("opensearch_dashboards")}/app/${PLUGIN_NAME}#/indices`); + cy.contains("Rows per page", { timeout: 60000 }); + }); + + it("Create a index successfully", () => { + // enter create page + cy.get('[data-test-subj="Create IndexButton"]').click(); + cy.contains("Create index"); + + // type field name + cy.get('[placeholder="Specify a name for the new index."]').type(SAMPLE_INDEX).blur(); + + cy.wait(1000); + + cy.get('[data-test-subj="comboBoxSearchInput"]').get('[title="alias_for_specific_1"]').should("exist"); + + cy.get('[data-test-subj="comboBoxSearchInput"]').type("some_test_alias{enter}"); + + cy.get('[data-test-subj="editorTypeJsonEditor"]').click().end(); + + cy.get('[data-test-subj="mappingsJsonEditorFormRow"] [data-test-subj="jsonEditor-valueDisplay"]').should(($editor) => { + expect(JSON.parse($editor.val())).to.deep.equal({ + properties: { + text: { + type: "text", + }, + }, + }); + }); + + cy.get('[data-test-subj="mappingsJsonEditorFormRow"] .ace_text-input') + .focus() + .clear({ force: true }) + .type( + JSON.stringify({ + properties: { + text: { + type: "text", + }, + }, + dynamic: true, + }), + { parseSpecialCharSequences: false, force: true } + ) + .end() + .wait(1000) + .get('[data-test-subj="editorTypeVisualEditor"]') + .click() + .end(); + + // add a field + cy.get('[data-test-subj="createIndexAddFieldButton"]').click().end(); + cy.get('[data-test-subj="mapping-visual-editor-1-field-name"]').type("text_mappings"); + + // click create + cy.get('[data-test-subj="createIndexCreateButton"]').click({ force: true }); + + // The index should exist + cy.get(`#_selection_column_${SAMPLE_INDEX}-checkbox`).should("have.exist"); + + // check the index detail + cy.visit(`${Cypress.env("opensearch_dashboards")}/app/${PLUGIN_NAME}#/create-index/${SAMPLE_INDEX}`); + + // index name and alias should exist + cy.get(`[title="${SAMPLE_INDEX}"]`) + .should("have.exist") + .end() + .get('[title="some_test_alias"]') + .should("have.exist") + .end() + .get('[data-test-subj="mapping-visual-editor-0-field-type"]') + .should("have.attr", "title", "text") + .end() + .get('[data-test-subj="mapping-visual-editor-1-field-name"]') + .should("have.attr", "title", "text_mappings") + .end() + .get('[data-test-subj="editorTypeJsonEditor"]') + .click() + .end() + .get('[data-test-subj="mappingsJsonEditorFormRow"] [data-test-subj="jsonEditor-valueDisplay"]') + .should(($editor) => { + expect(JSON.parse($editor.val())).to.deep.equal({ + dynamic: "true", + properties: {}, + }); + }); + }); + + it("Update alias successfully", () => { + cy.get(`[data-test-subj="viewIndexDetailButton-${SAMPLE_INDEX}"]`) + .click() + .get("#indexDetailModalAlias") + .click() + .get('[data-test-subj="detailModalEdit"]') + .click() + .end(); + + // add a alias and remove the exist alias + cy.get('[data-test-subj="comboBoxSearchInput"]') + .type("some_new_test_alias{enter}") + .end() + .get('[title="some_test_alias"] .euiBadge__iconButton') + .click() + .end() + .get('[data-test-subj="createIndexCreateButton"]') + .click({ force: true }) + .end(); + + cy.get('[title="some_test_alias"]').should("not.exist").end().get('[title="some_new_test_alias"]').should("exist").end(); + }); + + it("Update settings successfully", () => { + cy.get(`[data-test-subj="viewIndexDetailButton-${SAMPLE_INDEX}"]`) + .click() + .get("#indexDetailModalSettings") + .click() + .get('[data-test-subj="detailModalEdit"]') + .click() + .end(); + + cy.get('[data-test-subj="index-form-in-index-detail"] [aria-controls="accordionForCreateIndexSettings"]') + .click() + .end() + .get('[data-test-subj="index-form-in-index-detail"] .ace_text-input') + .focus() + .clear({ force: true }) + .type('{ "index.blocks.write": true, "index.number_of_shards": 2, "index.number_of_replicas": 3 }', { + parseSpecialCharSequences: false, + force: true, + }); + + cy.get('[data-test-subj="createIndexCreateButton"]').click({ force: true }); + + cy.contains(`Can't update non dynamic settings`).should("exist"); + + cy.get('[data-test-subj="index-form-in-index-detail"] .ace_text-input') + .focus() + .clear({ force: true }) + .type('{ "index.blocks.write": true }', { parseSpecialCharSequences: false, force: true }) + .end() + .wait(1000) + .get('[data-test-subj="index-form-in-index-detail"] [placeholder="The number of replica shards each primary shard should have."]') + .clear() + .type(2) + .end(); + + cy.get('[data-test-subj="createIndexCreateButton"]').click({ force: true }); + + cy.wait(1000).get('[data-test-subj="form-name-index.number_of_replicas"] .euiText').should("have.text", "2"); + }); + + it("Update mappings successfully", () => { + cy.get(`[data-test-subj="viewIndexDetailButton-${SAMPLE_INDEX}"]`) + .click() + .get("#indexDetailModalMappings") + .click() + .get('[data-test-subj="detailModalEdit"]') + .click() + .end(); + + cy.get('[data-test-subj="createIndexAddFieldButton"]') + .click() + .end() + .get('[data-test-subj="mapping-visual-editor-2-field-name"]') + .type("text_mappings_2") + .end() + .get('[data-test-subj="createIndexCreateButton"]') + .click({ force: true }); + + cy.get('[data-test-subj="mapping-visual-editor-2-field-type"]').should("have.attr", "title", "text").end(); + + cy.get('[data-test-subj="detailModalEdit"]') + .click() + .end() + .get('[data-test-subj="index-form-in-index-detail"] [data-test-subj="editorTypeJsonEditor"]') + .click() + .end() + .get('[data-test-subj="index-form-in-index-detail"] .ace_text-input') + .focus() + .clear({ force: true }) + .type('{ "dynamic": true }', { parseSpecialCharSequences: false, force: true }) + .end() + .wait(1000) + .get('[data-test-subj="createIndexCreateButton"]') + .click({ force: true }); + + cy.wait(1000) + .get('[data-test-subj="editorTypeJsonEditor"]') + .click() + .end() + .get('[data-test-subj="jsonEditor-valueDisplay"]') + .should( + "have.text", + JSON.stringify( + { + dynamic: "true", + properties: { + text: { + type: "text", + }, + text_mappings: { + type: "text", + }, + text_mappings_2: { + type: "text", + }, + }, + }, + null, + 2 + ) + ); + }); + }); + + after(() => { + cy.deleteTemplate("index-common-template"); + cy.deleteTemplate("index-specific-template"); + }); +}); diff --git a/cypress/integration/indices_spec.js b/cypress/integration/indices_spec.js index e20986940..7962b853b 100644 --- a/cypress/integration/indices_spec.js +++ b/cypress/integration/indices_spec.js @@ -132,12 +132,10 @@ describe("Indices", () => { cy.get(`[data-test-subj="checkboxSelectRow-${SAMPLE_INDEX}"]`).check({ force: true }); // Click apply policy button + cy.get('[data-test-subj="moreAction"]').click(); cy.get(`[data-test-subj="Apply policyButton"]`).click({ force: true }); - cy.get(`input[data-test-subj="comboBoxSearchInput"]`).focus().type(POLICY_ID, { - parseSpecialCharSequences: false, - delay: 1, - }); + cy.get(`input[data-test-subj="comboBoxSearchInput"]`).click().type(POLICY_ID); // Click the policy option cy.get(`button[role="option"]`).first().click({ force: true }); @@ -153,6 +151,257 @@ describe("Indices", () => { // Confirm our index is now being managed cy.get(`tbody > tr:contains("${SAMPLE_INDEX}") > td`).filter(`:nth-child(4)`).contains("Yes"); + + // Confirm the information shows in detail modal + cy.get(`[data-test-subj="viewIndexDetailButton-${SAMPLE_INDEX}"]`).click(); + cy.get(`[data-test-subj="index-detail-overview-item-Managed by policy"] .euiDescriptionList__description a`).contains(POLICY_ID); + }); + }); + + describe("can make indices deleted", () => { + before(() => { + cy.deleteAllIndices(); + cy.createIndex(SAMPLE_INDEX); + }); + + it("successfully", () => { + // Confirm we have our initial index + cy.contains(SAMPLE_INDEX); + + // Click actions button + cy.get('[data-test-subj="moreAction"]').click(); + + // Delete btn should be disabled if no items selected + cy.get('[data-test-subj="deleteAction"]').should("have.class", "euiContextMenuItem-isDisabled"); + + // click any where to hide actions + cy.get("#_selection_column_sample_index-checkbox").click(); + cy.get('[data-test-subj="deleteAction"]').should("not.exist"); + + // Click actions button + cy.get('[data-test-subj="moreAction"]').click(); + // Delete btn should be enabled + cy.get('[data-test-subj="deleteAction"]').should("exist").should("not.have.class", "euiContextMenuItem-isDisabled").click(); + // The confirm button should be disabled + cy.get('[data-test-subj="Delete Confirm button"]').should("have.class", "euiButton-isDisabled"); + // type delete + cy.get('[placeholder="delete"]').type("delete"); + cy.get('[data-test-subj="Delete Confirm button"]').should("not.have.class", "euiContextMenuItem-isDisabled"); + // click to delete + cy.get('[data-test-subj="Delete Confirm button"]').click(); + // the sample_index should not exist + cy.wait(500); + cy.get("#_selection_column_sample_index-checkbox").should("not.exist"); + }); + }); + + describe("shows detail of a index when click the item", () => { + before(() => { + cy.deleteAllIndices(); + cy.createIndex(SAMPLE_INDEX); + }); + + it("successfully", () => { + cy.get(`[data-test-subj="viewIndexDetailButton-${SAMPLE_INDEX}"]`).click(); + cy.get(`[data-test-subj="index-detail-overview-item-Index name"] .euiDescriptionList__description > span`).should( + "have.text", + SAMPLE_INDEX + ); + }); + }); + + describe("can search with reindex & recovery status", () => { + const reindexedIndex = "reindex_opensearch_dashboards_sample_data_ecommerce"; + const splittedIndex = "split_opensearch_dashboards_sample_data_logs"; + before(() => { + cy.deleteAllIndices(); + // Visit ISM OSD + cy.visit(`${Cypress.env("opensearch_dashboards")}/app/${PLUGIN_NAME}#/indices`); + + // Common text to wait for to confirm page loaded, give up to 60 seconds for initial load + cy.contains("Rows per page", { timeout: 60000 }); + + cy.request({ + method: "POST", + url: `${Cypress.env("opensearch_dashboards")}/api/sample_data/ecommerce`, + headers: { + "osd-xsrf": true, + }, + }).then((response) => { + expect(response.status).equal(200); + }); + + cy.request({ + method: "POST", + url: `${Cypress.env("opensearch_dashboards")}/api/sample_data/logs`, + headers: { + "osd-xsrf": true, + }, + }).then((response) => { + expect(response.status).equal(200); + }); + + cy.request({ + method: "PUT", + url: `${Cypress.env("opensearch")}/${splittedIndex}/_settings`, + body: { + "index.blocks.read_only": false, + }, + failOnStatusCode: false, + }); + cy.request({ + method: "DELETE", + url: `${Cypress.env("opensearch")}/${reindexedIndex}`, + failOnStatusCode: false, + }); + cy.request({ + method: "DELETE", + url: `${Cypress.env("opensearch")}/${splittedIndex}`, + failOnStatusCode: false, + }); + }); + + it("Successfully", () => { + cy.request({ + method: "PUT", + url: `${Cypress.env("opensearch")}/${reindexedIndex}`, + body: { + settings: { + index: { + number_of_shards: 1, + number_of_replicas: "0", + }, + }, + }, + }); + // do a simple reindex + cy.request("POST", `${Cypress.env("opensearch")}/_reindex?wait_for_completion=false`, { + source: { + index: "opensearch_dashboards_sample_data_ecommerce", + }, + dest: { + index: reindexedIndex, + }, + }); + + cy.get('[placeholder="Search"]').type("o"); + + // do a simple split + cy.request("PUT", `${Cypress.env("opensearch")}/opensearch_dashboards_sample_data_logs/_settings`, { + "index.blocks.write": true, + }); + + cy.request({ + method: "POST", + url: `${Cypress.env("opensearch_dashboards")}/api/ism/apiCaller`, + headers: { + "osd-xsrf": true, + }, + body: { + endpoint: "indices.split", + data: { + index: "opensearch_dashboards_sample_data_logs", + target: splittedIndex, + body: { + settings: { + index: { + number_of_shards: 2, + }, + }, + }, + }, + }, + }); + + cy.get('[placeholder="Search"]').type("p"); + }); + + after(() => { + cy.request({ + method: "DELETE", + url: `${Cypress.env("opensearch")}/${reindexedIndex}`, + failOnStatusCode: false, + }); + cy.request({ + method: "DELETE", + url: `${Cypress.env("opensearch")}/${splittedIndex}`, + failOnStatusCode: false, + }); + }); + }); + + describe("can close and open an index", () => { + before(() => { + cy.deleteAllIndices(); + cy.createIndex(SAMPLE_INDEX); + }); + + it("successfully close an index", () => { + cy.contains(SAMPLE_INDEX); + + cy.get('[data-test-subj="moreAction"]').click(); + // Close btn should be disabled if no items selected + cy.get('[data-test-subj="Close Action"]').should("have.class", "euiContextMenuItem-isDisabled"); + + // Select an index + cy.get(`[data-test-subj="checkboxSelectRow-${SAMPLE_INDEX}"]`).check({ force: true }); + + cy.get('[data-test-subj="moreAction"]').click(); + // Close btn should be enabled + cy.get('[data-test-subj="Close Action"]').should("exist").should("not.have.class", "euiContextMenuItem-isDisabled").click(); + + // Check for close index modal + cy.contains("Close indices"); + + // Close confirm button should be disabled + cy.get('[data-test-subj="Close Confirm button"]').should("have.class", "euiButton-isDisabled"); + // type close + cy.get('[placeholder="close"]').type("close"); + cy.get('[data-test-subj="Close Confirm button"]').should("not.have.class", "euiContextMenuItem-isDisabled"); + + // Click close confirm button + cy.get('[data-test-subj="Close Confirm button"]').click(); + + // Check for success toast + cy.contains("Close [sample_index] successfully"); + + // Confirm the index is closed + cy.get(`input[type="search"]`).focus().type(SAMPLE_INDEX); + cy.get("tbody > tr").should(($tr) => { + expect($tr, "1 row").to.have.length(1); + expect($tr, "item").to.contain("close"); + }); + }); + + it("successfully open an index", () => { + // Confirm we have our initial index + cy.contains(SAMPLE_INDEX); + + cy.get('[data-test-subj="moreAction"]').click(); + // Open btn should be disabled if no items selected + cy.get('[data-test-subj="Open Action"]').should("have.class", "euiContextMenuItem-isDisabled"); + + // Select an index + cy.get(`[data-test-subj="checkboxSelectRow-${SAMPLE_INDEX}"]`).check({ force: true }); + + cy.get('[data-test-subj="moreAction"]').click(); + // Open btn should be enabled + cy.get('[data-test-subj="Open Action"]').should("exist").should("not.have.class", "euiContextMenuItem-isDisabled").click(); + + // Check for open index modal + cy.contains("Open indices"); + + cy.get('[data-test-subj="Open Confirm button"]').click(); + + // Check for success toast + cy.contains("Open [sample_index] successfully"); + + // Confirm the index is open + cy.get(`input[type="search"]`).focus().type(SAMPLE_INDEX); + cy.get("tbody > tr").should(($tr) => { + expect($tr, "1 row").to.have.length(1); + expect($tr, "item").to.contain("open"); + }); }); }); }); diff --git a/models/interfaces.ts b/models/interfaces.ts index 136a900a2..1caf6843b 100644 --- a/models/interfaces.ts +++ b/models/interfaces.ts @@ -23,6 +23,66 @@ export interface ManagedIndexMetaData { info?: object; } +export type MappingsPropertiesObject = Record< + string, + { + type: string; + properties?: MappingsPropertiesObject; + } +>; + +export type MappingsProperties = { + fieldName: string; + type: string; + path?: string; + analyzer?: string; + properties?: MappingsProperties; +}[]; + +export interface IndexItem { + index: string; + indexUuid?: string; + data_stream: string | null; + settings?: { + index?: { + number_of_shards?: number; + number_of_replicas?: number; + creation_date?: string; + [key: string]: any; + }; + "index.number_of_shards"?: number; + "index.number_of_replicas"?: number; + "index.refresh_interval"?: string; + [key: string]: any; + }; + aliases?: Record; + mappings?: { + properties?: MappingsProperties; + [key: string]: any; + }; +} + +export interface IndexItemRemote extends Omit { + mappings?: { + properties?: MappingsPropertiesObject; + }; +} + +interface ITemplateExtras { + name: string; + data_stream?: {}; + version: number; + priority: number; + index_patterns: string[]; +} + +export interface TemplateItem extends ITemplateExtras { + template: Pick; +} +export interface TemplateItemRemote extends ITemplateExtras { + template: Pick; +} + /** * ManagedIndex item shown in the Managed Indices table */ diff --git a/public/pages/CreateIndex/components/AliasSelect/AliasSelect.test.tsx b/public/pages/CreateIndex/components/AliasSelect/AliasSelect.test.tsx new file mode 100644 index 000000000..b78e65f70 --- /dev/null +++ b/public/pages/CreateIndex/components/AliasSelect/AliasSelect.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from "react"; +import { render, waitFor } from "@testing-library/react"; +import AliasSelect, { AliasSelectProps } from "./index"; +import userEvent from "@testing-library/user-event"; + +const onChangeMock = jest.fn(); + +const AliasSelectWithOnchange = (props: AliasSelectProps) => { + const [tempValue, setTempValue] = useState(props.value); + return ( + { + onChangeMock(val); + setTempValue(val); + }} + /> + ); +}; + +describe(" spec", () => { + it("renders the component and remove duplicate aliases", async () => { + const onOptionsChange = jest.fn(); + const { container } = render( + + Promise.resolve({ + ok: true, + response: [ + { + alias: "a", + index: "a", + }, + { + alias: "a", + index: "b", + }, + ], + }) + } + onChange={() => {}} + onOptionsChange={onOptionsChange} + /> + ); + await waitFor( + () => { + expect(onOptionsChange).toBeCalledWith([ + { + label: "a", + }, + ]); + expect(container.firstChild).toMatchSnapshot(); + }, + { + timeout: 3000, + } + ); + }); + + it("renders with error", async () => { + const onOptionsChange = jest.fn(); + const { container } = render( + + Promise.resolve({ + ok: false, + error: "Some error", + }) + } + onChange={() => {}} + onOptionsChange={onOptionsChange} + /> + ); + await waitFor(() => {}); + expect(container).toMatchSnapshot(); + }); + + it("it should choose options or create one", async () => { + const { getByTestId } = render( + Promise.resolve({ ok: true, response: [{ alias: "test", index: "123", query: "test" }] })} + /> + ); + await waitFor(() => { + expect(getByTestId("comboBoxInput")).toBeInTheDocument(); + }); + await userEvent.click(getByTestId("comboBoxInput")); + await waitFor(() => { + expect(document.querySelector('button[title="test"]')).toBeInTheDocument(); + }); + await userEvent.click(document.querySelector('button[title="test"]') as Element); + await waitFor(() => { + expect(onChangeMock).toBeCalledTimes(1); + expect(onChangeMock).toBeCalledWith({ + test: {}, + }); + }); + await userEvent.type(getByTestId("comboBoxInput"), "test2{enter}"); + await waitFor(() => { + expect(onChangeMock).toBeCalledTimes(2); + expect(onChangeMock).toBeCalledWith({ + test: {}, + test2: {}, + }); + }); + await userEvent.type(getByTestId("comboBoxInput"), " {enter}"); + await waitFor(() => { + expect(onChangeMock).toBeCalledTimes(2); + }); + }); +}); diff --git a/public/pages/CreateIndex/components/AliasSelect/__snapshots__/AliasSelect.test.tsx.snap b/public/pages/CreateIndex/components/AliasSelect/__snapshots__/AliasSelect.test.tsx.snap new file mode 100644 index 000000000..fe44d3c78 --- /dev/null +++ b/public/pages/CreateIndex/components/AliasSelect/__snapshots__/AliasSelect.test.tsx.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component and remove duplicate aliases 1`] = ` + + } + > + <> + + + } + titleSize="s" + > + {content} + + ); + })()} + {templateSimulateLoading ? ( + + + We are simulating your template with existing templates, please wait for a second. + + ) : null} + + ); +}; + +// @ts-ignore +export default forwardRef(IndexDetail); diff --git a/public/pages/CreateIndex/components/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap b/public/pages/CreateIndex/components/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap new file mode 100644 index 000000000..351127895 --- /dev/null +++ b/public/pages/CreateIndex/components/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap @@ -0,0 +1,177 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +
+
+
+

+ Define index +

+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ Must be in lowercase letters. Cannot begin with underscores or hyphens. Spaces, commas, and characters :, ", *, +, /, , |, ?, #, > are not allowed. +
+
+
+
+
+
+
+
+
+ +
+
+
+ Allow this index to be referenced by existing aliases or specify a new alias. +
+ + +
+
+
+
+`; diff --git a/public/pages/CreateIndex/components/IndexDetail/index.ts b/public/pages/CreateIndex/components/IndexDetail/index.ts new file mode 100644 index 000000000..f46f9f20e --- /dev/null +++ b/public/pages/CreateIndex/components/IndexDetail/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import IndexDetail from "./IndexDetail"; + +export default IndexDetail; +export * from "./IndexDetail"; diff --git a/public/pages/CreateIndex/components/IndexMapping/IndexMapping.scss b/public/pages/CreateIndex/components/IndexMapping/IndexMapping.scss new file mode 100644 index 000000000..44d2f388a --- /dev/null +++ b/public/pages/CreateIndex/components/IndexMapping/IndexMapping.scss @@ -0,0 +1,19 @@ +.index-mapping-tree { + .euiTreeView__node { + max-height: unset; + line-height: unset; + } + .euiTreeView__nodeInner { + height: auto; + padding: 4px 0; + margin-top: 4px; + margin-bottom: 4px; + } +} + +.ism-index-mappings-field-line { + > * { + margin-right: 8px; + vertical-align: middle; + } +} \ No newline at end of file diff --git a/public/pages/CreateIndex/components/IndexMapping/IndexMapping.test.tsx b/public/pages/CreateIndex/components/IndexMapping/IndexMapping.test.tsx new file mode 100644 index 000000000..7e234d5d2 --- /dev/null +++ b/public/pages/CreateIndex/components/IndexMapping/IndexMapping.test.tsx @@ -0,0 +1,165 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { forwardRef, Ref, useRef, useState } from "react"; +import { render, fireEvent, waitFor, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import IndexMapping, { IIndexMappingsRef, IndexMappingProps, transformObjectToArray } from "./IndexMapping"; +import { MappingsProperties } from "../../../../../models/interfaces"; +import { renderHook } from "@testing-library/react-hooks"; + +const IndexMappingOnChangeWrapper = forwardRef((props: Partial, ref: Ref) => { + const [value, setValue] = useState(props.value as any); + return ( + { + setValue(val); + }} + /> + ); +}); + +describe(" spec", () => { + it("renders the component", () => { + const { container } = render( {}} />); + expect(container.firstChild).toMatchSnapshot(); + }); + + it("render mappings with object type", () => { + const { container } = render( + {}} + value={{ + properties: [{ fieldName: "object", type: "object", properties: [{ fieldName: "text", type: "text" }] }], + }} + /> + ); + expect(container).toMatchSnapshot(); + }); + + it("render mappings with oldValue in edit mode and all operation works well", async () => { + const { result } = renderHook(() => { + const ref = useRef(null); + const renderResult = render( + + ); + + return { + renderResult, + ref, + }; + }); + const { renderResult, ref } = result.current; + const { getByTestId, getByText, queryByTestId, queryByText } = renderResult; + + // old field disable check + expect(getByTestId("mapping-visual-editor-0-field-name")).toHaveAttribute("title", "object"); + expect(getByTestId("mapping-visual-editor-0.properties.0-field-name")).toHaveAttribute("title", "text"); + expect(document.querySelector('[data-test-subj="mapping-visual-editor-0-delete-field"]')).toBeNull(); + expect(document.querySelector('[data-test-subj="mapping-visual-editor-0.properties.0-add-sub-field"]')).toBeNull(); + expect(document.querySelector('[data-test-subj="mapping-visual-editor-0.properties.0-delete-field"]')).toBeNull(); + + // add a new field + userEvent.click(getByTestId("createIndexAddFieldButton")); + // new field should be editable + expect(getByTestId("mapping-visual-editor-1-field-name")).not.toHaveAttribute("disabled"); + expect(document.querySelector('[data-test-subj="mapping-visual-editor-1-delete-field"]')).not.toBeNull(); + + // empty and duplicate validation for field name + userEvent.click(document.querySelector('[data-test-subj="mapping-visual-editor-1-field-name"]') as Element); + expect(getByTestId("mapping-visual-editor-1-field-name")).toHaveValue(""); + expect(queryByText("Field name is required, please input")).toBeNull(); + userEvent.type(getByTestId("mapping-visual-editor-1-field-name"), "object"); + await waitFor(() => { + expect(getByText("Duplicate field name [object], please change your field name")).not.toBeNull(); + }); + await act(async () => { + expect(await ref.current?.validate()).toEqual("with error"); + }); + userEvent.clear(getByTestId("mapping-visual-editor-1-field-name")); + userEvent.type(getByTestId("mapping-visual-editor-1-field-name"), "new_object"); + await act(async () => { + expect(await ref.current?.validate()).toEqual(""); + }); + + await waitFor(() => { + expect(queryByText("Duplicate field name [object], please change your field name")).toBeNull(); + }); + + // only show the sub action for type of object + expect(queryByTestId("mapping-visual-editor-1-add-sub-field")).toBeNull(); + + // change type to object + fireEvent.change(getByTestId("mapping-visual-editor-1-field-type"), { + target: { + value: "object", + }, + }); + + // sub action for object + expect(getByTestId("mapping-visual-editor-1-add-sub-field")).not.toBeNull(); + userEvent.click(getByTestId("mapping-visual-editor-1-add-sub-field")); + // new sub field check + expect((getByTestId("mapping-visual-editor-1.properties.0-field-type") as HTMLSelectElement).value).toBe("text"); + await waitFor(() => { + userEvent.click(getByTestId("mapping-visual-editor-1.properties.0-delete-field")); + }); + + // add a new field + userEvent.click(getByTestId("createIndexAddFieldButton")); + // delete the new field + await waitFor(() => {}); + userEvent.click(getByTestId("mapping-visual-editor-2-delete-field")); + expect(queryByTestId("mapping-visual-editor-2-delete-field")).toBeNull(); + + await userEvent.click(getByTestId("editorTypeJsonEditor").querySelector("input") as Element); + await waitFor(() => {}); + userEvent.click(getByTestId("previousMappingsJsonButton")); + await waitFor(() => {}); + expect(queryByTestId("previousMappingsJsonModal-ok")).not.toBeNull(); + userEvent.click(getByTestId("previousMappingsJsonModal-ok")); + await waitFor(() => { + expect(queryByTestId("previousMappingsJsonModal-ok")).toBeNull(); + }); + }); + + it("it transformObjectToArray", () => { + expect( + transformObjectToArray({ + test: { + type: "text", + properties: { + test_children: { + type: "text", + }, + }, + }, + }) + ).toEqual([ + { + fieldName: "test", + type: "text", + properties: [ + { + fieldName: "test_children", + type: "text", + }, + ], + }, + ] as MappingsProperties); + }); +}); diff --git a/public/pages/CreateIndex/components/IndexMapping/IndexMapping.tsx b/public/pages/CreateIndex/components/IndexMapping/IndexMapping.tsx new file mode 100644 index 000000000..07b5445d0 --- /dev/null +++ b/public/pages/CreateIndex/components/IndexMapping/IndexMapping.tsx @@ -0,0 +1,366 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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, MappingsPropertiesObject } from "../../../../../models/interfaces"; +import CustomFormRow from "../../../../components/CustomFormRow"; +import MappingLabel, { IMappingLabelRef } from "../MappingLabel"; +import "./IndexMapping.scss"; + +export const transformObjectToArray = (obj: MappingsPropertiesObject): MappingsProperties => { + return Object.entries(obj).map(([fieldName, fieldSettings]) => { + const { properties, ...others } = fieldSettings; + const payload: MappingsProperties[number] = { + ...others, + fieldName, + }; + if (properties) { + payload.properties = transformObjectToArray(properties); + } + return payload; + }); +}; + +export const transformArrayToObject = (array: MappingsProperties): MappingsPropertiesObject => { + return array.reduce((total, current) => { + const { fieldName, properties, ...others } = current; + const payload: MappingsPropertiesObject[string] = { + ...others, + }; + if (properties) { + payload.properties = transformArrayToObject(properties); + } + return { + ...total, + [current.fieldName]: payload, + }; + }, {} as MappingsPropertiesObject); +}; + +const countNodesInTree = (array: MappingsProperties) => { + return array.reduce((total, current) => { + total = total + 1; + const { properties } = current; + if (properties) { + total = total + countNodesInTree(properties); + } + return total; + }, 0); +}; + +export type IndexMappingsAll = { + properties?: MappingsProperties; + [key: string]: any; +}; + +export type IndexMappingsObjectAll = { + properties?: MappingsPropertiesObject; + [key: string]: any; +}; + +export interface IndexMappingProps { + value?: IndexMappingsAll; + oldValue?: IndexMappingsAll; + originalValue?: IndexMappingsAll; + onChange: (value: IndexMappingProps["value"]) => void; + isEdit?: boolean; + oldMappingsEditable?: boolean; // in template edit case, existing mappings is editable + readonly?: boolean; +} + +export enum EDITOR_MODE { + JSON = "JSON", + VISUAL = "VISUAL", +} + +export interface IIndexMappingsRef { + validate: () => Promise; +} + +const IndexMapping = ( + { value: propsValue, onChange: propsOnChange, isEdit, oldValue, readonly, oldMappingsEditable }: IndexMappingProps, + ref: Ref +) => { + const value = propsValue?.properties || []; + const onChange = (val: MappingsProperties) => { + propsOnChange({ + ...propsValue, + properties: val, + }); + }; + const allFieldsRef = useRef>({}); + const JSONEditorRef = useRef(null); + useImperativeHandle(ref, () => ({ + validate: async () => { + const values = await Promise.all(Object.values(allFieldsRef.current).map((item) => item.validate())); + const JSONEditorValidateResult = await JSONEditorRef.current?.validate(); + return values.some((item) => item) || JSONEditorValidateResult ? "with error" : ""; + }, + })); + const [editorMode, setEditorMode] = useState(EDITOR_MODE.VISUAL); + const addField = useCallback( + (pos, fieldSettings?: Partial) => { + const newValue = [...(value || [])]; + const nowProperties = ((pos ? get(newValue, pos) : (newValue as MappingsProperties)) || []) as MappingsProperties; + nowProperties.push({ + fieldName: fieldSettings?.fieldName || "", + type: "text", + ...fieldSettings, + }); + if (pos) { + set(newValue, pos, nowProperties); + } + onChange(newValue); + }, + [onChange, value] + ); + const deleteField = useCallback( + (pos) => { + const newValue = [...(value || [])]; + const splittedArray = pos.split("."); + const index = splittedArray[splittedArray.length - 1]; + const prefix = splittedArray.slice(0, -1); + const prefixPos = prefix.join("."); + const nowProperties = ((prefixPos ? get(newValue, prefixPos) : (newValue as MappingsProperties)) || []) as MappingsProperties; + nowProperties.splice(index, 1); + + if (prefixPos) { + set(newValue, prefixPos, nowProperties); + } + + onChange(newValue); + }, + [onChange, value] + ); + const transformValueToTreeItems = (formValue: MappingsProperties, pos: string = ""): EuiTreeViewProps["items"] => { + let isFirstEditableField = false; + return (formValue || []).map((item, index) => { + const { fieldName, ...fieldSettings } = item; + const id = [pos, index].filter((item) => item !== "").join(".properties."); + const readonlyFlag = readonly || (isEdit && !!get(oldValue?.properties, id)); + let shouldShowLabel = false; + if (!readonlyFlag && !isFirstEditableField) { + isFirstEditableField = true; + shouldShowLabel = true; + } + const payload: EuiTreeViewProps["items"][number] = { + label: ( + { + if (ref) { + allFieldsRef.current[id] = ref; + } else { + delete allFieldsRef.current[id]; + } + }} + readonly={readonlyFlag} + value={item} + id={`mapping-visual-editor-${id}`} + onFieldNameCheck={(fieldName) => { + const hasDuplicateName = (formValue || []) + .filter((sibItem, sibIndex) => sibIndex < index) + .some((sibItem) => sibItem.fieldName === fieldName); + if (hasDuplicateName) { + return `Duplicate field name [${fieldName}], please change your field name`; + } + + return ""; + }} + onChange={(val, key, v) => { + const newValue = [...(value || [])]; + set(newValue, id, val); + onChange(newValue); + }} + onDeleteField={() => { + deleteField(id); + }} + onAddSubField={() => { + addField(`${id}.properties`); + }} + onAddSubObject={() => { + addField(`${id}.properties`, { + type: "", + }); + }} + /> + ), + id: `mapping-visual-editor-${id}`, + icon: , + iconWhenExpanded: , + }; + if (fieldSettings.properties) { + (payload.icon = ), + (payload.iconWhenExpanded = ), + (payload.children = transformValueToTreeItems(fieldSettings.properties, id)); + } + + return payload; + }); + }; + const transformedTreeItems = useMemo(() => transformValueToTreeItems(value), [value]); + const newValue = useMemo(() => { + const oldValueKeys = (oldValue?.properties || []).map((item) => item.fieldName); + return value?.filter((item) => !oldValueKeys.includes(item.fieldName)) || []; + }, [oldValue?.properties, value]); + const renderKey = useMemo(() => { + return countNodesInTree(value || []); + }, [value]); + return ( + <> + setEditorMode(id as EDITOR_MODE)} + legend="Editor Type" + options={[ + { + label: readonly ? "Tree view" : "Visual Editor", + id: EDITOR_MODE.VISUAL, + "data-test-subj": "editorTypeVisualEditor", + }, + { + label: readonly ? "JSON" : "JSON Editor", + id: EDITOR_MODE.JSON, + "data-test-subj": "editorTypeJsonEditor", + }, + ]} + /> + + {editorMode === EDITOR_MODE.VISUAL ? ( + <> + {transformedTreeItems.length ? ( + + ) : ( +

You have no field mappings.

+ )} + {readonly ? null : ( + <> + + addField("")}> + Add new field + + + addField("", { + type: "object", + }) + } + > + Add new object + + + )} + + ) : ( + <> + {isEdit && !readonly && !isEmpty(oldValue) ? ( + <> + { + Modal.show({ + title: "Previous mappings", + content: ( + + ), + "data-test-subj": "previousMappingsJsonModal", + onOk: () => {}, + }); + }} + > + See previous settings + + + + ) : null} + {readonly ? ( + + ) : ( + +
+ Specify mapping in JSON format.{" "} + + View mapping example. + +
+ {oldMappingsEditable ? null : ( +
+ {isEdit + ? "Mappings and field types cannot be changed once they have been added." + : "The existing mapping properties cannot be changed after the index is created."} +
+ )} +
+ } + fullWidth + > + { + const result: IndexMappingsObjectAll = JSON.parse(val); + propsOnChange({ + ...result, + properties: [...(oldValue?.properties || []), ...transformObjectToArray(result?.properties || {})], + }); + }} + width="100%" + ref={JSONEditorRef} + /> + + )} + + )} + + ); +}; + +// @ts-ignore +export default forwardRef(IndexMapping); diff --git a/public/pages/CreateIndex/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap b/public/pages/CreateIndex/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap new file mode 100644 index 000000000..95eb79bc3 --- /dev/null +++ b/public/pages/CreateIndex/components/IndexMapping/__snapshots__/IndexMapping.test.tsx.snap @@ -0,0 +1,794 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec render mappings with object type 1`] = ` +
+
+ + Editor Type + +
+ + +
+
+
+
+

+ You can quickly navigate this list using arrow keys. +

+
    +
  • + +
    +
    +
      +
    • + +
      +
    • +
    +
    +
    +
  • +
+
+
+ + +
+`; + +exports[` spec renders the component 1`] = ` +
+ + Editor Type + +
+ + +
+
+`; diff --git a/public/pages/CreateIndex/components/IndexMapping/index.ts b/public/pages/CreateIndex/components/IndexMapping/index.ts new file mode 100644 index 000000000..c5249554c --- /dev/null +++ b/public/pages/CreateIndex/components/IndexMapping/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import IndexMapping from "./IndexMapping"; + +export * from "./IndexMapping"; +export default IndexMapping; diff --git a/public/pages/CreateIndex/components/MappingLabel/MappingLabel.test.tsx b/public/pages/CreateIndex/components/MappingLabel/MappingLabel.test.tsx new file mode 100644 index 000000000..93ef6e14e --- /dev/null +++ b/public/pages/CreateIndex/components/MappingLabel/MappingLabel.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import { render } from "@testing-library/react"; +import MappingLabel from "./MappingLabel"; + +describe(" spec", () => { + // main unit test case is in IndexMapping.test.tsx + it("render component", async () => { + const { container } = render( + { + return ""; + }} + onFieldNameCheck={function (val: string): string { + throw new Error("Function not implemented."); + }} + onAddSubField={function (): void { + throw new Error("Function not implemented."); + }} + onAddSubObject={function (): void { + throw new Error("Function not implemented."); + }} + onDeleteField={function (): void { + throw new Error("Function not implemented."); + }} + id={""} + value={{ + fieldName: "text", + type: "text", + }} + /> + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/public/pages/CreateIndex/components/MappingLabel/MappingLabel.tsx b/public/pages/CreateIndex/components/MappingLabel/MappingLabel.tsx new file mode 100644 index 000000000..bfdb5ab61 --- /dev/null +++ b/public/pages/CreateIndex/components/MappingLabel/MappingLabel.tsx @@ -0,0 +1,268 @@ +import React, { forwardRef, useCallback, useRef, useImperativeHandle } from "react"; +import { + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiToolTip, + EuiCode, + EuiBadge, + EuiText, + EuiContextMenu, + 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"; + +const OLD_VALUE_DISABLED_REASON = "Old mappings can not be modified"; + +interface IMappingLabel { + value: MappingsProperties[number]; + onChange: (val: IMappingLabel["value"], key: string, value: string) => void | string; + onFieldNameCheck: (val: string) => string; + onAddSubField: () => void; + onAddSubObject: () => void; + onDeleteField: () => void; + readonly?: boolean; + id: string; + shouldShowLabel?: boolean; +} + +export interface IMappingLabelRef { + validate: () => Promise; +} + +const FormRow = (props: EuiFormRowProps & Pick) => { + const { shouldShowLabel, label, ...others } = props; + return ; +}; + +export const MappingLabel = forwardRef((props: IMappingLabel, forwardedRef: React.Ref) => { + const { onFieldNameCheck, onAddSubField, onAddSubObject, onDeleteField, id, readonly, shouldShowLabel } = props; + const propsRef = useRef(props); + propsRef.current = props; + const onFieldChange = useCallback( + (k, v) => { + let newValue = { ...propsRef.current.value }; + set(newValue, k, v); + if (k === "type") { + newValue = pick(newValue, ["fieldName", "type"]); + const findItem = INDEX_MAPPING_TYPES.find((item) => item.label === v); + const initValues = (findItem?.options?.fields || []).reduce((total, current) => { + if (current && current.initValue !== undefined) { + return { + ...total, + [transformNameToString(current.name)]: current.initValue, + }; + } + + return total; + }, {}); + newValue = { + ...newValue, + ...initValues, + }; + field.setValues(newValue); + } + return propsRef.current.onChange(newValue, k, v); + }, + [propsRef.current.value, propsRef.current.onChange] + ); + const field = useField({ + values: { + ...propsRef.current.value, + type: propsRef.current.value.type || "object", + }, + onChange: onFieldChange, + unmountComponent: true, + }); + const value = field.getValues(); + const type = value.type; + useImperativeHandle(forwardedRef, () => ({ + validate: async () => { + const { errors } = await field.validatePromise(); + if (errors) { + return "Validate Error"; + } else { + return ""; + } + }, + })); + + const findItem = INDEX_MAPPING_TYPES.find((item) => item.label === type); + const moreFields = findItem?.options?.fields || []; + + if (readonly) { + return ( + +
  • + + + {field.getValue("fieldName")} + + + {type} + + {moreFields.map((extraField) => ( + + {extraField.label}: {field.getValue(extraField.name)} + + ))} +
  • +
    + ); + } + + return ( + e.stopPropagation()}> + + + {readonly ? ( + {field.getValue("fieldName")} + ) : ( + { + const checkResult = onFieldNameCheck(value); + if (checkResult) { + return Promise.reject(checkResult); + } + + return Promise.resolve(""); + }, + }, + ], + })} + disabled={readonly} + disabledReason={readonly ? "" : OLD_VALUE_DISABLED_REASON} + compressed + data-test-subj={`${id}-field-name`} + /> + )} + + + + + {readonly ? ( + {type} + ) : ( + { + const allOptions = INDEX_MAPPING_TYPES.map((item) => ({ text: item.label, value: item.label })); + const typeValue = field.getValue("type"); + if (!allOptions.find((item) => item.value === typeValue)) { + allOptions.push({ + text: typeValue, + value: typeValue, + }); + } + return allOptions; + })()} + /> + )} + + + {moreFields.map((item) => { + const { label, type, ...others } = item; + const RenderComponent = readonly ? AllBuiltInComponents.Text : AllBuiltInComponents[type]; + return ( + + + + + + ); + })} + {readonly ? null : ( + + +
    { + e.stopPropagation(); + }} + > + {INDEX_MAPPING_TYPES_WITH_CHILDREN.includes(type) ? ( + + + + } + > + + + ) : null} + + + + + +
    +
    +
    + )} +
    + ); +}); + +export default MappingLabel; diff --git a/public/pages/CreateIndex/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap b/public/pages/CreateIndex/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap new file mode 100644 index 000000000..0e672e1fb --- /dev/null +++ b/public/pages/CreateIndex/components/MappingLabel/__snapshots__/MappingLabel.test.tsx.snap @@ -0,0 +1,240 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec render component 1`] = ` +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    + + EuiIconMock + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + EuiIconMock + + +
    +
    +
    +
    +
    +
    +`; diff --git a/public/pages/CreateIndex/components/MappingLabel/index.ts b/public/pages/CreateIndex/components/MappingLabel/index.ts new file mode 100644 index 000000000..c0deb2c16 --- /dev/null +++ b/public/pages/CreateIndex/components/MappingLabel/index.ts @@ -0,0 +1,4 @@ +import MappingLabel from "./MappingLabel"; + +export default MappingLabel; +export * from "./MappingLabel"; diff --git a/public/pages/CreateIndex/containers/CreateIndex/CreateIndex.test.tsx b/public/pages/CreateIndex/containers/CreateIndex/CreateIndex.test.tsx new file mode 100644 index 000000000..eb86d4cae --- /dev/null +++ b/public/pages/CreateIndex/containers/CreateIndex/CreateIndex.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 { CoreStart } from "opensearch-dashboards/public"; +import CreateIndex from "./CreateIndex"; +import { ServicesConsumer, ServicesContext } from "../../../../services"; +import { browserServicesMock, coreServicesMock, apiCallerMock } from "../../../../../test/mocks"; +import { BrowserServices } from "../../../../models/interfaces"; +import { ModalProvider, ModalRoot } from "../../../../components/Modal"; +import { ROUTES } from "../../../../utils/constants"; +import { CoreServicesConsumer, CoreServicesContext } from "../../../../components/core_services"; + +function renderCreateIndexWithRouter(initialEntries = [ROUTES.CREATE_INDEX] as string[]) { + return { + ...render( + + + + + {(services: BrowserServices | null) => + services && ( + + {(core: CoreStart | null) => + core && ( + + + + } + /> + } + /> + } + /> +

    location is: {ROUTES.INDEX_POLICIES}

    } + /> +
    +
    + ) + } +
    + ) + } +
    +
    +
    +
    + ), + }; +} + +describe(" spec", () => { + beforeEach(() => { + apiCallerMock(browserServicesMock); + }); + it("it goes to indices page when click cancel", async () => { + const { getByText } = renderCreateIndexWithRouter([`${ROUTES.CREATE_INDEX}/good_index`]); + await waitFor(() => {}); + userEvent.click(getByText("Cancel")); + await waitFor(() => { + expect(getByText(`location is: ${ROUTES.INDEX_POLICIES}`)).toBeInTheDocument(); + }); + }); + + it("it goes to indices page when click create successfully", async () => { + const { getByText, getByPlaceholderText } = renderCreateIndexWithRouter([`${ROUTES.CREATE_INDEX}`]); + + await waitFor(() => { + getByText("Define index"); + }); + + const indexNameInput = getByPlaceholderText("Specify a name for the new index."); + + userEvent.type(indexNameInput, `good_index`); + userEvent.click(document.body); + userEvent.click(getByText("Create")); + + await waitFor(() => { + expect(getByText(`location is: ${ROUTES.INDEX_POLICIES}`)).toBeInTheDocument(); + }); + }); +}); diff --git a/public/pages/CreateIndex/containers/CreateIndex/CreateIndex.tsx b/public/pages/CreateIndex/containers/CreateIndex/CreateIndex.tsx new file mode 100644 index 000000000..5da962e8b --- /dev/null +++ b/public/pages/CreateIndex/containers/CreateIndex/CreateIndex.tsx @@ -0,0 +1,61 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from "react"; +import { EuiSpacer, EuiTitle } from "@elastic/eui"; +import { RouteComponentProps } from "react-router-dom"; +import IndexForm from "../IndexForm"; +import { BREADCRUMBS, IndicesUpdateMode, ROUTES } from "../../../../utils/constants"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { CommonService } from "../../../../services/index"; + +interface CreateIndexProps extends RouteComponentProps<{ index?: string; mode?: IndicesUpdateMode }> { + isEdit?: boolean; + commonService: CommonService; +} + +export default class CreateIndex extends Component { + static contextType = CoreServicesContext; + + get index() { + return this.props.match.params.index; + } + + get isEdit() { + return this.props.match.params.index !== undefined; + } + + componentDidMount = async (): Promise => { + const isEdit = this.isEdit; + this.context.chrome.setBreadcrumbs([ + BREADCRUMBS.INDEX_MANAGEMENT, + BREADCRUMBS.INDICES, + isEdit ? BREADCRUMBS.EDIT_INDEX : BREADCRUMBS.CREATE_INDEX, + ]); + }; + + onCancel = (): void => { + this.props.history.push(ROUTES.INDICES); + }; + + render() { + const isEdit = this.isEdit; + + return ( +
    + +

    {isEdit ? "Edit" : "Create"} index

    +
    + + this.props.history.push(ROUTES.INDICES)} + /> +
    + ); + } +} diff --git a/public/pages/CreateIndex/containers/CreateIndex/index.ts b/public/pages/CreateIndex/containers/CreateIndex/index.ts new file mode 100644 index 000000000..78d14cd03 --- /dev/null +++ b/public/pages/CreateIndex/containers/CreateIndex/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import CreateIndex from "./CreateIndex"; + +export default CreateIndex; diff --git a/public/pages/CreateIndex/containers/IndexForm/IndexForm.test.tsx b/public/pages/CreateIndex/containers/IndexForm/IndexForm.test.tsx new file mode 100644 index 000000000..a7762d4fb --- /dev/null +++ b/public/pages/CreateIndex/containers/IndexForm/IndexForm.test.tsx @@ -0,0 +1,289 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import { render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import IndexForm, { IndexFormProps } from "./index"; +import { ServicesContext } from "../../../../services"; +import { browserServicesMock, coreServicesMock, apiCallerMock } from "../../../../../test/mocks"; +import { IndicesUpdateMode } from "../../../../utils/constants"; +import { CoreServicesContext } from "../../../../components/core_services"; + +function renderCreateIndexWithRouter(props: IndexFormProps) { + return { + ...render( + + + + + + ), + }; +} + +describe(" spec", () => { + beforeEach(() => { + apiCallerMock(browserServicesMock); + }); + it("show a toast if getIndices gracefully fails", async () => { + const { findByText } = renderCreateIndexWithRouter({ + index: "bad_index", + }); + + await findByText("Update"); + + expect(coreServicesMock.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(coreServicesMock.notifications.toasts.addDanger).toHaveBeenCalledWith("bad_error"); + }); + + it("shows error for index name input when clicking create", async () => { + const { queryByText, getByText } = renderCreateIndexWithRouter({}); + + await waitFor(() => getByText("Define index")); + + expect(queryByText("Invalid index name.")).toBeNull(); + + userEvent.click(getByText("Create")); + await waitFor(() => { + expect(queryByText("Invalid index name.")).not.toBeNull(); + }); + }); + + it("routes you back to indices and shows a success toast when successfully creating a index", async () => { + const { getByText, getByPlaceholderText, getByTestId } = renderCreateIndexWithRouter({}); + + await waitFor(() => { + getByText("Define index"); + }); + + const indexNameInput = getByPlaceholderText("Specify a name for the new index."); + + userEvent.type(indexNameInput, `bad_index`); + userEvent.click(document.body); + await waitFor(() => {}); + userEvent.clear(indexNameInput); + userEvent.type(indexNameInput, `good_index`); + userEvent.click(document.body); + await waitFor(() => expect(getByTestId("form-name-index.number_of_replicas").querySelector("input")).toHaveAttribute("value", "10")); + userEvent.click(getByText("Create")); + await waitFor(() => + expect(coreServicesMock.notifications.toasts.addSuccess).toHaveBeenCalledWith("good_index has been successfully created.") + ); + }); + + it("shows a danger toast when getting graceful error from create index", async () => { + const { getByText, getByPlaceholderText } = renderCreateIndexWithRouter({}); + + await waitFor(() => getByText("Define index")); + userEvent.type(getByPlaceholderText("Specify a name for the new index."), `bad_index`); + userEvent.click(getByText("Create")); + + await waitFor(() => expect(coreServicesMock.notifications.toasts.addDanger).toHaveBeenCalledWith("bad_index")); + }); + + it("it shows detail and does not call any api when nothing modified", async () => { + const { getByText } = renderCreateIndexWithRouter({ + index: "good_index", + }); + await waitFor(() => getByText("Define index")); + userEvent.click(getByText("Update")); + + await waitFor(() => { + // it shows detail and does not call any api when nothing modified + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(2); + }); + }); + + it("shows detail info and update others", async () => { + const { getByText, getByTestId, getByTitle } = renderCreateIndexWithRouter({ + index: "good_index", + }); + + await waitFor(() => getByText("Define index")); + + userEvent.click(getByTitle("update_test_1").querySelector("button") as Element); + userEvent.type(getByTestId("comboBoxSearchInput"), "test_1{enter}"); + userEvent.type(getByTestId("form-name-index.number_of_replicas").querySelector("input") as Element, "2"); + userEvent.click(getByTestId("createIndexAddFieldButton")); + await waitFor(() => {}); + await userEvent.clear(getByTestId("mapping-visual-editor-1-field-name")); + await userEvent.type(getByTestId("mapping-visual-editor-1-field-name"), "test_mapping_2"); + await userEvent.click(document.body); + userEvent.click(getByText("Update")); + + await waitFor(() => { + expect(coreServicesMock.notifications.toasts.addSuccess).toBeCalledTimes(1); + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "indices.updateAliases", + method: "PUT", + data: { + body: { + actions: [ + { + remove: { + index: "good_index", + alias: "update_test_1", + }, + }, + { + add: { + index: "good_index", + alias: "test_1", + }, + }, + ], + }, + }, + }); + + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "indices.putSettings", + method: "PUT", + data: { + index: "good_index", + flat_settings: true, + body: { + "index.number_of_replicas": "12", + }, + }, + }); + + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "indices.putMapping", + method: "PUT", + data: { + index: "good_index", + body: { + properties: { + test_mapping_2: { + type: "text", + }, + }, + }, + }, + }); + }); + }); + + it("shows detail alias and update alias only", async () => { + const { getByText, getByTestId, getByTitle } = renderCreateIndexWithRouter({ + index: "good_index", + mode: IndicesUpdateMode.alias, + }); + + await waitFor(() => {}); + + userEvent.click(getByTitle("update_test_1").querySelector("button") as Element); + userEvent.type(getByTestId("comboBoxSearchInput"), "test_1{enter}"); + await waitFor(() => {}); + userEvent.click(getByText("Update")); + + await waitFor(() => { + // shows detail alias and update alias only + expect(coreServicesMock.notifications.toasts.addSuccess).toBeCalledTimes(1); + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "indices.updateAliases", + method: "PUT", + data: { + body: { + actions: [ + { + remove: { + index: "good_index", + alias: "update_test_1", + }, + }, + { + add: { + index: "good_index", + alias: "test_1", + }, + }, + ], + }, + }, + }); + }); + }); + + it("shows detail mappings and update mappings only", async () => { + const { getByText, getByTestId } = renderCreateIndexWithRouter({ + index: "good_index", + mode: IndicesUpdateMode.mappings, + }); + + await waitFor(() => {}); + + userEvent.click(getByTestId("createIndexAddFieldButton")); + await waitFor(() => {}); + await userEvent.clear(getByTestId("mapping-visual-editor-1-field-name")); + await userEvent.type(getByTestId("mapping-visual-editor-1-field-name"), "test_mapping_2"); + await userEvent.click(document.body); + await waitFor(() => {}); + userEvent.click(getByText("Update")); + + await waitFor(() => { + expect(coreServicesMock.notifications.toasts.addSuccess).toBeCalledTimes(1); + + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "indices.putMapping", + method: "PUT", + data: { + index: "good_index", + body: { + properties: { + test_mapping_2: { + type: "text", + }, + }, + }, + }, + }); + }); + }); + + it("shows detail settings and update settings only", async () => { + const { getByText, getByTestId } = renderCreateIndexWithRouter({ + index: "good_index", + mode: IndicesUpdateMode.settings, + }); + + await waitFor(() => {}); + + userEvent.type(getByTestId("form-name-index.number_of_replicas").querySelector("input") as Element, "2"); + userEvent.click(getByText("Update")); + + await waitFor(() => { + expect(coreServicesMock.notifications.toasts.addSuccess).toBeCalledTimes(1); + + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "indices.putSettings", + method: "PUT", + data: { + index: "good_index", + flat_settings: true, + body: { + "index.number_of_replicas": "12", + }, + }, + }); + }); + }); + + it("it triggers onCancel when click cancel", async () => { + const onCancelMock = jest.fn(); + const { getByText } = renderCreateIndexWithRouter({ + index: "good_index", + onCancel: onCancelMock, + }); + await waitFor(() => {}); + userEvent.click(getByText("Cancel")); + await (() => { + expect(onCancelMock).toBeCalledTimes(1); + expect(onCancelMock).toBeCalledWith(undefined); + }); + }); +}); diff --git a/public/pages/CreateIndex/containers/IndexForm/index.tsx b/public/pages/CreateIndex/containers/IndexForm/index.tsx new file mode 100644 index 000000000..c151456cb --- /dev/null +++ b/public/pages/CreateIndex/containers/IndexForm/index.tsx @@ -0,0 +1,417 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component, forwardRef, useContext } from "react"; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiLoadingSpinner } from "@elastic/eui"; +import { get, set, differenceWith, isEqual, merge } from "lodash"; +import { diffArrays } from "diff"; +import flattern from "flat"; +import IndexDetail, { IndexDetailProps, IIndexDetailRef, defaultIndexSettings } from "../../components/IndexDetail"; +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 { ServerResponse } from "../../../../../server/models/types"; +import { BrowserServices } from "../../../../models/interfaces"; +import { ServicesContext } from "../../../../services"; + +export const getAliasActionsByDiffArray = ( + oldAliases: string[], + newAliases: string[], + callback: (val: string) => IAliasAction[string] +): IAliasAction[] => { + const diffedAliasArrayes = diffArrays(oldAliases, newAliases); + return diffedAliasArrayes.reduce((total: IAliasAction[], current) => { + if (current.added) { + return [ + ...total, + ...current.value.map((item) => ({ + add: callback(item), + })), + ]; + } else if (current.removed) { + return [ + ...total, + ...current.value.map((item) => ({ + remove: callback(item), + })), + ]; + } + + return total; + }, [] as IAliasAction[]); +}; + +export interface IndexFormProps extends Pick { + index?: string; + mode?: IndicesUpdateMode; + onCancel?: () => void; + onSubmitSuccess?: (indexName: string) => void; + hideButtons?: boolean; +} + +interface CreateIndexState { + indexDetail: IndexItem; + oldIndexDetail?: IndexItem; + isSubmitting: boolean; + loading: boolean; +} + +export class IndexForm extends Component { + static contextType = CoreServicesContext; + constructor(props: IndexFormProps & { services: BrowserServices }) { + super(props); + const isEdit = this.isEdit; + this.state = { + isSubmitting: false, + indexDetail: merge({}, defaultIndexSettings), + oldIndexDetail: undefined, + loading: isEdit, + }; + } + + componentDidMount(): void { + const isEdit = this.isEdit; + if (isEdit) { + this.refreshIndex(); + } + } + + indexDetailRef: IIndexDetailRef | null = null; + + get commonService() { + return this.props.services.commonService; + } + + get index() { + return this.props.index; + } + + get isEdit() { + return this.index !== undefined; + } + + get mode() { + return this.props.mode; + } + + getIndexDetail = async (indexName: string): Promise => { + const response = await this.commonService.apiCaller>({ + endpoint: "indices.get", + data: { + index: indexName, + flat_settings: true, + }, + }); + if (response.ok) { + return response.response[indexName]; + } + + this.context.notifications.toasts.addDanger(response.error); + return Promise.reject(); + }; + + refreshIndex = async () => { + this.setState({ + loading: true, + }); + try { + const indexDetail = await this.getIndexDetail(this.index as string); + const payload = { + ...indexDetail, + index: this.index, + }; + set(payload, "mappings.properties", transformObjectToArray(get(payload, "mappings.properties", {}))); + + this.setState({ + indexDetail: payload as IndexItem, + oldIndexDetail: JSON.parse(JSON.stringify(payload)), + }); + } catch (e) { + // do nothing + } finally { + this.setState({ + loading: false, + }); + } + }; + + onCancel = () => { + this.props.onCancel && this.props.onCancel(); + }; + + onDetailChange: IndexDetailProps["onChange"] = (value) => { + this.setState({ + indexDetail: { + ...this.state.indexDetail, + ...value, + }, + }); + }; + + updateAlias = async (): Promise> => { + const { indexDetail, oldIndexDetail } = this.state; + const { index } = indexDetail; + const aliasActions = getAliasActionsByDiffArray( + Object.keys(oldIndexDetail?.aliases || {}), + Object.keys(indexDetail.aliases || {}), + (alias) => ({ + index, + alias, + }) + ); + + if (aliasActions.length) { + return await this.commonService.apiCaller({ + endpoint: "indices.updateAliases", + method: "PUT", + data: { + body: { + actions: aliasActions, + }, + }, + }); + } + + return Promise.resolve({ + ok: true, + response: {}, + }); + }; + updateSettings = async (): Promise> => { + const { indexDetail, oldIndexDetail } = this.state; + const { index } = indexDetail; + + const newSettings = (indexDetail?.settings || {}) as Required["settings"]; + const oldSettings = (oldIndexDetail?.settings || {}) as Required["settings"]; + const differences = differenceWith(Object.entries(newSettings), Object.entries(oldSettings), isEqual); + if (!differences.length) { + return { + ok: true, + response: {}, + }; + } + + const finalSettings = differences.reduce((total, current) => { + if (newSettings[current[0]] !== undefined) { + return { + ...total, + [current[0]]: newSettings[current[0]], + }; + } + + return total; + }, {}); + + return await this.commonService.apiCaller({ + endpoint: "indices.putSettings", + method: "PUT", + data: { + index, + flat_settings: true, + // In edit mode, only dynamic settings can be modified + body: finalSettings, + }, + }); + }; + updateMappings = async (): Promise> => { + const { indexDetail, oldIndexDetail } = this.state; + const { index } = indexDetail; + // handle the mappings here + const newMappingProperties = indexDetail?.mappings?.properties || []; + const diffedMappingArrayes = diffArrays( + (oldIndexDetail?.mappings?.properties || []).map((item) => item.fieldName), + newMappingProperties.map((item) => item.fieldName) + ); + const newMappingFields: MappingsProperties = diffedMappingArrayes + .filter((item) => item.added) + .reduce((total, current) => [...total, ...current.value], [] as string[]) + .map((current) => newMappingProperties.find((item) => item.fieldName === current) as MappingsProperties[number]) + .filter((item) => item); + + const newMappingSettings = transformArrayToObject(newMappingFields); + if (!isEqual(indexDetail.mappings, oldIndexDetail?.mappings)) { + return await this.commonService.apiCaller({ + endpoint: "indices.putMapping", + method: "PUT", + data: { + index, + body: { + ...indexDetail.mappings, + properties: newMappingSettings, + }, + }, + }); + } + + return Promise.resolve({ + ok: true, + response: {}, + }); + }; + + chainPromise = async (promises: Promise>[]): Promise> => { + const newPromises = [...promises]; + while (newPromises.length) { + const result = (await newPromises.shift()) as ServerResponse; + if (!result?.ok) { + return result; + } + } + + return { + ok: true, + response: {}, + }; + }; + + getOrderedJson = (json: Record) => { + const entries = Object.entries(json); + entries.sort((a, b) => (a[0] < b[0] ? -1 : 1)); + return entries.reduce((total, [key, value]) => ({ ...total, [key]: value }), {}); + }; + + onSubmit = async (): Promise => { + const mode = this.mode; + const { indexDetail } = this.state; + const { index, mappings, ...others } = indexDetail; + if (!(await this.indexDetailRef?.validate())) { + return; + } + this.setState({ isSubmitting: true }); + let result: ServerResponse; + if (this.isEdit) { + let chainedPromises: Promise>[] = []; + if (!mode) { + chainedPromises.push(...[this.updateMappings(), this.updateAlias(), this.updateSettings()]); + } else { + switch (mode) { + case IndicesUpdateMode.alias: + chainedPromises.push(this.updateAlias()); + break; + case IndicesUpdateMode.settings: + chainedPromises.push(this.updateSettings()); + break; + case IndicesUpdateMode.mappings: + chainedPromises.push(this.updateMappings()); + break; + } + } + result = await this.chainPromise(chainedPromises); + } else { + result = await this.commonService.apiCaller({ + endpoint: "indices.create", + method: "PUT", + data: { + index, + body: { + ...others, + mappings: { + ...mappings, + properties: transformArrayToObject(mappings?.properties || []), + }, + }, + }, + }); + } + this.setState({ isSubmitting: false }); + + // handle all the response here + if (result && result.ok) { + this.context.notifications.toasts.addSuccess(`${indexDetail.index} has been successfully ${this.isEdit ? "updated" : "created"}.`); + this.props.onSubmitSuccess && this.props.onSubmitSuccess(indexDetail.index); + } else { + this.context.notifications.toasts.addDanger(result.error); + } + }; + + onSimulateIndexTemplate = (indexName: string): Promise> => { + return this.commonService + .apiCaller<{ template: IndexItemRemote }>({ + endpoint: "transport.request", + data: { + path: `/_index_template/_simulate_index/${indexName}`, + method: "POST", + }, + }) + .then((res) => { + if (res.ok && res.response && res.response.template) { + return { + ...res, + response: { + ...res.response.template, + settings: flattern(res.response.template?.settings || {}), + }, + }; + } + + return { + ok: false, + error: "", + } as ServerResponse; + }); + }; + + render() { + const isEdit = this.isEdit; + const { hideButtons, readonly } = this.props; + const { indexDetail, isSubmitting, oldIndexDetail, loading } = this.state; + + if (loading) { + return ; + } + + return ( + <> + (this.indexDetailRef = ref)} + isEdit={this.isEdit} + value={indexDetail} + oldValue={oldIndexDetail} + onChange={this.onDetailChange} + onSimulateIndexTemplate={this.onSimulateIndexTemplate} + sourceIndices={this.props.sourceIndices} + onGetIndexDetail={this.getIndexDetail} + refreshOptions={(aliasName) => + this.commonService.apiCaller({ + endpoint: "cat.aliases", + method: "GET", + data: { + format: "json", + name: aliasName, + expand_wildcards: "open", + }, + }) + } + /> + {hideButtons ? null : ( + <> + + + + + + Cancel + + + + + {isEdit ? "Update" : "Create"} + + + + + )} + + ); + } +} + +export default forwardRef(function IndexFormWrapper(props: IndexFormProps, ref: React.Ref) { + const services = useContext(ServicesContext); + return ; +}); diff --git a/public/pages/CreateIndex/index.ts b/public/pages/CreateIndex/index.ts new file mode 100644 index 000000000..a82e1ca1c --- /dev/null +++ b/public/pages/CreateIndex/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import CreateIndex from "./containers/CreateIndex"; + +export default CreateIndex; diff --git a/public/pages/IndexDetail/containers/IndexDetail/IndexDetail.test.tsx b/public/pages/IndexDetail/containers/IndexDetail/IndexDetail.test.tsx new file mode 100644 index 000000000..ceb1e2207 --- /dev/null +++ b/public/pages/IndexDetail/containers/IndexDetail/IndexDetail.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 IndexDetail, { IndexDetailModalProps } from "./IndexDetail"; +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("#indexDetailModalOverview")).not.toBeNull(); + expect(browserServicesMock.commonService.apiCaller).toBeCalledTimes(1); + expect(browserServicesMock.commonService.apiCaller).toBeCalledWith({ + endpoint: "indices.get", + data: { + index: "test_index", + }, + }); + }); + + userEvent.click(document.getElementById("indexDetailModalAlias") 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/IndexDetail/containers/IndexDetail/IndexDetail.tsx b/public/pages/IndexDetail/containers/IndexDetail/IndexDetail.tsx new file mode 100644 index 000000000..eccc7bd73 --- /dev/null +++ b/public/pages/IndexDetail/containers/IndexDetail/IndexDetail.tsx @@ -0,0 +1,415 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { + EuiTabbedContent, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexItem, + EuiSpacer, + EuiFlexGroup, + EuiButton, + EuiBasicTable, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiIcon, + EuiHealth, + EuiFormRow, + EuiLink, +} from "@elastic/eui"; +import { get } from "lodash"; +import { Link, RouteComponentProps } from "react-router-dom"; +import { IndexItem } from "../../../../../models/interfaces"; +import IndicesActions from "../../../Indices/containers/IndicesActions"; +import { ManagedCatIndex } from "../../../../../server/models/interfaces"; +import { BREADCRUMBS, IndicesUpdateMode, ROUTES } from "../../../../utils/constants"; +import { ServicesContext } from "../../../../services"; +import { BrowserServices } from "../../../../models/interfaces"; +import IndexFormWrapper, { IndexForm } from "../../../CreateIndex/containers/IndexForm"; +import { HEALTH_TO_COLOR } from "../../../Indices/utils/constants"; +import { CoreServicesContext } from "../../../../components/core_services"; +import { ContentPanel } from "../../../../components/ContentPanel"; + +export interface IndexDetailModalProps extends RouteComponentProps<{ index: string }> {} + +interface IFinalDetail extends ManagedCatIndex, IndexItem {} + +const OVERVIEW_DISPLAY_INFO: { + label: string; + value: string | React.FunctionComponent<{ detail: IFinalDetail }>; +}[] = [ + { + label: "Index name", + value: "index", + }, + { + label: "Health", + value: "health", + }, + { + label: "Status", + value: ({ detail }) => { + const health = detail.health; + const color = health ? HEALTH_TO_COLOR[health] : "subdued"; + const text = health || detail.status; + return ( + + {text} + + ); + }, + }, + { + label: "Creation date", + value: ({ detail }) => ( + + {detail.settings?.index?.creation_date ? new Date(parseInt(detail.settings?.index?.creation_date)).toLocaleString() : "-"} + + ), + }, + { + label: "Total size", + value: ({ detail }) => {detail["store.size"]}, + }, + { + label: "Size of primaries", + value: ({ detail }) => {detail["pri.store.size"]}, + }, + { + label: "Total documents", + value: ({ detail }) => {detail["docs.count"]}, + }, + { + label: "Deleted documents", + value: ({ detail }) => {detail["docs.deleted"]}, + }, + { + label: "Primaries", + value: "pri", + }, + { + label: "Replicas", + value: "rep", + }, + { + label: "Index blocks", + value: ({ detail }) => { + const blocks = Object.entries(detail.settings?.index?.blocks || {}).filter(([key, value]) => value === "true"); + if (!blocks.length) { + return -; + } + + return ( +
      + {blocks.map(([key, value]) => ( +
    • {key}
    • + ))} +
    + ); + }, + }, + { + label: "Managed by policy", + value: ({ detail }) => ( + + {detail.managedPolicy ? {detail.managedPolicy} : "-"} + + ), + }, +]; + +export default function IndexDetail(props: IndexDetailModalProps) { + const { index } = props.match.params; + const [record, setRecord] = useState(undefined); + const [editVisible, setEditVisible] = useState(false); + const [editMode, setEditMode] = useState(IndicesUpdateMode.settings); + const [detail, setDetail] = useState({} as IndexItem); + const ref = useRef(null); + const coreService = useContext(CoreServicesContext); + const finalDetail: IFinalDetail | undefined = useMemo(() => { + if (!detail || !record) { + return undefined; + } + + return { + ...record, + ...detail, + }; + }, [record, detail]); + const services = useContext(ServicesContext) as BrowserServices; + + const fetchIndicesDetail = () => + services.commonService + .apiCaller>({ + endpoint: "indices.get", + data: { + index, + }, + }) + .then((res) => { + if (!res.ok) { + return res; + } + + return { + ...res, + response: res.response[index], + }; + }) + .then((res) => { + if (res && res.ok) { + setDetail(res.response); + } else { + coreService?.notifications.toasts.addDanger(res.error || ""); + props.history.replace(ROUTES.INDICES); + } + + return res; + }); + + const fetchCatIndexDetail = async (props: { showDataStreams: "true" | "false" }) => { + const result = await services.indexService.getIndices({ + terms: index, + from: 0, + size: 10, + search: index, + sortField: "index", + sortDirection: "desc", + ...props, + }); + if (result.ok) { + const findItem = result.response.indices.find((item) => item.index === index); + setRecord(findItem); + } else { + coreService?.notifications.toasts.addDanger(result.error || ""); + } + }; + + const refreshDetails = async () => { + const result = await fetchIndicesDetail(); + if (result.ok) { + const { data_stream } = result.response; + const payload: { showDataStreams: "true" | "false"; search?: string; dataStreams?: string } = { + showDataStreams: data_stream ? "true" : "false", + }; + if (data_stream) { + payload.search = `data_streams: (${result.response.data_stream})`; + payload.dataStreams = data_stream; + } + + fetchCatIndexDetail(payload); + } + }; + + useEffect(() => { + refreshDetails(); + coreService?.chrome.setBreadcrumbs([ + BREADCRUMBS.INDEX_MANAGEMENT, + BREADCRUMBS.INDICES, + { + ...BREADCRUMBS.INDEX_DETAIL, + href: `#${props.location.pathname}`, + }, + ]); + }, []); + + const indexFormCommonProps = { + index, + onCancel: () => setEditVisible(false), + onSubmitSuccess: () => { + ref.current?.refreshIndex(); + setEditVisible(false); + fetchIndicesDetail(); + }, + }; + + const indexFormReadonlyCommonProps = { + ...indexFormCommonProps, + hideButtons: true, + readonly: true, + ref, + }; + + const onEdit = (editMode: IndicesUpdateMode) => { + setEditMode(editMode); + setEditVisible(true); + }; + + if (!record || !detail || !finalDetail) { + return null; + } + + return ( + + + {index} + + props.history.replace(ROUTES.INDICES)} + onOpen={refreshDetails} + onClose={refreshDetails} + onShrink={() => props.history.replace(ROUTES.INDICES)} + getIndices={async () => {}} + /> +
    + } + > + + +
    + {OVERVIEW_DISPLAY_INFO.map((item) => { + let valueContent = null; + if (typeof item.value === "string") { + valueContent = {get(finalDetail, item.value)}; + } else { + const ValueComponent = item.value; + valueContent = ; + } + return ( +
    + {item.label} + + {valueContent} + +
    + ); + })} +
    + + ), + }, + { + id: "indexDetailModalSettings", + name: "Settings", + content: ( + <> + + + +

    Advanced index settings

    +
    + + onEdit(IndicesUpdateMode.settings)}> + Edit + + +
    + + + + ), + }, + { + id: "indexDetailModalMappings", + name: "Mappings", + content: ( + <> + + + +

    Index mappings

    + + Define how documents and their fields are stored and indexed.{" "} + + Learn more. + +
    + } + > + <> + + + + onEdit(IndicesUpdateMode.mappings)}> + Add index mappings + + + + + + + ), + }, + { + id: `indexDetailModalAlias`, + name: "Alias", + content: ( + <> + + + +

    Index alias

    +
    + + onEdit(IndicesUpdateMode.alias)}> + Edit + + +
    + + ({ alias: item }))} + columns={[ + { + field: "alias", + name: "Alias name", + render: (val: string, record: { alias: string }) => ( + + {val} + + ), + }, + ]} + /> + + ), + }, + ]} + /> + {editVisible ? ( + null} hideCloseButton> + + + setEditVisible(false)} style={{ cursor: "pointer" }}> + + Edit index {editMode} + + + + + + + + ) : null} + + ); +} diff --git a/public/pages/IndexDetail/containers/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap b/public/pages/IndexDetail/containers/IndexDetail/__snapshots__/IndexDetail.test.tsx.snap new file mode 100644 index 000000000..bf8b6b363 --- /dev/null +++ b/public/pages/IndexDetail/containers/IndexDetail/__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/IndexDetail/containers/IndexDetail/index.ts b/public/pages/IndexDetail/containers/IndexDetail/index.ts new file mode 100644 index 000000000..622fb5b32 --- /dev/null +++ b/public/pages/IndexDetail/containers/IndexDetail/index.ts @@ -0,0 +1,3 @@ +import IndexDetail from "./IndexDetail"; + +export default IndexDetail; diff --git a/public/pages/IndexDetail/index.ts b/public/pages/IndexDetail/index.ts new file mode 100644 index 000000000..7298d4768 --- /dev/null +++ b/public/pages/IndexDetail/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import IndexDetail from "./containers/IndexDetail"; + +export default IndexDetail; diff --git a/public/pages/Indices/components/CloseIndexModal/CloseIndexModal.test.tsx b/public/pages/Indices/components/CloseIndexModal/CloseIndexModal.test.tsx new file mode 100644 index 000000000..8e9d61158 --- /dev/null +++ b/public/pages/Indices/components/CloseIndexModal/CloseIndexModal.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { render, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import CloseIndexModal from "./CloseIndexModal"; + +describe(" spec", () => { + it("renders the component", async () => { + render( {}} onClose={() => {}} />); + expect(document.body.children).toMatchSnapshot(); + }); + + it("calls close when cancel button clicked", () => { + const onClose = jest.fn(); + const { getByTestId } = render( {}} onClose={onClose} />); + fireEvent.click(getByTestId("Close Cancel button")); + expect(onClose).toHaveBeenCalled(); + }); + + it("Close button should be disabled unless a 'close' was input", async () => { + const { getByPlaceholderText } = render( {}} onClose={() => {}} />); + expect(document.querySelector(".euiButton")).toHaveAttribute("disabled"); + userEvent.type(getByPlaceholderText("close"), "close"); + expect(document.querySelector(".euiButton")).not.toHaveAttribute("disabled"); + }); + + it("Show warning when system indices are selected", async () => { + render( {}} onClose={() => {}} />); + expect(document.querySelector(".euiCallOut")).not.toHaveAttribute("hidden"); + }); + + it("No warning if no system indices are selected", async () => { + render( {}} onClose={() => {}} />); + expect(document.querySelector(".euiCallOut")).toHaveAttribute("hidden"); + }); +}); diff --git a/public/pages/Indices/components/CloseIndexModal/CloseIndexModal.tsx b/public/pages/Indices/components/CloseIndexModal/CloseIndexModal.tsx new file mode 100644 index 000000000..ad0c0fdea --- /dev/null +++ b/public/pages/Indices/components/CloseIndexModal/CloseIndexModal.tsx @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from "react"; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiCallOut, + EuiText, +} from "@elastic/eui"; +import { filterByMinimatch } from "../../../../../utils/helper"; +import { SYSTEM_INDEX } from "../../../../../utils/constants"; + +interface CloseIndexModalProps { + selectedItems: string[]; + visible: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export default function CloseIndexModal(props: CloseIndexModalProps) { + const [value, setValue] = useState(""); + const { onClose, onConfirm, visible, selectedItems } = props; + useEffect(() => { + if (visible) { + setValue(""); + } + }, [visible]); + if (!visible) { + return null; + } + + const showWarning = selectedItems.filter((item) => filterByMinimatch(item as string, SYSTEM_INDEX)).length > 0; + + return ( + + + Close indices + + + + <> + + + +
    +

    The following index will be closed. It is not possible to index documents or to search for documents in a closed index.

    +
      + {selectedItems.map((item) => ( +
    • {item}
    • + ))} +
    + + + To confirm your action, type close. + + setValue(e.target.value)} /> +
    +
    + + + + Cancel + + + Close + + +
    + ); +} diff --git a/public/pages/Indices/components/CloseIndexModal/__snapshots__/CloseIndexModal.test.tsx.snap b/public/pages/Indices/components/CloseIndexModal/__snapshots__/CloseIndexModal.test.tsx.snap new file mode 100644 index 000000000..c3a8f86fd --- /dev/null +++ b/public/pages/Indices/components/CloseIndexModal/__snapshots__/CloseIndexModal.test.tsx.snap @@ -0,0 +1,159 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +HTMLCollection [ +