diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 084f7aaf029f..7398aad65009 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -503,6 +503,10 @@ export interface AppMountParameters { * ``` */ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + /** + * Optional datasource id to pass while mounting app + */ + dataSourceId?: string; } /** diff --git a/src/plugins/console/opensearch_dashboards.json b/src/plugins/console/opensearch_dashboards.json index 64fa12392e7b..630eb58a6de7 100644 --- a/src/plugins/console/opensearch_dashboards.json +++ b/src/plugins/console/opensearch_dashboards.json @@ -4,6 +4,11 @@ "server": true, "ui": true, "requiredPlugins": ["devTools"], - "optionalPlugins": ["usageCollection", "home"], - "requiredBundles": ["opensearchUiShared", "opensearchDashboardsReact", "opensearchDashboardsUtils", "home"] + "optionalPlugins": ["usageCollection", "home", "dataSource"], + "requiredBundles": [ + "opensearchUiShared", + "opensearchDashboardsReact", + "opensearchDashboardsUtils", + "home" + ] } diff --git a/src/plugins/console/public/application/containers/editor/editor.tsx b/src/plugins/console/public/application/containers/editor/editor.tsx index c4cb5c542635..4d5723b123d4 100644 --- a/src/plugins/console/public/application/containers/editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/editor.tsx @@ -28,7 +28,7 @@ * under the License. */ -import React, { useCallback, memo } from 'react'; +import React, { useCallback, memo, useEffect } from 'react'; import { debounce } from 'lodash'; import { EuiProgress } from '@elastic/eui'; @@ -36,28 +36,42 @@ import { EditorContentSpinner } from '../../components'; import { Panel, PanelsContainer } from '../../../../../opensearch_dashboards_react/public'; import { Editor as EditorUI, EditorOutput } from './legacy/console_editor'; import { StorageKeys } from '../../../services'; -import { useEditorReadContext, useServicesContext, useRequestReadContext } from '../../contexts'; +import { + useEditorReadContext, + useServicesContext, + useRequestReadContext, + useRequestActionContext, +} from '../../contexts'; const INITIAL_PANEL_WIDTH = 50; const PANEL_MIN_WIDTH = '100px'; interface Props { loading: boolean; + dataSourceId?: string; } -export const Editor = memo(({ loading }: Props) => { +export const Editor = memo(({ loading, dataSourceId }: Props) => { const { services: { storage }, } = useServicesContext(); const { currentTextObject } = useEditorReadContext(); const { requestInFlight } = useRequestReadContext(); + const dispatch = useRequestActionContext(); const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ INITIAL_PANEL_WIDTH, INITIAL_PANEL_WIDTH, ]); + useEffect(() => { + dispatch({ + type: 'resetLastResult', + payload: undefined, + }); + }, [dispatch, dataSourceId]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const onPanelWidthChange = useCallback( debounce((widths: number[]) => { @@ -83,7 +97,7 @@ export const Editor = memo(({ loading }: Props) => { {loading ? ( ) : ( - + )} (null); @@ -184,7 +185,12 @@ function EditorUI({ initialTextValue }: EditorProps) { setInputEditor(editor); setTextArea(editorRef.current!.querySelector('textarea')); - retrieveAutoCompleteInfo(http, settingsService, settingsService.getAutocomplete()); + retrieveAutoCompleteInfo( + http, + settingsService, + settingsService.getAutocomplete(), + dataSourceId + ); const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor); setupAutosave(); @@ -197,7 +203,15 @@ function EditorUI({ initialTextValue }: EditorProps) { editorInstanceRef.current.getCoreEditor().destroy(); } }; - }, [saveCurrentTextObject, initialTextValue, history, setInputEditor, settingsService, http]); + }, [ + saveCurrentTextObject, + initialTextValue, + history, + setInputEditor, + settingsService, + http, + dataSourceId, + ]); useEffect(() => { const { current: editor } = editorInstanceRef; diff --git a/src/plugins/console/public/application/containers/main/main.tsx b/src/plugins/console/public/application/containers/main/main.tsx index 19104cd8d318..1967c14615bb 100644 --- a/src/plugins/console/public/application/containers/main/main.tsx +++ b/src/plugins/console/public/application/containers/main/main.tsx @@ -28,7 +28,7 @@ * under the License. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@osd/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiPageContent } from '@elastic/eui'; import { ConsoleHistory } from '../console_history'; @@ -48,7 +48,11 @@ import { useDataInit } from '../../hooks'; import { getTopNavConfig } from './get_top_nav'; -export function Main() { +interface MainProps { + dataSourceId?: string; +} + +export function Main({ dataSourceId }: MainProps) { const { services: { storage }, } = useServicesContext(); @@ -130,7 +134,7 @@ export function Main() { {showingHistory ? {renderConsoleHistory()} : null} - + @@ -143,7 +147,9 @@ export function Main() { /> ) : null} - {showSettings ? setShowSettings(false)} /> : null} + {showSettings ? ( + setShowSettings(false)} dataSourceId={dataSourceId} /> + ) : null} {showHelp ? setShowHelp(false)} /> : null} diff --git a/src/plugins/console/public/application/containers/settings.tsx b/src/plugins/console/public/application/containers/settings.tsx index 3030d8149d3e..533f02bc0ea4 100644 --- a/src/plugins/console/public/application/containers/settings.tsx +++ b/src/plugins/console/public/application/containers/settings.tsx @@ -48,9 +48,10 @@ const getAutocompleteDiff = (newSettings: DevToolsSettings, prevSettings: DevToo const refreshAutocompleteSettings = ( http: HttpSetup, settings: SettingsService, - selectedSettings: any + selectedSettings: any, + dataSourceId?: string ) => { - retrieveAutoCompleteInfo(http, settings, selectedSettings); + retrieveAutoCompleteInfo(http, settings, selectedSettings, dataSourceId); }; const fetchAutocompleteSettingsIfNeeded = ( @@ -79,19 +80,20 @@ const fetchAutocompleteSettingsIfNeeded = ( }, {} ); - retrieveAutoCompleteInfo(http, settings, changedSettings); + retrieveAutoCompleteInfo(http, settings, changedSettings, dataSourceId); } else if (isPollingChanged && newSettings.polling) { // If the user has turned polling on, then we'll fetch all selected autocomplete settings. - retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete()); + retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete(), dataSourceId); } } }; export interface Props { onClose: () => void; + dataSourceId?: string; } -export function Settings({ onClose }: Props) { +export function Settings({ onClose, dataSourceId }: Props) { const { services: { settings, http }, } = useServicesContext(); @@ -118,7 +120,7 @@ export function Settings({ onClose }: Props) { onClose={onClose} onSaveSettings={onSaveSettings} refreshAutocompleteSettings={(selectedSettings: any) => - refreshAutocompleteSettings(http, settings, selectedSettings) + refreshAutocompleteSettings(http, settings, selectedSettings, dataSourceId) } settings={settings.toJSON()} /> diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts index ad7ba440b6d4..4e1ae7267542 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts @@ -39,6 +39,7 @@ import { BaseResponseType } from '../../../types'; export interface OpenSearchRequestArgs { http: HttpSetup; requests: any; + dataSourceId?: string; } export interface OpenSearchRequestObject { @@ -101,7 +102,8 @@ export function sendRequestToOpenSearch( args.http, opensearchMethod, opensearchPath, - opensearchData + opensearchData, + args.dataSourceId ); if (reqId !== CURRENT_REQ_ID) { return; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts index f62c6485a838..81eabdf7a264 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts @@ -38,7 +38,7 @@ import { track } from './track'; // @ts-ignore import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; -export const useSendCurrentRequestToOpenSearch = () => { +export const useSendCurrentRequestToOpenSearch = (dataSourceId?: string) => { const { services: { history, settings, notifications, trackUiMetric, http }, } = useServicesContext(); @@ -64,7 +64,7 @@ export const useSendCurrentRequestToOpenSearch = () => { // Fire and forget setTimeout(() => track(requests, editor, trackUiMetric), 0); - const results = await sendRequestToOpenSearch({ http, requests }); + const results = await sendRequestToOpenSearch({ http, requests, dataSourceId }); results.forEach(({ request: { path, method, data } }) => { try { @@ -85,7 +85,7 @@ export const useSendCurrentRequestToOpenSearch = () => { // or templates may have changed, so we'll need to update this data. Assume that if // the user disables polling they're trying to optimize performance or otherwise // preserve resources, so they won't want this request sent either. - retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete()); + retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete(), dataSourceId); } dispatch({ @@ -112,5 +112,5 @@ export const useSendCurrentRequestToOpenSearch = () => { }); } } - }, [dispatch, settings, history, notifications, trackUiMetric, http]); + }, [dispatch, http, dataSourceId, settings, notifications.toasts, trackUiMetric, history]); }; diff --git a/src/plugins/console/public/application/index.tsx b/src/plugins/console/public/application/index.tsx index a7d757482e96..c1a107ac500a 100644 --- a/src/plugins/console/public/application/index.tsx +++ b/src/plugins/console/public/application/index.tsx @@ -46,6 +46,7 @@ export interface BootDependencies { notifications: NotificationsSetup; usageCollection?: UsageCollectionSetup; element: HTMLElement; + dataSourceId?: string; } export function renderApp({ @@ -55,6 +56,7 @@ export function renderApp({ usageCollection, element, http, + dataSourceId, }: BootDependencies) { const trackUiMetric = createUsageTracker(usageCollection); trackUiMetric.load('opened_app'); @@ -88,7 +90,7 @@ export function renderApp({ > -
+
diff --git a/src/plugins/console/public/application/stores/request.ts b/src/plugins/console/public/application/stores/request.ts index fc7752cbd757..5fccf28b7557 100644 --- a/src/plugins/console/public/application/stores/request.ts +++ b/src/plugins/console/public/application/stores/request.ts @@ -37,7 +37,8 @@ import { OpenSearchRequestResult } from '../hooks/use_send_current_request_to_op export type Actions = | { type: 'sendRequest'; payload: undefined } | { type: 'requestSuccess'; payload: { data: OpenSearchRequestResult[] } } - | { type: 'requestFail'; payload: OpenSearchRequestResult | undefined }; + | { type: 'requestFail'; payload: OpenSearchRequestResult | undefined } + | { type: 'resetLastResult'; payload: undefined }; export interface Store { requestInFlight: boolean; @@ -79,4 +80,11 @@ export const reducer: Reducer = (state, action) => draft.lastResult = { ...initialResultValue, error: action.payload }; return; } + + if (action.type === 'resetLastResult') { + draft.lastResult = initialResultValue; + return; + } + + return draft; }); diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js index e42296e481d4..be5490ca1c1c 100644 --- a/src/plugins/console/public/lib/mappings/mappings.js +++ b/src/plugins/console/public/lib/mappings/mappings.js @@ -269,7 +269,7 @@ export function clear() { templates = []; } -function retrieveSettings(http, settingsKey, settingsToRetrieve) { +function retrieveSettings(http, settingsKey, settingsToRetrieve, dataSourceId) { const settingKeyToPathMap = { fields: '_mapping', indices: '_aliases', @@ -278,7 +278,7 @@ function retrieveSettings(http, settingsKey, settingsToRetrieve) { // Fetch autocomplete info if setting is set to true, and if user has made changes. if (settingsToRetrieve[settingsKey] === true) { - return opensearch.send(http, 'GET', settingKeyToPathMap[settingsKey], null); + return opensearch.send(http, 'GET', settingKeyToPathMap[settingsKey], null, dataSourceId); } else { if (settingsToRetrieve[settingsKey] === false) { // If the user doesn't want autocomplete suggestions, then clear any that exist @@ -307,8 +307,13 @@ export function clearSubscriptions() { } } -const retrieveMappings = async (http, settingsToRetrieve) => { - const { body: mappings } = await retrieveSettings(http, 'fields', settingsToRetrieve); +const retrieveMappings = async (http, settingsToRetrieve, dataSourceId) => { + const { body: mappings } = await retrieveSettings( + http, + 'fields', + settingsToRetrieve, + dataSourceId + ); if (mappings) { const maxMappingSize = Object.keys(mappings).length > 10 * 1024 * 1024; let mappingsResponse; @@ -326,16 +331,26 @@ const retrieveMappings = async (http, settingsToRetrieve) => { } }; -const retrieveAliases = async (http, settingsToRetrieve) => { - const { body: aliases } = await retrieveSettings(http, 'indices', settingsToRetrieve); +const retrieveAliases = async (http, settingsToRetrieve, dataSourceId) => { + const { body: aliases } = await retrieveSettings( + http, + 'indices', + settingsToRetrieve, + dataSourceId + ); if (aliases) { loadAliases(aliases); } }; -const retrieveTemplates = async (http, settingsToRetrieve) => { - const { body: templates } = await retrieveSettings(http, 'templates', settingsToRetrieve); +const retrieveTemplates = async (http, settingsToRetrieve, dataSourceId) => { + const { body: templates } = await retrieveSettings( + http, + 'templates', + settingsToRetrieve, + dataSourceId + ); if (templates) { loadTemplates(templates); @@ -347,20 +362,20 @@ const retrieveTemplates = async (http, settingsToRetrieve) => { * @param settings Settings A way to retrieve the current settings * @param settingsToRetrieve any */ -export function retrieveAutoCompleteInfo(http, settings, settingsToRetrieve) { +export function retrieveAutoCompleteInfo(http, settings, settingsToRetrieve, dataSourceId) { clearSubscriptions(); Promise.allSettled([ - retrieveMappings(http, settingsToRetrieve), - retrieveAliases(http, settingsToRetrieve), - retrieveTemplates(http, settingsToRetrieve), + retrieveMappings(http, settingsToRetrieve, dataSourceId), + retrieveAliases(http, settingsToRetrieve, dataSourceId), + retrieveTemplates(http, settingsToRetrieve, dataSourceId), ]).then(() => { // Schedule next request. pollTimeoutId = setTimeout(() => { // This looks strange/inefficient, but it ensures correct behavior because we don't want to send // a scheduled request if the user turns off polling. if (settings.getPolling()) { - retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete()); + retrieveAutoCompleteInfo(http, settings, settings.getAutocomplete(), dataSourceId); } }, POLL_INTERVAL); }); diff --git a/src/plugins/console/public/lib/opensearch/opensearch.ts b/src/plugins/console/public/lib/opensearch/opensearch.ts index ab6b79469e88..b0158945eb25 100644 --- a/src/plugins/console/public/lib/opensearch/opensearch.ts +++ b/src/plugins/console/public/lib/opensearch/opensearch.ts @@ -45,12 +45,14 @@ export async function send( http: HttpSetup, method: string, path: string, - data: any + data: any, + dataSourceId?: string ): Promise { return await http.post('/api/console/proxy', { query: { path, method, + dataSourceId, }, body: data, prependBasePath: true, diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index 8e8e532eae84..5e1478875ec6 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -62,7 +62,7 @@ export class ConsoleUIPlugin implements Plugin { + mount: async ({ element, dataSourceId }) => { const [core] = await getStartServices(); const { @@ -79,6 +79,7 @@ export class ConsoleUIPlugin implements Plugin => async (ctx, request, response) => { const { body, query } = request; - const { path, method } = query; - const client = ctx.core.opensearch.client.asCurrentUser; - + const { path, method, dataSourceId } = query; + const client = dataSourceId + ? await ctx.dataSource.opensearch.getClient(dataSourceId) + : ctx.core.opensearch.client.asCurrentUser; let opensearchResponse: ApiResponse; if (!pathFilters.some((re) => re.test(path))) { @@ -99,12 +101,16 @@ export const createHandler = ({ } try { - const requestHeaders = { - ...getProxyHeaders(request), - }; - + // TODO: proxy header will fail sigv4 auth type in data source, need create issue in opensearch-js repo to track + const requestHeaders = dataSourceId + ? {} + : { + ...getProxyHeaders(request), + }; + + const bufferedBody = await buildBufferedBody(body); opensearchResponse = await client.transport.request( - { path: toUrlPath(path), method, body }, + { path: toUrlPath(path), method, body: bufferedBody }, { headers: requestHeaders } ); @@ -130,7 +136,7 @@ export const createHandler = ({ }); } catch (e: any) { const isResponseErrorFlag = isResponseError(e); - + if (!isResponseError) log.error(e); const errorMessage = isResponseErrorFlag ? JSON.stringify(e.meta.body) : e.message; // core http route handler has special logic that asks for stream readable input to pass error opaquely const errorResponseBody = new Readable({ diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts b/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts index a7fb88a8bdaa..95b243dd4734 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts @@ -28,7 +28,7 @@ * under the License. */ -import { getProxyRouteHandlerDeps } from './mocks'; +import { buildBufferedBodyMock, getProxyRouteHandlerDeps } from './mocks'; import expect from '@osd/expect'; @@ -47,7 +47,6 @@ describe('Console Proxy Route', () => { beforeEach(() => { request = (method: string, path: string, response: string) => { const mockResponse = opensearchServiceMock.createSuccessTransportRequestPromise(response); - const requestHandlerContextMock = coreMock.createRequestHandlerContext(); opensearchClient = requestHandlerContextMock.opensearch.client; diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts b/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts index 057fa178e543..bbbe0c734199 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts +++ b/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts @@ -32,6 +32,11 @@ jest.mock('../../../../../lib/proxy_request', () => ({ proxyRequest: jest.fn(), })); +export const buildBufferedBodyMock = jest.fn(); +jest.doMock('../utils.ts', () => ({ + buildBufferedBody: buildBufferedBodyMock, +})); + import { duration } from 'moment'; import { ProxyConfigCollection } from '../../../../../lib'; import { RouteDependencies, ProxyDependencies } from '../../../../../routes'; diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts b/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts index 4b4c412d4cd2..105bce3bfad2 100644 --- a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts +++ b/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts @@ -48,7 +48,7 @@ describe('Console Proxy Route', () => { return handler( { core: requestHandlerContextMock, dataSource: {} as any }, - { headers: {}, query: { method, path } } as any, + { headers: {}, query: { method, path }, body: jest.fn() } as any, opensearchDashboardsResponseFactory ); }; @@ -64,6 +64,7 @@ describe('Console Proxy Route', () => { it('treats the url as a path', async () => { await request('GET', 'http://evil.com/test'); const [[args]] = opensearchClient.asCurrentUser.transport.request.mock.calls; + expect(args.path).toBe('/http://evil.com/test?pretty=true'); }); }); diff --git a/src/plugins/console/server/routes/api/console/proxy/utils.ts b/src/plugins/console/server/routes/api/console/proxy/utils.ts new file mode 100644 index 000000000000..58bac48d65a4 --- /dev/null +++ b/src/plugins/console/server/routes/api/console/proxy/utils.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +import { Stream } from 'stream'; + +export const buildBufferedBody = (body: Stream): Promise => { + return new Promise((resolve, reject) => { + let buff: Buffer = Buffer.alloc(0); + + body.on('data', function (chunk: Buffer) { + buff = Buffer.concat([buff, chunk]); + }); + + body.on('end', function () { + resolve(buff); + }); + + body.on('error', function (err) { + reject(err); + }); + }); +}; diff --git a/src/plugins/console/server/routes/api/console/proxy/validation_config.ts b/src/plugins/console/server/routes/api/console/proxy/validation_config.ts index 135aa947f7c8..7d1e9583eaf8 100644 --- a/src/plugins/console/server/routes/api/console/proxy/validation_config.ts +++ b/src/plugins/console/server/routes/api/console/proxy/validation_config.ts @@ -51,6 +51,7 @@ export const routeValidationConfig = { query: schema.object({ method: acceptedHttpVerb, path: nonEmptyString, + dataSourceId: schema.maybe(schema.string()), }), body: schema.stream(), }; diff --git a/src/plugins/data_source_management/opensearch_dashboards.json b/src/plugins/data_source_management/opensearch_dashboards.json index e5b13f6c0a1f..58e81a337e7d 100644 --- a/src/plugins/data_source_management/opensearch_dashboards.json +++ b/src/plugins/data_source_management/opensearch_dashboards.json @@ -5,5 +5,6 @@ "ui": true, "requiredPlugins": ["management", "dataSource", "indexPatternManagement"], "optionalPlugins": [], - "requiredBundles": ["opensearchDashboardsReact"] + "requiredBundles": ["opensearchDashboardsReact"], + "extraPublicDirs": ["public/components/utils"] } diff --git a/src/plugins/dev_tools/opensearch_dashboards.json b/src/plugins/dev_tools/opensearch_dashboards.json index c9022b85c8f5..11fd6c3b62c7 100644 --- a/src/plugins/dev_tools/opensearch_dashboards.json +++ b/src/plugins/dev_tools/opensearch_dashboards.json @@ -3,5 +3,7 @@ "version": "opensearchDashboards", "server": false, "ui": true, - "requiredPlugins": ["urlForwarding"] + "optionalPlugins": ["dataSource"], + "requiredPlugins": ["urlForwarding"], + "requiredBundles": ["dataSourceManagement"] } diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index 21b7dede2462..859d62acee51 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -28,21 +28,43 @@ * under the License. */ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; -import { EuiTab, EuiTabs, EuiToolTip } from '@elastic/eui'; +import { + EuiTab, + EuiTabs, + EuiToolTip, + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiComboBoxOptionOption, +} from '@elastic/eui'; import { I18nProvider } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; -import { ApplicationStart, ChromeStart, ScopedHistory } from 'src/core/public'; +import { + ApplicationStart, + ChromeStart, + CoreStart, + NotificationsStart, + SavedObjectsStart, + ScopedHistory, +} from 'src/core/public'; +import { useEffectOnce } from 'react-use'; +// eslint-disable-next-line @osd/eslint/no-restricted-paths +import { getDataSources } from '../../data_source_management/public/components/utils'; import { DevToolApp } from './dev_tool'; +import { DevToolsSetupDependencies } from './plugin'; interface DevToolsWrapperProps { devTools: readonly DevToolApp[]; activeDevTool: DevToolApp; updateRoute: (newRoute: string) => void; + savedObjects: SavedObjectsStart; + notifications: NotificationsStart; + dataSourceEnabled: boolean; } interface MountedDevToolDescriptor { @@ -51,8 +73,22 @@ interface MountedDevToolDescriptor { unmountHandler: () => void; } -function DevToolsWrapper({ devTools, activeDevTool, updateRoute }: DevToolsWrapperProps) { +interface DataSourceOption extends EuiComboBoxOptionOption { + id: string; + label: string; +} + +function DevToolsWrapper({ + devTools, + activeDevTool, + updateRoute, + savedObjects, + notifications: { toasts }, + dataSourceEnabled, +}: DevToolsWrapperProps) { const mountedTool = useRef(null); + const [dataSources, setDataSources] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); useEffect( () => () => { @@ -63,25 +99,107 @@ function DevToolsWrapper({ devTools, activeDevTool, updateRoute }: DevToolsWrapp [] ); + useEffectOnce(() => { + fetchDataSources(); + }); + + const fetchDataSources = () => { + getDataSources(savedObjects.client) + .then((fetchedDataSources) => { + if (fetchedDataSources?.length) { + const dataSourceOptions = fetchedDataSources.map((dataSource) => ({ + id: dataSource.id, + label: dataSource.title, + })); + setDataSources(dataSourceOptions); + } + }) + .catch(() => { + toasts.addDanger( + i18n.translate('devTool.devToolWrapper.fetchDataSourceError', { + defaultMessage: 'Unable to fetch existing data sources', + }) + ); + }); + }; + + const onChange = async (e: Array>) => { + const dataSourceId = e[0] ? e[0].id : undefined; + setSelectedOptions(e); + await remount(mountedTool.current!.mountpoint, dataSourceId); + }; + + const remount = async (mountPoint: HTMLElement, dataSourceId?: string) => { + if (mountedTool.current) { + mountedTool.current.unmountHandler(); + } + + const params = { + element: mountPoint, + appBasePath: '', + onAppLeave: () => undefined, + setHeaderActionMenu: () => undefined, + // TODO: adapt to use Core's ScopedHistory + history: {} as any, + dataSourceId, + }; + const unmountHandler = await activeDevTool.mount(params); + + mountedTool.current = { + devTool: activeDevTool, + mountpoint: mountPoint, + unmountHandler, + }; + }; + return (
- - {devTools.map((currentDevTool) => ( - - { - if (!currentDevTool.isDisabled()) { - updateRoute(`/${currentDevTool.id}`); - } - }} - > - {currentDevTool.title} - - - ))} - +
+ + + + {devTools.map((currentDevTool) => ( + + { + if (!currentDevTool.isDisabled()) { + updateRoute(`/${currentDevTool.id}`); + } + }} + > + {currentDevTool.title} + + + ))} + + + {dataSourceEnabled ? ( + + + + ) : null} + +
+
undefined, - setHeaderActionMenu: () => undefined, - // TODO: adapt to use Core's ScopedHistory - history: {} as any, - }; - - const unmountHandler = await activeDevTool.mount(params); - - mountedTool.current = { - devTool: activeDevTool, - mountpoint: element, - unmountHandler, - }; + await remount(element); } }} /> @@ -164,12 +263,13 @@ function setBreadcrumbs(chrome: ChromeStart) { } export function renderApp( + { application, chrome, savedObjects, notifications }: CoreStart, element: HTMLElement, - application: ApplicationStart, - chrome: ChromeStart, history: ScopedHistory, - devTools: readonly DevToolApp[] + devTools: readonly DevToolApp[], + { dataSource }: DevToolsSetupDependencies ) { + const dataSourceEnabled = !!dataSource; if (redirectOnMissingCapabilities(application)) { return () => {}; } @@ -195,6 +295,9 @@ export function renderApp( updateRoute={props.history.push} activeDevTool={devTool} devTools={devTools} + savedObjects={savedObjects} + notifications={notifications} + dataSourceEnabled={dataSourceEnabled} /> )} /> diff --git a/src/plugins/dev_tools/public/index.scss b/src/plugins/dev_tools/public/index.scss index 4bec602ea42d..6ed9c38f106b 100644 --- a/src/plugins/dev_tools/public/index.scss +++ b/src/plugins/dev_tools/public/index.scss @@ -21,3 +21,8 @@ flex-direction: column; flex-grow: 1; } + +.dataSourceSelector { + margin: 5px 10px 5px 5px; + min-width: 400px; +} diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 364f1a93afda..0c0b3b07f5c1 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -34,12 +34,16 @@ import { AppUpdater } from 'opensearch-dashboards/public'; import { i18n } from '@osd/i18n'; import { sortBy } from 'lodash'; +import { DataSourcePluginStart } from 'src/plugins/data_source/public'; import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlForwardingSetup } from '../../url_forwarding/public'; import { CreateDevToolArgs, DevToolApp, createDevToolApp } from './dev_tool'; import './index.scss'; +export interface DevToolsSetupDependencies { + dataSource?: DataSourcePluginStart; +} export interface DevToolsSetup { /** * Register a developer tool. It will be available @@ -62,7 +66,10 @@ export class DevToolsPlugin implements Plugin { return sortBy([...this.devTools.values()], 'order'); } - public setup(coreSetup: CoreSetup, { urlForwarding }: { urlForwarding: UrlForwardingSetup }) { + public setup( + coreSetup: CoreSetup, + { urlForwarding }: { urlForwarding: UrlForwardingSetup } + ) { const { application: applicationSetup, getStartServices } = coreSetup; applicationSetup.register({ @@ -78,11 +85,10 @@ export class DevToolsPlugin implements Plugin { const { element, history } = params; element.classList.add('devAppWrapper'); - const [core] = await getStartServices(); - const { application, chrome } = core; + const [core, devSetup] = await getStartServices(); const { renderApp } = await import('./application'); - return renderApp(element, application, chrome, history, this.getSortedDevTools()); + return renderApp(core, element, history, this.getSortedDevTools(), devSetup); }, });