remove extraneous Cody onboarding steps (#63373)

- Remove the "personal or work" step
- Remove the "do you want us to call you?" step
- Cleaner design of the Cody dashboard
- Improve some wording
- Remove CodySurveyToast (unused) and user.CompletedPostSignup. These
have not been used in many months. On Amplitude, the relevant events
have no data.

Fix
https://linear.app/sourcegraph/issue/PRIME-375/remove-personal-or-work-survey-signup-step

<img width="2032" alt="image"
src="https://github.com/sourcegraph/sourcegraph/assets/1976/ef100e60-71b5-479b-ac63-2450cdad0220">


## Test plan

n/a
This commit is contained in:
Quinn Slack 2024-06-24 13:37:39 -07:00 committed by GitHub
parent 5413fd1fd4
commit 4d69b06d4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 347 additions and 2429 deletions

View File

@ -42,7 +42,6 @@ export const currentAuthStateQuery = gql`
viewerCanAdminister
tosAccepted
hasVerifiedEmail
completedPostSignup
emails {
email
verified

View File

@ -93,7 +93,6 @@ export interface TemporarySettingsSchema {
'admin.hasCompletedLicenseCheck': boolean
'simple.search.toggle': boolean
'cody.onboarding.completed': boolean
'cody.onboarding.step': number
/** OpenCodeGraph */
'openCodeGraph.annotations.visible': boolean
@ -161,7 +160,6 @@ const TEMPORARY_SETTINGS: Record<keyof TemporarySettings, null> = {
'admin.hasCompletedLicenseCheck': null,
'simple.search.toggle': null,
'cody.onboarding.completed': null,
'cody.onboarding.step': null,
'openCodeGraph.annotations.visible': null,
}

View File

@ -15,7 +15,6 @@ export const enum EventName {
CODY_CHAT_SCOPE_INFERRED_REPO_DISABLED = 'web:codyChat:inferredRepoDisabled',
CODY_CHAT_SCOPE_INFERRED_FILE_ENABLED = 'web:codyChat:inferredFileEnabled',
CODY_CHAT_SCOPE_INFERRED_FILE_DISABLED = 'web:codyChat:inferredFileDisabled',
VIEW_GET_CODY = 'GetCody',
CODY_EDITOR_WIDGET_VIEWED = 'web:codyEditorWidget:viewed',
CODY_SIDEBAR_CHAT_OPENED = 'web:codySidebar:chatOpened',

View File

@ -21,7 +21,6 @@ export const currentUserMock = {
emails: [{ email: 'felix@sourcegraph.com', isPrimary: true, verified: true }],
latestSettings: null,
hasVerifiedEmail: true,
completedPostSignup: true,
permissions: {
__typename: 'PermissionConnection',
nodes: [

View File

@ -33,12 +33,9 @@ const topLevelPaths = [
'search/cody',
'app',
'cody',
'get-cody',
'post-sign-up',
'unlock-account',
'password-reset',
'survey',
'welcome',
'embed',
'users',
'user',

View File

@ -160,7 +160,6 @@ ts_project(
"src/auth/AuthPageWrapper.tsx",
"src/auth/CloudSignUpPage.tsx",
"src/auth/OrDivider.tsx",
"src/auth/PostSignUpPage.tsx",
"src/auth/RequestAccessPage.tsx",
"src/auth/ResetPasswordPage.tsx",
"src/auth/SignInPage.tsx",
@ -228,7 +227,6 @@ ts_project(
"src/cody/components/ScopeSelector/index.tsx",
"src/cody/components/ScopeSelector/useRepoSuggestions.ts",
"src/cody/dashboard/CodyDashboardPage.tsx",
"src/cody/editorGroups.ts",
"src/cody/invites/AcceptInviteBanner.tsx",
"src/cody/invites/InviteUsers.tsx",
"src/cody/invites/useInviteParams.ts",
@ -263,14 +261,6 @@ ts_project(
"src/cody/management/subscription/manage/utils.ts",
"src/cody/management/subscription/new/CodyProCheckoutForm.tsx",
"src/cody/management/subscription/new/NewCodyProSubscriptionPage.tsx",
"src/cody/onboarding/CodyOnboarding.tsx",
"src/cody/onboarding/EditorStep.tsx",
"src/cody/onboarding/PurposeStep.tsx",
"src/cody/onboarding/WelcomeStep.tsx",
"src/cody/onboarding/instructions/CodyFeatures.tsx",
"src/cody/onboarding/instructions/JetBrains.tsx",
"src/cody/onboarding/instructions/NeoVim.tsx",
"src/cody/onboarding/instructions/VsCode.tsx",
"src/cody/sidebar/CodySidebar.tsx",
"src/cody/sidebar/Provider.tsx",
"src/cody/sidebar/index.tsx",
@ -1103,7 +1093,6 @@ ts_project(
"src/fuzzyFinder/SearchValue.ts",
"src/fuzzyFinder/SearchValueRankingCache.ts",
"src/fuzzyFinder/WordSensitiveFuzzySearch.ts",
"src/get-cody/GetCodyPage.tsx",
"src/global/GlobalAlert.tsx",
"src/global/GlobalAlerts.tsx",
"src/global/Notices.tsx",
@ -1126,7 +1115,6 @@ ts_project(
"src/marketing/components/TweetFeedback.tsx",
"src/marketing/page/SurveyForm.tsx",
"src/marketing/page/SurveyPage.tsx",
"src/marketing/toast/CodySurveyToast.tsx",
"src/marketing/toast/SurveySuccessToast.tsx",
"src/marketing/toast/SurveyToastContent.tsx",
"src/marketing/toast/SurveyToastTrigger.tsx",

View File

@ -98,15 +98,9 @@ export const LegacyLayout: FC<LegacyLayoutProps> = props => {
const isSiteInit = location.pathname === PageRoutes.SiteAdminInit.toString()
const isSignInOrUp =
routeMatch &&
[
PageRoutes.SignIn,
PageRoutes.SignUp,
PageRoutes.PasswordReset,
PageRoutes.Welcome,
PageRoutes.RequestAccess,
].includes(routeMatch as PageRoutes)
const isGetCodyPage = location.pathname === PageRoutes.GetCody.toString()
const isPostSignUpPage = location.pathname === PageRoutes.PostSignUp.toString()
[PageRoutes.SignIn, PageRoutes.SignUp, PageRoutes.PasswordReset, PageRoutes.RequestAccess].includes(
routeMatch as PageRoutes
)
const [newSearchNavigation] = useNewSearchNavigation()
const [enableContrastCompliantSyntaxHighlighting] = useFeatureFlag('contrast-compliant-syntax-highlighting')
@ -228,7 +222,7 @@ export const LegacyLayout: FC<LegacyLayoutProps> = props => {
telemetryRecorder={props.platformContext.telemetryRecorder}
/>
)}
{!isSiteInit && !isSignInOrUp && !isGetCodyPage && !isPostSignUpPage && (
{!isSiteInit && !isSignInOrUp && (
<>
{newSearchNavigation ? (
<NewGlobalNavigationBar

View File

@ -1,20 +0,0 @@
import React from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { CodyProRoutes } from '../cody/codyProRoutes'
import { getReturnTo } from './SignInSignUpCommon'
export const PostSignUpPage: React.FunctionComponent = () => {
const location = useLocation()
const returnTo = getReturnTo(location)
// Redirects Cody PLG users without asking
const params = new URLSearchParams()
params.set('returnTo', returnTo)
const navigateTo = CodyProRoutes.Manage + '?' + params.toString()
return <Navigate to={navigateTo.toString()} replace={true} />
}

View File

@ -11,13 +11,12 @@ import { Container, Link, Text } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../auth'
import { PageTitle } from '../components/PageTitle'
import type { SourcegraphContext } from '../jscontext'
import { PageRoutes } from '../routes.constants'
import { EventName } from '../util/constants'
import { AuthPageWrapper } from './AuthPageWrapper'
import { CloudSignUpPage, ShowEmailFormQueryParameter } from './CloudSignUpPage'
import { getReturnTo } from './SignInSignUpCommon'
import { type SignUpArguments, SignUpForm } from './SignUpForm'
import { SignUpForm, type SignUpArguments } from './SignUpForm'
import { VsCodeSignUpPage } from './VsCodeSignUpPage'
import styles from './SignUpPage.module.scss'
@ -92,8 +91,7 @@ export const SignUpPage: React.FunctionComponent<React.PropsWithChildren<SignUpP
const v2Source = query.get('editor') === 'vscode' ? 0 : 1
telemetryRecorder.recordEvent('auth.signUp', 'complete', { metadata: { source: v2Source } })
// Redirects to the /post-sign-up after successful signup on sourcegraphDotCom.
window.location.replace(context.sourcegraphDotComMode ? PageRoutes.PostSignUp : returnTo)
window.location.replace(returnTo)
return Promise.resolve()
})

View File

@ -1,41 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`PostSignUpPage > renders customized redirect when user has completed post signup flow 1`] = `<DocumentFragment />`;
exports[`PostSignUpPage > renders post signup page - with cody survey 1`] = `
<DocumentFragment>
<div
class="pageWrapper"
>
<div
class="container py-4 page"
>
<img
alt="Sourcegraph logo"
class="logo"
src="/.assets/img/sourcegraph-mark.svg?v2"
/>
</div>
</div>
</DocumentFragment>
`;
exports[`PostSignUpPage > renders post signup page - with email verification 1`] = `
<DocumentFragment>
<div
class="pageWrapper"
>
<div
class="container py-4 page"
>
<img
alt="Sourcegraph logo"
class="logo"
src="/.assets/img/sourcegraph-mark.svg?v2"
/>
</div>
</div>
</DocumentFragment>
`;
exports[`PostSignUpPage > renders redirect when user has completed post signup flow 1`] = `<DocumentFragment />`;

View File

@ -15,14 +15,12 @@ export type AuthPages =
| 'vscode-signup-page'
| 'cloud-signup-page'
| 'cody-marketing-page'
| 'get-cody-page'
| 'try-cody-widget-blob'
| 'try-cody-widget-repo'
const v2Pages: { [p in AuthPages]: number } = {
'vscode-signup-page': 0,
'cloud-signup-page': 1,
'cody-marketing-page': 2,
'get-cody-page': 3,
'try-cody-widget-blob': 4,
'try-cody-widget-repo': 5,
}

View File

@ -1,14 +1,14 @@
import React, { useEffect, useState } from 'react'
import {
mdiChevronRight,
mdiClose,
mdiCogOutline,
mdiDelete,
mdiDotsVertical,
mdiFormatListBulleted,
mdiOpenInNew,
mdiPlus,
mdiChevronRight,
mdiFormatListBulleted,
} from '@mdi/js'
import classNames from 'classnames'
import { useLocation, useNavigate } from 'react-router-dom'
@ -20,19 +20,19 @@ import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import {
Badge,
Button,
ButtonLink,
H3,
H4,
Icon,
Link,
Menu,
MenuButton,
MenuList,
MenuDivider,
MenuItem,
MenuLink,
MenuList,
PageHeader,
Link,
H4,
H3,
Text,
ButtonLink,
Tooltip,
} from '@sourcegraph/wildcard'
@ -47,7 +47,7 @@ import { ChatUI } from '../components/ChatUI'
import { CodyMarketingPage } from '../components/CodyMarketingPage'
import { HistoryList } from '../components/HistoryList'
import { isCodyEnabled } from '../isCodyEnabled'
import { type CodyChatStore, useCodyChat } from '../useCodyChat'
import { useCodyChat, type CodyChatStore } from '../useCodyChat'
import { CodyColorIcon } from './CodyPageIcon'
@ -180,7 +180,7 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
powerful recipes to help you understand codebases and generate and fix code more
accurately.
</Text>
<ButtonLink variant="primary" to="/help/cody#get-cody">
<ButtonLink variant="primary" to="/help/cody">
View editor extensions &rarr;
</ButtonLink>
</div>
@ -209,7 +209,7 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
{!isSourcegraphDotCom && isCTADismissed && (
<>
{' '}
<Link to="/help/cody#get-cody">Get Cody in your editor.</Link>
<Link to="/help/cody">Get Cody in your editor.</Link>
</>
)}
</>

View File

@ -27,8 +27,6 @@ interface CodyPlatformCardProps {
illustration: string
}
/* eslint-disable @sourcegraph/sourcegraph/check-help-links */
const onSpeakToAnEngineer = (): void => EVENT_LOGGER.log(EventName.SPEAK_TO_AN_ENGINEER_CTA)
const IDEIcon: React.FunctionComponent<{}> = () => (
@ -64,7 +62,7 @@ const codyPlatformCardItems = (
description: (
<>
The extensions combine an LLM with the context of your code to help you generate and fix code more
accurately. <Link to="/help/cody#get-cody">View supported editors.</Link>
accurately. <Link to="/help/cody">View supported editors.</Link>
</>
),
icon: <IDEIcon />,

View File

@ -39,7 +39,7 @@ const setupOptions: SetupOption[] = [
},
{
icon: <IntelliJIcon className={styles.linkSelectorIcon} />,
maker: 'Jetbrains',
maker: 'JetBrains',
name: 'IntelliJ',
setupLink: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
},
@ -61,20 +61,18 @@ export const CodyDashboardPage: FC<CodyDashboardPageProps> = ({ telemetryRecorde
Get started with <span className={styles.codyGradient}>Cody</span>
</H1>
<Text className={styles.dashboardHeroTagline}>
Hey! 👋 Lets get started with Cody your new AI coding assistant.
Hey! 👋 Lets get started with Cody, your AI coding assistant.
</Text>
</section>
<section className={styles.dashboardOnboarding}>
<section className={styles.dashboardOnboardingIde}>
<Text className={styles.dashboardText}>Download Cody for your favorite IDE</Text>
<Text className={styles.dashboardText}>Get Cody in your editor</Text>
<LinkSelector options={setupOptions} />
<Text className="text-muted">
Struggling with setup?{' '}
<Link to={codySetupLink} className={styles.dashboardOnboardingIdeInstallationLink}>
Explore installation docs
</Link>
.
</Text>
</section>
<section className={styles.dashboardOnboardingWeb}>

View File

@ -1,94 +0,0 @@
import type { IEditor } from './onboarding/CodyOnboarding'
import { JetBrainsInstructions } from './onboarding/instructions/JetBrains'
import { NeoVimInstructions } from './onboarding/instructions/NeoVim'
import { VSCodeInstructions } from './onboarding/instructions/VsCode'
export const editorGroups: IEditor[][] = [
[
{
id: 1,
icon: 'VsCode',
name: 'VS Code',
publisher: 'Microsoft',
releaseStage: 'Stable',
docs: 'https://sourcegraph.com/docs/cody/clients/install-vscode',
instructions: VSCodeInstructions,
},
{
id: 2,
icon: 'IntelliJ',
name: 'IntelliJ IDEA',
publisher: 'JetBrains',
releaseStage: 'Stable',
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
instructions: JetBrainsInstructions,
},
{
id: 3,
icon: 'PhpStorm',
name: 'PhpStorm ',
publisher: 'JetBrains',
releaseStage: 'Stable',
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
instructions: JetBrainsInstructions,
},
{
id: 4,
icon: 'PyCharm',
name: 'PyCharm',
publisher: 'JetBrains',
releaseStage: 'Stable',
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
instructions: JetBrainsInstructions,
},
],
[
{
id: 5,
icon: 'WebStorm',
name: 'WebStorm',
publisher: 'JetBrains',
releaseStage: 'Stable',
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
instructions: JetBrainsInstructions,
},
{
id: 6,
icon: 'RubyMine',
name: 'RubyMine',
publisher: 'JetBrains',
releaseStage: 'Stable',
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
instructions: JetBrainsInstructions,
},
{
id: 7,
icon: 'GoLand',
name: 'GoLand',
publisher: 'JetBrains',
releaseStage: 'Stable',
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
instructions: JetBrainsInstructions,
},
{
id: 8,
icon: 'AndroidStudio',
name: 'Android Studio',
publisher: 'Google',
releaseStage: 'Stable',
docs: 'https://sourcegraph.com/docs/cody/clients/install-jetbrains',
instructions: JetBrainsInstructions,
},
],
[
{
id: 9,
icon: 'NeoVim',
name: 'Neovim',
publisher: 'Neovim Team',
releaseStage: 'Experimental',
docs: 'https://sourcegraph.com/docs/cody/clients/install-neovim',
instructions: NeoVimInstructions,
},
],
]

View File

@ -1,14 +1,15 @@
@import 'wildcard/src/global-styles/breakpoints';
.responsive-container {
@media (--sm-breakpoint-down) {
flex-direction: column;
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 1px;
> div {
border: none !important;
align-items: center;
}
background-color: var(--border-color);
> * {
flex: 1;
min-width: 300px;
background-color: var(--color-bg-1);
}
}
@ -23,33 +24,6 @@
font-weight: 600;
}
.counter {
font-size: 1.25rem;
}
.ide-name {
font-size: 1rem;
}
.modal {
width: 50rem;
}
.release-stage {
color: var(--gray-07);
}
.ide-header {
padding: calc(var(--spacer) * 0.5);
margin: calc(var(--spacer) * -0.5);
border-radius: var(--border-radius);
}
.ide-header:hover {
cursor: pointer;
background: var(--subtle-bg);
}
.credit-card-emoji {
font-size: 2rem;
}

View File

@ -1,37 +1,35 @@
import React, { useCallback, useEffect } from 'react'
import { mdiCreditCardOutline, mdiPlusThick } from '@mdi/js'
import { mdiCreditCardOutline, mdiHelpCircleOutline, mdiPlusThick } from '@mdi/js'
import classNames from 'classnames'
import { useNavigate } from 'react-router-dom'
import { useQuery } from '@sourcegraph/http-client'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { ButtonLink, H1, H2, Icon, Link, PageHeader, Text, useSearchParameters, Button } from '@sourcegraph/wildcard'
import { Button, ButtonLink, H2, H3, Icon, Link, PageHeader, Text, useSearchParameters } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import { Page } from '../../components/Page'
import { PageTitle } from '../../components/PageTitle'
import {
CodySubscriptionPlan,
type UserCodyPlanResult,
type UserCodyPlanVariables,
type UserCodyUsageResult,
type UserCodyUsageVariables,
CodySubscriptionPlan,
} from '../../graphql-operations'
import { CodyProRoutes } from '../codyProRoutes'
import { CodyAlert } from '../components/CodyAlert'
import { ProIcon } from '../components/CodyIcon'
import { PageHeaderIcon } from '../components/PageHeaderIcon'
import { AcceptInviteBanner } from '../invites/AcceptInviteBanner'
import { InviteUsers } from '../invites/InviteUsers'
import { isCodyEnabled } from '../isCodyEnabled'
import { CodyOnboarding, type IEditor } from '../onboarding/CodyOnboarding'
import { USER_CODY_PLAN, USER_CODY_USAGE } from '../subscription/queries'
import { getManageSubscriptionPageURL } from '../util'
import { useSubscriptionSummary } from './api/react-query/subscriptions'
import { SubscriptionStats } from './SubscriptionStats'
import { UseCodyInEditorSection } from './UseCodyInEditorSection'
import { CodyEditorsAndClients } from './UseCodyInEditorSection'
import styles from './CodyManagementPage.module.scss'
@ -39,11 +37,6 @@ interface CodyManagementPageProps extends TelemetryV2Props {
authenticatedUser: AuthenticatedUser | null
}
export enum EditorStep {
SetupInstructions = 0,
CodyFeatures = 1,
}
export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps> = ({
authenticatedUser,
telemetryRecorder,
@ -79,9 +72,6 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
const subscriptionSummaryQueryResult = useSubscriptionSummary()
const isAdmin = subscriptionSummaryQueryResult?.data?.userRole === 'admin'
const [selectedEditor, setSelectedEditor] = React.useState<IEditor | null>(null)
const [selectedEditorStep, setSelectedEditorStep] = React.useState<EditorStep | null>(null)
const subscription = data?.currentUser?.codySubscription
useEffect(() => {
@ -129,107 +119,78 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
const isUserOnProTier = subscription.plan === CodySubscriptionPlan.PRO
return (
<>
<Page className={classNames('d-flex flex-column')}>
<PageTitle title="Dashboard" />
<AcceptInviteBanner onSuccess={refetch} />
{welcomeToPro && (
<CodyAlert variant="greenCodyPro">
<H2 className="mt-4">Welcome to Cody Pro</H2>
<Text size="small" className="mb-0">
You now have Cody Pro with access to unlimited autocomplete, chats, and commands.
</Text>
</CodyAlert>
)}
<PageHeader
className="my-4 d-inline-flex align-items-center"
actions={isAdmin && <div className="d-flex">{getTeamInviteButton()}</div>}
>
<PageHeader.Heading as="h1" className="text-3xl font-medium">
<PageHeaderIcon name="dashboard" className="mr-3" />
<Text as="span">Dashboard</Text>
</PageHeader.Heading>
</PageHeader>
{isAdmin && !!subscriptionSummaryQueryResult.data && (
<InviteUsers
telemetryRecorder={telemetryRecorder}
subscriptionSummary={subscriptionSummaryQueryResult.data}
/>
)}
{!isUserOnProTier && <UpgradeToProBanner onClick={onClickUpgradeToProCTA} />}
<div className={classNames('p-4 border bg-1 mt-3', styles.container)}>
<div className="d-flex justify-content-between align-items-center border-bottom pb-3">
<div>
<H2>My subscription</H2>
<Text className="text-muted mb-0">
{isUserOnProTier ? (
'You are on the Pro tier.'
) : (
<span>
You are on the Free tier.{' '}
<Link to={CodyProRoutes.Subscription}>Upgrade to the Pro tier.</Link>
</span>
)}
</Text>
</div>
{isUserOnProTier && (
<div>
<ButtonLink
variant="primary"
size="sm"
to={getManageSubscriptionPageURL()}
onClick={() => {
telemetryRecorder.recordEvent('cody.manageSubscription', 'click')
}}
>
<Icon svgPath={mdiCreditCardOutline} className="mr-1" aria-hidden={true} />
Manage subscription
</ButtonLink>
</div>
<Page className={classNames('d-flex flex-column')}>
<PageTitle title="Dashboard" />
<AcceptInviteBanner onSuccess={refetch} />
{welcomeToPro && (
<CodyAlert variant="greenCodyPro">
<H2 className="mt-4">Welcome to Cody Pro</H2>
<Text size="small" className="mb-0">
You now have Cody Pro with access to unlimited autocomplete, chats, and commands.
</Text>
</CodyAlert>
)}
<PageHeader
className="my-4 d-inline-flex align-items-center"
actions={
<div className="d-flex flex-column flex-gap-2">
{isAdmin ? (
getTeamInviteButton()
) : isUserOnProTier ? (
<ButtonLink
variant="primary"
to={getManageSubscriptionPageURL()}
onClick={() => {
telemetryRecorder.recordEvent('cody.manageSubscription', 'click')
}}
>
<Icon svgPath={mdiCreditCardOutline} className="mr-1" aria-hidden={true} />
Manage subscription
</ButtonLink>
) : (
<ButtonLink to="/cody/subscription" variant="primary" onClick={onClickUpgradeToProCTA}>
Upgrade plan
</ButtonLink>
)}
<Link
to="https://help.sourcegraph.com"
target="_blank"
rel="noreferrer"
className="text-muted text-sm"
>
<Icon svgPath={mdiHelpCircleOutline} className="mr-1" aria-hidden={true} />
Help &amp; community
</Link>
</div>
<SubscriptionStats {...{ subscription, usageData }} />
</div>
}
>
<PageHeader.Heading as="h1" className="text-3xl font-medium">
<PageHeaderIcon name="dashboard" className="mr-3" />
<Text as="span">Cody dashboard</Text>
</PageHeader.Heading>
</PageHeader>
<UseCodyInEditorSection
{...{
selectedEditor,
setSelectedEditor,
selectedEditorStep,
setSelectedEditorStep,
isUserOnProTier,
telemetryRecorder,
}}
{isAdmin && !!subscriptionSummaryQueryResult.data && (
<InviteUsers
telemetryRecorder={telemetryRecorder}
subscriptionSummary={subscriptionSummaryQueryResult.data}
/>
</Page>
<CodyOnboarding authenticatedUser={authenticatedUser} telemetryRecorder={telemetryRecorder} />
</>
)}
<div className={classNames('border bg-1 mb-2', styles.container)}>
<SubscriptionStats
subscription={subscription}
usageData={usageData}
onClickUpgradeToProCTA={onClickUpgradeToProCTA}
/>
</div>
<H3 className="mt-3 text-muted">Use Cody in...</H3>
<div className={classNames('border bg-1 mb-2', styles.container)}>
<CodyEditorsAndClients telemetryRecorder={telemetryRecorder} />
</div>
<div className="pb-3" />
</Page>
)
}
const UpgradeToProBanner: React.FunctionComponent<{
onClick: () => void
}> = ({ onClick }) => (
<CodyAlert variant="purpleCodyPro">
<div className="d-flex justify-content-between align-items-center p-4">
<div>
<H1>
Get unlimited help with <span className={styles.codyProGradientText}>Cody Pro</span>
</H1>
<ul className="pl-4 mb-0">
<li>Unlimited autocompletions</li>
<li>Unlimited chat messages</li>
</ul>
</div>
<div>
<ButtonLink to={CodyProRoutes.Subscription} variant="primary" size="sm" onClick={onClick}>
<ProIcon className="mr-1" />
Upgrade now
</ButtonLink>
</div>
</div>
</CodyAlert>
)

View File

@ -3,8 +3,8 @@ import React from 'react'
import classNames from 'classnames'
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
import { type CodySubscriptionStatus, CodySubscriptionPlan } from '@sourcegraph/shared/src/graphql-operations'
import { Text, LoadingSpinner, H4 } from '@sourcegraph/wildcard'
import { CodySubscriptionPlan, type CodySubscriptionStatus } from '@sourcegraph/shared/src/graphql-operations'
import { ButtonLink, H4, LoadingSpinner, Text } from '@sourcegraph/wildcard'
import type { UserCodyUsageResult } from '../../graphql-operations'
import { AutocompletesIcon, ChatMessagesIcon } from '../components/CodyIcon'
@ -22,11 +22,13 @@ interface SubscriptionStatsProps {
cancelAtPeriodEnd: boolean
}
usageData: UserCodyUsageResult | undefined
onClickUpgradeToProCTA: () => void
}
export const SubscriptionStats: React.FunctionComponent<SubscriptionStatsProps> = ({
subscription,
usageData,
onClickUpgradeToProCTA,
}: SubscriptionStatsProps) => {
const stats = usageData?.currentUser
const codyCurrentPeriodChatLimit = stats?.codyCurrentPeriodChatLimit || 0
@ -59,40 +61,42 @@ export const SubscriptionStats: React.FunctionComponent<SubscriptionStatsProps>
const codyProSubscriptionEndTime = subscription.currentPeriodEndAt
return (
<div className={classNames('d-flex align-items-center mt-3', styles.responsiveContainer)}>
<div className="d-flex flex-column align-items-center flex-grow-1 p-3">
<div className={styles.responsiveContainer}>
<div className="d-flex flex-column align-items-center justify-content-center p-3">
{isUserOnProTier ? <ProTierIcon /> : <Text className={classNames(styles.planName, 'mb-0')}>Free</Text>}
<Text className="text-muted mb-0" size="small">
tier
</Text>
{isUserOnProTier && subscription.cancelAtPeriodEnd && (
<Text className="text-muted mb-0 mt-4" size="small">
Subscription ends <Timestamp date={codyProSubscriptionEndTime} />
</Text>
)}
{!isUserOnProTier && (
<ButtonLink
variant="secondary"
to="/cody/subscription"
onClick={onClickUpgradeToProCTA}
className="mt-2"
size="sm"
>
Upgrade plan
</ButtonLink>
)}
</div>
<div className="d-flex flex-column align-items-center flex-grow-1 p-3 border-left border-right">
<div className="d-flex flex-column align-items-center justify-content-center p-3">
<AutocompletesIcon />
<div className="mb-2 mt-3">
<div className="my-2">
{subscription.applyProRateLimits ? (
<Text weight="bold" className={classNames('d-inline mb-0', styles.counter)}>
<Text weight="bold" className={classNames('d-inline mb-0')}>
Unlimited
</Text>
) : usageData?.currentUser ? (
<>
<Text
weight="bold"
className={classNames(
'd-inline mb-0',
styles.counter,
codeLimitReached ? 'text-danger' : 'text-muted'
)}
className={classNames('d-inline mb-0', codeLimitReached ? 'text-danger' : 'text-muted')}
>
{Math.min(codyCurrentPeriodCodeUsage, codyCurrentPeriodCodeLimit)} /
</Text>{' '}
<Text
className={classNames('d-inline b-0', codeLimitReached ? 'text-danger' : 'text-muted')}
size="small"
>
{codyCurrentPeriodCodeLimit}
</Text>
@ -115,28 +119,22 @@ export const SubscriptionStats: React.FunctionComponent<SubscriptionStatsProps>
</Text>
))}
</div>
<div className="d-flex flex-column align-items-center flex-grow-1 p-3">
<div className="d-flex flex-column align-items-center justify-content-center p-3">
<ChatMessagesIcon />
<div className="mb-2 mt-3">
<div className="my-2">
{subscription.applyProRateLimits ? (
<Text weight="bold" className={classNames('d-inline mb-0', styles.counter)}>
<Text weight="bold" className={classNames('d-inline mb-0')}>
Unlimited
</Text>
) : usageData?.currentUser ? (
<>
<Text
weight="bold"
className={classNames(
'd-inline mb-0',
styles.counter,
chatLimitReached ? 'text-danger' : 'text-muted'
)}
className={classNames('d-inline mb-0', chatLimitReached ? 'text-danger' : 'text-muted')}
>
{Math.min(codyCurrentPeriodChatUsage, codyCurrentPeriodChatLimit)} /
</Text>{' '}
<Text
className={classNames('d-inline b-0', chatLimitReached ? 'text-danger' : 'text-muted')}
size="small"
>
{codyCurrentPeriodChatLimit}
</Text>

View File

@ -1,145 +1,214 @@
import React from 'react'
import { mdiHelpCircleOutline, mdiInformationOutline, mdiOpenInNew } from '@mdi/js'
import { mdiOpenInNew } from '@mdi/js'
import classNames from 'classnames'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { H2, Text, Link, Icon, H5, Modal } from '@sourcegraph/wildcard'
import { editorGroups } from '../editorGroups'
import type { IEditor } from '../onboarding/CodyOnboarding'
import { EditorStep } from './CodyManagementPage'
import type { TelemetryRecorder, TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { Badge, ButtonLink, H3, Icon, Link, LinkOrSpan, Text } from '@sourcegraph/wildcard'
import styles from './CodyManagementPage.module.scss'
interface UseCodyInEditorSectionProps extends TelemetryV2Props {
selectedEditor: IEditor | null
setSelectedEditor: (editor: IEditor | null) => void
selectedEditorStep: EditorStep | null
setSelectedEditorStep: (step: EditorStep | null) => void
isUserOnProTier: boolean
}
interface UseCodyInEditorSectionProps extends TelemetryV2Props {}
export const UseCodyInEditorSection: React.FunctionComponent<UseCodyInEditorSectionProps> = props => (
<div className={classNames('p-4 border bg-1 mt-4 mb-5', styles.container)}>
<div className="d-flex justify-content-between align-items-center border-bottom pb-3">
<div>
<H2>Use Cody directly in your editor</H2>
<Text className="text-muted mb-0">Download the Cody extension in your editor to start using Cody.</Text>
</div>
{props.isUserOnProTier ? (
<div>
<Link
to="https://help.sourcegraph.com/"
target="_blank"
rel="noreferrer"
className="text-muted text-sm"
>
<Icon svgPath={mdiHelpCircleOutline} className="mr-1" aria-hidden={true} />
Join our community, read our docs, or get product/billing support
</Link>
</div>
) : null}
</div>
{editorGroups.map((group, index) => (
<div
key={group.map(editor => editor.name).join('-')}
className={classNames('d-flex mt-3', styles.responsiveContainer, {
'border-bottom pb-3': index < group.length - 1,
})}
>
{group.map((editor, index) => (
<div
key={editor.name}
className={classNames('d-flex flex-column flex-1 pt-3 px-3', {
'border-left': index !== 0,
})}
>
<div
className={classNames('d-flex mb-3 align-items-center', styles.ideHeader)}
onClick={() => {
props.setSelectedEditor(editor)
props.setSelectedEditorStep(EditorStep.SetupInstructions)
}}
role="button"
tabIndex={0}
onKeyDown={e => {
if (e.key === 'Enter') {
props.setSelectedEditor(editor)
props.setSelectedEditorStep(EditorStep.SetupInstructions)
}
}}
>
<div>
<img
alt={editor.name}
src={`https://storage.googleapis.com/sourcegraph-assets/ideIcons/ideIcon${editor.icon}.svg`}
width={34}
className="mr-3"
/>
</div>
<div>
<Text className="text-muted mb-0" size="small">
{editor.publisher}
</Text>
<Text className={classNames('mb-0', styles.ideName)}>{editor.name}</Text>
<H5 className={styles.releaseStage}>{editor.releaseStage}</H5>
</div>
</div>
{editor.instructions && (
<Link
to="#"
className="mb-2 text-muted d-flex align-items-center"
onClick={() => {
props.setSelectedEditor(editor)
props.setSelectedEditorStep(EditorStep.SetupInstructions)
}}
>
<Icon svgPath={mdiInformationOutline} aria-hidden={true} className="mr-1" /> Quickstart
guide
</Link>
)}
{editor.docs && (
<Link
to={editor.docs}
target="_blank"
rel="noopener"
className="text-muted d-flex align-items-center"
>
<Icon svgPath={mdiOpenInNew} aria-hidden={true} className="mr-1" /> Documentation
</Link>
)}
{props.selectedEditor?.name === editor.name &&
props.selectedEditorStep !== null &&
editor.instructions && (
<Modal
key={editor.name + '-modal'}
isOpen={true}
aria-label={`${editor.name} Info`}
className={styles.modal}
position="center"
>
<editor.instructions
showStep={props.selectedEditorStep}
onClose={() => {
props.setSelectedEditor(null)
props.setSelectedEditorStep(null)
}}
telemetryRecorder={props.telemetryRecorder}
/>
</Modal>
)}
</div>
))}
{group.length < 4
? [...new Array(4 - group.length)].map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<div key={index} className="flex-1 p-3" />
))
: null}
</div>
export const CodyEditorsAndClients: React.FunctionComponent<UseCodyInEditorSectionProps> = ({ telemetryRecorder }) => (
<div className={styles.responsiveContainer}>
{EDITOR_INSTRUCTIONS.map(editor => (
<EditorInstructions key={editor.name} editor={editor} telemetryRecorder={telemetryRecorder} />
))}
</div>
)
const EDITOR_ICON_HEIGHT = 34
const EditorInstructions: React.FunctionComponent<
{ editor: EditorInstructionsTile; className?: string } & TelemetryV2Props
> = ({ editor, telemetryRecorder, className }) => (
<div className={classNames('d-flex flex-column px-3', className)}>
{/* eslint-disable-next-line react/forbid-dom-props */}
<div className="d-flex my-3 align-items-center" style={{ minHeight: `${EDITOR_ICON_HEIGHT}px` }}>
{editor.icon && (
<img
alt={editor.name}
src={`https://storage.googleapis.com/sourcegraph-assets/ideIcons/ideIcon${editor.icon}.svg`}
width={EDITOR_ICON_HEIGHT}
height={EDITOR_ICON_HEIGHT}
className="mr-3"
/>
)}
<H3 className="mb-0 font-weight-normal">{editor.name}</H3>
</div>
{editor.instructions && <editor.instructions telemetryRecorder={telemetryRecorder} />}
</div>
)
interface EditorInstructionsTile {
/** Refers to gs://sourcegraph-assets/ideIcons/ideIcon${icon}.svg. */
icon?: string
name: string
instructions?: React.FunctionComponent<{
telemetryRecorder: TelemetryRecorder
}>
}
const EDITOR_INSTRUCTIONS: EditorInstructionsTile[] = [
{
icon: 'VsCode',
name: 'VS Code',
instructions: ({ telemetryRecorder }) => (
<div className="d-flex flex-column flex-gap-2 align-items-start">
<ButtonLink
variant="primary"
to="vscode:extension/sourcegraph.cody-ai"
target="_blank"
rel="noopener"
className="mb-2"
onClick={() => {
telemetryRecorder.recordEvent('cody.editorExtensionsInstructions', 'clickInstall', {
metadata: { vscode: 1 },
})
}}
>
Install Cody in VS Code
</ButtonLink>
<Link
to="https://marketplace.visualstudio.com/items?itemName=sourcegraph.cody-ai"
className="text-muted d-inline-flex align-items-center flex-gap-1"
target="_blank"
rel="noopener"
onClick={() => {
telemetryRecorder.recordEvent('cody.editorExtensionsInstructions', 'clickMarketplace', {
metadata: { vscode: 1 },
})
}}
>
View in VS Code Marketplace{' '}
<Icon aria-label="Open in new window" role="img" svgPath={mdiOpenInNew} />
</Link>
<Link
to="https://github.com/sourcegraph/cody"
className="text-muted d-inline-flex align-items-center flex-gap-1"
target="_blank"
rel="noopener"
onClick={() => {
telemetryRecorder.recordEvent('cody.editorExtensionsInstructions', 'clickSource', {
metadata: { vscode: 1 },
})
}}
>
Install from source <Icon aria-label="Open in new window" role="img" svgPath={mdiOpenInNew} />
</Link>
</div>
),
},
{
icon: 'JetBrains',
name: 'All JetBrains IDEs',
instructions: ({ telemetryRecorder }) => (
<div className="d-flex flex-column flex-gap-2 align-items-start">
<ButtonLink
variant="primary"
to="https://plugins.jetbrains.com/plugin/9682-sourcegraph-cody--code-search"
className="mb-2"
target="_blank"
rel="noopener"
onClick={() => {
telemetryRecorder.recordEvent('cody.editorExtensionsInstructions', 'clickMarketplace', {
metadata: { jetbrains: 1 },
})
}}
>
Install Cody from JetBrains&nbsp;Marketplace
</ButtonLink>
<Link
to="https://github.com/sourcegraph/jetbrains"
className="text-muted d-inline-flex align-items-center flex-gap-1"
target="_blank"
rel="noopener"
onClick={() => {
telemetryRecorder.recordEvent('cody.editorExtensionsInstructions', 'clickSource', {
metadata: { jetbrains: 1 },
})
}}
>
Install from source <Icon aria-label="Open in new window" role="img" svgPath={mdiOpenInNew} />
</Link>
<Text className="text-muted small mt-2">
Works in IntelliJ, PyCharm, GoLand, Android Studio, WebStorm, Rider, RubyMine, and all other
JetBrains IDEs.
</Text>
</div>
),
},
{
name: 'Other editors & clients',
instructions: ({ telemetryRecorder }) => (
<ul className="d-flex flex-column flex-gap-2 align-items-start list-unstyled">
{OTHER_CLIENTS.map(client => (
<li key={client.name} className="d-flex flex-gap-2 align-items-center">
<LinkOrSpan
to={client.url}
target="_blank"
rel="noopener"
className={client.url ? undefined : 'text-muted'}
onClick={() => {
telemetryRecorder.recordEvent('cody.editorExtensionsInstructions', 'clickOther', {
metadata: { [client.telemetryMetadataKey]: 1 },
})
}}
>
{client.name}
</LinkOrSpan>
{client.releaseStage && (
<Badge variant="outlineSecondary" small={true}>
{client.releaseStage}
</Badge>
)}
</li>
))}
</ul>
),
},
]
const OTHER_CLIENTS: {
name: string
url?: string
telemetryMetadataKey: string
releaseStage?: 'Experimental' | 'Coming soon'
}[] = [
{
name: 'Cody Web',
url: 'https://sourcegraph.com/docs/cody/clients/cody-with-sourcegraph',
telemetryMetadataKey: 'web',
releaseStage: 'Experimental',
},
{
name: 'Neovim',
url: 'https://github.com/sourcegraph/sg.nvim#setup',
telemetryMetadataKey: 'neovim',
releaseStage: 'Experimental',
},
{
name: 'Cody CLI',
url: 'https://sourcegraph.com/github.com/sourcegraph/cody@main/-/blob/cli/README.md',
telemetryMetadataKey: 'cli',
releaseStage: 'Experimental',
},
{
name: 'Visual Studio',
telemetryMetadataKey: 'visualstudio',
releaseStage: 'Coming soon',
},
{
name: 'Eclipse',
telemetryMetadataKey: 'eclipse',
releaseStage: 'Coming soon',
},
{
name: 'Emacs',
url: 'https://github.com/sourcegraph/cody-emacs',
telemetryMetadataKey: 'emacs',
releaseStage: 'Coming soon',
},
]

