diff --git a/common/constants/llm.ts b/common/constants/llm.ts index 3216967c..6e21fd94 100644 --- a/common/constants/llm.ts +++ b/common/constants/llm.ts @@ -16,9 +16,12 @@ export const ASSISTANT_API = { ABORT_AGENT_EXECUTION: `${API_BASE}/abort`, REGENERATE: `${API_BASE}/regenerate`, TRACE: `${API_BASE}/trace`, + ACCOUNT: `${API_BASE}/account`, } as const; export const NOTEBOOK_API = { CREATE_NOTEBOOK: `${NOTEBOOK_PREFIX}/note`, SET_PARAGRAPH: `${NOTEBOOK_PREFIX}/set_paragraphs/`, }; + +export const DEFAULT_USER_NAME = 'User'; diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index f286b75a..1b5cbb9d 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -11,7 +11,6 @@ "opensearchDashboardsUtils" ], "optionalPlugins": [ - "securityDashboards", "dataSource", "dataSourceManagement" ], diff --git a/public/chat_header_button.test.tsx b/public/chat_header_button.test.tsx index b5aa5254..f6ebae40 100644 --- a/public/chat_header_button.test.tsx +++ b/public/chat_header_button.test.tsx @@ -92,11 +92,10 @@ describe('', () => { render( ); @@ -142,11 +141,10 @@ describe('', () => { render( ); screen.getByLabelText('chat input').focus(); @@ -166,11 +164,10 @@ describe('', () => { render( ); expect(screen.getByLabelText('chat input')).not.toHaveFocus(); @@ -183,29 +180,14 @@ describe('', () => { expect(screen.getByLabelText('chat input')).toHaveFocus(); }); - it('should disable chat input when no access', () => { - render( - - ); - expect(screen.getByLabelText('chat input')).toBeDisabled(); - }); - it('should not focus on chat input when no access and pressing global shortcut', () => { render( ); expect(screen.getByLabelText('chat input')).not.toHaveFocus(); diff --git a/public/chat_header_button.tsx b/public/chat_header_button.tsx index 2f83e388..1c8240de 100644 --- a/public/chat_header_button.tsx +++ b/public/chat_header_button.tsx @@ -27,7 +27,6 @@ import { MountPointPortal } from '../../../src/plugins/opensearch_dashboards_rea interface HeaderChatButtonProps { application: ApplicationStart; - userHasAccess: boolean; messageRenderers: Record; actionExecutors: Record; assistantActions: AssistantActions; @@ -74,7 +73,6 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => { flyoutFullScreen, setFlyoutVisible, setFlyoutComponent, - userHasAccess: props.userHasAccess, messageRenderers: props.messageRenderers, actionExecutors: props.actionExecutors, currentAccount: props.currentAccount, @@ -92,7 +90,6 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => { flyoutFullScreen, selectedTabId, preSelectedTabId, - props.userHasAccess, props.messageRenderers, props.actionExecutors, props.currentAccount, @@ -157,9 +154,6 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => { }, []); useEffect(() => { - if (!props.userHasAccess) { - return; - } const onGlobalMouseUp = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === '/') { inputRef.current?.focus(); @@ -170,7 +164,7 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => { return () => { document.removeEventListener('keydown', onGlobalMouseUp); }; - }, [props.userHasAccess]); + }, []); useEffect(() => { const handleSuggestion = (event: { suggestion: string }) => { @@ -235,7 +229,6 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => { )} } - disabled={!props.userHasAccess} /> diff --git a/public/components/__tests__/chat_window_header_title.test.tsx b/public/components/__tests__/chat_window_header_title.test.tsx index 5c87cd55..ec3ece97 100644 --- a/public/components/__tests__/chat_window_header_title.test.tsx +++ b/public/components/__tests__/chat_window_header_title.test.tsx @@ -22,7 +22,11 @@ import { DataSourceServiceMock } from '../../services/data_source_service.mock'; const setup = ({ messages = [], ...rest -}: { messages?: IMessage[]; conversationId?: string | undefined } = {}) => { +}: { + messages?: IMessage[]; + conversationId?: string | undefined; + dataSource?: DataSourceServiceMock; +} = {}) => { const useCoreMock = { services: { ...coreMock.createStart(), @@ -38,7 +42,7 @@ const setup = ({ }), reload: jest.fn(), }, - dataSource: new DataSourceServiceMock(), + dataSource: rest.dataSource ?? new DataSourceServiceMock(false), }, }; useCoreMock.services.http.put.mockImplementation(() => Promise.resolve()); @@ -170,6 +174,19 @@ describe('', () => { }); }); + it('should hide save to notebook button when MDS enabled', async () => { + const { renderResult } = setup({ + messages: [{ type: 'input', contentType: 'text', content: 'foo' }], + dataSource: new DataSourceServiceMock(true), + }); + + fireEvent.click(renderResult.getByLabelText('toggle chat context menu')); + + expect( + renderResult.queryByRole('button', { name: 'Save to notebook' }) + ).not.toBeInTheDocument(); + }); + it('should disable "Save to notebook" button when message does not include input', async () => { const { renderResult } = setup({ messages: [{ type: 'output', content: 'bar', contentType: 'markdown' }], diff --git a/public/components/chat_window_header_title.tsx b/public/components/chat_window_header_title.tsx index b8c2523d..316f4bf8 100644 --- a/public/components/chat_window_header_title.tsx +++ b/public/components/chat_window_header_title.tsx @@ -12,7 +12,7 @@ import { EuiButtonIcon, EuiToolTip, } from '@elastic/eui'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useChatContext } from '../contexts'; import { useChatActions, useChatState, useSaveChat } from '../hooks'; import { NotebookNameModal } from './notebook/notebook_name_modal'; @@ -57,6 +57,11 @@ export const ChatWindowHeaderTitle = React.memo(() => { [chatContext, core.services.conversations] ); + const displayNotebookFeature = useMemo(() => { + // Notebook dashboard API doesn't support MDS for now, so we hide saving to notebook feature when MDS enabled. + return !core.services.dataSource.isMDSEnabled(); + }, [core.services.dataSource.isMDSEnabled]); + const handleSaveNotebookModalClose = () => { setSaveNotebookModalOpen(false); }; @@ -87,17 +92,19 @@ export const ChatWindowHeaderTitle = React.memo(() => { > New conversation , - { - closePopover(); - setSaveNotebookModalOpen(true); - }} - // User only can save conversation when he send a message at least. - disabled={chatState.messages.every((item) => item.type !== 'input')} - > - Save to notebook - , + displayNotebookFeature && ( + { + closePopover(); + setSaveNotebookModalOpen(true); + }} + // User only can save conversation when he send a message at least. + disabled={chatState.messages.every((item) => item.type !== 'input')} + > + Save to notebook + + ), ]; return ( diff --git a/public/contexts/__tests__/chat_context.test.tsx b/public/contexts/__tests__/chat_context.test.tsx index 2500c5ca..696b377b 100644 --- a/public/contexts/__tests__/chat_context.test.tsx +++ b/public/contexts/__tests__/chat_context.test.tsx @@ -17,10 +17,9 @@ describe('useChatContext', () => { flyoutFullScreen: true, setFlyoutVisible: jest.fn(), setFlyoutComponent: jest.fn(), - userHasAccess: true, messageRenderers: {}, actionExecutors: {}, - currentAccount: { username: 'foo', tenant: '' }, + currentAccount: { username: 'foo' }, setTitle: jest.fn(), setInteractionId: jest.fn(), }; diff --git a/public/contexts/chat_context.tsx b/public/contexts/chat_context.tsx index f7bc516c..747f6484 100644 --- a/public/contexts/chat_context.tsx +++ b/public/contexts/chat_context.tsx @@ -18,7 +18,6 @@ export interface IChatContext { flyoutFullScreen: boolean; setFlyoutVisible: React.Dispatch>; setFlyoutComponent: React.Dispatch>; - userHasAccess: boolean; messageRenderers: Record; actionExecutors: Record; currentAccount?: UserAccount; diff --git a/public/hooks/use_chat_actions.test.tsx b/public/hooks/use_chat_actions.test.tsx index 6418ae31..26ca0096 100644 --- a/public/hooks/use_chat_actions.test.tsx +++ b/public/hooks/use_chat_actions.test.tsx @@ -76,11 +76,9 @@ describe('useChatActions hook', () => { setInteractionId: setInteractionIdMock, flyoutVisible: false, flyoutFullScreen: false, - userHasAccess: false, messageRenderers: {}, currentAccount: { username: '', - tenant: '', }, }; diff --git a/public/plugin.tsx b/public/plugin.tsx index a42449e5..8e5c1151 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -32,6 +32,7 @@ import { } 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'; export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); @@ -46,7 +47,7 @@ export const IncontextInsightComponent: React.FC<{ props: any }> = (props) => ( ); interface UserAccountResponse { - data: { roles: string[]; user_name: string; user_requested_tenant?: string }; + user_name: string; } export class AssistantPlugin @@ -76,27 +77,15 @@ export class AssistantPlugin const actionExecutors: Record = {}; const assistantActions: AssistantActions = {} as AssistantActions; /** - * Returns {@link UserAccountResponse}. Provides default roles and user - * name if security plugin call fails. + * Returns {@link UserAccountResponse}. Provides user name. */ - const getAccount: () => Promise = (() => { - let account: UserAccountResponse; - return async () => { - if (setupDeps.securityDashboards === undefined) - return { data: { roles: ['all_access'], user_name: 'dashboards_user' } }; - if (account === undefined) { - account = await core.http - .get('/api/v1/configuration/account') - .catch((e) => { - console.error(`Failed to request user account information: ${String(e.body || e)}`); - return { data: { roles: [], user_name: '' } }; - }); - } - return account; - }; - })(); - const checkAccess = (account: Awaited>) => - account.data.roles.some((role) => ['all_access', 'assistant_user'].includes(role)); + const getAccount: () => Promise = async () => { + const account = await core.http.get(ASSISTANT_API.ACCOUNT).catch((e) => { + console.error(`Failed to request user account information: ${String(e.body || e)}`); + return { user_name: DEFAULT_USER_NAME }; + }); + return account; + }; const dataSourceSetupResult = this.dataSourceService.setup({ uiSettings: core.uiSettings, @@ -116,8 +105,7 @@ export class AssistantPlugin dataSource: this.dataSourceService, }); const account = await getAccount(); - const username = account.data.user_name; - const tenant = account.data.user_requested_tenant ?? ''; + const username = account.user_name; this.incontextInsightRegistry?.setIsEnabled(this.config.incontextInsight.enabled); coreStart.chrome.navControls.registerRight({ @@ -126,11 +114,10 @@ export class AssistantPlugin ), @@ -152,7 +139,6 @@ export class AssistantPlugin actionExecutors[actionType] = execute; }, chatEnabled: () => this.config.chat.enabled, - userHasAccess: async () => await getAccount().then(checkAccess), assistantActions, registerIncontextInsight: this.incontextInsightRegistry.register.bind( this.incontextInsightRegistry diff --git a/public/services/data_source_service.mock.ts b/public/services/data_source_service.mock.ts index 40fc2018..0ca0bea1 100644 --- a/public/services/data_source_service.mock.ts +++ b/public/services/data_source_service.mock.ts @@ -4,12 +4,23 @@ */ export class DataSourceServiceMock { - constructor() {} + private _isMDSEnabled = true; + constructor(isMDSEnabled?: boolean) { + this._isMDSEnabled = isMDSEnabled ?? true; + } getDataSourceQuery() { + const result = this._isMDSEnabled + ? { + dataSourceId: '', + } + : {}; return new Promise((resolve) => { - resolve({ dataSourceId: '' }); + resolve(result); }); - // return { dataSourceId: '' }; + } + + isMDSEnabled() { + return this._isMDSEnabled; } } diff --git a/public/tabs/chat/chat_page.tsx b/public/tabs/chat/chat_page.tsx index 97da71db..b74f86cd 100644 --- a/public/tabs/chat/chat_page.tsx +++ b/public/tabs/chat/chat_page.tsx @@ -60,7 +60,7 @@ export const ChatPage: React.FC = (props) => { diff --git a/public/tabs/chat/chat_page_content.test.tsx b/public/tabs/chat/chat_page_content.test.tsx index 67051717..784383e7 100644 --- a/public/tabs/chat/chat_page_content.test.tsx +++ b/public/tabs/chat/chat_page_content.test.tsx @@ -45,7 +45,6 @@ describe('', () => { }, currentAccount: { username: 'test_user', - tenant: 'private', }, }); diff --git a/public/types.ts b/public/types.ts index 9f92ab1a..ca6505b2 100644 --- a/public/types.ts +++ b/public/types.ts @@ -34,7 +34,6 @@ export interface AssistantPluginStartDependencies { export interface AssistantPluginSetupDependencies { embeddable: EmbeddableSetup; - securityDashboards?: {}; dataSourceManagement?: DataSourceManagementPluginSetup; } @@ -46,10 +45,6 @@ export interface AssistantSetup { * Returns true if chat UI is enabled. */ chatEnabled: () => boolean; - /** - * Returns true if current user has permission to use assistant features. - */ - userHasAccess: () => Promise; assistantActions: Omit; registerIncontextInsight: IncontextInsightRegistry['register']; renderIncontextInsight: (component: React.ReactNode) => React.ReactNode; @@ -61,7 +56,6 @@ export interface AssistantStart { export interface UserAccount { username: string; - tenant: string; } export interface ChatConfig { diff --git a/server/plugin.ts b/server/plugin.ts index 26b86e17..5918864a 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -12,10 +12,10 @@ import { Plugin, PluginInitializerContext, } from '../../../src/core/server'; -import { setupRoutes } from './routes/index'; import { AssistantPluginSetup, AssistantPluginStart, MessageParser } from './types'; import { BasicInputOutputParser } from './parsers/basic_input_output_parser'; import { VisualizationCardParser } from './parsers/visualization_card_parser'; +import { registerChatRoutes } from './routes/chat_routes'; export class AssistantPlugin implements Plugin { private readonly logger: Logger; @@ -42,8 +42,9 @@ export class AssistantPlugin implements Plugin ({ diff --git a/server/routes/chat_routes.ts b/server/routes/chat_routes.ts index e0809be5..dd985179 100644 --- a/server/routes/chat_routes.ts +++ b/server/routes/chat_routes.ts @@ -12,7 +12,7 @@ import { IRouter, RequestHandlerContext, } from '../../../../src/core/server'; -import { ASSISTANT_API } from '../../common/constants/llm'; +import { ASSISTANT_API, DEFAULT_USER_NAME } from '../../common/constants/llm'; import { OllyChatService } from '../services/chat/olly_chat_service'; import { AgentFrameworkStorageService } from '../services/storage/agent_framework_storage_service'; import { RoutesOptions } from '../types'; @@ -152,6 +152,11 @@ const feedbackRoute = { }, }; +const accountRoute = { + path: `${ASSISTANT_API.ACCOUNT}`, + validate: {}, +}; + export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) { const createStorageService = async (context: RequestHandlerContext, dataSourceId?: string) => new AgentFrameworkStorageService( @@ -430,4 +435,31 @@ export function registerChatRoutes(router: IRouter, routeOptions: RoutesOptions) } } ); + + router.get( + accountRoute, + async ( + context, + request, + response + ): Promise> => { + try { + const auth = routeOptions.auth.get<{ + authInfo?: { + user_name?: string; + }; + }>(request); + return response.ok({ + body: { + user_name: auth?.state?.authInfo?.user_name ?? DEFAULT_USER_NAME, + }, + }); + } catch (error) { + context.assistant_plugin.logger.error(error); + return response.ok({ + body: { user_name: DEFAULT_USER_NAME }, + }); + } + } + ); } diff --git a/server/routes/index.ts b/server/routes/index.ts deleted file mode 100644 index 6fc930e0..00000000 --- a/server/routes/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { RoutesOptions } from '../types'; -import { IRouter } from '../../../../src/core/server'; -import { registerChatRoutes } from './chat_routes'; - -export function setupRoutes(router: IRouter, routeOptions: RoutesOptions) { - registerChatRoutes(router, routeOptions); -} diff --git a/server/types.ts b/server/types.ts index b2067477..a47b42bd 100644 --- a/server/types.ts +++ b/server/types.ts @@ -4,7 +4,7 @@ */ import { IMessage, Interaction } from '../common/types/chat_saved_object_attributes'; -import { Logger } from '../../../src/core/server'; +import { Logger, HttpAuth } from '../../../src/core/server'; export interface AssistantPluginSetup { registerMessageParser: (message: MessageParser) => void; @@ -40,6 +40,7 @@ export interface MessageParser { export interface RoutesOptions { messageParsers: MessageParser[]; + auth: HttpAuth; } declare module '../../../src/core/server' {