From 57d313c77af732f251950bce4f3388e323ec35f8 Mon Sep 17 00:00:00 2001 From: kishor82 Date: Sat, 8 Apr 2023 22:18:18 +0530 Subject: [PATCH] feat: Add import functionality for dev tool queries Signed-off-by: kishor82 --- .../application/components/import_flyout.tsx | 313 ++++++++++++++++++ .../components/import_mode_control.tsx | 100 ++++++ .../public/application/components/index.ts | 1 + .../components/overwrite_modal.tsx | 66 ++++ .../containers/main/get_top_nav.ts | 15 + .../application/containers/main/main.tsx | 7 + .../application/contexts/services_context.tsx | 3 +- .../console/public/application/index.tsx | 5 +- src/plugins/console/public/plugin.ts | 2 + 9 files changed, 510 insertions(+), 2 deletions(-) create mode 100644 src/plugins/console/public/application/components/import_flyout.tsx create mode 100644 src/plugins/console/public/application/components/import_mode_control.tsx create mode 100644 src/plugins/console/public/application/components/overwrite_modal.tsx diff --git a/src/plugins/console/public/application/components/import_flyout.tsx b/src/plugins/console/public/application/components/import_flyout.tsx new file mode 100644 index 000000000000..4914f69cc811 --- /dev/null +++ b/src/plugins/console/public/application/components/import_flyout.tsx @@ -0,0 +1,313 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiCallOut, + EuiSpacer, + EuiFilePicker, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLoadingSpinner, + EuiText, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import moment from 'moment'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import React, { Fragment, useState } from 'react'; +import { ImportMode, ImportModeControl } from './import_mode_control'; +import { useEditorReadContext, useServicesContext } from '../contexts'; +import { TextObject } from '../../../common/text_object'; +import { OverwriteModal } from './overwrite_modal'; + +const OVERWRITE_ALL_DEFAULT = false; + +interface ImportFlyoutProps { + close: () => void; + refresh: () => void; +} + +const getErrorMessage = () => { + return i18n.translate('console.ImpoerFlyout.importFileErrorMessage', { + defaultMessage: 'The file could not be processed due to error.', + }); +}; + +export const ImportFlyout = ({ close, refresh }: ImportFlyoutProps) => { + const [error, setError] = useState(); + const [status, setStatus] = useState('idle'); + const [loadingMessage, setLoadingMessage] = useState(); + const [file, setFile] = useState(); + const [jsonData, setJsonData] = useState(); + const [showOverwriteModal, setShowOverwriteModal] = useState(false); + const [importMode, setImportMode] = useState({ + overwrite: OVERWRITE_ALL_DEFAULT, + }); + + const { + services: { + objectStorageClient, + uiSettings, + notifications: { toasts }, + }, + } = useServicesContext(); + const { currentTextObject } = useEditorReadContext(); + + const dateFormat = uiSettings.get('dateFormat'); + + const setImportFile = (files: FileList | null) => { + if (!files || !files[0]) { + setFile(undefined); + return; + } + const fileContent = files[0]; + const reader = new FileReader(); + + reader.onload = (event) => { + const fileData = event.target?.result; + if (typeof fileData === 'string') { + const parsedData = JSON.parse(fileData); + setJsonData(parsedData); + } + }; + + reader.readAsText(fileContent); + setFile(fileContent); + setStatus('idle'); + }; + + const renderError = () => { + if (status !== 'error') { + return null; + } + + return ( + + + } + color="danger" + > +

{error}

+
+ +
+ ); + }; + + const renderBody = () => { + if (status === 'loading') { + return ( + + + + + +

{loadingMessage}

+
+
+
+ ); + } + + return ( + + + } + > + + } + onChange={setImportFile} + /> + + + setImportMode(newValues)} + /> + + + ); + }; + + const importFile = async (isOverwriteConfirmed?: boolean) => { + setStatus('loading'); + setError(undefined); + try { + if (jsonData && jsonData.text) { + if (importMode.overwrite) { + if (!isOverwriteConfirmed) { + setShowOverwriteModal(true); + return; + } else { + setLoadingMessage('Importing queries and overwriting existing ones...'); + const newObject = { + createdAt: Date.now(), + updatedAt: Date.now(), + text: jsonData.text, + }; + if (currentTextObject) { + await objectStorageClient.text.update({ + ...currentTextObject, + ...newObject, + }); + } else { + await objectStorageClient.text.create({ + ...newObject, + }); + } + } + toasts.addSuccess('Queries overwritten.'); + } else { + setLoadingMessage('Importing queries and merging with existing ones...'); + if (currentTextObject) { + await objectStorageClient.text.update({ + ...currentTextObject, + createdAt: Date.now(), + updatedAt: Date.now(), + text: currentTextObject.text.concat( + `\n#Imported on ${moment(Date.now()).format(dateFormat)}\n\n${jsonData.text}` + ), + }); + toasts.addSuccess('Queries merged.'); + } + } + } else { + setStatus('error'); + setError( + i18n.translate('console.ImpoerFlyout.importFileErrorMessage', { + defaultMessage: 'The selected file is not valid. Please select a valid JSON file.', + }) + ); + } + refresh(); + setLoadingMessage(undefined); + setStatus('idle'); + close(); + } catch (e) { + setStatus('error'); + setError(getErrorMessage(e)); + return; + } + }; + + const onConfirm = () => { + setShowOverwriteModal(false); + importFile(true); + }; + + const onSkip = () => { + setShowOverwriteModal(false); + setStatus('idle'); + }; + + const renderFooter = () => { + return ( + + + + + + + + importFile(false)} + size="s" + fill + isLoading={status === 'loading'} + > + + + + + ); + }; + + return ( + + + +

+ +

+
+
+ + + {renderError()} + {renderBody()} + + + {renderFooter()} + {showOverwriteModal && } +
+ ); +}; diff --git a/src/plugins/console/public/application/components/import_mode_control.tsx b/src/plugins/console/public/application/components/import_mode_control.tsx new file mode 100644 index 000000000000..48f37694bd8b --- /dev/null +++ b/src/plugins/console/public/application/components/import_mode_control.tsx @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { EuiFormFieldset, EuiTitle, EuiRadioGroup } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface ImportModeControlProps { + initialValues: ImportMode; + isLegacyFile: boolean; + updateSelection: (result: ImportMode) => void; +} + +export interface ImportMode { + overwrite: boolean; +} + +const overwriteEnabled = { + id: 'overwriteEnabled', + label: i18n.translate('console.importModeControl.overwrite.enabledLabel', { + defaultMessage: 'Overwrite existing queries', + }), +}; +const overwriteDisabled = { + id: 'overwriteDisabled', + label: i18n.translate('console.importModeControl.overwrite.disabledLabel', { + defaultMessage: 'Merge with existing queries', + }), +}; +const importOptionsTitle = i18n.translate('console.importModeControl.importOptionsTitle', { + defaultMessage: 'Import options', +}); + +export const ImportModeControl = ({ + initialValues, + isLegacyFile, + updateSelection, +}: ImportModeControlProps) => { + const [overwrite, setOverwrite] = useState(initialValues.overwrite); + + const onChange = (partial: Partial) => { + if (partial.overwrite !== undefined) { + setOverwrite(partial.overwrite); + } + updateSelection({ overwrite, ...partial }); + }; + + const overwriteRadio = ( + onChange({ overwrite: id === overwriteEnabled.id })} + /> + ); + + if (isLegacyFile) { + return overwriteRadio; + } + + return ( + + {importOptionsTitle} + + ), + }} + > + {overwriteRadio} + + ); +}; diff --git a/src/plugins/console/public/application/components/index.ts b/src/plugins/console/public/application/components/index.ts index d6cceb7c7ca8..dca2bb72d211 100644 --- a/src/plugins/console/public/application/components/index.ts +++ b/src/plugins/console/public/application/components/index.ts @@ -36,3 +36,4 @@ export { WelcomePanel } from './welcome_panel'; export { AutocompleteOptions, DevToolsSettingsModal } from './settings_modal'; export { HelpPanel } from './help_panel'; export { EditorContentSpinner } from './editor_content_spinner'; +export { ImportFlyout } from './import_flyout'; diff --git a/src/plugins/console/public/application/components/overwrite_modal.tsx b/src/plugins/console/public/application/components/overwrite_modal.tsx new file mode 100644 index 000000000000..180a930034f0 --- /dev/null +++ b/src/plugins/console/public/application/components/overwrite_modal.tsx @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal } from '@elastic/eui'; + +export interface OverwriteModalProps { + onSkip: () => void; + onConfirm: () => void; +} +export const OverwriteModal = ({ onSkip, onConfirm }: OverwriteModalProps) => { + return ( + +

+ {i18n.translate('console.overwriteModal.body.conflict', { + defaultMessage: + 'Are you sure you want to overwrite the existing queries? This action cannot be undone. All existing queries will be deleted and replaced with the imported queries. If you are unsure, please choose the "{option}" option instead', + values: { option: 'Merge with existing queries' }, + })} +

+
+ ); +}; diff --git a/src/plugins/console/public/application/containers/main/get_top_nav.ts b/src/plugins/console/public/application/containers/main/get_top_nav.ts index b1e3511c9b2f..cd21321993bb 100644 --- a/src/plugins/console/public/application/containers/main/get_top_nav.ts +++ b/src/plugins/console/public/application/containers/main/get_top_nav.ts @@ -35,6 +35,7 @@ interface Props { onClickSettings: () => void; onClickHelp: () => void; onClickExport: () => void; + onClickImport: () => void; } export function getTopNavConfig({ @@ -42,6 +43,7 @@ export function getTopNavConfig({ onClickSettings, onClickHelp, onClickExport, + onClickImport, }: Props) { return [ { @@ -96,5 +98,18 @@ export function getTopNavConfig({ }, testId: 'consoleExportButton', }, + { + id: 'import', + label: i18n.translate('console.topNav.importTabLabel', { + defaultMessage: 'Import', + }), + description: i18n.translate('console.topNav.importTabDescription', { + defaultMessage: 'Import', + }), + onClick: () => { + onClickImport(); + }, + testId: 'consoleImportButton', + }, ]; } diff --git a/src/plugins/console/public/application/containers/main/main.tsx b/src/plugins/console/public/application/containers/main/main.tsx index 154d81e08b48..bbe5bd9856eb 100644 --- a/src/plugins/console/public/application/containers/main/main.tsx +++ b/src/plugins/console/public/application/containers/main/main.tsx @@ -43,6 +43,7 @@ import { HelpPanel, SomethingWentWrongCallout, NetworkRequestStatusBar, + ImportFlyout, } from '../../components'; import { useServicesContext, useEditorReadContext, useRequestReadContext } from '../../contexts'; @@ -73,6 +74,7 @@ export function Main({ dataSourceId }: MainProps) { const [showingHistory, setShowHistory] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showHelp, setShowHelp] = useState(false); + const [showImportFlyout, setShowImportFlyout] = useState(false); const onExport = async () => { const results = await objectStorageClient.text.findAll(); @@ -121,6 +123,7 @@ export function Main({ dataSourceId }: MainProps) { onClickSettings: () => setShowSettings(true), onClickHelp: () => setShowHelp(!showHelp), onClickExport: () => onExport(), + onClickImport: () => setShowImportFlyout(!showImportFlyout), })} /> @@ -162,6 +165,10 @@ export function Main({ dataSourceId }: MainProps) { ) : null} {showHelp ? setShowHelp(false)} /> : null} + + {showImportFlyout ? ( + setShowImportFlyout(false)} /> + ) : null} ); } diff --git a/src/plugins/console/public/application/contexts/services_context.tsx b/src/plugins/console/public/application/contexts/services_context.tsx index fc9ab157f783..0e8398ea8b83 100644 --- a/src/plugins/console/public/application/contexts/services_context.tsx +++ b/src/plugins/console/public/application/contexts/services_context.tsx @@ -29,7 +29,7 @@ */ import React, { createContext, useContext, useEffect } from 'react'; -import { HttpSetup, NotificationsSetup } from 'opensearch-dashboards/public'; +import { HttpSetup, IUiSettingsClient, NotificationsSetup } from 'opensearch-dashboards/public'; import { History, Settings, Storage } from '../../services'; import { ObjectStorageClient } from '../../../common/types'; import { MetricsTracker } from '../../types'; @@ -44,6 +44,7 @@ interface ContextServices { trackUiMetric: MetricsTracker; opensearchHostService: OpenSearchHostService; http: HttpSetup; + uiSettings: IUiSettingsClient; } export interface ContextValue { diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx index c1a107ac500a..ac32909735b2 100644 --- a/src/plugins/console/public/application/index.tsx +++ b/src/plugins/console/public/application/index.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { HttpSetup, NotificationsSetup } from 'src/core/public'; +import { HttpSetup, IUiSettingsClient, NotificationsSetup } from 'src/core/public'; import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings } from '../services'; @@ -47,6 +47,7 @@ export interface BootDependencies { usageCollection?: UsageCollectionSetup; element: HTMLElement; dataSourceId?: string; + uiSettings: IUiSettingsClient; } export function renderApp({ @@ -57,6 +58,7 @@ export function renderApp({ element, http, dataSourceId, + uiSettings, }: BootDependencies) { const trackUiMetric = createUsageTracker(usageCollection); trackUiMetric.load('opened_app'); @@ -85,6 +87,7 @@ export function renderApp({ trackUiMetric, objectStorageClient, http, + uiSettings, }, }} > diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index 5e1478875ec6..300d57d4b75d 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -68,6 +68,7 @@ export class ConsoleUIPlugin implements Plugin