From 1110e0cd482672c1edb58c89d2e1ef0822f11f5c Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 22 Dec 2023 01:06:01 +0800 Subject: [PATCH] [Feature] Enable ppl visualization in Chatbot (#1304) Signed-off-by: SuZhou-Joe --- opensearch_dashboards.json | 3 +- .../components/ppl_visualization_model.tsx | 78 +++++++++++++++++++ public/dependencies/register_assistant.tsx | 63 +++++++++++++++ public/framework/core_refs.ts | 3 +- public/plugin.ts | 4 + public/types.ts | 11 ++- server/parsers/ppl_parser.ts | 54 +++++++++++++ server/plugin.ts | 12 ++- server/types.ts | 8 ++ 9 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 public/dependencies/components/ppl_visualization_model.tsx create mode 100644 public/dependencies/register_assistant.tsx create mode 100644 server/parsers/ppl_parser.ts diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 7abafdabd9..c05e9a9023 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -19,6 +19,7 @@ "visualizations" ], "optionalPlugins": [ - "managementOverview" + "managementOverview", + "assistantDashboards" ] } \ No newline at end of file diff --git a/public/dependencies/components/ppl_visualization_model.tsx b/public/dependencies/components/ppl_visualization_model.tsx new file mode 100644 index 0000000000..68a01da448 --- /dev/null +++ b/public/dependencies/components/ppl_visualization_model.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiCodeBlock, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import React from 'react'; +import { SavedVisualization } from '../../../common/types/explorer'; +import { SavedObjectVisualization } from '../../components/visualizations/saved_object_visualization'; +import { PPLSavedVisualizationClient } from '../../services/saved_objects/saved_object_client/ppl'; + +interface PPLVisualizationModelProps { + savedVisualization: SavedVisualization; + onClose: () => void; +} + +export const PPLVisualizationModal: React.FC = (props) => { + return ( + <> + + + {props.savedVisualization.name} + + + + +
+ {props.savedVisualization.query} + +
+
+ + + { + const response = await savePPLVisualization(props.savedVisualization); + props.onClose(); + window.open(`./observability-logs#/explorer/${response.objectId}`, '_blank'); + }} + fill + > + Save + + Close + + + ); +}; + +const savePPLVisualization = (savedVisualization: SavedVisualization) => { + const createParams = { + query: savedVisualization.query, + name: savedVisualization.name, + dateRange: [ + savedVisualization.selected_date_range.start, + savedVisualization.selected_date_range.end, + ], + fields: [], + timestamp: '', + type: savedVisualization.type, + sub_type: 'visualization', + }; + return PPLSavedVisualizationClient.getInstance().create(createParams); +}; diff --git a/public/dependencies/register_assistant.tsx b/public/dependencies/register_assistant.tsx new file mode 100644 index 0000000000..ebacf4d6bc --- /dev/null +++ b/public/dependencies/register_assistant.tsx @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { merge } from 'lodash'; +import React from 'react'; +import { toMountPoint } from '../../../../src/plugins/opensearch_dashboards_react/public'; +import { SavedVisualization } from '../../common/types/explorer'; +import { SavedObjectVisualization } from '../components/visualizations/saved_object_visualization'; +import { coreRefs } from '../framework/core_refs'; +import { AssistantSetup } from '../types'; +import { PPLVisualizationModal } from './components/ppl_visualization_model'; + +export const registerAsssitantDependencies = (setup?: AssistantSetup) => { + if (!setup) return; + + setup.registerContentRenderer('ppl_visualization', (content) => { + const params = content as Partial; + const savedVisualization = createSavedVisualization(params); + return ( + + ); + }); + + setup.registerActionExecutor('view_ppl_visualization', async (params) => { + const savedVisualization = createSavedVisualization(params as Partial); + const modal = coreRefs.core!.overlays.openModal( + toMountPoint( + modal.close()} + /> + ) + ); + }); +}; + +const createSavedVisualization = (params: Partial) => { + return merge( + { + query: params.query, + selected_date_range: { start: 'now-14d', end: 'now', text: '' }, + selected_timestamp: { name: 'timestamp', type: 'timestamp' }, + selected_fields: { tokens: [], text: '' }, + name: params.name, + description: '', + type: 'line', + sub_type: 'visualization', + }, + { + selected_date_range: params.selected_date_range, + selected_timestamp: params.selected_timestamp, + type: params.type, + } + ) as SavedVisualization; +}; diff --git a/public/framework/core_refs.ts b/public/framework/core_refs.ts index c12e3d323b..6883e5a03a 100644 --- a/public/framework/core_refs.ts +++ b/public/framework/core_refs.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ApplicationStart, ChromeStart, HttpStart, IToasts } from '../../../../src/core/public'; +import { ApplicationStart, ChromeStart, CoreStart, HttpStart, IToasts } from '../../../../src/core/public'; import { SavedObjectsClientContract } from '../../../../src/core/public'; import { DashboardStart } from '../../../../src/plugins/dashboard/public'; import PPLService from '../services/requests/ppl'; @@ -11,6 +11,7 @@ import PPLService from '../services/requests/ppl'; class CoreRefs { private static _instance: CoreRefs; + public core?: CoreStart; public http?: HttpStart; public savedObjectsClient?: SavedObjectsClientContract; public pplService?: PPLService; diff --git a/public/plugin.ts b/public/plugin.ts index 4348eb4790..db3673380f 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -79,6 +79,7 @@ import { S3DataSource } from './framework/datasources/s3_datasource'; import { DataSourcePluggable } from './framework/datasource_pluggables/datasource_pluggable'; import { DirectSearch } from './components/common/search/sql_search'; import { Search } from './components/common/search/search'; +import { registerAsssitantDependencies } from './dependencies/register_assistant'; export class ObservabilityPlugin implements @@ -306,6 +307,8 @@ export class ObservabilityPlugin }, }); + registerAsssitantDependencies(setupDeps.assistantDashboards); + // Return methods that should be available to other plugins return {}; } @@ -313,6 +316,7 @@ export class ObservabilityPlugin public start(core: CoreStart, startDeps: AppPluginStartDependencies): ObservabilityStart { const pplService: PPLService = new PPLService(core.http); + coreRefs.core = core; coreRefs.http = core.http; coreRefs.savedObjectsClient = core.savedObjects.client; coreRefs.pplService = pplService; diff --git a/public/types.ts b/public/types.ts index 4b6cd96a4e..cfa38acb25 100644 --- a/public/types.ts +++ b/public/types.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CoreStart } from '../../../src/core/public'; import { SavedObjectsClient } from '../../../src/core/server'; import { DashboardStart } from '../../../src/plugins/dashboard/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; @@ -12,6 +11,7 @@ import { ManagementOverViewPluginSetup } from '../../../src/plugins/management_o import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; +import { AssistantSetup } from './types'; export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; @@ -27,6 +27,7 @@ export interface SetupDependencies { data: DataPublicPluginSetup; uiActions: UiActionsStart; managementOverview?: ManagementOverViewPluginSetup; + assistantDashboards?: AssistantSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -34,3 +35,11 @@ export interface ObservabilitySetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ObservabilityStart {} + +/** + * Introduce a compile dependency on dashboards-assistant + * as observerability need some types from the plugin. + * It will gives an type error when dashboards-assistant is not installed so add a ts-ignore to suppress the error. + */ +// @ts-ignore +export type { AssistantSetup } from "../../dashboards-assistant/public"; diff --git a/server/parsers/ppl_parser.ts b/server/parsers/ppl_parser.ts new file mode 100644 index 0000000000..41c8fa4007 --- /dev/null +++ b/server/parsers/ppl_parser.ts @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MessageParser } from '../types'; + +const extractPPLQueries = (content: string) => { + return Array.from(content.matchAll(/(^|[\n\r]|:)\s*(source\s*=\s*.+)/gi)).map( + (match) => match[2] + ); +}; + +export const PPLParsers: MessageParser = { + id: 'ppl_visualization_message', + async parserProvider(interaction) { + const ppls: string[] = (interaction.additional_info?.["PPLTool.output"] as string[] | null)?.flatMap((item: string) => { + let ppl: string = "" + try { + const outputResp = JSON.parse(item); + ppl = outputResp.ppl; + } catch (e) { + ppl = item; + } + + return extractPPLQueries(ppl); + }) || []; + + if (!ppls.length) return []; + + const statsPPLs = ppls.filter((ppl) => /\|\s*stats\s+[^|]+\sby\s/i.test(ppl)); + if (!statsPPLs.length) { + return []; + } + + return statsPPLs.map((query) => { + const finalQuery = query + .replace(/`/g, '') // workaround for https://github.com/opensearch-project/dashboards-observability/issues/509, https://github.com/opensearch-project/dashboards-observability/issues/557 + .replace(/\bSPAN\(/g, 'span('); // workaround for https://github.com/opensearch-project/dashboards-observability/issues/759 + return ({ + type: 'output', + content: finalQuery, + contentType: 'ppl_visualization', + suggestedActions: [ + { + message: 'View details', + actionType: 'view_ppl_visualization', + metadata: { query: finalQuery, question: interaction.input }, + }, + ], + }); + }); + }, +}; diff --git a/server/plugin.ts b/server/plugin.ts index 59e3e41247..ea98af80ee 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -19,7 +19,8 @@ import { searchSavedObject, visualizationSavedObject, } from './saved_objects/observability_saved_object'; -import { ObservabilityPluginSetup, ObservabilityPluginStart } from './types'; +import { ObservabilityPluginSetup, ObservabilityPluginStart, AssistantPluginSetup } from './types'; +import { PPLParsers } from './parsers/ppl_parser'; export class ObservabilityPlugin implements Plugin { @@ -29,7 +30,10 @@ export class ObservabilityPlugin this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, deps: { + assistantDashboards?: AssistantPluginSetup + }) { + const { assistantDashboards } = deps; this.logger.debug('Observability: Setup'); const router = core.http.createRouter(); const openSearchObservabilityClient: ILegacyClusterClient = core.opensearch.legacy.createClient( @@ -121,6 +125,8 @@ export class ObservabilityPlugin }, })); + assistantDashboards?.registerMessageParser(PPLParsers); + return {}; } @@ -129,5 +135,5 @@ export class ObservabilityPlugin return {}; } - public stop() {} + public stop() { } } diff --git a/server/types.ts b/server/types.ts index e936737f3b..e70eeb0c44 100644 --- a/server/types.ts +++ b/server/types.ts @@ -7,3 +7,11 @@ export interface ObservabilityPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ObservabilityPluginStart {} + +/** + * Introduce a compile dependency on dashboards-assistant + * as observerability need some types from the plugin. + * It will gives an type error when dashboards-assistant is not installed so add a ts-ignore to suppress the error. + */ +// @ts-ignore +export type { AssistantPluginSetup, MessageParser } from "../../dashboards-assistant/server";