View File

@ -1,195 +0,0 @@
.root {
:global(.theme-dark) & {
background: linear-gradient(180deg, rgba(17, 20, 27, 0.92) 0%, rgba(13, 16, 25, 0.92) 100%);
backdrop-filter: blur(7px);
}
:global(.theme-light) & {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.94) 0%, rgba(244, 244, 244, 0.9) 100%);
backdrop-filter: blur(7px);
}
}
.modal {
width: 54rem;
border-radius: 6px;
background-image: repeating-conic-gradient(
rgba(112, 72, 232, 0.5) 50%,
rgba(0, 203, 236, 0.5) 75%,
rgba(161, 18, 255, 0.5) 100%,
rgba(255, 85, 67, 0.5) 125%,
rgba(112, 72, 232, 0.5) 150%
);
background-origin: border-box;
/* to allow a gradient stroke, this is actually where the color of the background is set */
box-shadow: inset 0 100vw var(--theme-bg-plain);
/* to let the gradient on the background act as a stroke */
border: 1px solid transparent;
/* to let gradient shadow be behind the modal */
transform-style: preserve-3d;
/* Colorful shadow on the modal */
&::after {
content: '';
position: absolute;
width: 90%;
height: 2.8125rem;
background: linear-gradient(90deg, #7048e8 0%, #4ac1e8 32.21%, #4d0b79 65.39%, #ff5543 104.43%), #eff2f5;
transform: translateZ(-1px);
filter: blur(30px);
opacity: 0.9;
animation: modal-shadow 6s ease-in-out infinite;
}
:global(.theme-dark) & {
background-image: repeating-conic-gradient(
rgba(112, 72, 232, 0.3) 50%,
rgba(0, 203, 236, 0.3) 75%,
rgba(161, 18, 255, 0.5) 100%,
rgba(255, 85, 67, 0.3) 125%,
rgba(112, 72, 232, 0.3) 150%
);
}
}
@keyframes modal-shadow {
0% {
transform: translateY(0) translateZ(-1px);
opacity: 0.4;
}
50% {
transform: translateY(-25px) translateZ(-1px);
opacity: 0.9;
}
100% {
transform: translateY(0) translateZ(-1px);
opacity: 0.4;
}
}
.highlight-step {
:global(.theme-light) & {
background: radial-gradient(50% 100% at 50% 100%, var(--gray-04) 0%, transparent 100%);
}
:global(.theme-dark) & {
background: radial-gradient(50% 100% at 50% 100%, var(--gray-09) 0%, transparent 100%);
}
}
.release-stage {
color: var(--gray-07);
}
/* Step section on instructions */
.step {
background: #343a4d;
color: #ffffff;
padding: 0.25rem 0.675rem;
border-radius: 50%;
}
.ide-grid:hover {
background: var(--subtle-bg);
transition: all 0.25s ease;
}
.instructions-container {
/* adds vertical scroll to long instructions and prevents "next" and "back buttons" to be under the fold */
max-height: 80vh;
overflow-y: auto;
}
.responsive-container {
@media (--sm-breakpoint-down) {
flex-direction: column;
align-items: center;
> div {
border: none !important;
align-items: center;
}
}
}
.ide-name {
font-size: 1rem;
}
/* Initial welcome */
.welcome-title {
font-size: 2.5rem;
font-weight: 600;
letter-spacing: -0.2px;
}
.welcome-subtitle {
font-size: 1.1rem;
font-weight: 300;
}
.fade-in {
animation: 0.8s fadeInUp ease-in-out;
}
.fade-first {
animation-delay: 3.3s;
opacity: 0;
/* to keep opacity */
animation-fill-mode: forwards;
}
.fade-second {
animation-delay: 3.5s;
opacity: 0;
/* to keep opacity */
animation-fill-mode: forwards;
}
.fade-third {
animation-delay: 3.8s;
opacity: 0;
/* to keep opacity */
animation-fill-mode: forwards;
}
.welcome-video {
animation: 0.8s moveUp 3.5s ease-in-out;
transform: translateY(70%);
animation-fill-mode: forwards;
}
@keyframes moveUp {
0% {
transform: translateY(70%);
}
100% {
transform: translateY(0%);
}
}
@keyframes fadeInUp {
0% {
transform: translateY(100%);
opacity: 0;
}
100% {
transform: translateY(0%);
opacity: 1;
}
}
.fade-in-up-animation {
animation: 1.5s fadeInUp;
}
.blank-placeholder {
min-width: 11.25rem;
min-height: 17rem;
}

View File

@ -1,129 +0,0 @@
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
import type { TelemetryRecorder, TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { Modal, useSearchParameters } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
import { EditorStep } from './EditorStep'
import { PurposeStep } from './PurposeStep'
import { WelcomeStep } from './WelcomeStep'
import styles from './CodyOnboarding.module.scss'
export interface IEditor {
id: number // a unique number identifier for telemetry
icon: string
name: string
publisher: string
releaseStage: string
docs?: string
instructions?: React.FC<{
onBack?: () => void
onClose: () => void
showStep?: number
telemetryRecorder: TelemetryRecorder
}>
}
interface CodyOnboardingProps extends TelemetryV2Props {
authenticatedUser: AuthenticatedUser | null
}
export function CodyOnboarding({ authenticatedUser, telemetryRecorder }: CodyOnboardingProps): JSX.Element | null {
const [showEditorStep, setShowEditorStep] = useState(false)
const [completed = false, setOnboardingCompleted] = useTemporarySetting('cody.onboarding.completed', false)
const [signUpFlowEnabled, signUpFlowStatus] = useFeatureFlag('ab-shortened-install-first-signup-flow-cody-2024-04')
// steps start from 0
const [step = -1, setOnboardingStep] = useTemporarySetting('cody.onboarding.step', 0)
const onNext = (): void => setOnboardingStep(currentsStep => (currentsStep || 0) + 1)
const parameters = useSearchParameters()
const enrollPro = parameters.get('pro') === 'true'
const returnToURL = parameters.get('returnTo')
// All calls with a `requestFrom` query param to this call or in the returnTo URL come from Cody clients.
const isCody = !!parameters.get('requestFrom') || !!returnToURL?.includes('requestFrom')
const navigate = useNavigate()
useEffect(() => {
if (completed && returnToURL) {
navigate(returnToURL)
}
}, [completed, returnToURL, navigate])
useEffect(() => {
if (signUpFlowStatus === 'loaded' && signUpFlowEnabled && isCody) {
setOnboardingStep(currentsStep => (currentsStep || 0) + 2)
setOnboardingCompleted(true)
setShowEditorStep(true)
}
if (signUpFlowStatus === 'loaded' && isCody) {
const metadataKey = signUpFlowEnabled ? 'treatmentVariant' : 'controlVariant'
telemetryRecorder.recordEvent('cody.onboarding.ABShortenedSignupFlowForInstalls202404', 'enroll', {
metadata: { [metadataKey]: 1 },
})
}
}, [signUpFlowEnabled, signUpFlowStatus, isCody, setOnboardingStep, setOnboardingCompleted, telemetryRecorder])
if (completed && returnToURL) {
return null
}
if (!showEditorStep && (completed || step === -1 || step > 1)) {
return null
}
if (!authenticatedUser) {
return null
}
if (signUpFlowStatus !== 'loaded') {
return null
}
const handleShowLastStep = (): void => {
setOnboardingCompleted(true)
setShowEditorStep(true)
telemetryRecorder.recordEvent('cody.onboarding.hubspotForm.fromWorkPersonalToHandRaiserTest', 'enroll', {
metadata: { controlVariant: 1 },
})
}
return (
<Modal
isOpen={true}
position="center"
aria-label="Cody Onboarding"
className={styles.modal}
containerClassName={styles.root}
>
{step === 0 && <WelcomeStep onNext={onNext} pro={enrollPro} telemetryRecorder={telemetryRecorder} />}
{step === 1 && (
<PurposeStep
authenticatedUser={authenticatedUser}
onNext={() => {
onNext()
handleShowLastStep()
}}
pro={enrollPro}
telemetryRecorder={telemetryRecorder}
/>
)}
{showEditorStep && (
<EditorStep
onCompleted={() => {
setShowEditorStep(false)
}}
pro={enrollPro}
telemetryRecorder={telemetryRecorder}
/>
)}
</Modal>
)
}

View File

@ -1,118 +0,0 @@
import { useEffect, useState } from 'react'
import classNames from 'classnames'
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { H2, Text, H5 } from '@sourcegraph/wildcard'
import { editorGroups } from '../editorGroups'
import type { IEditor } from './CodyOnboarding'
import styles from './CodyOnboarding.module.scss'
export function EditorStep({
onCompleted,
pro,
telemetryRecorder,
}: {
onCompleted: () => void
pro: boolean
telemetryRecorder: TelemetryRecorder
}): JSX.Element {
useEffect(() => {
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'view', { metadata: { tier: pro ? 1 : 0 } })
}, [pro, telemetryRecorder])
const [editor, setEditor] = useState<null | IEditor>(null)
const onBack = (): void => setEditor(null)
if (editor?.instructions) {
const Instructions = editor.instructions
return <Instructions onBack={onBack} onClose={onCompleted} telemetryRecorder={telemetryRecorder} />
}
return (
<>
<div className="border-bottom pb-3 mb-3">
<H2 className="mb-1">Choose your editor</H2>
<Text className="mb-0 text-muted" size="small">
Most of Cody experience happens in the IDE. Let's get that set up.
</Text>
</div>
<div className="mb-3 border-bottom pb-3">
{editorGroups.map((group, groupIndex) => (
<div
key={group[0].id}
className={classNames('d-flex mt-3', styles.responsiveContainer, {
'border-bottom pb-3': groupIndex < group.length - 1,
})}
>
{group.map((editor, editorIndex) => (
<div
key={editor.id}
className={classNames('d-flex flex-column flex-1 p-3 cursor-pointer', styles.ideGrid, {
'border-left': editorIndex !== 0,
})}
role="button"
tabIndex={0}
onKeyDown={() => {
setEditor(editor)
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'select', {
metadata: { tier: pro ? 1 : 0, editor: editor.id },
})
}}
onClick={() => {
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'select', {
metadata: { tier: pro ? 1 : 0, editor: editor.id },
})
setEditor(editor)
}}
>
<div className="d-flex align-items-center">
<div>
<img
alt={editor.name}
src={`https://storage.googleapis.com/sourcegraph-assets/ideIcons/ideIcon${editor.icon}.svg`}
width={34}
className="mr-3"
/>
</div>
<div>
<Text className="text-muted mb-0 text-truncate" size="small">
{editor.publisher}
</Text>
<Text className={classNames('mb-0', styles.ideName)}>{editor.name}</Text>
<H5 className={styles.releaseStage}>{editor.releaseStage}</H5>
</div>
</div>
</div>
))}
{group.length < 4
? Array.from(new Array(4 - group.length).keys()).map(item => (
<div key={item} className="flex-1 p-3" />
))
: null}
</div>
))}
</div>
<div className="d-flex justify-content-end align-items-center">
<Text
className="mb-0 text-muted cursor-pointer"
size="small"
onClick={() => {
onCompleted()
telemetryRecorder.recordEvent('cody.onboarding.chooseEditor', 'skip', {
metadata: { tier: pro ? 1 : 0 },
})
}}
>
Skip for now
</Text>
</div>
</>
)
}

