From cb8addf4880ae6ae32adef02d656645276f4d43c Mon Sep 17 00:00:00 2001 From: Piotr Kukielka Date: Thu, 9 Jan 2025 15:45:50 +0100 Subject: [PATCH] Allow to force usage of pre-defined endpoint --- vscode/src/chat/chat-view/ChatController.ts | 1 + vscode/src/chat/protocol.ts | 1 + vscode/webviews/App.story.tsx | 1 + vscode/webviews/App.tsx | 1 + vscode/webviews/AuthPage.tsx | 31 ++- vscode/webviews/CodyPanel.tsx | 11 +- .../webviews/components/AccountSwitcher.tsx | 249 ------------------ vscode/webviews/components/UserMenu.story.tsx | 2 - vscode/webviews/components/UserMenu.tsx | 26 +- vscode/webviews/tabs/AccountTab.story.tsx | 95 ------- vscode/webviews/tabs/AccountTab.tsx | 145 ---------- vscode/webviews/tabs/TabsBar.story.tsx | 8 - vscode/webviews/tabs/TabsBar.tsx | 4 +- vscode/webviews/tabs/index.ts | 1 - 14 files changed, 40 insertions(+), 536 deletions(-) delete mode 100644 vscode/webviews/components/AccountSwitcher.tsx delete mode 100644 vscode/webviews/tabs/AccountTab.story.tsx delete mode 100644 vscode/webviews/tabs/AccountTab.tsx diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index 61352ac5f8a0..a6033c50f800 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -551,6 +551,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv webviewType, multipleWebviewsEnabled: !sidebarViewOnly, internalDebugContext: configuration.internalDebugContext, + allowEndpointChange: configuration.overrideServerEndpoint === undefined, } } diff --git a/vscode/src/chat/protocol.ts b/vscode/src/chat/protocol.ts index f7004c609cb2..309d7be41f7a 100644 --- a/vscode/src/chat/protocol.ts +++ b/vscode/src/chat/protocol.ts @@ -253,6 +253,7 @@ export interface ConfigurationSubsetForWebview // Whether support running multiple webviews (e.g. sidebar w/ multiple editor panels). multipleWebviewsEnabled?: boolean | undefined | null endpointHistory?: string[] | undefined | null + allowEndpointChange: boolean } /** diff --git a/vscode/webviews/App.story.tsx b/vscode/webviews/App.story.tsx index fc3bf674ea07..9630a24d5da1 100644 --- a/vscode/webviews/App.story.tsx +++ b/vscode/webviews/App.story.tsx @@ -30,6 +30,7 @@ const dummyVSCodeAPI: VSCodeWrapper = { experimentalNoodle: false, smartApply: false, hasEditCapability: false, + allowEndpointChange: true, }, clientCapabilities: CLIENT_CAPABILITIES_FIXTURE, authStatus: { diff --git a/vscode/webviews/App.tsx b/vscode/webviews/App.tsx index d3fc84f3b5e9..9c5433558585 100644 --- a/vscode/webviews/App.tsx +++ b/vscode/webviews/App.tsx @@ -193,6 +193,7 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc codyIDE={config.clientCapabilities.agentIDE} endpoints={config.config.endpointHistory ?? []} authStatus={config.authStatus} + allowEndpointChange={config.config.allowEndpointChange} /> ) : ( diff --git a/vscode/webviews/AuthPage.tsx b/vscode/webviews/AuthPage.tsx index 5f568ea1caa2..748141b9066f 100644 --- a/vscode/webviews/AuthPage.tsx +++ b/vscode/webviews/AuthPage.tsx @@ -20,6 +20,7 @@ interface LoginProps { codyIDE: CodyIDE endpoints: string[] authStatus: AuthStatus + allowEndpointChange: boolean } interface SignInButtonProps { @@ -38,9 +39,10 @@ export const AuthPage: React.FunctionComponent uiKindIsWeb, vscodeAPI, authStatus, + allowEndpointChange, }) => { const telemetryRecorder = useTelemetryRecorder() - const [isEnterpriseSignin, setIsEnterpriseSignin] = useState(false) + const [isEnterpriseSignin, setIsEnterpriseSignin] = useState(!allowEndpointChange) // Extracted common button props and styles const commonButtonProps = { @@ -127,25 +129,28 @@ export const AuthPage: React.FunctionComponent () => (
- + {allowEndpointChange && ( + + )}
), - [authStatus, vscodeAPI, telemetryRecorder] + [authStatus, vscodeAPI, telemetryRecorder, allowEndpointChange] ) return ( @@ -265,6 +270,7 @@ const WebLogin: React.FunctionComponent< interface ClientSignInFormProps { vscodeAPI: VSCodeWrapper telemetryRecorder: TelemetryRecorder + allowEndpointChange: boolean authStatus?: AuthStatus className?: string } @@ -273,7 +279,7 @@ interface ClientSignInFormProps { * The form allows users to input their Sourcegraph instance URL and access token manually. */ const ClientSignInForm: React.FC = memo( - ({ className, authStatus, vscodeAPI, telemetryRecorder }) => { + ({ className, authStatus, vscodeAPI, telemetryRecorder, allowEndpointChange }) => { // Combine related state into a single object to reduce re-renders const [formState, setFormState] = useState({ showAccessTokenField: false, @@ -354,6 +360,7 @@ const ClientSignInForm: React.FC = memo( className="tw-w-full tw-my-2 !tw-p-4" required onChange={handleInputChange} + disabled={!allowEndpointChange} /> Invalid URL. URL is required. diff --git a/vscode/webviews/CodyPanel.tsx b/vscode/webviews/CodyPanel.tsx index cc6cf9d71a4b..0e304c025811 100644 --- a/vscode/webviews/CodyPanel.tsx +++ b/vscode/webviews/CodyPanel.tsx @@ -19,7 +19,7 @@ import { useClientActionDispatcher } from './client/clientState' import { Notices } from './components/Notices' import { StateDebugOverlay } from './components/StateDebugOverlay' import { TabContainer, TabRoot } from './components/shadcn/ui/tabs' -import { AccountTab, HistoryTab, PromptsTab, SettingsTab, TabsBar, View } from './tabs' +import { HistoryTab, PromptsTab, SettingsTab, TabsBar, View } from './tabs' import type { VSCodeWrapper } from './utils/VSCodeApi' import { useUserAccountInfo } from './utils/useConfig' import { useFeatureFlag } from './utils/useFeatureFlags' @@ -155,15 +155,6 @@ export const CodyPanel: FunctionComponent = ({ isPromptsV2Enabled={isPromptsV2Enabled} /> )} - {view === View.Account && ( - - )} {view === View.Settings && } diff --git a/vscode/webviews/components/AccountSwitcher.tsx b/vscode/webviews/components/AccountSwitcher.tsx deleted file mode 100644 index c5aab8af2624..000000000000 --- a/vscode/webviews/components/AccountSwitcher.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { ChevronDown, ChevronRight, ChevronsUpDown, CircleMinus, Plus } from 'lucide-react' -import type * as React from 'react' -import { type KeyboardEvent, useCallback, useState } from 'react' -import { isSourcegraphToken } from '../../src/chat/protocol' -import { Badge } from '../components/shadcn/ui/badge' -import { - Form, - FormControl, - FormField, - FormLabel, - FormMessage, - FormSubmit, -} from '../components/shadcn/ui/form' -import { getVSCodeAPI } from '../utils/VSCodeApi' -import { Button } from './shadcn/ui/button' -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './shadcn/ui/collapsible' -import { Popover, PopoverContent, PopoverTrigger } from './shadcn/ui/popover' - -interface AccountSwitcherProps { - activeEndpoint: string - endpoints: string[] - setLoading: (loading: boolean) => void -} - -export const AccountSwitcher: React.FC = ({ - activeEndpoint, - endpoints, - setLoading, -}) => { - type PopoverView = 'switch' | 'remove' | 'add' - const [getPopoverView, setPopoverView] = useState('switch') - const [isOpen, setIsOpen] = useState(false) - - const [endpointToRemove, setEndpointToRemove] = useState(null) - const [addFormData, setAddFormData] = useState({ - endpoint: 'https://', - accessToken: '', - }) - - const onKeyDownInPopoverContent = (event: KeyboardEvent): void => { - if (event.key === 'Escape' && isOpen) { - onOpenChange(false) - } - } - - const onOpenChange = (open: boolean): void => { - setIsOpen(open) - if (!open) { - setEndpointToRemove(null) - setPopoverView('switch') - setAddFormData(() => ({ - endpoint: 'https://', - accessToken: '', - })) - } - } - - const popoverEndpointsList = endpoints.map(endpoint => ( - - - - - )) - - const popoverSwitchAccountPanel = ( -
-
- - Active - - {activeEndpoint} -
-
- {popoverEndpointsList} -
- -
- ) - - const popoverRemoveAccountPanel = ( -
- Remove Account? -
{endpointToRemove}
- -
- ) - - const handleInputChange = useCallback((e: React.ChangeEvent) => { - const { name, value } = e.target - setAddFormData(prev => ({ ...prev, [name]: value })) - }, []) - - function addAndSwitchAccount() { - getVSCodeAPI().postMessage({ - command: 'auth', - authKind: 'signin', - endpoint: addFormData.endpoint, - value: addFormData.accessToken, - }) - onOpenChange(false) - } - - const popoverAddAccountPanel = ( -
- Account Details -
- - - - Invalid URL. - URL is required. - - - - - - - - - - !isSourcegraphToken(addFormData.accessToken)}> - Invalid access token. - - Access token is required. - - - - - - -
-
- ) - - function getPopoverContent() { - switch (getPopoverView) { - case 'add': - return popoverAddAccountPanel - case 'remove': - return popoverRemoveAccountPanel - case 'switch': - return popoverSwitchAccountPanel - default: - return null - } - } - - return ( - - setIsOpen(!isOpen)}> - - - -
{getPopoverContent()}
-
-
- ) -} diff --git a/vscode/webviews/components/UserMenu.story.tsx b/vscode/webviews/components/UserMenu.story.tsx index 251cbe5fb75d..fecd6a86969c 100644 --- a/vscode/webviews/components/UserMenu.story.tsx +++ b/vscode/webviews/components/UserMenu.story.tsx @@ -1,7 +1,6 @@ import { AUTH_STATUS_FIXTURE_AUTHED } from '@sourcegraph/cody-shared' import type { Meta, StoryObj } from '@storybook/react' import { VSCodeStandaloneComponent } from '../storybook/VSCodeStoryDecorator' -import type { View } from '../tabs' import { UserMenu } from './UserMenu' const meta: Meta = { @@ -9,7 +8,6 @@ const meta: Meta = { component: UserMenu, decorators: [story =>
{story()}
, VSCodeStandaloneComponent], args: { - setView: (view: View) => console.log('View changed to:', view), endpointHistory: ['https://sourcegraph.com', 'https://sourcegraph.example.com'], __storybook__open: true, // Keep menu open for story display }, diff --git a/vscode/webviews/components/UserMenu.tsx b/vscode/webviews/components/UserMenu.tsx index 5047fa3a5104..6a541a3199c5 100644 --- a/vscode/webviews/components/UserMenu.tsx +++ b/vscode/webviews/components/UserMenu.tsx @@ -15,7 +15,6 @@ import { import { useCallback, useState } from 'react' import { URI } from 'vscode-uri' import { ACCOUNT_USAGE_URL, isSourcegraphToken } from '../../src/chat/protocol' -import type { View } from '../tabs' import { getVSCodeAPI } from '../utils/VSCodeApi' import { useTelemetryRecorder } from '../utils/telemetry' import { UserAvatar } from './UserAvatar' @@ -31,9 +30,9 @@ interface UserMenuProps { isProUser: boolean authStatus: AuthenticatedAuthStatus endpointHistory: string[] - setView: (view: View) => void className?: string onCloseByEscape?: () => void + allowEndpointChange: boolean __storybook__open?: boolean } @@ -44,8 +43,8 @@ export const UserMenu: React.FunctionComponent = ({ authStatus, endpointHistory, className, - setView, onCloseByEscape, + allowEndpointChange, __storybook__open, }) => { const telemetryRecorder = useTelemetryRecorder() @@ -388,20 +387,23 @@ export const UserMenu: React.FunctionComponent = ({ - onMenuViewChange('switch')}> - - Switch Account - - + {allowEndpointChange && ( + onMenuViewChange('switch')}> + + Switch Account + + + )} onSignOutClick(endpoint)}> Sign Out + = { - title: 'cody/AccountTab', - component: AccountTab, - decorators: [ - story =>
{story()}
, - VSCodeStandaloneComponent, - ], - args: {}, -} - -export default meta - -type Story = StoryObj - -const createMockConfig = (overrides = {}) => ({ - config: { - smartApply: false, - experimentalNoodle: false, - serverEndpoint: 'https://sourcegraph.com', - uiKindIsWeb: false, - } as ConfigurationSubsetForWebview & LocalEnv, - clientCapabilities: { - isVSCode: false, - agentIDE: CodyIDE.VSCode, - } as ClientCapabilitiesWithLegacyFields, - authStatus: { - authenticated: true, - endpoint: 'https://sourcegraph.com', - username: 'testuser', - displayName: 'Test User', - pendingValidation: false, - hasVerifiedEmail: true, - requiresVerifiedEmail: false, - } as AuthenticatedAuthStatus, - isDotComUser: true, - userProductSubscription: null, - ...overrides, -}) - -export const CodyProUser: Story = { - render: args => ( - - - - ), -} - -export const CodyFreeUser: Story = { - render: args => ( - - - - ), -} - -export const EnterpriseUser: Story = { - render: args => ( - - - - ), -} diff --git a/vscode/webviews/tabs/AccountTab.tsx b/vscode/webviews/tabs/AccountTab.tsx deleted file mode 100644 index 0c742bb92304..000000000000 --- a/vscode/webviews/tabs/AccountTab.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { - type AuthStatus, - type AuthenticatedAuthStatus, - type ClientCapabilitiesWithLegacyFields, - type UserProductSubscription, - isCodyProUser, -} from '@sourcegraph/cody-shared' -import { useCallback, useEffect, useState } from 'react' -import { URI } from 'vscode-uri' -import { - ACCOUNT_UPGRADE_URL, - ACCOUNT_USAGE_URL, - type ConfigurationSubsetForWebview, - type LocalEnv, -} from '../../src/chat/protocol' -import { AccountSwitcher } from '../components/AccountSwitcher' -import { UserAvatar } from '../components/UserAvatar' -import { USER_MENU_AVATAR_SIZE } from '../components/UserMenu' -import { Button } from '../components/shadcn/ui/button' -import { getVSCodeAPI } from '../utils/VSCodeApi' - -interface AccountTabProps { - config: LocalEnv & ConfigurationSubsetForWebview - clientCapabilities: ClientCapabilitiesWithLegacyFields - authStatus: AuthStatus - isDotComUser: boolean - userProductSubscription: UserProductSubscription | null | undefined -} - -// TODO: Implement the AccountTab component once the design is ready. -export const AccountTab: React.FC = ({ - config, - clientCapabilities, - authStatus, - isDotComUser, - userProductSubscription, -}) => { - if (!authStatus.authenticated || userProductSubscription === undefined) { - return null - } - - const [isLoading, setIsLoading] = useState(true) - useEffect(() => { - setIsLoading(!authStatus.authenticated) - }, [authStatus]) - - const { displayName, username, primaryEmail, endpoint } = authStatus as AuthenticatedAuthStatus - const isProUser = isCodyProUser(authStatus, userProductSubscription) - - function createButton(text: string, onClick: () => void) { - return ( - - ) - } - - const endpoints: string[] = config.endpointHistory ?? [] - const switchableEndpoints = endpoints.filter(e => e !== endpoint) - const accountSwitcher = ( - - ) - - const upgradeButton = createButton('Upgrade', () => - getVSCodeAPI().postMessage({ command: 'links', value: ACCOUNT_UPGRADE_URL.toString() }) - ) - - const manageAccountButton = createButton( - 'Manage Account', - useCallback(() => { - if (username) { - const uri = URI.parse(ACCOUNT_USAGE_URL.toString()).with({ - query: `cody_client_user=${encodeURIComponent(username)}`, - }) - getVSCodeAPI().postMessage({ command: 'links', value: uri.toString() }) - } - }, [username]) - ) - - const settingButton = createButton('Settings', () => - getVSCodeAPI().postMessage({ command: 'command', id: 'cody.status-bar.interacted' }) - ) - - const signOutButton = createButton('Sign Out', () => { - getVSCodeAPI().postMessage({ command: 'auth', authKind: 'signout' }) - setIsLoading(true) - }) - - const accountPanelView = ( -
-

Account

-
-
-
- -
-

{displayName ?? username}

-

{primaryEmail}

-
- {accountSwitcher} -
-
-
-
Plan:
-
- {isDotComUser ? (isProUser ? 'Cody Pro' : 'Cody Free') : 'Enterprise'} -
-
Endpoint:
- -
-
- {isDotComUser && !isProUser && upgradeButton} - {isDotComUser && manageAccountButton} - {settingButton} - {signOutButton} -
- ) - - const loadingView = ( -
-
-
Switching Account...
-
- ) - - return isLoading ? loadingView : accountPanelView -} diff --git a/vscode/webviews/tabs/TabsBar.story.tsx b/vscode/webviews/tabs/TabsBar.story.tsx index 4892cb69bcad..5baa411827a2 100644 --- a/vscode/webviews/tabs/TabsBar.story.tsx +++ b/vscode/webviews/tabs/TabsBar.story.tsx @@ -69,11 +69,3 @@ export const SettingsTab: Story = { user: mockUser, }, } - -export const AccountTab: Story = { - args: { - currentView: View.Account, - setView: () => {}, - user: mockUser, - }, -} diff --git a/vscode/webviews/tabs/TabsBar.tsx b/vscode/webviews/tabs/TabsBar.tsx index 6221fca2b6ab..85263da0827c 100644 --- a/vscode/webviews/tabs/TabsBar.tsx +++ b/vscode/webviews/tabs/TabsBar.tsx @@ -72,7 +72,7 @@ export const TabsBar = memo(props => { const { isCodyProUser, IDE } = user const tabItems = useTabs({ user }) const { - config: { webviewType, multipleWebviewsEnabled }, + config: { webviewType, multipleWebviewsEnabled, allowEndpointChange }, } = useConfig() const currentViewSubActions = tabItems.find(tab => tab.view === currentView)?.subActions ?? [] @@ -155,7 +155,7 @@ export const TabsBar = memo(props => { authStatus={user.user as AuthenticatedAuthStatus} isProUser={isCodyProUser} endpointHistory={endpointHistory} - setView={setView} + allowEndpointChange={allowEndpointChange} className="!tw-opacity-100 tw-h-full" /> )} diff --git a/vscode/webviews/tabs/index.ts b/vscode/webviews/tabs/index.ts index e131d74f1747..8f4e01efa3d2 100644 --- a/vscode/webviews/tabs/index.ts +++ b/vscode/webviews/tabs/index.ts @@ -1,4 +1,3 @@ -export { AccountTab } from './AccountTab' export { PromptsTab } from '../prompts/PromptsTab' export { HistoryTab } from './HistoryTab' export { SettingsTab } from './SettingsTab'