diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 43bec39f..aef99132 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -11,7 +11,8 @@ "opensearchDashboardsReact", "opensearchDashboardsUtils", "visualizations", - "savedObjects" + "savedObjects", + "uiActions" ], "optionalPlugins": [ "dataSource", diff --git a/public/assets/assistant_trigger.svg b/public/assets/assistant_trigger.svg new file mode 100644 index 00000000..cc98544e --- /dev/null +++ b/public/assets/assistant_trigger.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/components/ui_action_context_menu.tsx b/public/components/ui_action_context_menu.tsx new file mode 100644 index 00000000..5794f499 --- /dev/null +++ b/public/components/ui_action_context_menu.tsx @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useRef } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { EuiButtonIcon, EuiContextMenu, EuiPopover } from '@elastic/eui'; + +import { buildContextMenuForActions } from '../../../../src/plugins/ui_actions/public'; +import { AI_ASSISTANT_QUERY_EDITOR_TRIGGER } from '../ui_triggers'; +import { getUiActions } from '../services'; +import assistantTriggerIcon from '../assets/assistant_trigger.svg'; + +export const ActionContextMenu = () => { + const uiActions = getUiActions(); + const actionsRef = useRef(uiActions.getTriggerActions(AI_ASSISTANT_QUERY_EDITOR_TRIGGER)); + const [open, setOpen] = useState(false); + + const panels = useAsync( + () => + buildContextMenuForActions({ + actions: actionsRef.current.map((action) => ({ + action, + context: {}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + trigger: AI_ASSISTANT_QUERY_EDITOR_TRIGGER as any, + })), + closeMenu: () => setOpen(false), + }), + [] + ); + + if (actionsRef.current.length === 0) { + return null; + } + + return ( + setOpen(!open)} + /> + } + isOpen={open} + panelPaddingSize="none" + anchorPosition="downRight" + closePopover={() => setOpen(false)} + > + + + ); +}; diff --git a/public/index.scss b/public/index.scss index 733b86f4..9b7f9e77 100644 --- a/public/index.scss +++ b/public/index.scss @@ -179,3 +179,7 @@ button.llm-chat-error-refresh-button.llm-chat-error-refresh-button { display: none; } } + +.osdQueryEditorExtensionComponent__assistant-query-actions { + margin-left: auto; +} diff --git a/public/plugin.tsx b/public/plugin.tsx index f3b4ab4e..bc152665 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -6,7 +6,7 @@ import { i18n } from '@osd/i18n'; import { EuiLoadingSpinner } from '@elastic/eui'; import React, { lazy, Suspense } from 'react'; -import { Subscription } from 'rxjs'; +import { of, Subscription } from 'rxjs'; import { AppMountParameters, AppNavLinkStatus, @@ -39,12 +39,16 @@ import { setNotifications, setIncontextInsightRegistry, setConfigSchema, + setUiActions, } from './services'; import { ConfigSchema } from '../common/types/config'; import { DataSourceService } from './services/data_source_service'; import { ASSISTANT_API, DEFAULT_USER_NAME } from '../common/constants/llm'; import { IncontextInsightProps } from './components/incontext_insight'; import { AssistantService } from './services/assistant_service'; +import { ActionContextMenu } from './components/ui_action_context_menu'; +import { AI_ASSISTANT_QUERY_EDITOR_TRIGGER, bootstrap } from './ui_triggers'; +import { TEXT2VIZ_APP_ID } from './text2viz'; export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); @@ -102,6 +106,9 @@ export class AssistantPlugin return account; }; + // setup ui trigger + bootstrap(setupDeps.uiActions); + const dataSourceSetupResult = this.dataSourceService.setup({ uiSettings: core.uiSettings, dataSourceManagement: setupDeps.dataSourceManagement, @@ -132,7 +139,7 @@ export class AssistantPlugin }); core.application.register({ - id: 'text2viz', + id: TEXT2VIZ_APP_ID, title: i18n.translate('dashboardAssistant.feature.text2viz', { defaultMessage: 'Natural language previewer', }), @@ -188,6 +195,19 @@ export class AssistantPlugin setupChat(); } + setupDeps.data.__enhance({ + editor: { + queryEditorExtension: { + id: 'assistant-query-actions', + order: 2000, + isEnabled$: () => of(true), + getComponent: () => { + return ; + }, + }, + }, + }); + return { dataSource: dataSourceSetupResult, registerMessageRenderer: (contentType, render) => { @@ -203,6 +223,9 @@ export class AssistantPlugin chatEnabled: () => this.config.chat.enabled, nextEnabled: () => this.config.next.enabled, assistantActions, + assistantTriggers: { + AI_ASSISTANT_QUERY_EDITOR_TRIGGER, + }, registerIncontextInsight: this.incontextInsightRegistry.register.bind( this.incontextInsightRegistry ), @@ -215,12 +238,28 @@ export class AssistantPlugin }; } - public start(core: CoreStart): AssistantStart { + public start( + core: CoreStart, + { data, uiActions }: AssistantPluginStartDependencies + ): AssistantStart { const assistantServiceStart = this.assistantService.start(core.http); setCoreStart(core); setChrome(core.chrome); setNotifications(core.notifications); setConfigSchema(this.config); + setUiActions(uiActions); + + if (this.config.next.enabled) { + uiActions.addTriggerAction(AI_ASSISTANT_QUERY_EDITOR_TRIGGER, { + id: 'assistant_generate_visualization_action', + order: 1, + getDisplayName: () => 'Generate visualization', + getIconType: () => 'visLine' as const, + execute: async () => { + core.application.navigateToApp(TEXT2VIZ_APP_ID); + }, + }); + } return { dataSource: this.dataSourceService.start(), diff --git a/public/services/index.ts b/public/services/index.ts index 7d774bbc..c6963e74 100644 --- a/public/services/index.ts +++ b/public/services/index.ts @@ -4,6 +4,7 @@ */ import { createGetterSetter } from '../../../../src/plugins/opensearch_dashboards_utils/public'; +import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { ChromeStart, NotificationsStart } from '../../../../src/core/public'; import { IncontextInsightRegistry } from './incontext_insight'; import { ConfigSchema } from '../../common/types/config'; @@ -24,4 +25,6 @@ export const [getNotifications, setNotifications] = createGetterSetter('ConfigSchema'); +export const [getUiActions, setUiActions] = createGetterSetter('uiActions'); + export { DataSourceService, DataSourceServiceContract } from './data_source_service'; diff --git a/public/text2viz.tsx b/public/text2viz.tsx index 634cc191..5b066d90 100644 --- a/public/text2viz.tsx +++ b/public/text2viz.tsx @@ -10,6 +10,8 @@ import { Text2Viz } from './components/visualization/text2viz'; import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; import { StartServices } from './types'; +export const TEXT2VIZ_APP_ID = 'text2viz'; + export const renderText2VizApp = (params: AppMountParameters, services: StartServices) => { ReactDOM.render( diff --git a/public/types.ts b/public/types.ts index 033f4a83..b7250bab 100644 --- a/public/types.ts +++ b/public/types.ts @@ -17,6 +17,7 @@ import { import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; import { AppMountParameters, CoreStart } from '../../../src/core/public'; import { AssistantClient } from './services/assistant_client'; +import { UiActionsSetup, UiActionsStart } from '../../../src/plugins/ui_actions/public'; export interface RenderProps { props: MessageContentProps; @@ -40,6 +41,7 @@ export interface AssistantPluginStartDependencies { visualizations: VisualizationsStart; embeddable: EmbeddableStart; dashboard: DashboardStart; + uiActions: UiActionsStart; } export interface AssistantPluginSetupDependencies { @@ -47,6 +49,7 @@ export interface AssistantPluginSetupDependencies { visualizations: VisualizationsSetup; embeddable: EmbeddableSetup; dataSourceManagement?: DataSourceManagementPluginSetup; + uiActions: UiActionsSetup; } export interface AssistantSetup { @@ -62,6 +65,7 @@ export interface AssistantSetup { */ nextEnabled: () => boolean; assistantActions: Omit; + assistantTriggers: { AI_ASSISTANT_QUERY_EDITOR_TRIGGER: string }; registerIncontextInsight: IncontextInsightRegistry['register']; renderIncontextInsight: (component: React.ReactNode) => React.ReactNode; } diff --git a/public/ui_triggers.ts b/public/ui_triggers.ts new file mode 100644 index 00000000..48718b45 --- /dev/null +++ b/public/ui_triggers.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Trigger, UiActionsSetup } from '../../../src/plugins/ui_actions/public'; + +export const AI_ASSISTANT_QUERY_EDITOR_TRIGGER = 'AI_ASSISTANT_QUERY_EDITOR_TRIGGER'; + +declare module '../../../src/plugins/ui_actions/public' { + export interface TriggerContextMapping { + [AI_ASSISTANT_QUERY_EDITOR_TRIGGER]: {}; + } +} + +const aiAssistantTrigger: Trigger<'AI_ASSISTANT_QUERY_EDITOR_TRIGGER'> = { + id: AI_ASSISTANT_QUERY_EDITOR_TRIGGER, +}; + +export const bootstrap = (uiActions: UiActionsSetup) => { + uiActions.registerTrigger(aiAssistantTrigger); +};