View File

@ -1,61 +0,0 @@
import { useEffect } from 'react'
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { H2, Text } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import { HubSpotForm } from '../../marketing/components/HubSpotForm'
export function PurposeStep({
onNext,
pro,
authenticatedUser,
telemetryRecorder,
}: {
onNext: () => void
pro: boolean
authenticatedUser: AuthenticatedUser
telemetryRecorder: TelemetryRecorder
}): JSX.Element {
useEffect(() => {
telemetryRecorder.recordEvent('cody.onboarding.purpose', 'view', { metadata: { tier: pro ? 1 : 0 } })
}, [pro, telemetryRecorder])
const primaryEmail = authenticatedUser.emails.find(email => email.isPrimary)?.email
const handleFormSubmit = (form: HTMLFormElement): void => {
const choice = form[0].querySelector<HTMLInputElement>('input[name="cody_form_hand_raiser"]')
if (choice) {
telemetryRecorder.recordEvent('cody.onboarding.purpose', 'select', {
metadata: { onboardingCall: choice.checked ? 1 : 0 },
})
}
}
return (
<>
<div className="border-bottom pb-3 mb-3">
<H2 className="mb-1">Would you like to learn more about Cody for enterprise?</H2>
<Text className="mb-0 text-muted" size="small">
If you're not ready for a conversation, we'll stick to sharing Cody onboarding resources.
</Text>
</div>
<div className="d-flex align-items-center border-bottom mb-3 pb-3 justify-content-center">
<HubSpotForm
formId="19f34edd-1a98-4fc9-9b2b-c1edca727720"
onFormSubmitted={() => {
onNext()
}}
onFormLoadError={() => {
onNext()
}}
userId={authenticatedUser.id}
userEmail={primaryEmail}
masterFormName="qualificationSurvey"
onFormSubmit={handleFormSubmit}
/>
</div>
</>
)
}

