mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
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:
parent
5413fd1fd4
commit
4d69b06d4d
@ -42,7 +42,6 @@ export const currentAuthStateQuery = gql`
|
||||
viewerCanAdminister
|
||||
tosAccepted
|
||||
hasVerifiedEmail
|
||||
completedPostSignup
|
||||
emails {
|
||||
email
|
||||
verified
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -33,12 +33,9 @@ const topLevelPaths = [
|
||||
'search/cody',
|
||||
'app',
|
||||
'cody',
|
||||
'get-cody',
|
||||
'post-sign-up',
|
||||
'unlock-account',
|
||||
'password-reset',
|
||||
'survey',
|
||||
'welcome',
|
||||
'embed',
|
||||
'users',
|
||||
'user',
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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} />
|
||||
}
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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 />`;
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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 →
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -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 />,
|
||||
|
||||
@ -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! 👋 Let’s get started with Cody — your new AI coding assistant.
|
||||
Hey! 👋 Let’s 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}>
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
]
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 & 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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 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',
|
||||
},
|
||||
]
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -17,10 +17,6 @@
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.ide-name {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.pro-title {
|
||||
font-size: 2.25rem;
|
||||
color: #820dde;
|
||||
|
||||
@ -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 }],
|
||||
|
||||
@ -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 }],
|
||||
|
||||
@ -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 }],
|
||||
|
||||
@ -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 <></>
|
||||
}
|
||||
@ -81,7 +81,6 @@ export function overrideInsightsGraphQLApi(props: OverrideGraphQLExtensionsProps
|
||||
url: '/users/test',
|
||||
settingsURL: '/users/test/settings',
|
||||
hasVerifiedEmail: true,
|
||||
completedPostSignup: true,
|
||||
organizations: {
|
||||
nodes: [
|
||||
{
|
||||
|
||||
@ -67,7 +67,6 @@ export type SourcegraphContextCurrentUser = Pick<
|
||||
| 'latestSettings'
|
||||
| 'permissions'
|
||||
| 'hasVerifiedEmail'
|
||||
| 'completedPostSignup'
|
||||
>
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export { SurveyToastTrigger as SurveyToast } from './SurveyToastTrigger'
|
||||
export { CodySurveyToast } from './CodySurveyToast'
|
||||
|
||||
@ -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/*',
|
||||
|
||||
@ -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} />} />,
|
||||
|
||||
@ -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={[]}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -852,7 +852,6 @@ type User struct {
|
||||
BuiltinAuth bool
|
||||
InvalidatedSessionsAt time.Time
|
||||
TosAccepted bool
|
||||
CompletedPostSignup bool
|
||||
SCIMControlled bool
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user