From 103fb8428f6497efa68ad649856b45c32f118e75 Mon Sep 17 00:00:00 2001 From: rohitratannagar Date: Tue, 8 Sep 2026 06:25:20 +0530 Subject: [PATCH] feat(intelligent-assistant): implement permission-denied UI for chat, notebooks, and MCP tools Fixes: https://redhat.atlassian.net/browse/RHIDP-15932 Fixes: https://redhat.atlassian.net/browse/RHIDP-15933 Fixes: https://redhat.atlassian.net/browse/RHIDP-15934 Signed-off-by: rohitratannagar Co-authored-by: Cursor --- .../.changeset/brave-foxes-dance.md | 5 + .../intelligent-assistant/report-alpha.api.md | 7 + .../src/components/LightSpeedChat.tsx | 167 ++++---- .../components/LightspeedChatBoxHeader.tsx | 10 +- .../components/LightspeedChatContainer.tsx | 26 +- .../src/components/McpServersSettings.tsx | 375 ++++++++++-------- .../__tests__/LightspeedChat.test.tsx | 41 +- .../__tests__/NotebookCard.test.tsx | 1 + .../components/notebooks/DocumentSidebar.tsx | 70 ++-- .../src/components/notebooks/NotebookCard.tsx | 59 ++- .../src/components/notebooks/NotebookView.tsx | 3 + .../src/components/notebooks/NotebooksTab.tsx | 3 + .../useLightspeedNotebooksPermission.ts | 17 +- .../src/hooks/useLightspeedViewPermission.ts | 9 +- .../intelligent-assistant/src/index.tsx | 9 + .../src/translations/de.ts | 11 + .../src/translations/es.ts | 11 + .../src/translations/fr.ts | 11 + .../src/translations/it.ts | 12 + .../src/translations/ja.ts | 12 + .../src/translations/ref.ts | 13 + 21 files changed, 520 insertions(+), 352 deletions(-) create mode 100644 workspaces/intelligent-assistant/.changeset/brave-foxes-dance.md diff --git a/workspaces/intelligent-assistant/.changeset/brave-foxes-dance.md b/workspaces/intelligent-assistant/.changeset/brave-foxes-dance.md new file mode 100644 index 00000000000..ac11f558de0 --- /dev/null +++ b/workspaces/intelligent-assistant/.changeset/brave-foxes-dance.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-intelligent-assistant': minor +--- + +Added permission-denied UI gating for chat, notebooks, and MCP tools permissions diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/report-alpha.api.md b/workspaces/intelligent-assistant/plugins/intelligent-assistant/report-alpha.api.md index a7f7bd2fc8a..a74acecba20 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/report-alpha.api.md +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/report-alpha.api.md @@ -54,6 +54,8 @@ export const intelligentAssistantTranslationRef: TranslationRef< readonly 'notebooks.documents_other': string; readonly 'notebooks.actions.rename': string; readonly 'notebooks.actions.delete': string; + readonly 'notebooks.manage.renameDisabled': string; + readonly 'notebooks.manage.deleteDisabled': string; readonly 'notebooks.rename.inline.tooltip': string; readonly 'notebooks.rename.inline.error': string; readonly 'notebooks.delete.title': string; @@ -128,6 +130,9 @@ export const intelligentAssistantTranslationRef: TranslationRef< readonly 'permission.subject.plugin': string; readonly 'permission.subject.notebooks': string; readonly 'permission.notebooks.goBack': string; + readonly 'permission.chat.readOnlyUse': string; + readonly 'permission.chat.readOnlyUseTooltip': string; + readonly 'permission.chat.readOnlyPlaceholder': string; readonly 'lcore.notConfigured.title': string; readonly 'lcore.notConfigured.description': string; readonly 'lcore.notConfigured.developerLightspeedDocs': string; @@ -236,6 +241,8 @@ export const intelligentAssistantTranslationRef: TranslationRef< readonly 'mcp.settings.selectedCount': string; readonly 'mcp.settings.closeAriaLabel': string; readonly 'mcp.settings.readOnlyAccess': string; + readonly 'mcp.settings.permissionDenied': string; + readonly 'mcp.settings.permissionDeniedDescription': string; readonly 'mcp.settings.tableAriaLabel': string; readonly 'mcp.settings.enabled': string; readonly 'mcp.settings.name': string; diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx index 8df82e14905..a97af12a4ad 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx @@ -33,8 +33,9 @@ import { import { useLocation, useMatch, useNavigate } from 'react-router-dom'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { usePermission } from '@backstage/plugin-permission-react'; -import { Button, makeStyles } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core'; import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; import { @@ -77,6 +78,11 @@ import { import { RhUiAiExperienceIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-experience-icon'; import { useQueryClient } from '@tanstack/react-query'; +import { + iaChatUsePermission, + iaMcpUsePermission, +} from '@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common'; + import { LIGHTSPEED_PATH, supportedFileTypes, @@ -133,7 +139,6 @@ import { SidebarCollapseIcon, SidebarExpandIcon, } from './notebooks/SidebarCollapseIcon'; -import PermissionRequiredState from './PermissionRequiredState'; import { RenameConversationModal } from './RenameConversationModal'; import { ToastAlertGroup } from './ToastAlertGroup'; @@ -658,6 +663,7 @@ type LightspeedChatProps = { profileLoading: boolean; handleSelectedModel: (item: string) => void; models: { label: string; value: string; provider: string }[]; + chatUseAllowed: boolean; }; export const LightspeedChat = ({ @@ -669,6 +675,7 @@ export const LightspeedChat = ({ profileLoading, handleSelectedModel, models, + chatUseAllowed, }: LightspeedChatProps) => { const isMobile = useIsMobile(); const classes = useStyles(); @@ -699,7 +706,6 @@ export const LightspeedChat = ({ const isOnNotebookRoute = Boolean( notebooksRouteMatch || notebookViewRouteMatch, ); - const shouldShowTabs = notebooksEnabled || isOnNotebookRoute; const { displayMode, setDisplayMode, @@ -737,11 +743,18 @@ export const LightspeedChat = ({ }); const { allowed: hasNotebooksAccess, + canManage: canManageNotebooks, loading: notebooksPermissionLoading, - iaNotebooksUsePermissionName, } = useLightspeedNotebooksPermission(); + const mcpUsePermission = usePermission({ + permission: iaMcpUsePermission, + }); + const canUseMcp = mcpUsePermission.allowed; const notebooksPermissionResolved = !notebooksPermissionLoading && hasNotebooksAccess; + const shouldShowTabs = + (notebooksEnabled || isOnNotebookRoute) && notebooksPermissionResolved; + const chatActionsDisabled = !chatUseAllowed; const { data: notebookConversationIdsArray = [] } = useNotebookConversationIds(); @@ -795,6 +808,7 @@ export const LightspeedChat = ({ const [conversationId, setConversationId] = useState(''); const [requestId, setRequestId] = useState(''); const [newChatCreated, setNewChatCreated] = useState(false); + const newChatButtonDisabled = newChatCreated || chatActionsDisabled; const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); const [targetConversationId, setTargetConversationId] = useState(''); @@ -1547,8 +1561,9 @@ export const LightspeedChat = ({ const maxPrompts = getMaxPrompts(); const welcomePrompts = - (newChatCreated && conversationMessages.length === 0) || - (!conversationFound && conversationMessages.length === 0) + !chatActionsDisabled && + ((newChatCreated && conversationMessages.length === 0) || + (!conversationFound && conversationMessages.length === 0)) ? samplePrompts?.slice(0, maxPrompts).map(prompt => { const p = prompt as { title: string; message: string }; return { @@ -1905,52 +1920,68 @@ export const LightspeedChat = ({ className={`${classes.footer} ${classes.fullscreenFooter}`} > - , - }, - microphone: { - tooltipContent: { - active: t('tooltip.microphone.active'), - inactive: t('tooltip.microphone.inactive'), + trigger={chatActionsDisabled ? 'mouseenter focus' : 'manual'} + > + , }, - }, - send: { - tooltipContent: t('tooltip.send'), - }, - }} - additionalActions={ - 0} - disabledTooltip={t('modelSelector.disabledTooltip')} - /> - } - forceMultilineLayout - allowedFileTypes={supportedFileTypes} - onAttachRejected={onAttachRejected} - placeholder={t('chatbox.message.placeholder')} - /> + microphone: { + tooltipContent: { + active: t('tooltip.microphone.active'), + inactive: t('tooltip.microphone.inactive'), + }, + }, + send: { + tooltipContent: t('tooltip.send'), + }, + }} + additionalActions={ + 0} + disabledTooltip={t('modelSelector.disabledTooltip')} + /> + } + forceMultilineLayout + allowedFileTypes={supportedFileTypes} + onAttachRejected={onAttachRejected} + placeholder={ + chatActionsDisabled + ? t('permission.chat.readOnlyPlaceholder') + : t('chatbox.message.placeholder') + } + /> + @@ -2076,7 +2107,7 @@ export const LightspeedChat = ({ @@ -2084,7 +2115,7 @@ export const LightspeedChat = ({ style={{ width: 18, height: 18, - color: newChatCreated + color: newChatButtonDisabled ? undefined : 'var(--pf-t--global--color--brand--default)', }} @@ -2135,7 +2166,10 @@ export const LightspeedChat = ({ setDisplayMode={setDisplayModeFromHeader} displayMode={displayMode} onPinnedChatsToggle={handlePinningChatsToggle} - onMcpSettingsClick={() => setIsMcpSettingsOpen(true)} + onMcpSettingsClick={ + canUseMcp ? () => setIsMcpSettingsOpen(true) : undefined + } + mcpSettingsDisabled={!canUseMcp} /> {(isFullscreenMode || shouldShowTabs) && ( @@ -2226,7 +2260,7 @@ export const LightspeedChat = ({ setIsChatHistoryDrawerOpen(true)} onNewChat={onNewChat} - newChatDisabled={newChatCreated} + newChatDisabled={newChatButtonDisabled} /> )} {children} @@ -2260,7 +2294,7 @@ export const LightspeedChat = ({ newChatButtonText={t('button.newChat')} newChatButtonProps={{ icon: , - isDisabled: newChatCreated, + isDisabled: newChatButtonDisabled, }} handleTextInputChange={handleFilter} searchInputPlaceholder={t('chatbox.search.placeholder')} @@ -2338,6 +2372,7 @@ export const LightspeedChat = ({ isUploadModalOpen={notebookUploadModalOpen} onUploadModalOpenChange={setNotebookUploadModalOpen} onUploadsInProgressChange={setNotebookUploadsInProgress} + canManage={canManageNotebooks} /> )} {showNotebooksPanel && @@ -2379,31 +2414,11 @@ export const LightspeedChat = ({ onRename={handleRenameNotebook} onDelete={setDeleteNotebookId} onCreateNotebook={handleCreateNotebook} + canManage={canManageNotebooks} t={t} /> )} - {showNotebooksPanel && - !notebooksPermissionLoading && - !hasNotebooksAccess && ( - { - setActiveTab(0); - setShellViewTab(0); - }} - > - {t('permission.notebooks.goBack')} - - } - /> - )} diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedChatBoxHeader.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedChatBoxHeader.tsx index cc18b4cdfba..a97edb40fd0 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedChatBoxHeader.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedChatBoxHeader.tsx @@ -50,7 +50,8 @@ type LightspeedChatBoxHeaderProps = { models: { label: string; value: string; provider: string }[]; isPinningChatsEnabled: boolean; onPinnedChatsToggle: (state: boolean) => void; - onMcpSettingsClick: () => void; + onMcpSettingsClick?: () => void; + mcpSettingsDisabled?: boolean; isModelSelectorDisabled?: boolean; hideModelSelector?: boolean; /** When false, omits pinned-chats and MCP entries (Chat tab only). */ @@ -88,6 +89,7 @@ export const LightspeedChatBoxHeader = ({ isPinningChatsEnabled, onPinnedChatsToggle, onMcpSettingsClick, + mcpSettingsDisabled = false, isModelSelectorDisabled = false, hideModelSelector = false, showChatTabOptions = true, @@ -251,6 +253,12 @@ export const LightspeedChatBoxHeader = ({ key="mcpSettings" icon={} onClick={onMcpSettingsClick} + isDisabled={mcpSettingsDisabled} + tooltipProps={ + mcpSettingsDisabled + ? { content: t('mcp.settings.permissionDenied') } + : undefined + } > {t('settings.mcp.label')}