View File

@ -1,75 +0,0 @@
import { useState, useEffect } from 'react'
import classNames from 'classnames'
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import { Text, Button } from '@sourcegraph/wildcard'
import { EventName } from '../../util/constants'
import styles from './CodyOnboarding.module.scss'
export function WelcomeStep({
onNext,
pro,
telemetryRecorder,
}: {
onNext: () => void
pro: boolean
telemetryRecorder: TelemetryRecorder
}): JSX.Element {
const [show, setShow] = useState(false)
const isLightTheme = useIsLightTheme()
useEffect(() => {
EVENT_LOGGER.log(
EventName.CODY_ONBOARDING_WELCOME_VIEWED,
{ tier: pro ? 'pro' : 'free' },
{ tier: pro ? 'pro' : 'free' }
)
telemetryRecorder.recordEvent('cody.onboarding.welcome', 'view', { metadata: { tier: pro ? 1 : 0 } })
}, [pro, telemetryRecorder])
useEffect(() => {
// theme is not ready on first render, it defaults to system theme.
// so we need to wait a bit before showing the welcome video.
setTimeout(() => {
setShow(true)
}, 500)
}, [])
return (
<div className={classNames('d-flex flex-column align-items-center p-5')}>
{show ? (
<>
<video width="180" className={classNames('mb-5', styles.welcomeVideo)} autoPlay={true} muted={true}>
<source
src={
isLightTheme
? 'https://storage.googleapis.com/sourcegraph-assets/hiCodyWhite.mp4'
: 'https://storage.googleapis.com/sourcegraph-assets/hiCodyDark.mp4'
}
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
<Text className={classNames('mb-4 pb-4', styles.fadeIn, styles.fadeSecond, styles.welcomeSubtitle)}>
Ready to breeze through the basics and get comfortable with Cody
{pro ? ' Pro' : ''}?
</Text>
<Button
onClick={onNext}
variant="primary"
size="lg"
className={classNames(styles.fadeIn, styles.fadeThird)}
>
Sure, let's dive in!
</Button>
</>
) : (
<div className={styles.blankPlaceholder} />
)}
</div>
)
}

