Skip to content

Commit

Permalink
add permission support in lightspeed
Browse files Browse the repository at this point in the history
Signed-off-by: Karthik <[email protected]>
  • Loading branch information
karthikjeeyar committed Nov 22, 2024
1 parent 63c80a2 commit 1967890
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 15 deletions.
6 changes: 5 additions & 1 deletion workspaces/lightspeed/plugins/lightspeed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"pluginPackage": "@red-hat-developer-hub/backstage-plugin-lightspeed",
"pluginPackages": [
"@red-hat-developer-hub/backstage-plugin-lightspeed",
"@red-hat-developer-hub/backstage-plugin-lightspeed-backend"
"@red-hat-developer-hub/backstage-plugin-lightspeed-backend",
"@red-hat-developer-hub/backstage-plugin-lightspeed-common"
]
},
"sideEffects": [
Expand All @@ -39,10 +40,13 @@
"dependencies": {
"@backstage/core-components": "^0.15.1",
"@backstage/core-plugin-api": "^1.10.0",
"@backstage/plugin-permission-react": "^0.4.28",
"@backstage/theme": "^0.6.0",
"@material-ui/core": "^4.9.13",
"@material-ui/lab": "^4.0.0-alpha.61",
"@patternfly/react-core": "6.0.0-prerelease.21",
"@patternfly/virtual-assistant": "2.0.0-alpha.61",
"@red-hat-developer-hub/backstage-plugin-lightspeed-common": "workspace:^",
"@tanstack/react-query": "^5.59.15",
"openai": "^4.52.6",
"react-markdown": "^9.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/
import React from 'react';

import { makeStyles } from '@material-ui/core';
import { ErrorPanel } from '@backstage/core-components';

import { Box, makeStyles } from '@material-ui/core';
import { DropdownItem, Title } from '@patternfly/react-core';
import {
Chatbot,
Expand All @@ -38,6 +40,7 @@ import { useConversationMessages } from '../hooks/useConversationMessages';
import { useConversations } from '../hooks/useConversations';
import { useCreateConversation } from '../hooks/useCreateConversation';
import { useDeleteConversation } from '../hooks/useDeleteConversation';
import { useLightspeedDeletePermission } from '../hooks/useLightspeedDeletePermission';
import { ConversationSummary } from '../types';
import {
getCategorizeMessages,
Expand Down Expand Up @@ -92,12 +95,14 @@ export const LightspeedChat = ({
const [newChatCreated, setNewChatCreated] = React.useState<boolean>(true);
const [isSendButtonDisabled, setIsSendButtonDisabled] =
React.useState<boolean>(false);
const [error, setError] = React.useState<Error | null>(null);

const queryClient = useQueryClient();

const { data: conversations = [] } = useConversations();
const { mutateAsync: createConversation } = useCreateConversation();
const { mutateAsync: deleteConversation } = useDeleteConversation();
const { allowed: hasDeleteAccess } = useLightspeedDeletePermission();

React.useEffect(() => {
if (user) {
Expand All @@ -108,6 +113,7 @@ export const LightspeedChat = ({
.catch(e => {
// eslint-disable-next-line
console.warn(e);
setError(e);
});
}
}, [user, setConversationId, createConversation]);
Expand Down Expand Up @@ -162,24 +168,25 @@ export const LightspeedChat = ({
(conversationSummary: ConversationSummary) => ({
menuItems: (
<DropdownItem
isDisabled={!hasDeleteAccess}
onClick={async () => {
try {
await deleteConversation({
conversation_id: conversationSummary.conversation_id,
invalidateCache: false,
});
onNewChat();
} catch (error) {
} catch (e) {
// eslint-disable-next-line no-console
console.warn({ error });
console.warn(e);
}
}}
>
Delete
</DropdownItem>
),
}),
[deleteConversation, onNewChat],
[deleteConversation, onNewChat, hasDeleteAccess],
);
const categorizedMessages = getCategorizeMessages(
conversations,
Expand Down Expand Up @@ -250,6 +257,14 @@ export const LightspeedChat = ({
setIsDrawerOpen(isOpen => !isOpen);
}, []);

if (error) {
return (
<Box padding={1}>
<ErrorPanel error={error} />
</Box>
);
}

return (
<>
<Chatbot displayMode={ChatbotDisplayMode.embedded}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import { Content, Page } from '@backstage/core-components';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';

import { createStyles, makeStyles, useTheme } from '@material-ui/core/styles';
import { Alert, AlertTitle } from '@material-ui/lab';
import { QueryClientProvider } from '@tanstack/react-query';

import { useAllModels } from '../hooks/useAllModels';
import { useLightspeedViewPermission } from '../hooks/useLightspeedViewPermission';
import queryClient from '../utils/queryClient';
import { LightspeedChat } from './LightSpeedChat';

Expand All @@ -46,6 +48,7 @@ const LightspeedPageInner = () => {
const identityApi = useApi(identityApiRef);

const { data: models } = useAllModels();
const { allowed: hasViewAccess, loading } = useLightspeedViewPermission();

const { value: profile, loading: profileLoading } = useAsync(
async () => await identityApi.getProfileInfo(),
Expand All @@ -72,19 +75,31 @@ const LightspeedPageInner = () => {
if (modelsItems.length > 0) setSelectedModel(modelsItems[0].value);
}, [modelsItems]);

if (loading) {
return null;
}
return (
<Page themeId="tool">
<Content className={classes.container}>
<LightspeedChat
selectedModel={selectedModel}
handleSelectedModel={item => {
setSelectedModel(item);
}}
models={modelsItems}
userName={profile?.displayName}
avatar={profile?.picture}
profileLoading={profileLoading}
/>
{!hasViewAccess ? (
<Alert severity="warning" data-testid="no-permission-alert">
<AlertTitle>Permission required</AlertTitle>
To view lightspeed plugin, contact your administrator to give you
the `lightspeed.conversations.read` and
`lightspeed.conversations.create` permission.
</Alert>
) : (
<LightspeedChat
selectedModel={selectedModel}
handleSelectedModel={item => {
setSelectedModel(item);
}}
models={modelsItems}
userName={profile?.displayName}
avatar={profile?.picture}
profileLoading={profileLoading}
/>
)}
</Content>
</Page>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';

import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import { usePermission } from '@backstage/plugin-permission-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';

import { screen, waitFor } from '@testing-library/react';

import { LightspeedPage } from '../LightspeedPage';

jest.mock('../LightSpeedChat', () => ({
LightspeedChat: () => <>LightspeedChat</>,
}));

jest.mock('@backstage/plugin-permission-react', () => ({
usePermission: jest.fn(),
RequirePermission: jest.fn(),
}));

const identityApi = {
async getCredentials() {
return { token: 'test-token' };
},
} as IdentityApi;

jest.mock('@mui/material', () => ({
...jest.requireActual('@mui/material'),
makeStyles: () => () => {
return {
container: 'container',
};
},
}));

jest.mock('../../hooks/useAllModels', () => ({
useAllModels: jest.fn().mockResolvedValue({
data: [],
}),
}));

const mockUsePermission = usePermission as jest.MockedFunction<
typeof usePermission
>;

describe('LightspeedPage', () => {
it('should not display chatbot if permission checks are in loading phase', async () => {
mockUsePermission.mockReturnValue({ loading: true, allowed: true });

await renderInTestApp(
<TestApiProvider apis={[[identityApiRef, identityApi]]}>
<LightspeedPage />
</TestApiProvider>,
);

await waitFor(() => {
expect(screen.queryByText('LightspeedChat')).not.toBeInTheDocument();
});
});

it('should display permission required alert', async () => {
mockUsePermission.mockReturnValue({ loading: false, allowed: false });

await renderInTestApp(
<TestApiProvider apis={[[identityApiRef, identityApi]]}>
<LightspeedPage />
</TestApiProvider>,
);

await waitFor(() => {
expect(screen.getByText('Permission required')).toBeInTheDocument();
});
});

it('should display lightspeed chatbot', async () => {
mockUsePermission.mockReturnValue({ loading: false, allowed: true });

await renderInTestApp(
<TestApiProvider apis={[[identityApiRef, identityApi]]}>
<LightspeedPage />
</TestApiProvider>,
);

await waitFor(() => {
expect(screen.getByText('LightspeedChat')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { usePermission } from '@backstage/plugin-permission-react';

import { lightspeedConversationsDeletePermission } from '@red-hat-developer-hub/backstage-plugin-lightspeed-common';

export const useLightspeedDeletePermission = () => {
const lightspeedDeletePermissionResult = usePermission({
permission: lightspeedConversationsDeletePermission,
});

return lightspeedDeletePermissionResult;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { usePermission } from '@backstage/plugin-permission-react';

import {
lightspeedConversationsCreatePermission,
lightspeedConversationsReadPermission,
} from '@red-hat-developer-hub/backstage-plugin-lightspeed-common';

export const useLightspeedViewPermission = () => {
const canReadConversations = usePermission({
permission: lightspeedConversationsReadPermission,
});

const canCreateConversations = usePermission({
permission: lightspeedConversationsCreatePermission,
});

return {
loading: canReadConversations.loading || canCreateConversations.loading,
allowed: canReadConversations.allowed && canCreateConversations.allowed,
};
};

0 comments on commit 1967890

Please sign in to comment.