View File

@ -1,105 +0,0 @@
import { H2, Text, Link, Button } from '@sourcegraph/wildcard'
import { EditorStep } from '../../management/CodyManagementPage'
export function CodyFeatures({
onClose,
showStep,
setStep,
}: {
onClose: () => void
showStep?: EditorStep
setStep: (step: EditorStep) => void
}): JSX.Element {
return (
<>
<div className="mb-3 pb-3 border-bottom">
<H2>Cody features</H2>
</div>
<div className="d-flex">
<div className="flex-1 p-3 border-right d-flex flex-column justify-content-center align-items-center">
<Text className="mb-1 w-100" weight="bold">
Autocomplete
</Text>
<Text className="mb-0 w-100 text-muted" size="small">
Let Cody automatically write code for you. Start writing a comment or a line of code and Cody
will suggest the next few lines.
</Text>
<img
alt="Cody Autocomplete"
width="100%"
className="mt-4"
src="https://storage.googleapis.com/sourcegraph-assets/codyFeaturesImgs/featureAutoCompletions.png"
/>
</div>
<div className="flex-1 p-3 d-flex flex-column justify-content-center align-items-center">
<Text className="mb-1 w-100" weight="bold">
Chat
</Text>
<Text className="mb-0 text-muted w-100" size="small">
Answer questions about programming topics generally or your codebase specifically with Cody
chat.
</Text>
<img
alt="Cody Chat"
width="100%"
className="mt-4"
src="https://storage.googleapis.com/sourcegraph-assets/codyFeaturesImgs/featureChat.png"
/>
</div>
</div>
<div className="d-flex my-3 py-3 border-top border-bottom">
<div className="flex-1 p-3 border-right d-flex flex-column justify-content-center align-items-center">
<Text className="mb-1 w-100" weight="bold">
Commands
</Text>
<Text className="mb-0 text-muted w-100" size="small">
Streamline your development process by using Cody commands to understand, improve, fix,
document, and generate unit tests for your code.
</Text>
<img
alt="Cody Commands"
width="100%"
className="mt-4"
src="https://storage.googleapis.com/sourcegraph-assets/codyFeaturesImgs/featureCommands.png"
/>
</div>
<div className="flex-1 p-3 d-flex flex-column justify-content-center align-items-center">
<Text className="mb-1 w-100" weight="bold">
Feedback
</Text>
<Text className="mb-0 text-muted w-100" size="small">
Feel free to join our Discord to leave feedback or ask questions about Cody.
</Text>
<div className="mt-4 d-flex flex-column justify-content-center h-100">
<Link to="https://discord.gg/rDPqBejz93" className="d-flex w-100 justify-content-center ">
<strong>Discord chat</strong>
</Link>
<Link
to="https://github.com/sourcegraph/cody/discussions/new?category=product-feedback"
className="d-flex w-100 justify-content-center mt-4"
>
<strong>GitHub Discussions</strong>
</Link>
</div>
</div>
</div>
{showStep === undefined ? (
<div className="mt-3 d-flex justify-content-between">
<Button variant="secondary" onClick={() => setStep(0)} outline={true} size="sm">
Back
</Button>
<Button variant="primary" onClick={onClose} size="sm">
Close
</Button>
</div>
) : (
<div className="mt-3 d-flex justify-content-end">
<Button variant="primary" onClick={onClose} size="sm">
Close
</Button>
</div>
)}
</>
)
}

View File

@ -1,167 +0,0 @@
import { useState, useEffect } from 'react'
import classNames from 'classnames'
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import { Button, H2, Link, Text } from '@sourcegraph/wildcard'
import { EventName } from '../../../util/constants'
import { EditorStep } from '../../management/CodyManagementPage'
import { CodyFeatures } from './CodyFeatures'
import styles from '../CodyOnboarding.module.scss'
export function JetBrainsInstructions({
onBack,
onClose,
showStep,
telemetryRecorder,
}: {
onBack?: () => void
onClose: () => void
showStep?: EditorStep
telemetryRecorder: TelemetryRecorder
}): JSX.Element {
const [step, setStep] = useState<EditorStep>(showStep || 0)
const marketplaceUrl = 'https://plugins.jetbrains.com/plugin/9682-sourcegraph-cody--code-search'
useEffect(() => {
if (step === EditorStep.SetupInstructions) {
EVENT_LOGGER.log(EventName.CODY_EDITOR_SETUP_VIEWED, { editor: 'JetBrains' })
telemetryRecorder.recordEvent('cody.editorSetupPage', 'view', { metadata: { jetBrains: 1 } })
} else if (step === EditorStep.CodyFeatures) {
EVENT_LOGGER.log(EventName.CODY_EDITOR_FEATURES_VIEWED, { editor: 'JetBrains' })
telemetryRecorder.recordEvent('cody.editorFeaturesPage', 'view', { metadata: { jetBrains: 1 } })
}
}, [step, telemetryRecorder])
return (
<>
{step === EditorStep.SetupInstructions && (
<>
<div className="pb-3 border-bottom">
<H2>Setup instructions for JetBrains</H2>
</div>
<div className={classNames('pt-3 px-3', styles.instructionsContainer)}>
<div className={classNames('d-flex flex-column border-bottom')}>
<div className="d-flex align-items-center">
<div className="mr-1">
<div className={classNames('mr-2', styles.step)}>1</div>
</div>
<div>
<Text className="mb-1" weight="bold">
Open the Plugins Page (or via the{' '}
<Link
to={marketplaceUrl}
target="_blank"
rel="noopener"
onClick={event => {
event.preventDefault()
EVENT_LOGGER.log(EventName.CODY_EDITOR_SETUP_OPEN_MARKETPLACE, {
editor: 'JetBrains',
})
telemetryRecorder.recordEvent(
'cody.onboarding.openMarketplace',
'click',
{ metadata: { jetBrains: 1 } }
)
window.location.href = marketplaceUrl
}}
>
JetBrains Marketplace
</Link>
)
</Text>
<Text className="text-muted mb-0" size="small">
Click the cog [] icon in the top right corner of your IDE and select{' '}
<strong>Plugins</strong>
<br />
Alternatively, go to the settings option (
<strong> [] + [,] on macOS, or File Settings on Windows </strong>), then
select "Plugins" from the menu on the left.
</Text>
</div>
</div>
<img
alt="JetBrains Menu"
className="mt-2 m-auto"
width="70%"
src="https://storage.googleapis.com/sourcegraph-assets/jetBrainsInstructions/jetBrainsMenu.png"
/>
</div>
<div className="mt-3 d-flex flex-column border-bottom">
<div className="d-flex align-items-center">
<div className="mr-1">
<div className={classNames('mr-2', styles.step)}>2</div>
</div>
<div>
<Text className="mb-1" weight="bold">
Install the Cody plugin
</Text>
<Text className="text-muted mb-0" size="small">
Type "Cody" in the search bar and <strong>install</strong> the plugin.
</Text>
</div>
</div>
<img
alt="jetBrains Menu"
className="mt-2 m-auto"
width="70%"
src="https://storage.googleapis.com/sourcegraph-assets/jetBrainsInstructions/jetBrainsPluginList.png"
/>
</div>
<div className="mt-3 d-flex flex-column border-bottom">
<div className="d-flex align-items-center">
<div className="mr-1">
<div className={classNames('mr-2', styles.step)}>3</div>
</div>
<div>
<Text className="mb-1" weight="bold">
Open the plugin and log in
</Text>
<Text className="text-muted mb-0" size="small">
Cody will be available on the right side of your IDE. Click the Cody icon to
open the sidebar and login.
<br />
Log in with the same method you use to create this account.
</Text>
</div>
</div>
<img
alt="jetBrains Menu"
className="mt-2 m-auto"
width="70%"
src="https://storage.googleapis.com/sourcegraph-assets/jetBrainsInstructions/jetBrainsOnboarding.png"
/>
</div>
</div>
{showStep === undefined ? (
<div className="mt-3 d-flex justify-content-between">
<Button variant="secondary" onClick={onBack} outline={true} size="sm">
Back
</Button>
<Button variant="primary" onClick={() => setStep(1)} size="sm">
Next
</Button>
</div>
) : (
<div className="mt-3 d-flex justify-content-end">
<Button variant="primary" onClick={onClose} size="sm">
Close
</Button>
</div>
)}
</>
)}
{step === EditorStep.CodyFeatures && (
<CodyFeatures onClose={onClose} showStep={showStep} setStep={setStep} />
)}
</>
)
}

View File

@ -1,114 +0,0 @@
import { useState, useEffect } from 'react'
import classNames from 'classnames'
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import { Button, ButtonLink, H2, Text } from '@sourcegraph/wildcard'
import { EventName } from '../../../util/constants'
import { EditorStep } from '../../management/CodyManagementPage'
import { CodyFeatures } from './CodyFeatures'
import styles from '../CodyOnboarding.module.scss'
export function NeoVimInstructions({
onBack,
onClose,
showStep,
telemetryRecorder,
}: {
onBack?: () => void
onClose: () => void
showStep?: EditorStep
telemetryRecorder: TelemetryRecorder
}): JSX.Element {
const [step, setStep] = useState<EditorStep>(showStep || 0)
const marketplaceUrl = 'https://github.com/sourcegraph/sg.nvim#setup'
useEffect(() => {
if (step === EditorStep.SetupInstructions) {
EVENT_LOGGER.log(EventName.CODY_EDITOR_SETUP_VIEWED, { editor: 'NeoVim' })
telemetryRecorder.recordEvent('cody.editorSetupPage', 'view', { metadata: { neoVim: 1 } })
} else if (step === EditorStep.CodyFeatures) {
EVENT_LOGGER.log(EventName.CODY_EDITOR_FEATURES_VIEWED, { editor: 'NeoVim' })
telemetryRecorder.recordEvent('cody.editorFeaturesPage', 'view', { metadata: { neoVim: 1 } })
}
}, [step, telemetryRecorder])
return (
<>
{step === EditorStep.SetupInstructions && (
<>
<div className="pb-3 border-bottom">
<H2>Setup instructions for Neovim</H2>
</div>
<div className={classNames('pt-3 px-3', styles.instructionsContainer)}>
<div className={classNames('d-flex flex-column border-bottom')}>
<div className="d-flex align-items-center">
<div className="mr-1">
<div className={classNames('mr-2', styles.step)}>1</div>
</div>
<div>
<Text className="mb-1" weight="bold">
Open the plugin repo on GitHub
</Text>
<Text className="text-muted mb-0" size="small">
Follow the instructions detailed in the <strong>readme.md</strong> file to
install the plugin.
</Text>
</div>
</div>
<div className="d-flex flex-column justify-content-center align-items-center mt-4">
<ButtonLink
variant="primary"
to={marketplaceUrl}
target="_blank"
onClick={event => {
event.preventDefault()
EVENT_LOGGER.log(EventName.CODY_EDITOR_SETUP_OPEN_MARKETPLACE, {
editor: 'NeoVim',
})
telemetryRecorder.recordEvent('cody.onboarding.openMarketplace', 'click', {
metadata: { neoVim: 1 },
})
window.location.href = marketplaceUrl
}}
>
Navigate to GitHub repo
</ButtonLink>
<img
alt="Neovim Repo"
className="mt-2 m-auto"
width="70%"
src="https://storage.googleapis.com/sourcegraph-assets/NeoVimInstructions/NeovimStep1.png"
/>
</div>
</div>
</div>
{showStep === undefined ? (
<div className="mt-3 d-flex justify-content-between">
<Button variant="secondary" onClick={onBack} outline={true} size="sm">
Back
</Button>
<Button variant="primary" onClick={() => setStep(1)} size="sm">
Next
</Button>
</div>
) : (
<div className="mt-3 d-flex justify-content-end">
<Button variant="primary" onClick={onClose} size="sm">
Close
</Button>
</div>
)}
</>
)}
{step === EditorStep.CodyFeatures && (
<CodyFeatures onClose={onClose} showStep={showStep} setStep={setStep} />
)}
</>
)
}

View File

@ -1,162 +0,0 @@
import { useState, useEffect } from 'react'
import classNames from 'classnames'
import type { TelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import { Button, ButtonLink, H2, Text } from '@sourcegraph/wildcard'
import { EventName } from '../../../util/constants'
import { EditorStep } from '../../management/CodyManagementPage'
import { CodyFeatures } from './CodyFeatures'
import styles from '../CodyOnboarding.module.scss'
export function VSCodeInstructions({
onBack,
onClose,
showStep,
telemetryRecorder,
}: {
onBack?: () => void
onClose: () => void
showStep?: EditorStep
telemetryRecorder: TelemetryRecorder
}): JSX.Element {
const [step, setStep] = useState<EditorStep>(showStep || 0)
const marketplaceUrl = 'https://marketplace.visualstudio.com/items?itemName=sourcegraph.cody-ai'
useEffect(() => {
if (step === EditorStep.SetupInstructions) {
EVENT_LOGGER.log(EventName.CODY_EDITOR_SETUP_VIEWED, { editor: 'VS Code' })
telemetryRecorder.recordEvent('cody.editorSetupPage', 'view', { metadata: { vsCode: 1 } })
} else if (step === EditorStep.CodyFeatures) {
EVENT_LOGGER.log(EventName.CODY_EDITOR_FEATURES_VIEWED, { editor: 'VS Code' })
telemetryRecorder.recordEvent('cody.editorFeaturesPage', 'view', { metadata: { vsCode: 1 } })
}
}, [step, telemetryRecorder])
return (
<>
{step === EditorStep.SetupInstructions && (
<>
<div className="pb-3 border-bottom">
<H2>Setup instructions for VS Code</H2>
</div>
<div className={classNames('pt-3 px-3', styles.instructionsContainer)}>
<div className={classNames('border-bottom', styles.highlightStep)}>
<div className="d-flex align-items-center">
<div className="mr-1">
<div className={classNames('mr-2', styles.step)}>1</div>
</div>
<div>
<Text className="mb-1" weight="bold">
Install Cody
</Text>
<Text className="text-muted mb-0" size="small">
Alternatively, you can reach this page by clicking{' '}
<strong>View {'>'} Extensions</strong> and searching for{' '}
<strong>Cody AI</strong>
</Text>
</div>
</div>
<div className="d-flex flex-column justify-content-center align-items-center mt-4">
<ButtonLink
variant="primary"
to={marketplaceUrl}
target="_blank"
onClick={event => {
event.preventDefault()
EVENT_LOGGER.log(EventName.CODY_EDITOR_SETUP_OPEN_MARKETPLACE, {
editor: 'VS Code',
})
telemetryRecorder.recordEvent('cody.onboarding.openMarketplace', 'click', {
metadata: { vsCode: 1 },
})
window.location.href = marketplaceUrl
}}
>
Open Marketplace
</ButtonLink>
<img
alt="VS Code Marketplace"
className="mt-4"
width="70%"
src="https://storage.googleapis.com/sourcegraph-assets/VSCodeInstructions/__step1.png"
/>
</div>
</div>
<div className="mt-3 border-bottom">
<div className="d-flex align-items-center">
<div className="mr-1">
<div className={classNames('mr-2', styles.step)}>2</div>
</div>
<div>
<Text className="mb-1" weight="bold">
Open Cody from the sidebar on the left
</Text>
<Text className="text-muted mb-0" size="small">
Typically Cody will be the last item in the sidebar
</Text>
</div>
</div>
<div className="d-flex flex-column justify-content-center align-items-center mt-4">
<img
alt="VS Code Marketplace"
className="mt-2"
width="70%"
src="https://storage.googleapis.com/sourcegraph-assets/VSCodeInstructions/__step2.png"
/>
</div>
</div>
<div className="mt-3 border-bottom">
<div className="d-flex align-items-center">
<div className="mr-1">
<div className={classNames('mr-2', styles.step)}>3</div>
</div>
<div>
<Text className="mb-1" weight="bold">
Log in
</Text>
<Text className="text-muted mb-0" size="small">
Choose the same login method you used when you created your account
</Text>
</div>
</div>
<div className="d-flex flex-column justify-content-center align-items-center mt-4">
<img
alt="VS Code Marketplace"
className="mt-2"
width="70%"
src="https://storage.googleapis.com/sourcegraph-assets/VSCodeInstructions/__step3.png"
/>
</div>
</div>
</div>
{showStep === undefined ? (
<div className="mt-3 d-flex justify-content-between">
<Button variant="secondary" onClick={onBack} outline={true} size="sm">
Back
</Button>
<Button variant="primary" onClick={() => setStep(1)} size="sm">
Next
</Button>
</div>
) : (
<div className="mt-3 d-flex justify-content-end">
<Button variant="primary" onClick={onClose} size="sm">
Close
</Button>
</div>
)}
</>
)}
{step === EditorStep.CodyFeatures && (
<CodyFeatures onClose={onClose} showStep={showStep} setStep={setStep} />
)}
</>
)
}

View File

@ -17,10 +17,6 @@
font-size: 1.25rem;
}
.ide-name {
font-size: 1rem;
}
.pro-title {
font-size: 2.25rem;
color: #820dde;

View File

@ -58,7 +58,6 @@ const authUser: AuthenticatedUser = {
},
viewerCanAdminister: true,
hasVerifiedEmail: true,
completedPostSignup: true,
databaseID: 0,
tosAccepted: true,
emails: [{ email: 'alice@sourcegraph.com', isPrimary: true, verified: true }],

View File

@ -22,7 +22,6 @@ export const mockUser: AuthenticatedUser = {
nodes: [],
},
hasVerifiedEmail: true,
completedPostSignup: true,
session: { __typename: 'Session', canSignOut: true },
tosAccepted: true,
emails: [{ email: 'user@me.com', isPrimary: true, verified: true }],

View File

@ -83,7 +83,6 @@ const authUser: AuthenticatedUser = {
},
viewerCanAdminister: true,
hasVerifiedEmail: true,
completedPostSignup: true,
databaseID: 0,
tosAccepted: true,
emails: [{ email: 'alice@sourcegraph.com', isPrimary: true, verified: true }],

View File

@ -1,25 +0,0 @@
import { useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import { CodyProRoutes } from '../cody/codyProRoutes'
interface GetCodyPageProps {
authenticatedUser: AuthenticatedUser | null
}
export const GetCodyPage: React.FunctionComponent<GetCodyPageProps> = ({ authenticatedUser }) => {
const navigate = useNavigate()
const location = useLocation()
const [search] = useState(location.search)
if (authenticatedUser) {
navigate(`${CodyProRoutes.Manage}${search || ''}`)
} else {
window.location.href = '/cody'
}
return <></>
}

View File

@ -81,7 +81,6 @@ export function overrideInsightsGraphQLApi(props: OverrideGraphQLExtensionsProps
url: '/users/test',
settingsURL: '/users/test/settings',
hasVerifiedEmail: true,
completedPostSignup: true,
organizations: {
nodes: [
{

View File

@ -67,7 +67,6 @@ export type SourcegraphContextCurrentUser = Pick<
| 'latestSettings'
| 'permissions'
| 'hasVerifiedEmail'
| 'completedPostSignup'
>
/**

View File

@ -1,101 +0,0 @@
.cody-survey-toast-modal {
background: linear-gradient(0deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05)),
radial-gradient(63.3% 72.11% at 95.66% -10.78%, rgba(255, 255, 255, 0.13) 0%, rgba(255, 255, 255, 0) 100%),
var(--gray-01);
box-shadow: 0 0 10px rgba(96, 120, 169, 0.1);
border-radius: 0.5rem;
color: var(--black);
.cody-survey-toast-modal-button {
background: var(--violet-05);
border-radius: 0.3125rem;
color: var(--white);
border: none;
&:not(:disabled):not([aria-disabled='true']) {
&:hover,
&:focus {
background: var(--violet-05) !important;
}
}
&:disabled,
&[aria-disabled='true'] {
background: var(--violet-03);
}
}
label {
&:hover {
color: var(--black);
}
}
}
.email-icon {
background: var(--violet-01);
border-radius: 0.3125rem;
color: var(--violet-05);
height: 2rem;
width: 2rem;
}
.cody-icon {
height: 1.6875rem;
width: 1.875rem;
margin-right: 0.5rem;
}
.resend-button {
color: var(--violet-05);
}
.modal-overlay {
background-color: unset;
}
.modal-checkbox[type='checkbox'] {
display: none;
}
.modal-checkbox + label {
display: inline-block;
position: relative;
padding-left: 1.5625rem;
cursor: pointer;
}
.modal-checkbox + label::before {
content: '';
display: inline-block;
width: 1rem;
height: 1rem;
background-color: var(--white); /* Initial background color */
border: 1px solid var(--gray-07);
border-radius: 0.25rem;
position: absolute;
left: 0;
top: 2px;
}
.modal-checkbox:checked + label::before {
background-color: var(--violet-05); /* Background color when checked */
border-color: var(--violet-05); /* Border color when checked */
}
.modal-checkbox + label::after {
content: '';
display: none;
width: 0.375rem;
height: 0.625rem;
border: solid var(--white); /* Tick color */
border-width: 0 2px 2px 0;
transform: rotate(45deg);
position: absolute;
left: 5px;
top: 4px;
}
.modal-checkbox:checked + label::after {
display: block;
}

View File

@ -1,390 +0,0 @@
import { useState, useCallback, useEffect } from 'react'
import { mdiEmail } from '@mdi/js'
import classNames from 'classnames'
import { useLocation } from 'react-router-dom'
import { asError, type ErrorLike } from '@sourcegraph/common'
import { gql, useMutation } from '@sourcegraph/http-client'
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Checkbox, Form, H3, Modal, Text, Button, Icon, AnchorLink } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import { getReturnTo } from '../../auth/SignInSignUpCommon'
import { CodyColorIcon } from '../../cody/chat/CodyPageIcon'
import { CodyProRoutes } from '../../cody/codyProRoutes'
import { LoaderButton } from '../../components/LoaderButton'
import type {
SubmitCodySurveyResult,
SubmitCodySurveyVariables,
SetCompletedPostSignupVariables,
SetCompletedPostSignupResult,
} from '../../graphql-operations'
import { resendVerificationEmail } from '../../user/settings/emails/UserEmail'
import { HubSpotForm } from '../components/HubSpotForm'
import styles from './CodySurveyToast.module.scss'
export const SUBMIT_CODY_SURVEY = gql`
mutation SubmitCodySurvey($isForWork: Boolean!, $isForPersonal: Boolean!) {
submitCodySurvey(isForWork: $isForWork, isForPersonal: $isForPersonal) {
alwaysNil
}
}
`
const SET_COMPLETED_POST_SIGNUP = gql`
mutation SetCompletedPostSignup($userID: ID!) {
setCompletedPostSignup(userID: $userID) {
alwaysNil
}
}
`
const CodySurveyToastInner: React.FC<
{ onSubmitEnd: () => void; userId: string; hasVerifiedEmail: boolean } & TelemetryProps & TelemetryV2Props
> = ({ userId, onSubmitEnd, telemetryService, telemetryRecorder, hasVerifiedEmail }) => {
const [isCodyForWork, setIsCodyForWork] = useState(false)
const [isCodyForPersonalStuff, setIsCodyForPersonalStuff] = useState(false)
const handleCodyForWorkChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(event => {
setIsCodyForWork(event.target.checked)
}, [])
const handleCodyForPersonalStuffChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(event => {
setIsCodyForPersonalStuff(event.target.checked)
}, [])
const [submitCodySurvey, { loading: loadingCodySurvey, error: submitSurveyError }] = useMutation<
SubmitCodySurveyResult,
SubmitCodySurveyVariables
>(SUBMIT_CODY_SURVEY, {
variables: {
isForWork: isCodyForWork,
isForPersonal: isCodyForPersonalStuff,
},
})
const [updatePostSignupCompletion, { loading: loadingPostSignup, error: setPostSignupError }] = useMutation<
SetCompletedPostSignupResult,
SetCompletedPostSignupVariables
>(SET_COMPLETED_POST_SIGNUP, {
variables: {
userID: userId,
},
})
const loading = loadingCodySurvey || loadingPostSignup
const error = !!submitSurveyError || !!setPostSignupError
const handleSubmit = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
const eventParams = { isCodyForPersonalStuff, isCodyForWork }
telemetryService.log('CodyUsageToastSubmitted', eventParams, eventParams)
telemetryRecorder.recordEvent('codySurvey', 'submit', {
metadata: { forWork: isCodyForWork ? 1 : 0, forPersonal: isCodyForPersonalStuff ? 1 : 0 },
})
event.preventDefault()
try {
await submitCodySurvey()
if (hasVerifiedEmail) {
await updatePostSignupCompletion()
}
onSubmitEnd()
} catch (error) {
/* eslint-disable no-console */
console.error(error)
}
},
[
hasVerifiedEmail,
isCodyForPersonalStuff,
isCodyForWork,
onSubmitEnd,
submitCodySurvey,
updatePostSignupCompletion,
telemetryService,
telemetryRecorder,
]
)
useEffect(() => {
telemetryService.log('CodySurveyToastViewed')
telemetryRecorder.recordEvent('codySurvey', 'view')
}, [telemetryService, telemetryRecorder])
return (
<Modal
className={styles.codySurveyToastModal}
position="center"
aria-label="Welcome message"
containerClassName={styles.modalOverlay}
>
<H3 className="mb-4 d-flex align-items-center">
<CodyColorIcon className={styles.codyIcon} />
<span>Just one more thing...</span>
</H3>
<Text className="mb-3">How will you be using Cody, our AI assistant?</Text>
<Form onSubmit={handleSubmit}>
<Checkbox
id="cody-for-work"
label="for work"
wrapperClassName="mb-2"
checked={isCodyForWork}
disabled={loading}
onChange={handleCodyForWorkChange}
className={styles.modalCheckbox}
/>
<Checkbox
id="cody-for-personal"
label="for personal stuff"
wrapperClassName="mb-2"
checked={isCodyForPersonalStuff}
disabled={loading}
onChange={handleCodyForPersonalStuffChange}
className={styles.modalCheckbox}
/>
{error && (
<Text size="small" className="text-danger mt-3 mb-2">
An error occurred. Please reload the page and try again. If this persists, contact support at
support@sourcegraph.com
</Text>
)}
<div className="d-flex justify-content-end">
<LoaderButton
className={styles.codySurveyToastModalButton}
type="submit"
loading={loading}
label="Get started"
disabled={!(isCodyForPersonalStuff || isCodyForWork)}
/>
</div>
</Form>
</Modal>
)
}
const CodyQualificationSurveryToastInner: React.FC<
{ onSubmitEnd: () => void; authenticatedUser: AuthenticatedUser } & TelemetryProps & TelemetryV2Props
> = ({ onSubmitEnd, telemetryService, telemetryRecorder, authenticatedUser }) => {
const [updatePostSignupCompletion, { error: setPostSignupError }] = useMutation<
SetCompletedPostSignupResult,
SetCompletedPostSignupVariables
>(SET_COMPLETED_POST_SIGNUP, {
variables: {
userID: authenticatedUser.id,
},
})
const handleFormReady = useCallback(
(form: HTMLFormElement) => {
const input = form.querySelector('input[name="using_cody_for_work"]')
// Trigger telemetry event whenever the cody for work is selected.
const handleChange = (e: Event): void => {
const target = e.target as HTMLInputElement
const isChecked = target.checked
if (isChecked) {
telemetryService.log('ViewCodyWorkQuestionnarie')
telemetryRecorder.recordEvent('codySurvey.forWorkQuestionnaire', 'view')
}
}
input?.addEventListener('change', handleChange)
return () => {
input?.removeEventListener('change', handleChange)
}
},
[telemetryService, telemetryRecorder]
)
const primaryEmail = authenticatedUser.emails.find(email => email.isPrimary)?.email
const handleSubmit = useCallback(async () => {
try {
if (authenticatedUser.hasVerifiedEmail) {
await updatePostSignupCompletion()
}
onSubmitEnd()
} catch (error) {
/* eslint-disable no-console */
console.error(error)
}
}, [authenticatedUser.hasVerifiedEmail, onSubmitEnd, updatePostSignupCompletion])
useEffect(() => {
telemetryService.log('ViewCodyforWorkorPersonalForm')
telemetryRecorder.recordEvent('codySurvey.forWorkOrPersonal', 'view')
}, [telemetryService, telemetryRecorder])
return (
<Modal
className={styles.codySurveyToastModal}
position="center"
aria-label="View cody for work or personal form"
data-testid="cody-qualification-survey-form"
containerClassName={styles.modalOverlay}
>
<H3 className="mb-4 d-flex align-items-center">
<CodyColorIcon className={styles.codyIcon} />
<span>Quick question...</span>
</H3>
<Text>How will you be using Cody, our AI assistant?</Text>
<HubSpotForm
onFormSubmitted={handleSubmit}
userId={authenticatedUser?.id}
userEmail={primaryEmail}
onFormReady={handleFormReady}
masterFormName="qualificationSurvey"
/>
{!!setPostSignupError && (
<Text size="small" className="text-danger mt-3 mb-2">
An error occurred. Please reload the page and try again. If this persists, contact support at
support@sourcegraph.com
</Text>
)}
</Modal>
)
}
const CodyVerifyEmailToast: React.FC<
{ onNext: () => void; authenticatedUser: AuthenticatedUser } & TelemetryProps & TelemetryV2Props
> = ({ onNext, authenticatedUser, telemetryService, telemetryRecorder }) => {
const [sending, setSending] = useState(false)
const [resentEmailTo, setResentEmailTo] = useState<string | null>(null)
const [resendEmailError, setResendEmailError] = useState<ErrorLike | null>(null)
const resend = useCallback(async () => {
const email = (authenticatedUser.emails || []).find(({ verified }) => !verified)?.email
if (email) {
setSending(true)
await resendVerificationEmail(authenticatedUser.id, email, telemetryRecorder, {
onSuccess: () => {
setResentEmailTo(email)
setResendEmailError(null)
setSending(false)
},
onError: (errors: ErrorLike) => {
setResendEmailError(asError(errors))
setResentEmailTo(null)
setSending(false)
},
})
}
}, [authenticatedUser, telemetryRecorder])
useEffect(() => {
telemetryService.log('VerifyEmailToastViewed')
telemetryRecorder.recordEvent('codySurvey.veryEmailToast', 'view')
}, [telemetryService, telemetryRecorder])
return (
<Modal
className={styles.codySurveyToastModal}
position="center"
aria-label="Welcome message"
containerClassName={styles.modalOverlay}
>
<H3 className="mb-4">
<Icon svgPath={mdiEmail} className={classNames('mr-2', styles.emailIcon)} aria-hidden={true} />
Verify your email address
</H3>
<Text>To use Cody, our AI Assistant, you'll need to verify your email address.</Text>
<Text className="d-flex align-items-center">
<span className="mr-1">Didn't get an email?</span>
{sending ? (
<span>Sending...</span>
) : (
<>
<span>Click to </span>
<Button variant="link" className={classNames('p-0 ml-1', styles.resendButton)} onClick={resend}>
resend
</Button>
.
</>
)}
</Text>
{resentEmailTo && (
<Text>
Sent verification email to <strong>{resentEmailTo}</strong>.
</Text>
)}
{resendEmailError && <Text>{resendEmailError.message}.</Text>}
<div className="d-flex justify-content-end mt-4">
<AnchorLink className="mr-3 mt-auto mb-auto" to="/-/sign-out">
Sign out
</AnchorLink>
<Button className={styles.codySurveyToastModalButton} variant="primary" onClick={onNext}>
Next
</Button>
</div>
</Modal>
)
}
export const CodySurveyToast: React.FC<
{
authenticatedUser: AuthenticatedUser
showQualificationSurvey?: boolean
} & TelemetryProps &
TelemetryV2Props
> = ({ authenticatedUser, telemetryService, telemetryRecorder, showQualificationSurvey }) => {
const [showVerifyEmail, setShowVerifyEmail] = useState(!authenticatedUser.hasVerifiedEmail)
const location = useLocation()
const handleSubmitEnd = (): void => {
// Redirects once user submits the post-sign-up form
const returnTo = getReturnTo(location, CodyProRoutes.Manage)
window.location.replace(returnTo)
}
const dismissVerifyEmail = useCallback(() => {
telemetryService.log('VerifyEmailToastDismissed')
telemetryRecorder.recordEvent('codySurvey.verifyEmailToast', 'dismissed')
setShowVerifyEmail(false)
}, [telemetryService, telemetryRecorder])
useEffect(() => {
telemetryService.log('CustomerQualificationSurveyExperiment001Enrolled')
telemetryRecorder.recordEvent('experiment', 'enroll', { metadata: { experimentId: 1 } })
}, [telemetryService, telemetryRecorder])
if (showVerifyEmail) {
return (
<CodyVerifyEmailToast
onNext={dismissVerifyEmail}
authenticatedUser={authenticatedUser}
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
/>
)
}
if (showQualificationSurvey) {
return (
<CodyQualificationSurveryToastInner
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
onSubmitEnd={handleSubmitEnd}
authenticatedUser={authenticatedUser}
/>
)
}
return (
<CodySurveyToastInner
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
onSubmitEnd={handleSubmitEnd}
userId={authenticatedUser.id}
hasVerifiedEmail={authenticatedUser.hasVerifiedEmail}
/>
)
}

View File

@ -1,2 +1 @@
export { SurveyToastTrigger as SurveyToast } from './SurveyToastTrigger'
export { CodySurveyToast } from './CodySurveyToast'

View File

@ -4,9 +4,7 @@ export enum PageRoutes {
SearchConsole = '/search/console',
SignIn = '/sign-in',
SignUp = '/sign-up',
PostSignUp = '/post-sign-up',
UnlockAccount = '/unlock-account/:token',
Welcome = '/welcome',
Settings = '/settings',
User = '/user/*',
Organizations = '/organizations/*',
@ -23,7 +21,6 @@ export enum PageRoutes {
SetupWizard = '/setup',
Teams = '/teams/*',
RequestAccess = '/request-access/*',
GetCody = '/get-cody',
BatchChanges = '/batch-changes/*',
CodeMonitoring = '/code-monitoring/*',
Insights = '/insights/*',

View File

@ -30,8 +30,6 @@ const RepoContainer = lazyComponent(() => import('./repo/RepoContainer'), 'RepoC
const TeamsArea = lazyComponent(() => import('./team/TeamsArea'), 'TeamsArea')
const CodySidebarStoreProvider = lazyComponent(() => import('./cody/sidebar/Provider'), 'CodySidebarStoreProvider')
const CodyIgnoreProvider = lazyComponent(() => import('./cody/useCodyIgnore'), 'CodyIgnoreProvider')
const GetCodyPage = lazyComponent(() => import('./get-cody/GetCodyPage'), 'GetCodyPage')
const PostSignUpPage = lazyComponent(() => import('./auth/PostSignUpPage'), 'PostSignUpPage')
const GlobalNotebooksArea = lazyComponent(() => import('./notebooks/GlobalNotebooksArea'), 'GlobalNotebooksArea')
const GlobalBatchChangesArea = lazyComponent(
@ -87,14 +85,6 @@ const PassThroughToServer: React.FC = () => {
* See https://reacttraining.com/react-router/web/example/sidebar
*/
export const routes: RouteObject[] = [
{
path: PageRoutes.GetCody,
element: <LegacyRoute render={props => <GetCodyPage {...props} />} />,
},
{
path: PageRoutes.PostSignUp,
element: <LegacyRoute render={() => <PostSignUpPage />} />,
},
{
path: PageRoutes.Index,
element: <Index />,
@ -241,11 +231,6 @@ export const routes: RouteObject[] = [
/>
),
},
{
path: PageRoutes.Welcome,
// This route is deprecated after we removed the post-sign-up page experimental feature, but we keep it for now to not break links.
element: <Navigate replace={true} to={PageRoutes.Search} />,
},
{
path: PageRoutes.Settings,
element: <LegacyRoute render={props => <RedirectToUserSettings {...props} />} />,

View File

@ -1,12 +1,12 @@
import React, { Suspense, useCallback, useLayoutEffect, useState } from 'react'
import classNames from 'classnames'
import { Outlet, useLocation, Navigate, useMatches, useMatch } from 'react-router-dom'
import { Navigate, Outlet, useLocation, useMatch, useMatches } from 'react-router-dom'
import { useKeyboardShortcut } from '@sourcegraph/shared/src/keyboardShortcuts/useKeyboardShortcut'
import { Shortcut } from '@sourcegraph/shared/src/react-shortcuts'
import { useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settings'
import { useTheme, Theme } from '@sourcegraph/shared/src/theme'
import { Theme, useTheme } from '@sourcegraph/shared/src/theme'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { FeedbackPrompt, LoadingSpinner, useLocalStorage } from '@sourcegraph/wildcard'
@ -21,7 +21,7 @@ import { useFeatureFlag } from '../../../featureFlags/useFeatureFlag'
import { GlobalAlerts } from '../../../global/GlobalAlerts'
import { useHandleSubmitFeedback } from '../../../hooks'
import type { LegacyLayoutRouteContext } from '../../../LegacyRouteContext'
import { CodySurveyToast, SurveyToast } from '../../../marketing/toast'
import { SurveyToast } from '../../../marketing/toast'
import { GlobalNavbar } from '../../../nav/GlobalNavbar'
import { PageRoutes } from '../../../routes.constants'
import { parseSearchURLQuery } from '../../../search'
@ -47,9 +47,8 @@ function useIsSignInOrSignUpPage(): boolean {
const isSignInPage = useMatch(PageRoutes.SignIn)
const isSignUpPage = useMatch(PageRoutes.SignUp)
const isPasswordResetPage = useMatch(PageRoutes.PasswordReset)
const isWelcomePage = useMatch(PageRoutes.Welcome)
const isRequestAccessPage = useMatch(PageRoutes.RequestAccess)
return !!(isSignInPage || isSignUpPage || isPasswordResetPage || isWelcomePage || isRequestAccessPage)
return !!(isSignInPage || isSignUpPage || isPasswordResetPage || isRequestAccessPage)
}
export const Layout: React.FC<LegacyLayoutProps> = props => {
const location = useLocation()
@ -94,7 +93,6 @@ export const Layout: React.FC<LegacyLayoutProps> = props => {
const needsRepositoryConfiguration = window.context?.needsRepositoryConfiguration
const isSiteInit = location.pathname === PageRoutes.SiteAdminInit
const isSignInOrUp = useIsSignInOrSignUpPage()
const isGetCodyPage = location.pathname === PageRoutes.GetCody
const [enableContrastCompliantSyntaxHighlighting] = useFeatureFlag('contrast-compliant-syntax-highlighting')
@ -200,14 +198,7 @@ export const Layout: React.FC<LegacyLayoutProps> = props => {
telemetryRecorder={props.platformContext.telemetryRecorder}
/>
)}
{!isSiteInit && props.isSourcegraphDotCom && props.authenticatedUser && (
<CodySurveyToast
telemetryService={props.telemetryService}
telemetryRecorder={props.platformContext.telemetryRecorder}
authenticatedUser={props.authenticatedUser}
/>
)}
{!isSiteInit && !isSignInOrUp && !isGetCodyPage && (
{!isSiteInit && !isSignInOrUp && (
<GlobalNavbar
{...props}
routes={[]}

View File

@ -17,7 +17,6 @@ export const enum EventName {
CODY_CHAT_SCOPE_INFERRED_REPO_DISABLED = 'web:codyChat:inferredRepoDisabled',
CODY_CHAT_SCOPE_INFERRED_FILE_ENABLED = 'web:codyChat:inferredFileEnabled',
CODY_CHAT_SCOPE_INFERRED_FILE_DISABLED = 'web:codyChat:inferredFileDisabled',
VIEW_GET_CODY = 'GetCody',
CODY_EDITOR_WIDGET_VIEWED = 'web:codyEditorWidget:viewed',
CODY_SIDEBAR_CHAT_OPENED = 'web:codySidebar:chatOpened',

View File

@ -14,6 +14,18 @@
min-width: 0;
}
.flex-gap-1 {
gap: 0.25rem;
}
.flex-gap-2 {
gap: 0.5rem;
}
.flex-gap-4 {
gap: 1rem;
}
@each $breakpoint in map-keys($grid-breakpoints) {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);

View File

@ -284,13 +284,6 @@ type Mutation {
"""
setTosAccepted(userID: ID): EmptyResponse!
"""
Sets the user to have completed the post-signup flow.
If the ID is omitted, the current user is assumed.
Only the user or site admins may perform this mutation.
"""
setCompletedPostSignup(userID: ID): EmptyResponse!
"""
Creates an access token that grants the privileges of the specified user (referred to as the access token's
"subject" user after token creation). The result is the access token value, which the caller is responsible
for storing (it is not accessible by Sourcegraph after creation).
@ -931,10 +924,6 @@ type Mutation {
use the default quota.
"""
setUserCodeCompletionsQuota(user: ID!, quota: Int): User!
"""
Submits a post-signup user survey about intended Cody usage.
"""
submitCodySurvey(isForWork: Boolean!, isForPersonal: Boolean!): EmptyResponse!
}
"""
@ -6551,11 +6540,6 @@ type User implements Node & SettingsSubject & Namespace {
"""
hasVerifiedEmail: Boolean!
"""
Whether the user has completed the post-signup flow.
Only the user and site admins can access this field.
"""
completedPostSignup: Boolean!
"""
The user's verified primary email address (if any).
On dotcom only the user and site admins can access this field.
"""

View File

@ -413,16 +413,6 @@ func (r *UserResolver) TosAccepted(_ context.Context) bool {
return r.user.TosAccepted
}
func (r *UserResolver) CompletedPostSignup(ctx context.Context) (bool, error) {
// 🚨 SECURITY: Only the user and admins are allowed to state of
// post-signup flow completion.
if err := auth.CheckSiteAdminOrSameUserFromActor(r.actor, r.db, r.user.ID); err != nil {
return false, err
}
return r.user.CompletedPostSignup, nil
}
type updateUserArgs struct {
User graphql.ID
Username *string
@ -705,24 +695,6 @@ func (r *schemaResolver) SetTosAccepted(ctx context.Context, args *userMutationA
return r.updateAffectedUser(ctx, affectedUserID, update)
}
func (r *schemaResolver) SetCompletedPostSignup(ctx context.Context, args *userMutationArgs) (*EmptyResponse, error) {
affectedUserID, err := r.affectedUserID(ctx, args)
if err != nil {
return nil, err
}
has, err := backend.NewUserEmailsService(r.db, r.logger).HasVerifiedEmail(ctx, affectedUserID)
if err != nil {
return nil, err
} else if !has {
return nil, errors.New("must have a verified email to complete post-signup flow")
}
completedPostSignup := true
update := database.UserUpdate{CompletedPostSignup: &completedPostSignup}
return r.updateAffectedUser(ctx, affectedUserID, update)
}
func (r *schemaResolver) updateAffectedUser(ctx context.Context, affectedUserID int32, update database.UserUpdate) (*EmptyResponse, error) {
// 🚨 SECURITY: Only the user and admins are allowed to set the Terms of Service accepted flag.
if err := auth.CheckSiteAdminOrSameUser(ctx, r.db, affectedUserID); err != nil {

View File

@ -1023,114 +1023,6 @@ func TestSchema_SetUserCodeCompletionsQuota(t *testing.T) {
})
}
func TestSchema_SetCompletedPostSignup(t *testing.T) {
db := dbmocks.NewMockDB()
currentUserID := int32(2)
t.Run("not site admin, not current user", func(t *testing.T) {
users := dbmocks.NewMockUserStore()
users.GetByIDFunc.SetDefaultHook(func(ctx context.Context, id int32) (*types.User, error) {
return &types.User{
ID: id,
Username: strconv.Itoa(int(id)),
}, nil
})
// Different user.
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{ID: currentUserID, Username: "2"}, nil)
db.UsersFunc.SetDefaultReturn(users)
userID := MarshalUserID(1)
result, err := newSchemaResolver(db, gitserver.NewTestClient(t)).SetCompletedPostSignup(context.Background(),
&userMutationArgs{UserID: &userID},
)
got := fmt.Sprintf("%v", err)
want := auth.ErrMustBeSiteAdminOrSameUser.Error()
assert.Equal(t, want, got)
assert.Nil(t, result)
})
t.Run("current user can set field on themselves", func(t *testing.T) {
currentUser := &types.User{ID: currentUserID, Username: "2", SiteAdmin: true}
users := dbmocks.NewMockUserStore()
users.GetByIDFunc.SetDefaultReturn(currentUser, nil)
users.GetByCurrentAuthUserFunc.SetDefaultReturn(currentUser, nil)
db.UsersFunc.SetDefaultReturn(users)
var called bool
users.UpdateFunc.SetDefaultHook(func(ctx context.Context, id int32, update database.UserUpdate) error {
called = true
return nil
})
userEmails := dbmocks.NewMockUserEmailsStore()
userEmails.HasVerifiedEmailFunc.SetDefaultReturn(true, nil)
db.UserEmailsFunc.SetDefaultReturn(userEmails)
RunTest(t, &Test{
Context: actor.WithActor(context.Background(), &actor.Actor{UID: currentUserID}),
Schema: mustParseGraphQLSchema(t, db),
Query: `
mutation {
setCompletedPostSignup(userID: "VXNlcjoy") {
alwaysNil
}
}
`,
ExpectedResult: `
{
"setCompletedPostSignup": {
"alwaysNil": null
}
}
`,
})
if !called {
t.Errorf("updatefunc was not called, but should have been")
}
})
t.Run("site admin can set post-signup complete", func(t *testing.T) {
mockUser := &types.User{
ID: 1,
Username: "alice",
}
users := dbmocks.NewMockUserStore()
users.GetByIDFunc.SetDefaultReturn(mockUser, nil)
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{ID: currentUserID, Username: "2", SiteAdmin: true}, nil)
db.UsersFunc.SetDefaultReturn(users)
var called bool
users.UpdateFunc.SetDefaultHook(func(ctx context.Context, id int32, update database.UserUpdate) error {
called = true
return nil
})
RunTest(t, &Test{
Context: actor.WithActor(context.Background(), &actor.Actor{UID: 1}),
Schema: mustParseGraphQLSchema(t, db),
Query: `
mutation {
setCompletedPostSignup(userID: "VXNlcjox") {
alwaysNil
}
}
`,
ExpectedResult: `
{
"setCompletedPostSignup": {
"alwaysNil": null
}
}
`,
})
if !called {
t.Errorf("updatefunc was not called, but should have been")
}
})
}
func TestUser_EvaluateFeatureFlag(t *testing.T) {
users := dbmocks.NewMockUserStore()

View File

@ -17,12 +17,10 @@ import (
"github.com/sourcegraph/sourcegraph/internal/auth"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/dotcom"
"github.com/sourcegraph/sourcegraph/internal/errcode"
"github.com/sourcegraph/sourcegraph/internal/featureflag"
"github.com/sourcegraph/sourcegraph/internal/trace"
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/internal/usagestats"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
func (r *UserResolver) UsageStatistics(ctx context.Context) (*userUsageStatisticsResolver, error) {
@ -333,41 +331,3 @@ func exportPrometheusSearchRanking(payload json.RawMessage) error {
searchRankingResultClicked.WithLabelValues(v.Type, resultsLength, ranked).Observe(v.Index)
return nil
}
type codySurveySubmissionForHubSpot struct {
Email string `url:"email"`
IsForWork bool `url:"using_cody_for_work"`
IsForPersonal bool `url:"using_cody_for_personal"`
}
func (r *schemaResolver) SubmitCodySurvey(ctx context.Context, args *struct {
IsForWork bool
IsForPersonal bool
}) (*EmptyResponse, error) {
if !dotcom.SourcegraphDotComMode() {
return nil, errors.New("Cody survey is not supported outside sourcegraph.com")
}
// If user is authenticated, use their uid and overwrite the optional email field.
actor := actor.FromContext(ctx)
if !actor.IsAuthenticated() {
return nil, errors.New("user must be authenticated to submit a Cody survey")
}
email, _, err := r.db.UserEmails().GetPrimaryEmail(ctx, actor.UID)
if err != nil && !errcode.IsNotFound(err) {
return nil, err
}
// Submit form to HubSpot
if err := hubspotutil.Client().SubmitForm(hubspotutil.CodySurveyFormID, &codySurveySubmissionForHubSpot{
Email: email,
IsForWork: args.IsForWork,
IsForPersonal: args.IsForPersonal,
}); err != nil {
// Log an error, but don't return one if the only failure was in submitting survey results to HubSpot.
log15.Error("Unable to submit cody survey results to Sourcegraph remote", "error", err)
}
return &EmptyResponse{}, nil
}

View File

@ -114,7 +114,6 @@ type CurrentUser struct {
ViewerCanAdminister bool `json:"viewerCanAdminister"`
TosAccepted bool `json:"tosAccepted"`
HasVerifiedEmail bool `json:"hasVerifiedEmail"`
CompletedPostSignUp bool `json:"completedPostSignup"`
Organizations *UserOrganizationsConnection `json:"organizations"`
Session *UserSession `json:"session"`
@ -537,11 +536,6 @@ func createCurrentUser(ctx context.Context, user *types.User, db database.DB) *C
return nil
}
completedPostSignup, err := userResolver.CompletedPostSignup(ctx)
if err != nil {
return nil
}
return &CurrentUser{
GraphQLTypename: "User",
AvatarURL: userResolver.AvatarURL(),
@ -560,7 +554,6 @@ func createCurrentUser(ctx context.Context, user *types.User, db database.DB) *C
ViewerCanAdminister: canAdminister,
Permissions: resolveUserPermissions(ctx, userResolver),
HasVerifiedEmail: hasVerifiedEmail,
CompletedPostSignUp: completedPostSignup,
}
}

View File

@ -27,7 +27,6 @@ const (
RequestAccess = "request-access"
UnlockAccount = "unlock-account"
UnlockUserAccount = "unlock-user-account"
Welcome = "welcome"
SiteInit = "site-init"
VerifyEmail = "verify-email"
ResetPasswordInit = "reset-password.init"
@ -72,7 +71,6 @@ func newRouter() *mux.Router {
base.Path("/-/sign-up").Methods("POST").Name(SignUp)
base.Path("/-/request-access").Methods("POST").Name(RequestAccess)
base.Path("/-/welcome").Methods("GET").Name(Welcome)
base.Path("/-/site-init").Methods("POST").Name(SiteInit)
base.Path("/-/verify-email").Methods("GET").Name(VerifyEmail)
base.Path("/-/sign-in").Methods("POST").Name(SignIn)

View File

@ -155,13 +155,10 @@ func InitRouter(db database.DB) {
{path: "/cody", name: "cody", title: "Cody", index: false},
// TODO: [TEMPORARY] remove this redirect route when the marketing page is added.
{path: "/cody/{chatID}", name: "cody-chat", title: "Cody", index: false},
{path: "/get-cody", name: "get-cody", title: "Cody", index: false},
{path: "/post-sign-up", name: "post-sign-up", title: "Cody", index: false},
{path: "/unlock-account/{token}", name: uirouter.RouteUnlockAccount, title: "Unlock Your Account", index: false},
{path: "/password-reset", name: uirouter.RoutePasswordReset, title: "Reset password", index: false},
{path: "/survey", name: "survey", title: "Survey", index: false},
{path: "/survey/{score}", name: "survey-score", title: "Survey", index: false},
{path: "/welcome", name: "welcome", title: "Welcome", index: false},
}
config := conf.Get()

View File

@ -542,7 +542,6 @@ type UserUpdate struct {
// - If pointer to a non-empty string, the value in the DB is set to the string.
DisplayName, AvatarURL *string
TosAccepted *bool
CompletedPostSignup *bool
}
// Update updates a user's profile information.
@ -583,9 +582,6 @@ func (u *userStore) Update(ctx context.Context, id int32, update UserUpdate) (er
if update.TosAccepted != nil {
fieldUpdates = append(fieldUpdates, sqlf.Sprintf("tos_accepted=%s", *update.TosAccepted))
}
if update.CompletedPostSignup != nil {
fieldUpdates = append(fieldUpdates, sqlf.Sprintf("completed_post_signup=%s", *update.CompletedPostSignup))
}
query := sqlf.Sprintf("UPDATE users SET %s WHERE id=%d", sqlf.Join(fieldUpdates, ", "), id)
res, err := tx.ExecResult(ctx, query)
if err != nil {
@ -1344,7 +1340,6 @@ SELECT u.id,
u.passwd IS NOT NULL,
u.invalidated_sessions_at,
u.tos_accepted,
u.completed_post_signup,
EXISTS (SELECT 1 FROM user_external_accounts WHERE service_type = 'scim' AND user_id = u.id AND deleted_at IS NULL) AS scim_controlled
FROM users u %s`, query)
rows, err := u.Query(ctx, q)
@ -1357,7 +1352,7 @@ FROM users u %s`, query)
for rows.Next() {
var u types.User
var displayName, avatarURL sql.NullString
err := rows.Scan(&u.ID, &u.Username, &displayName, &avatarURL, &u.CreatedAt, &u.UpdatedAt, &u.SiteAdmin, &u.BuiltinAuth, &u.InvalidatedSessionsAt, &u.TosAccepted, &u.CompletedPostSignup, &u.SCIMControlled)
err := rows.Scan(&u.ID, &u.Username, &displayName, &avatarURL, &u.CreatedAt, &u.UpdatedAt, &u.SiteAdmin, &u.BuiltinAuth, &u.InvalidatedSessionsAt, &u.TosAccepted, &u.SCIMControlled)
if err != nil {
return nil, err
}

View File

@ -572,9 +572,6 @@ func TestUsers_Update(t *testing.T) {
if want := "a1"; user.AvatarURL != want {
t.Errorf("got avatar URL %q, want %q", user.AvatarURL, want)
}
if want := false; user.CompletedPostSignup != want {
t.Errorf("got wrong CompletedPostSignUp %t, want %t", user.CompletedPostSignup, want)
}
if err := db.Users().Update(ctx, user.ID, UserUpdate{
DisplayName: pointers.Ptr(""),
@ -595,20 +592,6 @@ func TestUsers_Update(t *testing.T) {
t.Errorf("got avatar URL %q, want %q", user.AvatarURL, want)
}
// Update CompletedPostSignUp
if err := db.Users().Update(ctx, user.ID, UserUpdate{
CompletedPostSignup: pointers.Ptr(true),
}); err != nil {
t.Fatal(err)
}
user, err = db.Users().GetByID(ctx, user.ID)
if err != nil {
t.Fatal(err)
}
if want := true; user.CompletedPostSignup != want {
t.Errorf("got wrong CompletedPostSignUp %t, want %t", user.CompletedPostSignup, want)
}
// Can't update to duplicate username.
user2, err := db.Users().Create(ctx, NewUser{
Email: "a2@a.com",

View File

@ -852,7 +852,6 @@ type User struct {
BuiltinAuth bool
InvalidatedSessionsAt time.Time
TosAccepted bool
CompletedPostSignup bool
SCIMControlled bool
}