remove Cody "upsells"—and all Cody links if Cody is disabled (#63430)

At a high level, we don't want to show annoying ads/upsells for Cody
that are not useful. And if Cody is disabled, we don't want to show
*any* links to Cody.

### Detailed desired behavior

- Dotcom
  - Navbar
    - Unauthed: "Cody" single link to /cody (marketing page)
- Authed: "Cody" dropdown with "Dashboard" (/cody/manage) and "Chat"
(/cody/chat)
  - Routes
    - /cody: always the marketing page
- /cody/manage: requires sign-in, shows Cody PLG subscription status for
the user (Free plan is auto-opted-into by default)
    - /cody/chat: requires sign-in
- Enterprise with Cody enabled on instance
  - Navbar
- Cody NOT enabled for current user: "Cody" single link to
/cody/dashboard
- Cody enabled for current user: "Cody" dropdown with "Dashboard"
(/cody/manage) and "Chat" (/cody/chat)
  - Routes
- /cody: this link should not be present anywhere, but redirect to
/cody/dashboard
- /cody/manage: informational page, with editor/web links for
Cody-enabled users and a "contact admin to get access" message for
Cody-disabled users
- /cody/chat: chat for Cody-enabled users, redirect to /cody/manage for
Cody-disabled users
- Enterprise with Cody NOT enabled on instance (`"cody.enabled": false`
in site config)
  - Navbar: no Cody link or dropdown
  - Routes: all Cody routes 404
- All
  - Do not show a Cody upsell on the /search page

This is an example of what we will KEEP for users on instances with Cody
enabled but who do not themselves yet have access to Cody. This is
useful because it informs users how to get access to Cody, and
presumably their site admin wants people to request it who want to use
it.


![image](https://github.com/sourcegraph/sourcegraph/assets/1976/c2adb086-44ec-4240-ad44-95981763fb72)


Fixes
https://linear.app/sourcegraph/issue/SRCH-529/hide-cody-ai-tab-and-cody-upsell-if-cody-is-not-enabled

### Unexpected code changes needed

This ended up being a much bigger change than I expected because I found
error-prone code that needed cleaning up:
    
- Improve how we determine if Cody is enabled in the frontend code.
Previously, we checked the license features in some places,
`cody.enabled` site config in others, and the user's current RBAC
permissions for Cody in yet others. The most error-prone was checking
the license features, since a license may entitle the instance to Cody
but the site admin may still choose to disable it. There were no places
in the frontend code where checking the license's entitlements was
actually correct, so I changed everything to checking either
`window.context.codyEnabledOnInstance` or
`window.context.codyEnabledForCurrentUser`.
- Did the same for `window.context.codeSearchEnabledOnInstance` for
symmetry.
- Removed "helper" functions that just checked 1 or 2 boolean values on
`window.context` related to this, in favor of accessing `window.context`
directly. Globals aren't great, and we should use React context or
something similar, but now that the JSContext has the right fields
(i.e., enabled instead of licensed), it's simpler and there is no need
for helper functions.
- Removed prop drilling of the `licenseFeatures` that was unnecessary
since these values are available in globals and were being set from
globals at some arbitrary point in the React component hierarchy anyway.
- Updated the GlobalNavbar test snapshots.

## Test plan

Run in 3 modes: (1) dotcom mode, (2) `"cody.enabled": false` in site
config, (3) normal `sg start`.

## Changelog

- When Cody is disabled in site config (with `"cody.enabled": false`),
all links and UI elements about Cody are hidden from all users.
Previously, when Cody was disabled, users would see some links informing
them about Cody.
This commit is contained in:
Quinn Slack 2024-06-26 22:29:54 -07:00 committed by GitHub
parent 13b315c7c5
commit 0021f95d6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 760 additions and 4956 deletions

View File

@ -41,5 +41,5 @@
selectedFilters={data.queryFilters}
/>
{:else}
<SearchHome {queryState} codyHref={data.codyHref} showDotcomFooterLinks={data.showDotcomFooterLinks} />
<SearchHome {queryState} codyHref={data.codyHref} showDotcomStuff={data.showDotcomFooterLinks} />
{/if}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { setContext, onMount } from 'svelte'
import { onMount, setContext } from 'svelte'
import { logoLight, logoDark } from '$lib/images'
import { logoDark, logoLight } from '$lib/images'
import SearchInput from '$lib/search/input/SearchInput.svelte'
import type { QueryStateStore } from '$lib/search/state'
import type { SearchPageContext } from '$lib/search/utils'
@ -9,13 +9,12 @@
import { isLightTheme } from '$lib/stores'
import { TELEMETRY_RECORDER } from '$lib/telemetry'
import CodyUpsellBanner from './cody-upsell/CodyUpsellBanner.svelte'
import DotcomFooterLinks from './DotcomFooterLinks.svelte'
import SearchHomeNotifications from './SearchHomeNotifications.svelte'
export let queryState: QueryStateStore
export let codyHref: string = '/cody'
export let showDotcomFooterLinks: boolean = false
export let showDotcomStuff: boolean = false
setContext<SearchPageContext>('search-context', {
setQuery(newQuery) {
@ -41,8 +40,7 @@
<SearchInput {queryState} autoFocus onSubmit={handleSubmit} />
<SearchHomeNotifications />
</div>
<CodyUpsellBanner {codyHref} />
{#if showDotcomFooterLinks}
{#if showDotcomStuff}
<DotcomFooterLinks />
{/if}
</div>

View File

@ -1,59 +0,0 @@
<script lang="ts">
import Icon from '$lib/Icon.svelte'
import MultiLineCompletion from './MultiLineCompletion.svelte'
export let codyHref = '/cody'
</script>
<section>
<div class="meta">
<div class="logo"><Icon icon={ISgCody} aria-label="Cody logo" /></div>
<h2 class="title">Introducing Cody: your new AI coding assistant.</h2>
<p class="description">
Cody autocompletes single lines, or entire code blocks, in any programming language, keeping all of your
companys codebase in mind.
</p>
<a href={codyHref}>Explore Cody</a>
</div>
<div class="image"><MultiLineCompletion /></div>
</section>
<style lang="scss">
section {
isolation: isolate;
padding: 1.75rem 2.5rem;
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 1rem;
@media (--sm-breakpoint-down) {
grid-template-columns: 1fr;
}
.meta {
align-self: center;
.logo {
--icon-color: initial;
--icon-size: 2.5rem;
margin-bottom: 1rem;
}
.title {
font-size: 1.5rem;
font-weight: 500;
margin-bottom: 0.75rem;
}
.description {
margin-bottom: 1.25rem;
}
}
.image {
filter: drop-shadow(-7px -16px 32px #a112ff24);
width: 100%;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@ -210,6 +210,7 @@ ts_project(
"src/cody/chat/new-chat/components/skeleton/Skeleton.tsx",
"src/cody/chat/old-chat/CodyChatPage.tsx",
"src/cody/codyProRoutes.tsx",
"src/cody/codyRoutes.tsx",
"src/cody/components/ChatUI/ChatUi.tsx",
"src/cody/components/ChatUI/index.tsx",
"src/cody/components/CodeMirrorEditor.ts",
@ -217,8 +218,6 @@ ts_project(
"src/cody/components/CodyContainer.tsx",
"src/cody/components/CodyIcon.tsx",
"src/cody/components/CodyLogo.tsx",
"src/cody/components/CodyMarketingPage/CodyMarketingPage.tsx",
"src/cody/components/CodyMarketingPage/index.ts",
"src/cody/components/CodyProBadgeDeck.tsx",
"src/cody/components/FileContentEditor.ts",
"src/cody/components/GettingStarted.tsx",
@ -237,7 +236,6 @@ ts_project(
"src/cody/invites/InviteUsers.tsx",
"src/cody/invites/useInviteParams.ts",
"src/cody/invites/useInviteState.ts",
"src/cody/isCodyEnabled.tsx",
"src/cody/management/CodyManagementPage.tsx",
"src/cody/management/SubscriptionStats.tsx",
"src/cody/management/UseCodyInEditorSection.tsx",
@ -279,13 +277,7 @@ ts_project(
"src/cody/switch-account/CodySwitchAccountPage.tsx",
"src/cody/team/CodyManageTeamPage.tsx",
"src/cody/team/TeamMemberList.tsx",
"src/cody/upsell/ChatBrandIcon.tsx",
"src/cody/upsell/CodyUpsellPage.tsx",
"src/cody/upsell/CompletionsBrandIcon.tsx",
"src/cody/upsell/ContextDiagram.tsx",
"src/cody/upsell/ContextExample.tsx",
"src/cody/upsell/IntelliJ.tsx",
"src/cody/upsell/MultilineCompletion.tsx",
"src/cody/upsell/vs-code.tsx",
"src/cody/useCodyChat.tsx",
"src/cody/useCodyIgnore.tsx",
@ -1367,8 +1359,6 @@ ts_project(
"src/repo/components/RepoHeaderActions/index.ts",
"src/repo/components/RepoRevision/RepoRevision.tsx",
"src/repo/components/RepoRevision/index.ts",
"src/repo/components/TryCodyWidget/TryCodyWidget.tsx",
"src/repo/components/TryCodyWidget/WidgetIcons.tsx",
"src/repo/constants.ts",
"src/repo/icon-utils.ts",
"src/repo/linkifiy/Linkified.tsx",
@ -1632,11 +1622,9 @@ ts_project(
"src/storm/pages/LayoutPage/LayoutPage.tsx",
"src/storm/pages/SearchPage/AddCodeHostWidget.tsx",
"src/storm/pages/SearchPage/CodeSearchSimpleSearch.tsx",
"src/storm/pages/SearchPage/CodyUpsell.tsx",
"src/storm/pages/SearchPage/FindChangesSimpleSearch.tsx",
"src/storm/pages/SearchPage/KeywordSearchCtaSection.tsx",
"src/storm/pages/SearchPage/KeywordSearchStarsIcon.tsx",
"src/storm/pages/SearchPage/MultilineCompletion.tsx",
"src/storm/pages/SearchPage/RepoSearchSimpleSearch.tsx",
"src/storm/pages/SearchPage/SearchPage.loader.ts",
"src/storm/pages/SearchPage/SearchPage.tsx",
@ -1741,7 +1729,6 @@ ts_project(
"src/util/dom.ts",
"src/util/getReactElements.ts",
"src/util/index.tsx",
"src/util/license.ts",
"src/util/permission.ts",
"src/util/prettyBytesBigint.ts",
"src/util/rbac.ts",
@ -1977,6 +1964,7 @@ ts_project(
"src/nav/StatusMessagesNavItem.mocks.ts",
"src/nav/StatusMessagesNavItem.test.tsx",
"src/nav/UserNavItem.test.tsx",
"src/nav/new-global-navigation/NewGlobalNavigationBar.test.tsx",
"src/notebooks/serialize/convertMarkdownToBlocks.test.ts",
"src/notebooks/serialize/index.test.ts",
"src/onboarding/OnboardingChecklist.mocks.ts",
@ -2029,7 +2017,6 @@ ts_project(
"src/util/checkRequestAccessAllowed.test.ts",
"src/util/codeStatsUtils.test.ts",
"src/util/getReactElements.test.tsx",
"src/util/license.test.ts",
"src/util/prettyBytesBigint.test.ts",
"src/util/size.test.ts",
"src/util/time.test.ts",
@ -2221,7 +2208,6 @@ ts_project(
"src/auth/VsCodeSignUpPage.story.tsx",
"src/batches/RepoBatchChangesButton.story.tsx",
"src/codeintel/ReferencesPanel.story.tsx",
"src/cody/components/CodyMarketingPage/CodyMarketingPage.story.tsx",
"src/communitySearchContexts/CommunitySearchContextPage.story.tsx",
"src/components/Breadcrumbs.story.tsx",
"src/components/Byline/CreatedByAndUpdatedByInfoByline.story.tsx",

View File

@ -38,9 +38,10 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
batchChangesDisableWebhooksWarning: false,
batchChangesWebhookLogsEnabled: true,
executorsEnabled: false,
codyEnabled: true,
codyEnabledOnInstance: true,
codyEnabledForCurrentUser: true,
codyRequiresVerifiedEmail: false,
codeSearchEnabledOnInstance: true,
codeIntelAutoIndexingEnabled: false,
codeIntelAutoIndexingAllowGlobalPolicies: false,
codeIntelligenceEnabled: true,

View File

@ -3,13 +3,10 @@ import type { FC } from 'react'
import { Navigate } from 'react-router-dom'
import { PageRoutes } from './routes.constants'
import { isCodyOnlyLicense } from './util/license'
export const IndexPage: FC = () => {
let redirectRoute = PageRoutes.Search
if (isCodyOnlyLicense()) {
redirectRoute = PageRoutes.Cody
}
return <Navigate replace={true} to={redirectRoute} />
}
export const IndexPage: FC = () => (
<Navigate
replace={true}
to={window.context?.codeSearchEnabledOnInstance ? PageRoutes.Search : PageRoutes.CodyDashboard}
/>
)

View File

@ -1,19 +1,19 @@
import { type FC, type PropsWithChildren, createContext, useContext, useCallback } from 'react'
import { createContext, useCallback, useContext, type FC, type PropsWithChildren } from 'react'
import type { Observable } from 'rxjs'
import { isMacPlatform } from '@sourcegraph/common'
import { type FetchFileParameters, fetchHighlightedFileLineRanges } from '@sourcegraph/shared/src/backend/file'
import { fetchHighlightedFileLineRanges, type FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
import type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import {
createSearchContext,
deleteSearchContext,
fetchSearchContext,
fetchSearchContextBySpec,
fetchSearchContexts,
fetchSearchContext,
getUserSearchContextNamespaces,
createSearchContext,
updateSearchContext,
deleteSearchContext,
isSearchContextSpecAvailable,
updateSearchContext,
type SearchContextProps,
} from '@sourcegraph/shared/src/search'
import { aggregateStreamingSearch } from '@sourcegraph/shared/src/search/stream'
@ -25,9 +25,8 @@ import { isBatchChangesExecutionEnabled } from './batches'
import { useBreadcrumbs, type BreadcrumbSetters, type BreadcrumbsProps } from './components/Breadcrumbs'
import { NotFoundPage } from './components/HeroPage'
import type { SearchStreamingProps } from './search'
import type { StaticSourcegraphWebAppContext, DynamicSourcegraphWebAppContext } from './SourcegraphWebApp'
import type { DynamicSourcegraphWebAppContext, StaticSourcegraphWebAppContext } from './SourcegraphWebApp'
import type { StaticAppConfig } from './staticAppConfig'
import { getLicenseFeatures } from './util/license'
export interface StaticLegacyRouteContext extends LegacyRouteComputedContext, LegacyRouteStaticInjections {}
@ -88,12 +87,7 @@ export interface LegacyLayoutRouteContext
extends StaticAppConfig,
StaticSourcegraphWebAppContext,
DynamicSourcegraphWebAppContext,
StaticLegacyRouteContext {
licenseFeatures: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
StaticLegacyRouteContext {}
interface LegacyRouteProps {
render: (props: LegacyLayoutRouteContext) => JSX.Element
@ -168,7 +162,6 @@ export const LegacyRouteContextProvider: FC<PropsWithChildren<LegacyRouteContext
...injections,
...computedContextFields,
...context,
licenseFeatures: getLicenseFeatures(),
} satisfies LegacyLayoutRouteContext
return <LegacyRouteContext.Provider value={legacyContext}>{children}</LegacyRouteContext.Provider>
@ -179,7 +172,6 @@ export const LegacyRouteContext = createContext<LegacyLayoutRouteContext | null>
/**
* DO NOT USE OUTSIDE OF STORM ROUTES!
* A convenience hook to return the LegacyRouteContext.
*
* @deprecated This can be used only in components migrated under Storm routes.
* Please use Apollo instead to make GraphQL requests and `useSettings` to access settings.
*/
@ -193,7 +185,6 @@ export const useLegacyContext_onlyInStormRoutes = (): LegacyLayoutRouteContext =
/**
* A convenience hook to return the platform context.
*
* @deprecated This should not be used for new code anymore, please use Apollo instead to make
* GraphQL requests and `useSettings` to access settings.
*/

View File

@ -3,34 +3,34 @@ import 'focus-visible'
import * as React from 'react'
import { ApolloProvider } from '@apollo/client'
import { RouterProvider, createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom'
import { combineLatest, from, Subscription, fromEvent, type Observable } from 'rxjs'
import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom'
import { combineLatest, from, fromEvent, Subscription, type Observable } from 'rxjs'
import { logger } from '@sourcegraph/common'
import { type GraphQLClient, HTTPStatusError } from '@sourcegraph/http-client'
import { HTTPStatusError, type GraphQLClient } from '@sourcegraph/http-client'
import { SharedSpanName, TraceSpanProvider } from '@sourcegraph/observability-client'
import { type FetchFileParameters, fetchHighlightedFileLineRanges } from '@sourcegraph/shared/src/backend/file'
import { fetchHighlightedFileLineRanges, type FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
import type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import { ShortcutProvider } from '@sourcegraph/shared/src/react-shortcuts'
import {
getUserSearchContextNamespaces,
fetchSearchContexts,
createSearchContext,
deleteSearchContext,
fetchSearchContext,
fetchSearchContextBySpec,
createSearchContext,
updateSearchContext,
deleteSearchContext,
fetchSearchContexts,
getDefaultSearchContextSpec,
getUserSearchContextNamespaces,
isSearchContextSpecAvailable,
SearchQueryStateStoreProvider,
getDefaultSearchContextSpec,
updateSearchContext,
} from '@sourcegraph/shared/src/search'
import { FilterType } from '@sourcegraph/shared/src/search/query/filters'
import { filterExists } from '@sourcegraph/shared/src/search/query/validate'
import { aggregateStreamingSearch } from '@sourcegraph/shared/src/search/stream'
import {
EMPTY_SETTINGS_CASCADE,
type SettingsCascadeProps,
SettingsProvider,
type SettingsCascadeProps,
} from '@sourcegraph/shared/src/settings/settings'
import { TemporarySettingsProvider } from '@sourcegraph/shared/src/settings/temporary/TemporarySettingsProvider'
import { TemporarySettingsStorage } from '@sourcegraph/shared/src/settings/temporary/TemporarySettingsStorage'
@ -38,7 +38,7 @@ import { NoOpTelemetryRecorderProvider } from '@sourcegraph/shared/src/telemetry
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import { WildcardThemeContext, type WildcardTheme } from '@sourcegraph/wildcard'
import { authenticatedUser as authenticatedUserSubject, type AuthenticatedUser, authenticatedUserValue } from './auth'
import { authenticatedUser as authenticatedUserSubject, authenticatedUserValue, type AuthenticatedUser } from './auth'
import { getWebGraphQLClient } from './backend/graphql'
import { isBatchChangesExecutionEnabled } from './batches'
import { ComponentsComposer } from './components/ComponentsComposer'
@ -55,7 +55,6 @@ import type { StaticAppConfig } from './staticAppConfig'
import { setQueryStateFromSettings, useDeveloperSettings, useNavbarQueryState } from './stores'
import { TelemetryRecorderProvider } from './telemetry'
import { UserSessionStores } from './UserSessionStores'
import { getLicenseFeatures } from './util/license'
import { siteSubjectNoAdmin, viewerSubjectFromSettings } from './util/settings'
interface LegacySourcegraphWebAppState extends SettingsCascadeProps {
@ -237,7 +236,6 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
updateSearchContext={updateSearchContext}
deleteSearchContext={deleteSearchContext}
streamSearch={aggregateStreamingSearch}
licenseFeatures={getLicenseFeatures()}
/>
}
/>

View File

@ -5,6 +5,7 @@ import { useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settin
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { withAuthenticatedUser } from '../../auth/withAuthenticatedUser'
import type { SourcegraphContext } from '../../jscontext'
import { CodyChatPage as OldCodyChatPage } from './old-chat/CodyChatPage'
@ -15,17 +16,20 @@ const LazyNewCodyChatPage = lazyComponent(() => import('./new-chat/NewCodyChatPa
interface CodyChatPageProps extends TelemetryV2Props {
isSourcegraphDotCom: boolean
authenticatedUser: AuthenticatedUser | null
authenticatedUser: AuthenticatedUser
context: Pick<SourcegraphContext, 'externalURL'>
}
export const CodyChatPage: FC<CodyChatPageProps> = props => {
const { isSourcegraphDotCom, authenticatedUser, context, telemetryRecorder } = props
const AuthenticatedCodyChatPage: FC<CodyChatPageProps> = ({
isSourcegraphDotCom,
authenticatedUser,
context,
telemetryRecorder,
}) => {
// We have two different version of Cody Web, first was created as original
// Cody Web chat, second version (NewCodyChatPage) is a port from VSCode
// cody extension.
const newCodyWeb = useExperimentalFeatures(features => features.newCodyWeb)
const newCodyWeb = !useExperimentalFeatures(features => features.newCodyWeb)
// Load new cody web only for authorized users, fallback on old cody web
// for better non-logged-in user experience.
@ -40,3 +44,5 @@ export const CodyChatPage: FC<CodyChatPageProps> = props => {
/>
)
}
export const CodyChatPage = withAuthenticatedUser(AuthenticatedCodyChatPage)

View File

@ -1,11 +1,14 @@
import type { FC } from 'react'
import { CodyWebChatProvider, ChatHistory } from 'cody-web-experimental'
import { ChatHistory, CodyWebChatProvider } from 'cody-web-experimental'
import { Navigate } from 'react-router-dom'
import { Badge, Link, PageHeader, Text } from '@sourcegraph/wildcard'
import { Badge, ButtonLink, PageHeader, Text } from '@sourcegraph/wildcard'
import { Page } from '../../../components/Page'
import { PageTitle } from '../../../components/PageTitle'
import { PageRoutes } from '../../../routes.constants'
import { CodyProRoutes } from '../../codyProRoutes'
import { CodyColorIcon } from '../CodyPageIcon'
import { ChatHistoryList } from './components/chat-history-list/ChatHistoryList'
@ -72,12 +75,25 @@ interface CodyPageHeaderProps {
const CodyPageHeader: FC<CodyPageHeaderProps> = props => {
const { isSourcegraphDotCom, className } = props
const codyDashboardLink = isSourcegraphDotCom ? '/cody/manage' : '/cody'
const codyDashboardLink = isSourcegraphDotCom ? CodyProRoutes.Manage : PageRoutes.CodyDashboard
if (!window.context?.codyEnabledForCurrentUser) {
return <Navigate to={PageRoutes.CodyDashboard} />
}
return (
<PageHeader
className={className}
description="Cody answers code questions and writes code for you using your entire codebase and the code graph."
actions={
<div className="d-flex flex-gap-1">
<ButtonLink variant="link" to={codyDashboardLink}>
Editor extensions
</ButtonLink>
<ButtonLink variant="secondary" to={codyDashboardLink}>
Dashboard
</ButtonLink>
</div>
}
>
<PageHeader.Heading as="h2" styleAs="h1">
<PageHeader.Breadcrumb icon={CodyColorIcon}>
@ -86,11 +102,6 @@ const CodyPageHeader: FC<CodyPageHeaderProps> = props => {
<Badge variant="info" className="ml-2">
Experimental
</Badge>
<Link to={codyDashboardLink}>
<Text className="mb-0 ml-2" size="small">
Manage
</Text>
</Link>
</div>
</PageHeader.Breadcrumb>
</PageHeader.Heading>

View File

@ -1,27 +1,16 @@
import React, { useEffect, useState } from 'react'
import {
mdiChevronRight,
mdiClose,
mdiCogOutline,
mdiDelete,
mdiDotsVertical,
mdiFormatListBulleted,
mdiOpenInNew,
mdiPlus,
} from '@mdi/js'
import { mdiCogOutline, mdiDelete, mdiDotsVertical, mdiFormatListBulleted, mdiOpenInNew, mdiPlus } from '@mdi/js'
import classNames from 'classnames'
import { useLocation, useNavigate } from 'react-router-dom'
import { Navigate, useLocation, useNavigate } from 'react-router-dom'
import { CodyLogo } from '@sourcegraph/cody-ui'
import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
import { type AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import {
Badge,
Button,
ButtonLink,
H3,
H4,
Icon,
Link,
@ -32,21 +21,18 @@ import {
MenuLink,
MenuList,
PageHeader,
Text,
Tooltip,
} from '@sourcegraph/wildcard'
import { MarketingBlock } from '../../../components/MarketingBlock'
import { Page } from '../../../components/Page'
import { PageTitle } from '../../../components/PageTitle'
import { useFeatureFlag } from '../../../featureFlags/useFeatureFlag'
import type { SourcegraphContext } from '../../../jscontext'
import { PageRoutes } from '../../../routes.constants'
import { EventName } from '../../../util/constants'
import { CodyProRoutes } from '../../codyProRoutes'
import { ChatUI } from '../../components/ChatUI'
import { CodyMarketingPage } from '../../components/CodyMarketingPage'
import { HistoryList } from '../../components/HistoryList'
import { isCodyEnabled } from '../../isCodyEnabled'
import { useCodyChat, type CodyChatStore } from '../../useCodyChat'
import { CodyColorIcon } from '../CodyPageIcon'
@ -54,7 +40,7 @@ import styles from './CodyChatPage.module.scss'
interface CodyChatPageProps extends TelemetryV2Props {
isSourcegraphDotCom: boolean
authenticatedUser: AuthenticatedUser | null
authenticatedUser: AuthenticatedUser
context: Pick<SourcegraphContext, 'externalURL'>
}
@ -91,7 +77,6 @@ const onTranscriptHistoryLoad = (
export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
authenticatedUser,
context,
isSourcegraphDotCom,
telemetryRecorder,
}) => {
@ -117,9 +102,6 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
deleteHistoryItem,
logTranscriptEvent,
} = codyChatStore
const [isCTADismissed = true, setIsCTADismissed] = useTemporarySetting('cody.chatPageCta.dismissed', false)
const onCTADismiss = (): void => setIsCTADismissed(true)
useEffect(() => {
logTranscriptEvent(EventName.CODY_CHAT_PAGE_VIEWED, 'cody.chat', 'view')
}, [logTranscriptEvent])
@ -127,7 +109,7 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
const transcriptId = transcript?.id
useEffect(() => {
if (!loaded || !transcriptId || !authenticatedUser || !isCodyEnabled()) {
if (!loaded || !transcriptId || !authenticatedUser || !window.context?.codyEnabledForCurrentUser) {
return
}
const idFromUrl = transcriptIdFromUrl(pathname)
@ -149,85 +131,35 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
return null
}
if (!authenticatedUser || !isCodyEnabled()) {
return (
<CodyMarketingPage
isSourcegraphDotCom={isSourcegraphDotCom}
authenticatedUser={authenticatedUser}
context={context}
telemetryRecorder={telemetryRecorder}
/>
)
if (!window.context?.codyEnabledForCurrentUser) {
return <Navigate to={PageRoutes.CodyDashboard} />
}
const codyDashboardLink = isSourcegraphDotCom ? CodyProRoutes.Manage : '/cody'
const codyDashboardLink = isSourcegraphDotCom ? CodyProRoutes.Manage : PageRoutes.CodyDashboard
return (
<Page className={classNames('d-flex flex-column', styles.page)}>
<PageTitle title="Cody chat" />
{!isSourcegraphDotCom && !isCTADismissed && (
<MarketingBlock
wrapperClassName="mb-5"
contentClassName={classNames(styles.ctaWrapper, styles.ctaContent)}
>
<div className="d-flex">
<CodyCTAIcon className="flex-shrink-0" />
<div className="ml-3">
<H3>Cody is more powerful in your editor</H3>
<Text>
Cody adds powerful AI assistant functionality like inline completions and assist, and
powerful recipes to help you understand codebases and generate and fix code more
accurately.
</Text>
<ButtonLink variant="primary" to="/help/cody">
View editor extensions &rarr;
</ButtonLink>
</div>
</div>
<Icon
svgPath={mdiClose}
aria-label="Close Cody editor extensions CTA"
className={classNames(styles.closeButton, 'position-absolute m-0')}
onClick={onCTADismiss}
/>
</MarketingBlock>
)}
<PageHeader
actions={
<div className="d-flex">
<div className="d-flex flex-gap-1">
<ButtonLink variant="link" to={codyDashboardLink}>
Editor extensions
</ButtonLink>
<ButtonLink variant="secondary" to={codyDashboardLink}>
Dashboard
</ButtonLink>
<Button variant="primary" onClick={initializeNewChat}>
<Icon aria-hidden={true} svgPath={mdiPlus} />
New chat
</Button>
</div>
}
description={
<>
Cody answers code questions and writes code for you using your entire codebase and the code
graph.
{!isSourcegraphDotCom && isCTADismissed && (
<>
{' '}
<Link to="/help/cody">Get Cody in your editor.</Link>
</>
)}
</>
}
className={styles.pageHeader}
>
<PageHeader.Heading as="h2" styleAs="h1">
<PageHeader.Breadcrumb icon={CodyColorIcon}>
<div className="d-inline-flex align-items-center">
Cody Chat
<Badge variant="info" className="ml-2">
Experimental
</Badge>
<Link to={codyDashboardLink}>
<Text className="mb-0 ml-2" size="small">
Manage
</Text>
</Link>
</div>
<div className="d-inline-flex align-items-center">Cody Chat</div>
</PageHeader.Breadcrumb>
</PageHeader.Heading>
</PageHeader>
@ -272,51 +204,6 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
deleteHistoryItem={deleteHistoryItem}
/>
</div>
{isSourcegraphDotCom && !isCTADismissed && (
<MarketingBlock
wrapperClassName="d-flex"
contentClassName={classNames(
'flex-grow-1 d-flex flex-column justify-content-between',
styles.ctaWrapper
)}
>
<H3 className="d-flex align-items-center mb-4">Use Cody in your editor</H3>
<Text>
Autocomplete, test generation, refactors, code Q&A, and more&mdash;with the context of
your code.
</Text>
<div className="mb-2">
<Link
to={CodyProRoutes.Manage}
className={classNames(
'd-inline-flex align-items-center text-merged',
styles.ctaLink
)}
onClick={() =>
logTranscriptEvent(
EventName.CODY_CHAT_GET_EDITOR_EXTENSION,
'cody.chat.getEditorExtensionCTA',
'click'
)
}
>
Get Cody in your editor
<Icon svgPath={mdiChevronRight} aria-hidden={true} />
</Link>
</div>
<img
src="https://storage.googleapis.com/sourcegraph-assets/TryCodyVSCodeExtension.png"
alt="Try Cody VS Code Extension"
width={666}
/>
<Icon
svgPath={mdiClose}
aria-label="Close try Cody widget"
className={classNames(styles.closeButton, 'position-absolute m-0')}
onClick={onCTADismiss}
/>
</MarketingBlock>
)}
</div>
<div className={classNames('col-md-9 h-100', styles.chatMainWrapper)}>
@ -383,38 +270,3 @@ export const CodyChatPage: React.FunctionComponent<CodyChatPageProps> = ({
</Page>
)
}
const CodyCTAIcon: React.FunctionComponent<{ className?: string }> = ({ className }) => (
<svg
width="146"
height="112"
viewBox="0 0 146 112"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect x="24" y="24" width="98" height="64" rx="6" fill="#E8D1FF" />
<path
d="M56.25 65.3333C56.25 65.687 56.3817 66.0261 56.6161 66.2761C56.8505 66.5262 57.1685 66.6667 57.5 66.6667H60V69.3333H56.875C56.1875 69.3333 55 68.7333 55 68C55 68.7333 53.8125 69.3333 53.125 69.3333H50V66.6667H52.5C52.8315 66.6667 53.1495 66.5262 53.3839 66.2761C53.6183 66.0261 53.75 65.687 53.75 65.3333V46.6667C53.75 46.313 53.6183 45.9739 53.3839 45.7239C53.1495 45.4738 52.8315 45.3333 52.5 45.3333H50V42.6667H53.125C53.8125 42.6667 55 43.2667 55 44C55 43.2667 56.1875 42.6667 56.875 42.6667H60V45.3333H57.5C57.1685 45.3333 56.8505 45.4738 56.6161 45.7239C56.3817 45.9739 56.25 46.313 56.25 46.6667V65.3333Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M88.9095 45C90.3781 45 91.5686 46.1789 91.5686 47.6331V52.314C91.5686 53.7682 90.3781 54.9471 88.9095 54.9471C87.4409 54.9471 86.2504 53.7682 86.2504 52.314V47.6331C86.2504 46.1789 87.4409 45 88.9095 45Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M72.068 51.1437C72.068 49.6895 73.2585 48.5106 74.7271 48.5106H79.4544C80.923 48.5106 82.1135 49.6895 82.1135 51.1437C82.1135 52.5978 80.923 53.7767 79.4544 53.7767H74.7271C73.2585 53.7767 72.068 52.5978 72.068 51.1437Z"
fill="#A305E1"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M95.2643 58.8091C96.2107 59.6994 96.2491 61.1808 95.35 62.1179L94.5134 62.99C87.9666 69.8138 76.9295 69.6438 70.6002 62.6216C69.731 61.6572 69.8159 60.1777 70.7898 59.317C71.7637 58.4563 73.2579 58.5403 74.1271 59.5047C78.6157 64.4848 86.4432 64.6053 91.0861 59.7659L91.9227 58.8939C92.8218 57.9568 94.3179 57.9188 95.2643 58.8091Z"
fill="#A305E1"
/>
</svg>
)

View File

@ -2,7 +2,7 @@ import type { RouteObject } from 'react-router-dom'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { type LegacyLayoutRouteContext, LegacyRoute } from '../LegacyRouteContext'
import { LegacyRoute, type LegacyLayoutRouteContext } from '../LegacyRouteContext'
import { QueryClientProvider } from './management/api/react-query/QueryClientProvider'
import { isEmbeddedCodyProUIEnabled } from './util'
@ -42,8 +42,8 @@ export const codyProRoutes: RouteObject[] = Object.values(CodyProRoutes).map(pat
telemetryRecorder={props.platformContext.telemetryRecorder}
/>
)}
condition={({ isSourcegraphDotCom, licenseFeatures }) =>
isSourcegraphDotCom && licenseFeatures.isCodyEnabled && isRouteEnabled(path)
condition={({ isSourcegraphDotCom }) =>
isSourcegraphDotCom && window.context?.codyEnabledOnInstance && isRouteEnabled(path)
}
/>
),

View File

@ -0,0 +1,72 @@
import { Navigate, type RouteObject } from 'react-router-dom'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { LegacyRoute } from '../LegacyRouteContext'
import { PageRoutes } from '../routes.constants'
import { CodyIgnoreProvider } from './useCodyIgnore'
const CodyChatPage = lazyComponent(() => import('./chat/CodyChatPage'), 'CodyChatPage')
const CodySwitchAccountPage = lazyComponent(
() => import('./switch-account/CodySwitchAccountPage'),
'CodySwitchAccountPage'
)
const CodyDashboardPage = lazyComponent(() => import('./dashboard/CodyDashboardPage'), 'CodyDashboardPage')
/**
* Use {@link codyProRoutes} for Cody PLG routes.
*/
export const codyRoutes: RouteObject[] = [
{
path: PageRoutes.CodyRedirectToMarketingOrDashboard,
element: (
<LegacyRoute
render={({ isSourcegraphDotCom }) => (
<Navigate
to={isSourcegraphDotCom ? 'https://sourcegraph.com/cody' : PageRoutes.CodyDashboard}
replace={true}
/>
)}
condition={() => window.context?.codyEnabledOnInstance}
/>
),
},
{
path: PageRoutes.CodySwitchAccount,
element: (
<LegacyRoute
render={props => (
<CodySwitchAccountPage {...props} telemetryRecorder={props.platformContext.telemetryRecorder} />
)}
condition={() => window.context?.codyEnabledOnInstance}
/>
),
},
{
path: `${PageRoutes.CodyChat}/*`,
element: (
<LegacyRoute
render={props => (
<CodyIgnoreProvider isSourcegraphDotCom={props.isSourcegraphDotCom}>
<CodyChatPage
{...props}
context={window.context}
telemetryRecorder={props.platformContext.telemetryRecorder}
/>
</CodyIgnoreProvider>
)}
condition={() => window.context?.codyEnabledOnInstance}
/>
),
},
{
path: PageRoutes.CodyDashboard,
element: (
<LegacyRoute
render={props => <CodyDashboardPage {...props} />}
condition={() => window.context?.codyEnabledOnInstance}
/>
),
},
]

View File

@ -1,14 +1,14 @@
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
mdiClose,
mdiSend,
mdiArrowDown,
mdiPencil,
mdiThumbUp,
mdiThumbDown,
mdiCheck,
mdiClose,
mdiPencil,
mdiSend,
mdiStopCircleOutline,
mdiThumbDown,
mdiThumbUp,
} from '@mdi/js'
import classNames from 'classnames'
import { useLocation } from 'react-router-dom'
@ -25,12 +25,12 @@ import {
import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import { Button, Icon, TextArea, Link, Tooltip, Alert, Text, H2 } from '@sourcegraph/wildcard'
import { Alert, Button, H2, Icon, Link, Text, TextArea, Tooltip } from '@sourcegraph/wildcard'
import { CodyPageIcon } from '../../chat/CodyPageIcon'
import { isCodyEnabled, isEmailVerificationNeededForCody, isSignInRequiredForCody } from '../../isCodyEnabled'
import { useCodySidebar } from '../../sidebar/Provider'
import type { CodyChatStore } from '../../useCodyChat'
import { currentUserRequiresEmailVerificationForCody } from '../../util'
import { GettingStarted } from '../GettingStarted'
import { ScopeSelector } from '../ScopeSelector'
import type { ScopeSelectorProps } from '../ScopeSelector/ScopeSelector'
@ -173,7 +173,7 @@ export const ChatUI: React.FC<IChatUIProps> = ({
transcriptActionClassName={styles.transcriptAction}
FeedbackButtonsContainer={FeedbackButtons}
feedbackButtonsOnSubmit={onFeedbackSubmit}
needsEmailVerification={isEmailVerificationNeededForCody()}
needsEmailVerification={currentUserRequiresEmailVerificationForCody()}
needsEmailVerificationNotice={NeedsEmailVerificationNotice}
codyNotEnabledNotice={CodyNotEnabledNotice}
contextStatusComponent={ScopeSelector}
@ -182,7 +182,7 @@ export const ChatUI: React.FC<IChatUIProps> = ({
gettingStartedComponentProps={gettingStartedComponentProps}
abortMessageInProgressComponent={AbortMessageInProgress}
onAbortMessageInProgress={abortMessageInProgress}
isCodyEnabled={isCodyEnabled()}
isCodyEnabled={window.context?.codyEnabledForCurrentUser}
/>
</>
)
@ -360,9 +360,9 @@ export const AutoResizableTextArea: React.FC<AutoResizableTextAreaProps> = React
return (
<Tooltip
content={
isSignInRequiredForCody()
!window.context.isAuthenticatedUser
? 'Sign in to get access to Cody.'
: isEmailVerificationNeededForCody()
: currentUserRequiresEmailVerificationForCody()
? 'Verify your email to use Cody.'
: ''
}
@ -370,7 +370,7 @@ export const AutoResizableTextArea: React.FC<AutoResizableTextAreaProps> = React
<TextArea
ref={textAreaRef}
className={className}
value={isSignInRequiredForCody() ? 'Sign in to get access to use Cody' : value}
value={!window.context.isAuthenticatedUser ? 'Sign in to get access to use Cody' : value}
onChange={handleChange}
rows={1}
autoFocus={false}
@ -417,7 +417,7 @@ const CodyNotEnabledNotice: React.FunctionComponent = React.memo(function CodyNo
<div className="d-flex align-items-start">
<CodyNotEnabledIcon className="flex-shrink-0" />
<Text className="ml-2">
{isSignInRequiredForCody() ? (
{!window.context?.isAuthenticatedUser ? (
<>
<Link to={`/sign-in?returnTo=${location.pathname}`}>Sign in</Link> to get access to Cody.
You can learn more about Cody{' '}

View File

@ -1,198 +0,0 @@
@import 'wildcard/src/global-styles/breakpoints';
:root {
--platform-icon-color: #4d52f4;
}
:global(.theme-light) {
--marketing-block-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
:global(.theme-dark) {
--marketing-block-shadow: 0 4px 16px -6px rgba(46, 34, 119, 0.1);
}
.page-header {
:global(.theme-dark) & {
color: var(--gray-05);
.page-header-breadcrumb {
color: var(--gray-04);
}
}
:global(.theme-light) & {
color: var(--gray-07);
.page-header-breadcrumb {
color: var(--gray-08);
}
}
}
.header-section {
margin-top: 1.5rem;
padding: 1.5rem 0;
display: flex;
flex-direction: row;
justify-content: center;
gap: 4rem;
@media (--md-breakpoint-down) {
flex-direction: column;
align-items: center;
}
.cody-conversation {
padding: 0.25rem 1rem;
border: 2px solid var(--gray-04);
border-radius: 0.25rem 0.25rem 0.25rem 0;
width: fit-content;
}
.cody-sign-up-panel {
background-color: var(--color-bg-1);
padding: 1.5rem;
box-shadow: 0 0.5rem 1rem -6px rgba(46, 34, 119, 0.1);
border-radius: 3px;
}
.cody-sign-up-panel-wrapper {
box-shadow: var(--marketing-block-shadow);
height: fit-content;
max-width: 598px;
}
.button-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1.5rem;
margin-bottom: 1.26rem;
}
.auth-button {
text-align: center;
background-color: var(--white);
color: var(--black);
padding: 0.375rem 2.5rem;
border: 1px solid var(--border-color);
border-radius: 3px;
width: fit-content;
margin: 0;
}
.button-icon {
margin-right: 0.125 !important;
}
.terms-privacy-link {
text-decoration: underline;
:global(.theme-dark) & {
color: var(--gray-04);
}
:global(.theme-light) & {
color: var(--gray-08);
}
}
}
.learn-more-section {
margin-top: 3rem;
margin-bottom: 1.5rem;
.starting-point-wrapper {
gap: 16px;
}
.learn-more-items-wrapper {
margin-top: 2rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
padding-bottom: 4rem;
.learn-more-items {
padding: 1rem;
flex-grow: 1;
border: 1px solid var(--gray-03);
border-radius: 3px;
:global(.theme-dark) & {
background-color: var(--gray-10);
border: 1px solid var(--gray-09);
}
:global(.theme-light) & {
background-color: var(--gray-03);
}
.learn-more-items-title {
font-weight: 600;
}
}
}
}
.footer {
padding: 2.125rem 0.625rem 2rem;
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: 1.5rem;
:global(.theme-dark) & {
border-top: 1px solid var(--gray-09);
}
:global(.theme-light) & {
border-top: 1px solid var(--gray-03);
}
.footer-description {
margin-bottom: 1.3125rem;
font-size: 1.125rem;
max-width: 852px;
}
.footer-cta-link {
color: var(--violet-09) !important;
font-weight: 600;
font-size: 1rem;
}
}
.cody-platform-card-wrapper {
padding: 1rem;
border-radius: 0.25rem;
background-color: var(--color-bg-1);
:global(.theme-dark) & {
border: 1px solid var(--gray-09);
}
:global(.theme-light) & {
border: 1px solid var(--gray-03);
}
.cody-platform-card-icon {
width: 1.5rem;
height: 1.5rem;
color: var(--platform-icon-color);
}
.cody-platform-card-description {
max-width: 326px;
}
.cody-platform-card-image {
max-width: 326px;
@media (--xs-breakpoint-down) {
max-width: 300px;
}
}
}

View File

@ -1,57 +0,0 @@
import type { Meta, StoryFn } from '@storybook/react'
import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import type { AuthenticatedUser } from '../../../auth'
import { WebStory } from '../../../components/WebStory'
import type { SourcegraphContext } from '../../../jscontext'
import { CodyMarketingPage } from './CodyMarketingPage'
const config: Meta = {
title: 'web/src/cody/CodyMarketingPage',
}
export default config
const context: Pick<SourcegraphContext, 'externalURL'> = {
externalURL: 'https://sourcegraph.test:3443',
}
export const SourcegraphDotCom: StoryFn = () => (
<WebStory>
{() => (
<CodyMarketingPage
context={context}
isSourcegraphDotCom={true}
authenticatedUser={null}
telemetryRecorder={noOpTelemetryRecorder}
/>
)}
</WebStory>
)
export const Enterprise: StoryFn = () => (
<WebStory>
{() => (
<CodyMarketingPage
context={context}
isSourcegraphDotCom={false}
authenticatedUser={null}
telemetryRecorder={noOpTelemetryRecorder}
/>
)}
</WebStory>
)
export const EnterpriseSiteAdmin: StoryFn = () => (
<WebStory>
{() => (
<CodyMarketingPage
context={context}
isSourcegraphDotCom={false}
authenticatedUser={{ siteAdmin: true } as AuthenticatedUser}
telemetryRecorder={noOpTelemetryRecorder}
/>
)}
</WebStory>
)

View File

@ -1,338 +0,0 @@
import { useEffect } from 'react'
import { mdiChevronRight, mdiCodeBracesBox, mdiGit } from '@mdi/js'
import classNames from 'classnames'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
import { Theme, useTheme } from '@sourcegraph/shared/src/theme'
import { Badge, H1, H2, H3, H4, Icon, Link, PageHeader, Text } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../../auth'
import { ExternalsAuth } from '../../../auth/components/ExternalsAuth'
import { MarketingBlock } from '../../../components/MarketingBlock'
import { Page } from '../../../components/Page'
import { PageTitle } from '../../../components/PageTitle'
import type { SourcegraphContext } from '../../../jscontext'
import { MeetCodySVG } from '../../../repo/components/TryCodyWidget/WidgetIcons'
import { EventName } from '../../../util/constants'
import { CodyColorIcon, CodyHelpIcon, CodyWorkIcon } from '../../chat/CodyPageIcon'
import styles from './CodyMarketingPage.module.scss'
interface CodyPlatformCardProps {
icon: string | JSX.Element
title: string | JSX.Element
description: string | JSX.Element
illustration: string
}
const onSpeakToAnEngineer = (): void => EVENT_LOGGER.log(EventName.SPEAK_TO_AN_ENGINEER_CTA)
const IDEIcon: React.FunctionComponent<{}> = () => (
<svg viewBox="-4 -4 31 31" fill="none" xmlns="http://www.w3.org/2000/svg" className={styles.codyPlatformCardIcon}>
<rect x="0.811523" y="0.366669" width="25" height="25" rx="3" fill="#4D52F4" />
<path
d="M13.8115 20.1583C13.8115 20.4346 13.9169 20.6996 14.1044 20.8949C14.292 21.0903 14.5463 21.2 14.8115 21.2H16.8115V23.2833H14.3115C13.7615 23.2833 12.8115 22.8146 12.8115 22.2417C12.8115 22.8146 11.8615 23.2833 11.3115 23.2833H8.81152V21.2H10.8115C11.0767 21.2 11.3311 21.0903 11.5186 20.8949C11.7062 20.6996 11.8115 20.4346 11.8115 20.1583V5.57501C11.8115 5.29874 11.7062 5.03379 11.5186 4.83844C11.3311 4.64309 11.0767 4.53335 10.8115 4.53335H8.81152V2.45001H11.3115C11.8615 2.45001 12.8115 2.91876 12.8115 3.49168C12.8115 2.91876 13.7615 2.45001 14.3115 2.45001H16.8115V4.53335H14.8115C14.5463 4.53335 14.292 4.64309 14.1044 4.83844C13.9169 5.03379 13.8115 5.29874 13.8115 5.57501V20.1583Z"
fill="white"
/>
</svg>
)
const codyPlatformCardItems = (
isSourcegraphDotCom: boolean
): {
title: string | JSX.Element
description: string | JSX.Element
icon: string | JSX.Element
illustration: { dark: string; light: string }
}[] => [
{
title: 'Knows your code',
description:
'Cody knows about your codebase and can use that knowledge to explain, generate, and improve your code.',
icon: mdiCodeBracesBox,
illustration: {
dark: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-knows-your-code-illustration-dark.png',
light: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-knows-your-code-illustration-light.png',
},
},
{
title: 'Cody in your editor',
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">View supported editors.</Link>
</>
),
icon: <IDEIcon />,
illustration: {
dark: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-vs-code-illustration-dark.png',
light: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-vs-code-illustration-light.png',
},
},
...(isSourcegraphDotCom
? [
{
title: (
<>
Try it on sourcegraph.com{' '}
<Badge variant="info" className="d-inline">
Experimental
</Badge>
</>
),
description:
'Cody explains, generates, convert code, and more within the context of public repositories.',
icon: mdiGit,
illustration: {
dark: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-dark.png',
light: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-light.png',
},
},
]
: [
{
title: 'Recipes accelerate your flow',
description:
'Cody explains, generates, convert code, and more within the context of your repositories.',
icon: mdiGit,
illustration: {
dark: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-dark.png',
light: 'https://storage.googleapis.com/sourcegraph-assets/app-images/cody-com-illustration-light.png',
},
},
]),
]
export interface CodyMarketingPageProps extends TelemetryV2Props {
isSourcegraphDotCom: boolean
context: Pick<SourcegraphContext, 'externalURL'>
authenticatedUser: AuthenticatedUser | null
}
export const CodyMarketingPage: React.FunctionComponent<CodyMarketingPageProps> = ({
context,
isSourcegraphDotCom,
authenticatedUser,
telemetryRecorder,
}) => {
const { theme } = useTheme()
const isDarkTheme = theme === Theme.Dark
useEffect(() => telemetryRecorder.recordEvent('cody.marketing', 'view'), [telemetryRecorder])
return (
<Page>
<PageTitle title="Cody" />
<PageHeader
description={
<>
Cody answers code questions and writes code for you by reading your entire codebase and the code
graph.
</>
}
className={classNames('mb-3', styles.pageHeader)}
>
<PageHeader.Heading as="h2" styleAs="h1">
<PageHeader.Breadcrumb icon={CodyColorIcon}>
<div className={classNames('d-inline-flex align-items-center', styles.pageHeaderBreadcrumb)}>
Cody
</div>
</PageHeader.Breadcrumb>
</PageHeader.Heading>
</PageHeader>
{/* Page content */}
<div className={styles.headerSection}>
<div>
{isSourcegraphDotCom && <H1>Meet Cody, your AI assistant</H1>}
<div className="ml-3">
<Text className={styles.codyConversation}>AI-powered chat for you code</Text>
<Text className={styles.codyConversation}>Autocomplete</Text>
<Text className={styles.codyConversation}>Find, fix and explain code</Text>
<Text className={styles.codyConversation}>Create documentation</Text>
<Text className={styles.codyConversation}>Generate unit tests</Text>
<Text className={styles.codyConversation}>Build custom commands</Text>
</div>
<CodyHelpIcon />
</div>
{isSourcegraphDotCom ? (
<MarketingBlock
contentClassName={styles.codySignUpPanel}
wrapperClassName={styles.codySignUpPanelWrapper}
>
<H2>Sign up to get free access</H2>
<Text className="mt-3">
Cody answers technical questions and writes code directly in your IDE, using your code graph
for context and accuracy. Sign up with:
</Text>
<div className={styles.buttonWrapper}>
<ExternalsAuth
page="cody-marketing-page"
context={context}
githubLabel="GitHub"
gitlabLabel="GitLab"
googleLabel="Google"
withCenteredText={true}
onClick={() => {}}
ctaClassName={styles.authButton}
iconClassName={styles.buttonIcon}
telemetryRecorder={telemetryRecorder}
telemetryService={EVENT_LOGGER}
/>
</div>
<Text className="mt-3 mb-0">
By registering, you agree to our{' '}
<Link
className={styles.termsPrivacyLink}
to="https://sourcegraph.com/terms"
target="_blank"
rel="noopener"
>
Terms of Service
</Link>{' '}
and{' '}
<Link
className={styles.termsPrivacyLink}
to="https://sourcegraph.com/privacy"
target="_blank"
rel="noopener"
>
Privacy Policy
</Link>
.
</Text>
</MarketingBlock>
) : (
<MarketingBlock
contentClassName={styles.codySignUpPanel}
wrapperClassName={styles.codySignUpPanelWrapper}
>
<H2>Meet Cody, your AI assistant</H2>
<Text className="mt-3">
Cody is an AI assistant that leverages the code graph to know more about your code. Use it
to:
</Text>
<ul>
<li>Onboard to new codebases</li>
<li>Evaluate and fix code</li>
<li>Write code faster</li>
</ul>
<Text className="mb-0">
<Link to="https://sourcegraph.com/cody">Learn more about Cody &rarr;</Link>
{authenticatedUser?.siteAdmin && (
<>
{' '}
or <Link to="/help/cody/explanations/enabling_cody_enterprise">enable it now</Link>.
</>
)}
</Text>
</MarketingBlock>
)}
</div>
<div className={styles.learnMoreSection}>
<H2>Enhancing productivity with Cody</H2>
<div
className={classNames(
'd-flex flex-row flex-wrap mt-3 justify-content-center',
styles.startingPointWrapper
)}
>
{codyPlatformCardItems(isSourcegraphDotCom).map((item, index) => (
<CodyPlatformCard
key={index}
title={item.title}
description={item.description}
illustration={isDarkTheme ? item.illustration.dark : item.illustration.light}
icon={item.icon}
/>
))}
</div>
<div className={styles.learnMoreItemsWrapper}>
{isSourcegraphDotCom ? (
<div className={styles.learnMoreItems}>
<H4 className={styles.learnMoreItemsTitle}>Overview</H4>
<Text className="mb-0">
Visit the{' '}
<Link to="https://sourcegraph.com/cody" target="_blank" rel="noopener">
product page
</Link>{' '}
and see what devs are building with Cody.
</Text>
</div>
) : (
<div className="d-flex align-items-center">
<div>
<MeetCodySVG />
</div>
<Text className="ml-3">
<Link to="https://sourcegraph.com/cody">Learn about Cody</Link>, Sourcegraph's AI coding
assistant.
</Text>
</div>
)}
<div className={styles.learnMoreItems}>
<H4 className={styles.learnMoreItemsTitle}>Documentation</H4>
<Text className="mb-0">
Learn about Codys use cases, commands, and FAQs on the{' '}
<Link to="/help/cody" target="_blank" rel="noopener">
documentation page
</Link>
.
</Text>
</div>
</div>
</div>
{isSourcegraphDotCom && (
<div className={styles.footer}>
<CodyWorkIcon />
<div>
<H1 className="mb-2">Get Cody for work</H1>
<Text className={styles.footerDescription}>
Cody for Sourcegraph Enterprise utilizes Sourcegraph's code graph to deliver context-aware
answers based on your private codebase, enabling enhanced code comprehension and
productivity.
</Text>
<div className="mb-2">
<Link
to="https://sourcegraph.com/demo"
className={classNames('d-inline-flex align-items-center', styles.footerCtaLink)}
onClick={onSpeakToAnEngineer}
>
Speak to an engineer
<Icon svgPath={mdiChevronRight} aria-hidden={true} />
</Link>
</div>
</div>
</div>
)}
</Page>
)
}
const CodyPlatformCard: React.FunctionComponent<CodyPlatformCardProps> = ({
icon,
title,
description,
illustration,
}) => (
<div className={styles.codyPlatformCardWrapper}>
<div className="d-flex flex-row align-items-center">
{typeof icon === 'string' ? (
<Icon svgPath={icon} aria-hidden={true} className={styles.codyPlatformCardIcon} />
) : (
<>{icon}</>
)}
<H3 className="ml-2 mb-0">{title}</H3>
</div>
<Text className={classNames('mt-2', styles.codyPlatformCardDescription)}>{description}</Text>
<img src={illustration} alt="Cody platform card" className={styles.codyPlatformCardImage} />
</div>
)

View File

@ -1 +0,0 @@
export * from './CodyMarketingPage'

View File

@ -33,7 +33,7 @@
}
&-header {
font-size: 2.5rem;
font-size: 2rem;
font-weight: 700;
margin-top: 0.5rem;
}

View File

@ -6,6 +6,7 @@ import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import {
ButtonLink,
H1,
H2,
Icon,
Link,
Menu,
@ -34,7 +35,7 @@ const setupOptions: SetupOption[] = [
{
icon: <VSCodeIcon className={styles.linkSelectorIcon} />,
maker: 'Microsoft',
name: 'VSCode',
name: 'VS Code',
setupLink: 'https://sourcegraph.com/docs/cody/clients/install-vscode',
},
{
@ -53,36 +54,63 @@ export const CodyDashboardPage: FC<CodyDashboardPageProps> = ({ telemetryRecorde
}, [telemetryRecorder])
const codySetupLink = 'https://sourcegraph.com/docs/cody'
return (
return !window.context?.codyEnabledOnInstance ? (
// This page should not be linked from anywhere if Cody is disabled on the instance, but add
// a check here just in case to avoid confusing users if they find their way here.
<section className={styles.dashboardContainer}>
<section className={styles.dashboardHero}>
<CodyColorIcon className={styles.dashboardCodyIcon} />
<H1 className={styles.dashboardHeroHeader}>
Get started with <span className={styles.codyGradient}>Cody</span>
</H1>
<H1 className={styles.dashboardHeroHeader}>Cody is not enabled</H1>
<Text className={styles.dashboardHeroTagline}>
Hey! 👋 Lets get started with Cody, your AI coding assistant.
Contact your Sourcegraph admin if this is unexpected.
</Text>
</section>
<section className={styles.dashboardOnboarding}>
<section className={styles.dashboardOnboardingIde}>
<Text className={styles.dashboardText}>Get Cody in your editor</Text>
<LinkSelector options={setupOptions} />
<Text className="text-muted">
<Link to={codySetupLink} className={styles.dashboardOnboardingIdeInstallationLink}>
Explore installation docs
</section>
) : (
<section className={styles.dashboardContainer}>
{window.context?.codyEnabledForCurrentUser ? (
<>
<section className={styles.dashboardHero}>
<CodyColorIcon className={styles.dashboardCodyIcon} />
<H1 className={styles.dashboardHeroHeader}>
Get started with <span className={styles.codyGradient}>Cody</span>
</H1>
<Text className={styles.dashboardHeroTagline}>
Hey! 👋 Lets get started with Cody your new AI coding assistant.
</Text>
</section>
<section className={styles.dashboardOnboarding}>
<section className={styles.dashboardOnboardingIde}>
<Text className={styles.dashboardText}>Use Cody in your editor</Text>
<LinkSelector options={setupOptions} />
<Text className="text-muted">
<Link to={codySetupLink} className={styles.dashboardOnboardingIdeInstallationLink}>
Documentation
</Link>
</Text>
</section>
<section className={styles.dashboardOnboardingWeb}>
<Text className={styles.dashboardText}>... or try it on the web</Text>
<ButtonLink to="/cody/chat" outline={true} className={styles.dashboardOnboardingWebLink}>
<CodyColorIcon className={styles.dashboardOnboardingCodyIcon} />
<span>Cody Web</span>
</ButtonLink>
</section>
</section>
</>
) : (
<section className={styles.dashboardHero}>
<CodyColorIcon className={styles.dashboardCodyIcon} />
<H2 className={styles.dashboardHeroHeader}>
Your user account doesn't have access to <span className={styles.codyGradient}>Cody</span>
</H2>
<Text className={styles.dashboardHeroTagline}>
Ask your Sourcegraph admin to{' '}
<Link to="/help/cody/clients/enable-cody-enterprise#enable-cody-only-for-some-users">
enable Cody for you
</Link>
</Text>
</section>
<section className={styles.dashboardOnboardingWeb}>
<Text className={styles.dashboardText}>... or try it on the web</Text>
<ButtonLink to="/cody/chat" outline={true} className={styles.dashboardOnboardingWebLink}>
<CodyColorIcon className={styles.dashboardOnboardingCodyIcon} />
<span>Cody for web</span>
</ButtonLink>
</section>
</section>
)}
</section>
)
}

View File

@ -1,12 +0,0 @@
export const isEmailVerificationNeededForCody = (): boolean =>
window.context?.codyRequiresVerifiedEmail && !window.context?.currentUser?.hasVerifiedEmail
export const isCodyEnabled = (): boolean => {
if (!window.context?.codyEnabled || !window.context?.codyEnabledForCurrentUser) {
return false
}
return true
}
export const isSignInRequiredForCody = (): boolean => !window.context.isAuthenticatedUser

View File

@ -23,7 +23,6 @@ import { CodyAlert } from '../components/CodyAlert'
import { PageHeaderIcon } from '../components/PageHeaderIcon'
import { AcceptInviteBanner } from '../invites/AcceptInviteBanner'
import { InviteUsers } from '../invites/InviteUsers'
import { isCodyEnabled } from '../isCodyEnabled'
import { USER_CODY_PLAN, USER_CODY_USAGE } from '../subscription/queries'
import { getManageSubscriptionPageURL, isEmbeddedCodyProUIEnabled } from '../util'
@ -91,7 +90,7 @@ export const CodyManagementPage: React.FunctionComponent<CodyManagementPageProps
throw dataError || usageDateError
}
if (!isCodyEnabled() || !subscription) {
if (!window.context?.codyEnabledForCurrentUser || !subscription) {
return null
}

View File

@ -1,6 +1,6 @@
import React, { type ReactElement, useEffect, useMemo } from 'react'
import React, { useEffect, useMemo, type ReactElement } from 'react'
import { mdiArrowLeft, mdiInformationOutline, mdiTrendingUp, mdiCreditCardOutline } from '@mdi/js'
import { mdiArrowLeft, mdiCreditCardOutline, mdiInformationOutline, mdiTrendingUp } from '@mdi/js'
import classNames from 'classnames'
import { useNavigate } from 'react-router-dom'
@ -10,10 +10,10 @@ import {
Badge,
Button,
ButtonLink,
Link,
H1,
H2,
Icon,
Link,
PageHeader,
Text,
Tooltip,
@ -23,12 +23,11 @@ import {
import type { AuthenticatedUser } from '../../auth'
import { Page } from '../../components/Page'
import { PageTitle } from '../../components/PageTitle'
import { CodySubscriptionPlan } from '../../graphql-operations'
import type { UserCodyPlanResult, UserCodyPlanVariables } from '../../graphql-operations'
import { CodySubscriptionPlan } from '../../graphql-operations'
import { CodyProRoutes } from '../codyProRoutes'
import { ProIcon } from '../components/CodyIcon'
import { PageHeaderIcon } from '../components/PageHeaderIcon'
import { isCodyEnabled } from '../isCodyEnabled'
import { getManageSubscriptionPageURL, isEmbeddedCodyProUIEnabled, manageSubscriptionRedirectURL } from '../util'
import { USER_CODY_PLAN } from './queries'
@ -65,7 +64,7 @@ export const CodySubscriptionPage: React.FunctionComponent<CodySubscriptionPageP
throw dataError
}
if (!isCodyEnabled() || !data?.currentUser || !authenticatedUser) {
if (!window.context?.codyEnabledForCurrentUser || !data?.currentUser || !authenticatedUser) {
return null
}

View File

@ -1,27 +0,0 @@
import type { FC } from 'react'
export const ChatBrandIcon: FC = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 40 40">
<path
fill="url(#paint0_linear_571_94691)"
fillRule="evenodd"
d="M5.477 1.846h29.046a3.641 3.641 0 013.63 3.63v21.785a3.642 3.642 0 01-3.63 3.631H9.108l-7.262 7.262V5.477a3.641 3.641 0 013.63-3.63zm3.63 25.416h25.416V5.477H5.477v25.415l3.63-3.63z"
clipRule="evenodd"
/>
<defs>
<linearGradient
id="paint0_linear_571_94691"
x1="-3.385"
x2="37.634"
y1="7.385"
y2="38.709"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.121" stopColor="#7048E8" />
<stop offset="0.308" stopColor="#00CBEC" />
<stop offset="0.642" stopColor="#A112FF" />
<stop offset="0.92" stopColor="#FF5543" />
</linearGradient>
</defs>
</svg>
)

View File

@ -1,340 +0,0 @@
@import 'wildcard/src/global-styles/breakpoints';
.container {
--container-margin: 6rem;
width: 100%;
margin: var(--container-margin);
height: fit-content;
@media (--md-breakpoint-down) {
margin: 3rem;
}
@media (--sm-breakpoint-down) {
margin: 1rem;
}
}
.hero {
margin-right: calc(var(--container-margin) * -1);
display: grid;
grid-template-columns: 0.75fr 1fr;
gap: 1.875rem;
@media (--md-breakpoint-down) {
grid-template-columns: 1fr;
gap: 5rem;
margin-right: 0;
}
&-logo {
width: 2.25rem;
height: 2.25rem;
}
&-completion-image {
justify-self: flex-end;
filter: drop-shadow(-7px -16px 32px #a112ff24);
@media (--md-breakpoint-down) {
width: 100%;
margin-right: -3rem;
}
@media (--sm-breakpoint-down) {
margin: -1rem;
}
}
&-headline {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
&-cody {
font-size: 2rem;
}
&-enterprise {
font-size: 1.125rem;
color: var(--text-muted);
}
&-header {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.75rem;
}
&-text {
background: linear-gradient(90.64deg, var(--logo-blue) 3.11%, #c66fff 44.21%, #ff8578 83.64%);
background-clip: text !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
font-size: 3rem;
margin-top: 1.5rem;
}
&-description {
font-size: 1.3125rem;
font-weight: 300;
margin-bottom: 2rem;
color: var(--text-body);
}
}
}
.cody-availability {
font-size: 1rem;
font-weight: 500;
}
.ide {
&-container {
margin-top: 2.5rem;
}
&-list {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
margin: 0.5rem 0;
@media (--sm-breakpoint-down) {
grid-template-columns: 1fr;
width: 100%;
}
}
&-detail {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.625rem;
}
&-logo {
width: 2rem;
height: 2.5rem;
}
&-maker {
font-size: 0.8125rem;
}
&-name {
font-size: 1rem;
}
}
.about {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 2.5rem;
margin: 5rem 0;
@media (--md-breakpoint-down) {
grid-template-columns: 1fr;
gap: 5rem;
}
&-grid-one {
background-color: var(--color-bg-1);
border-radius: 0.5rem;
padding: 2.5rem;
font-size: 1rem;
box-shadow: 0 0.25rem 2rem 0 #670bdb0a;
&-header {
margin-block: 0.75rem;
font-size: 1.625rem;
font-weight: 500;
}
&-description {
font-size: 1.125rem;
}
}
&-grid-two {
background-color: var(--color-bg-1);
border-radius: 0.5rem;
padding: 2.5rem;
font-size: 1rem;
height: fit-content;
box-shadow: 0 0.25rem 2rem 0 #670bdb0a;
&-header {
margin-block: 0.75rem;
font-size: 1.625rem;
font-weight: 500;
}
&-text {
font-size: 1.125rem;
}
&-list {
list-style-type: disclosure-closed;
padding-inline: 2rem;
font-size: 0.875rem;
font-family: monospace;
li {
padding-inline-start: 0.25rem;
&:not(:last-child) {
margin-bottom: 0.5rem;
}
}
}
}
&-grid-three {
background-color: var(--color-bg-1);
border-radius: 0.5rem;
padding: 2.5rem;
font-size: 1rem;
box-shadow: 0 0.25rem 2rem 0 #670bdb0a;
&-container {
margin-top: 2rem;
background: linear-gradient(
90.64deg,
var(--logo-blue) 3.11%,
var(--logo-purple) 44.21%,
var(--logo-orange) 83.64%
);
border-radius: 0.5rem;
padding: 0.0625rem;
}
&-text {
background: linear-gradient(
90.64deg,
var(--logo-blue) 3.11%,
var(--logo-purple) 44.21%,
var(--logo-orange) 83.64%
);
background-clip: text !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
margin: 0;
font-size: 1.5rem;
}
}
}
.testimonial {
margin: 0 5rem;
padding-bottom: 2rem;
&-grid {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
padding: 0.5rem 0;
@media (--md-breakpoint-down) {
grid-template-columns: 1fr;
}
}
&-header {
text-align: center;
margin: 3rem 0;
font-weight: 500;
font-size: 2rem;
}
&-container {
background-color: transparent;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 2rem;
height: fit-content;
&:hover {
background-color: var(--color-bg-1);
cursor: pointer;
}
}
&-author {
&-info {
display: flex;
flex-direction: column;
justify-self: center;
align-items: flex-start;
}
&-username {
font-size: 0.875rem;
}
&-name {
font-size: 0.875rem;
font-weight: 500;
}
&-avatar {
background: var(--color-bg-2);
width: 2.5rem;
&::after {
background: transparent;
}
span {
color: var(--text-muted);
}
}
}
&-meta {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
&-text {
font-size: 1.125rem;
color: var(--text-muted);
margin: 0;
}
}
.context {
margin: 0 6rem;
gap: 4rem;
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
justify-items: center;
@media (--md-breakpoint-down) {
grid-template-columns: 1fr;
}
&-header {
margin-top: 1rem;
font-size: 2rem;
}
&-description {
margin-bottom: 1.75rem;
color: var(--text-muted);
font-size: 1.125rem;
}
&-diagram {
@media (--md-breakpoint-down) {
width: 100%;
}
}
}

View File

@ -1,219 +0,0 @@
import type { FC, ComponentType } from 'react'
import classNames from 'classnames'
import { UserAvatar } from '@sourcegraph/shared/src/components/UserAvatar'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import { H1, H2, Text, ButtonLink, Icon } from '@sourcegraph/wildcard'
import { CodyLogo } from '../components/CodyLogo'
import { ChatBrandIcon } from './ChatBrandIcon'
import { CompletionsBrandIcon } from './CompletionsBrandIcon'
import { ContextDiagram } from './ContextDiagram'
import { ContextExample } from './ContextExample'
import { IntelliJIcon } from './IntelliJ'
import { MultiLineCompletion } from './MultilineCompletion'
import { VSCodeIcon } from './vs-code'
import styles from './CodyUpsellPage.module.scss'
interface CodyIDEDetails {
maker?: string
name: string
icon: ComponentType<{ className?: string }>
}
const availableIDEsForCody: CodyIDEDetails[] = [
{
maker: 'Microsoft',
name: 'VSCode',
icon: () => <VSCodeIcon className={styles.ideLogo} />,
},
{
maker: 'JetBrains',
name: 'IntelliJ',
icon: () => <IntelliJIcon className={styles.ideLogo} />,
},
{
name: 'Cody for Web',
icon: () => <CodyLogo withColor={true} className={styles.ideLogo} />,
},
]
interface CodyTestimonial {
author: string
username: string
comment: string
}
const codyTestimonials: CodyTestimonial[] = [
{
author: 'Joe Previte',
username: '@jsjoeio',
comment:
"I've started using Cody this week and dude, absolute gamechanger especially with me onboarding to Haskell at my new job literally just gave me the answer, explained it will and it just fixed my error.",
},
{
author: 'Joshua Coetzer',
username: 'VS Code marketplace review',
comment:
"Absolutely loved using Cody in VSCode for the last few months. It's been a game-changer for me. The way it summarises code blocks and fills in gaps in log statements, error messages, and code comments is incredibly smart.",
},
{
author: 'Reza Shabani',
username: '@truerezashabani',
comment:
'Recently Ive been super impressed with Cody, and am using it constantly. Its especially good at answering questions about large repos.',
},
]
export const CodyUpsellPage: FC = () => {
const isLightTheme = useIsLightTheme()
const contactSalesLink = 'https://sourcegraph.com/contact/request-info'
return (
<section className={styles.container}>
<section className={styles.hero}>
<div className={styles.heroHeadline}>
<div className={styles.heroHeadlineHeader}>
<CodyLogo withColor={true} className={styles.heroLogo} />
<Text className={classNames('m-0', styles.heroHeadlineCody)}>Cody</Text>
<Text className={classNames('m-0', styles.heroHeadlineEnterprise)}>for enterprise</Text>
</div>
<H1 className={styles.heroHeadlineText}>Code more, type less.</H1>
<Text className={styles.heroHeadlineDescription}>
Cody is a coding AI assistant that uses AI and a deep understanding of your organisations
codebases to help you write and understand code faster.
</Text>
<ButtonLink to={contactSalesLink} variant="primary" className="py-2 px-5">
Contact sales
</ButtonLink>
<div className={styles.ideContainer}>
<Text className={classNames('text-muted', styles.codyAvailability)}>
Cody is available for your favourite IDE...
</Text>
<div className={styles.ideList}>
{availableIDEsForCody.map((ide, index) => (
<div key={index} className={styles.ideDetail}>
<Icon as={ide.icon} aria-hidden={true} />
<div>
<Text className={classNames('mb-0 text-muted', styles.ideMaker)}>
{ide.maker}
</Text>
<Text className={classNames('mb-0', styles.ideName)}>{ide.name}</Text>
</div>
</div>
))}
</div>
</div>
</div>
<MultiLineCompletion isLightTheme={isLightTheme} className={styles.heroCompletionImage} />
</section>
<section className={styles.about}>
<div>
<div className={styles.aboutGridOne}>
<CompletionsBrandIcon />
<Text className={styles.aboutGridOneHeader}>Code faster with AI-assisted autocomplete</Text>
<Text className={classNames('text-muted', styles.aboutGridOneDescription)}>
Cody autocompletes single lines, or whole functions, in any programming language,
configuration file, or documentation.
</Text>
</div>
<div className={styles.aboutGridThreeContainer}>
<div className={styles.aboutGridThree}>
<Text className={styles.aboutGridThreeText}>
Every day, Cody helps developers write &gt;25,000 lines of code
</Text>
</div>
</div>
</div>
<div className={styles.aboutGridTwo}>
<ChatBrandIcon />
<Text className={styles.aboutGridTwoHeader}>AI-powered chat for your code</Text>
<Text className={classNames('text-muted', styles.aboutGridTwoText)}>
Cody chat helps unblock you when youre jumping into new projects, trying to understand legacy
code, or taking on tricky problems.
</Text>
<ul className={classNames('text-muted', styles.aboutGridTwoList)}>
<li>How is this repository structured?</li>
<li>What does this file do?</li>
<li>Where is this component defined?</li>
<li>Why isn't this code working?</li>
</ul>
</div>
</section>
<section className={styles.testimonial}>
<H1 className={classNames('text-muted', styles.testimonialHeader)}>What people say about Cody...</H1>
<section className={styles.testimonialGrid}>
{codyTestimonials.map((testimonial, index) => (
<Testimonial key={index} testimonial={testimonial} />
))}
</section>
</section>
<section className={styles.context}>
<div>
<SearchIcon />
<H2 className={styles.contextHeader}>Sourcegraph powered context</H2>
<Text className={styles.contextDescription}>
Sourcegraphs code graph and analysis tools allows Cody to autocomplete, explain, and edit your
code with additional context.
</Text>
<ContextExample isLightTheme={isLightTheme} />
</div>
<ContextDiagram isLightTheme={isLightTheme} className={styles.contextDiagram} />
</section>
</section>
)
}
interface TestimonialProps {
testimonial: CodyTestimonial
}
const Testimonial: FC<TestimonialProps> = ({ testimonial }) => (
<section className={styles.testimonialContainer}>
<div className={styles.testimonialMeta}>
<UserAvatar
className={styles.testimonialAuthorAvatar}
capitalizeInitials={true}
user={{ displayName: testimonial.author, username: testimonial.username, avatarURL: null }}
/>
<div className={styles.testimonialAuthorInfo}>
<span className={styles.testimonialAuthorName}>{testimonial.author}</span>
<span className={classNames('text-muted', styles.testimonialAuthorUsername)}>
{testimonial.username}
</span>
</div>
</div>
<Text className={styles.testimonialText}>{testimonial.comment}</Text>
</section>
)
const SearchIcon: FC = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="37" height="40" fill="none" viewBox="0 0 37 40">
<path
fill="url(#paint0_linear_571_94711)"
fillRule="evenodd"
d="M18.067 4.53c-7.441 0-13.5 6.029-13.5 13.5 0 7.47 6.059 13.5 13.5 13.5a2.274 2.274 0 012.284 2.264 2.274 2.274 0 01-2.284 2.265C8.074 36.059 0 27.972 0 18.029 0 8.087 8.074 0 18.067 0c9.994 0 18.068 8.087 18.068 18.03 0 4.964-2.013 9.463-5.268 12.724l5.393 5.386a2.251 2.251 0 01-.011 3.202 2.296 2.296 0 01-3.23-.01l-7.101-7.094a2.254 2.254 0 01.243-3.402 13.476 13.476 0 005.408-10.807c0-7.47-6.06-13.5-13.502-13.5z"
clipRule="evenodd"
/>
<defs>
<linearGradient
id="paint0_linear_571_94711"
x1="0.885"
x2="30.949"
y1="26.786"
y2="27.094"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#FF5543" />
<stop offset="1" stopColor="#A112FF" />
</linearGradient>
</defs>
</svg>
)

View File

@ -1,42 +0,0 @@
import type { FC } from 'react'
export const CompletionsBrandIcon: FC = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 40 40">
<path
fill="url(#paint0_linear_646_2840)"
d="M14.461 10.166h3.548c.741 0 1.342.6 1.342 1.342v17.138c0 .741-.6 1.342-1.342 1.342h-3.548v1.858h3.548a3.19 3.19 0 002.271-.946 3.19 3.19 0 002.272.946h3.082v-1.858h-3.082c-.741 0-1.342-.601-1.342-1.342V11.508c0-.741.6-1.342 1.342-1.342h3.082V8.308h-3.082a3.19 3.19 0 00-2.271.946 3.19 3.19 0 00-2.272-.946h-3.548v1.858z"
/>
<path
fill="url(#paint1_linear_646_2840)"
d="M4.216 1.538h31.709l2.537 2.537v31.709l-2.537 2.678H4.216l-2.678-2.678V4.075l2.678-2.537zm0 34.246h31.709V4.075H4.216v31.709z"
/>
<defs>
<linearGradient
id="paint0_linear_646_2840"
x1="14.292"
x2="32.609"
y1="8.608"
y2="20.721"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.125" stopColor="#7048E8" />
<stop offset="0.308" stopColor="#00CBEC" />
<stop offset="0.642" stopColor="#A112FF" />
<stop offset="1" stopColor="#FF5543" />
</linearGradient>
<linearGradient
id="paint1_linear_646_2840"
x1="0.98"
x2="30.561"
y1="2.009"
y2="43.225"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.125" stopColor="#7048E8" />
<stop offset="0.308" stopColor="#00CBEC" />
<stop offset="0.642" stopColor="#A112FF" />
<stop offset="1" stopColor="#FF5543" />
</linearGradient>
</defs>
</svg>
)

View File

@ -1,805 +0,0 @@
import type { FC } from 'react'
interface ContextDiagramProps {
isLightTheme: boolean
className: string
}
export const ContextDiagram: FC<ContextDiagramProps> = ({ isLightTheme, className }) =>
isLightTheme ? <ContextDiagramLight className={className} /> : <ContextDiagramDark className={className} />
const ContextDiagramDark: FC<Pick<ContextDiagramProps, 'className'>> = ({ className }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="492"
height="329"
fill="none"
viewBox="0 0 492 329"
className={className}
>
<g clipPath="url(#clip0_571_94747)">
<mask id="mask0_571_94747" width="492" height="281" x="0" y="24" maskUnits="userSpaceOnUse">
<path fill="#fff" d="M491.946 24.5H.054v280h491.892v-280z" />
</mask>
<g mask="url(#mask0_571_94747)">
<g filter="url(#filter0_di_571_94747)">
<path
fill="url(#paint0_linear_571_94747)"
stroke="url(#paint1_radial_571_94747)"
strokeWidth="1"
d="M185.123 251.469L64.656 171.303c-2.9-1.93-7.598-1.922-10.492.018L20.43 193.938c-2.894 1.94-2.888 5.078.012 7.007l120.467 80.167c2.901 1.93 7.598 1.922 10.492-.019l33.735-22.616c2.894-1.94 2.889-5.077-.012-7.008z"
/>
<path
fill="#794AA8"
d="M137.496 230.016l-34.325-22.843c-1.791-1.192-4.692-1.187-6.478.011-1.787 1.199-1.784 3.135.007 4.327l34.326 22.843c1.791 1.191 4.691 1.186 6.478-.011 1.787-1.198 1.783-3.136-.008-4.327zM146.629 236.094l21.306 14.178c1.791 1.192 1.795 3.129.008 4.327s-4.687 1.203-6.478.011l-21.306-14.178c-1.791-1.191-1.794-3.129-.007-4.327 1.786-1.197 4.687-1.202 6.477-.011zM93.29 200.598l-1.977-1.316c-1.791-1.191-4.691-1.186-6.478.012-1.786 1.197-1.783 3.135.008 4.327l1.976 1.315c1.79 1.192 4.692 1.186 6.478-.012 1.787-1.197 1.784-3.134-.007-4.326z"
/>
<path
fill="#FFF388"
fillOpacity="0.8"
d="M120.733 234.085l-18.693-12.439c-1.791-1.192-4.691-1.187-6.478.011-1.787 1.197-1.784 3.135.007 4.327l18.694 12.439c1.79 1.192 4.691 1.188 6.478-.011 1.787-1.198 1.783-3.135-.008-4.327z"
/>
<path
fill="#794AA8"
d="M137.299 259.346l-41.187-27.408c-1.791-1.192-4.691-1.187-6.478.011-1.787 1.197-1.784 3.134.007 4.327l41.187 27.408c1.79 1.191 4.691 1.186 6.478-.012 1.787-1.197 1.784-3.135-.007-4.326z"
/>
<path
fill="#FFF388"
d="M80.597 221.613l4.3 2.862c1.79 1.191 1.794 3.128.007 4.327-1.787 1.198-4.687 1.202-6.478.011l-4.3-2.862c-1.79-1.191-1.794-3.129-.007-4.327 1.787-1.197 4.687-1.202 6.478-.011z"
/>
<path
fill="#FFF388"
fillOpacity="0.4"
d="M150.498 253.893l-20.368-13.554c-1.791-1.193-4.691-1.188-6.478.011-1.787 1.198-1.784 3.134.007 4.327l20.369 13.554c1.791 1.191 4.691 1.186 6.478-.011 1.786-1.198 1.783-3.135-.008-4.327z"
/>
<path
fill="#794AA8"
d="M71.008 215.233l-2.135-1.42c-1.79-1.193-4.69-1.188-6.478.011-1.786 1.198-1.783 3.134.008 4.327l2.135 1.42c1.79 1.192 4.69 1.187 6.478-.011 1.786-1.198 1.783-3.135-.008-4.327zM92.503 215.298l-12.971-8.631c-1.791-1.192-4.691-1.187-6.478.011s-1.784 3.135.007 4.327l12.97 8.631c1.792 1.193 4.693 1.188 6.48-.011 1.786-1.198 1.783-3.135-.008-4.327z"
/>
<path
fill="#CE9CFF"
d="M55.67 190.103l3.95 2.629-.008-5.272-3.943 2.643zm-.012-6.71l5.747 3.824.01 5.751-8.6 5.766c-.381.255-.896.399-1.435.399-.539.002-1.055-.14-1.436-.394l-8.62-5.736c-.381-.254-.595-.598-.596-.958 0-.359.213-.705.593-.959l11.467-7.688c.795-.533 2.073-.536 2.87-.005zm-5.002 11.99l1.433-.961-6.465-4.302-1.433.961 6.465 4.302zm5.022-.487l1.434-.962-8.62-5.736-1.434.96 8.62 5.738z"
/>
<path stroke="#fff" strokeLinecap="round" strokeOpacity="0.2" d="M50.253 212.974l32.554-21.968" />
</g>
<g filter="url(#filter1_di_571_94747)">
<path
fill="url(#paint2_linear_571_94747)"
stroke="url(#paint3_radial_571_94747)"
strokeWidth="1"
d="M239.753 212.425l-120.468-80.166c-2.901-1.93-7.598-1.922-10.492.018l-33.735 22.617c-2.894 1.94-2.888 5.077.012 7.008l120.468 80.166c2.901 1.93 7.598 1.922 10.492-.018l33.734-22.617c2.894-1.94 2.889-5.078-.011-7.008z"
/>
<path
fill="#FFF388"
fillOpacity="0.8"
d="M192.126 190.972L157.8 168.129c-1.791-1.191-4.692-1.186-6.479.012-1.786 1.197-1.783 3.134.008 4.326l34.326 22.843c1.791 1.192 4.691 1.186 6.478-.012 1.787-1.197 1.784-3.134-.007-4.326z"
/>
<path
fill="#794AA8"
d="M201.259 197.051l21.306 14.178c1.791 1.191 1.794 3.129.007 4.326-1.786 1.198-4.687 1.203-6.478.012l-21.305-14.179c-1.791-1.192-1.795-3.128-.008-4.327 1.787-1.198 4.687-1.203 6.478-.01z"
/>
<path
fill="#FFF388"
fillOpacity="0.4"
d="M147.918 161.554l-1.976-1.315c-1.791-1.192-4.691-1.187-6.477.011-1.787 1.198-1.784 3.135.007 4.327l1.976 1.315c1.791 1.192 4.691 1.187 6.478-.011 1.787-1.199 1.783-3.135-.008-4.327z"
/>
<path
fill="#794AA8"
d="M175.363 195.042l-18.694-12.44c-1.791-1.192-4.691-1.187-6.478.01-1.787 1.199-1.783 3.136.007 4.327l18.694 12.441c1.791 1.191 4.691 1.186 6.477-.011 1.788-1.198 1.785-3.136-.006-4.327zM191.928 220.302l-41.187-27.408c-1.791-1.192-4.691-1.187-6.478.01-1.786 1.199-1.783 3.136.008 4.327l41.186 27.408c1.791 1.193 4.692 1.188 6.479-.01 1.786-1.199 1.783-3.135-.008-4.327zM135.227 182.57l4.299 2.861c1.791 1.191 1.794 3.129.008 4.327-1.787 1.197-4.687 1.202-6.478.011l-4.3-2.862c-1.791-1.191-1.795-3.128-.008-4.326 1.787-1.199 4.688-1.204 6.479-.011zM205.128 214.85l-20.369-13.556c-1.791-1.191-4.691-1.186-6.478.012-1.787 1.197-1.783 3.135.008 4.327l20.368 13.554c1.791 1.192 4.691 1.187 6.478-.011s1.784-3.135-.007-4.326zM125.637 176.189l-2.135-1.421c-1.79-1.191-4.691-1.186-6.478.012-1.787 1.197-1.783 3.135.008 4.326l2.135 1.421c1.791 1.192 4.691 1.187 6.478-.011 1.786-1.198 1.783-3.135-.008-4.327zM147.131 176.255l-12.97-8.632c-1.791-1.192-4.691-1.187-6.478.012-1.787 1.197-1.784 3.134.007 4.326l12.971 8.632c1.791 1.191 4.691 1.186 6.478-.011 1.787-1.198 1.783-3.136-.008-4.327z"
/>
<path
fill="#CE9CFF"
d="M110.298 151.059l3.952 2.629-.01-5.272-3.942 2.643zm-.011-6.71l5.747 3.824.01 5.752-8.601 5.765c-.38.255-.896.399-1.434.4-.539.001-1.055-.141-1.436-.395l-8.62-5.736c-.381-.254-.596-.598-.596-.958 0-.359.213-.704.593-.959l11.467-7.688c.795-.533 2.073-.535 2.87-.005zm-5.002 11.991l1.433-.961-6.465-4.303-1.433.961 6.465 4.303zm5.022-.488l1.433-.961-8.62-5.736-1.434.96 8.621 5.737z"
/>
<path stroke="#fff" strokeLinecap="round" strokeOpacity="0.2" d="M104.883 173.93l32.554-21.968" />
</g>
<g filter="url(#filter2_dd_571_94747)">
<path
fill="url(#paint4_linear_571_94747)"
stroke="url(#paint5_radial_571_94747)"
strokeWidth="1.088"
d="M306.816 138.911l-70.797-47.112c-1.705-1.135-5.985-.111-9.559 2.285l-41.675 27.94c-3.575 2.396-5.091 5.259-3.387 6.394l70.798 47.113c1.705 1.134 5.984.111 9.56-2.286l41.674-27.94c3.575-2.396 5.092-5.259 3.386-6.394z"
/>
<path
fill="#794AA8"
d="M271.145 121.311L234.743 97.1c-1.335-.888-3.497-.884-4.828.008-1.332.893-1.33 2.336.005 3.224l36.401 24.211c1.335.888 3.497.884 4.829-.009 1.332-.892 1.33-2.335-.005-3.223zM256.675 123.029l-13.935-9.268c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.33 2.336.005 3.224l13.935 9.268c1.335.888 3.497.885 4.829-.008 1.332-.892 1.33-2.336-.005-3.224zM259.537 135.54l-24.741-16.455c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.005 3.224l24.742 16.455c1.335.888 3.497.885 4.829-.008 1.332-.892 1.329-2.336-.006-3.224zM286.251 142.7l-22.467-14.942c-1.335-.888-3.497-.884-4.829.008-1.331.893-1.329 2.336.006 3.224l22.466 14.942c1.335.888 3.497.885 4.829-.008 1.332-.892 1.33-2.336-.005-3.224zM227.686 114.356l-9.669-6.431c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.33 2.336.005 3.224l9.669 6.431c1.335.888 3.497.884 4.829-.008 1.332-.893 1.33-2.336-.005-3.224z"
/>
<path
fill="#005482"
d="M206.435 130.13l-9.669-6.431c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.329 2.336.006 3.224l9.669 6.431c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224z"
/>
<path
fill="#794AA8"
d="M223.863 142.338l-9.669-6.431c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.329 2.336.006 3.224l9.669 6.431c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224zM235.63 109.033l-9.669-6.431c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.005 3.224l9.67 6.431c1.335.888 3.497.884 4.829-.009 1.332-.892 1.329-2.335-.006-3.223z"
/>
</g>
<g filter="url(#filter3_d_571_94747)">
<path
fill="url(#paint6_linear_571_94747)"
stroke="url(#paint7_radial_571_94747)"
strokeWidth="1.088"
d="M221.172 79.291l-76.376-50.825c-1.839-1.223-4.961-1.122-6.972.227l-23.449 15.72c-2.012 1.35-2.152 3.435-.313 4.659l76.376 50.825c1.839 1.223 4.96 1.122 6.972-.227l23.45-15.72c2.011-1.35 2.151-3.435.312-4.659z"
/>
<path
fill="#794AA8"
d="M187.645 61.991l-36.401-24.21c-1.335-.889-3.497-.885-4.829.007-1.332.893-1.33 2.336.005 3.224l36.402 24.211c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224z"
/>
<path
fill="#1E8DCA"
d="M173.175 63.71l-22.524-14.98c-1.335-.889-3.497-.885-4.829.008-1.332.892-1.329 2.335.006 3.223l22.523 14.98c1.335.889 3.497.885 4.829-.007 1.332-.893 1.33-2.336-.005-3.224z"
/>
<path
fill="#794AA8"
d="M176.588 76.603l-24.741-16.456c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.33 2.336.005 3.224l24.742 16.456c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224z"
/>
<path
fill="#005482"
d="M202.75 83.381L180.284 68.44c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.006 3.224l22.466 14.942c1.335.888 3.497.885 4.829-.008 1.332-.892 1.329-2.336-.006-3.224z"
/>
<path
fill="#794AA8"
d="M136.559 49.964l-2.043-1.359c-1.335-.888-3.497-.884-4.829.009-1.332.892-1.329 2.335.006 3.223l2.042 1.359c1.335.888 3.497.884 4.829-.008 1.332-.893 1.33-2.336-.005-3.224zM145.003 55.709l-2.042-1.359c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.33 2.336.005 3.224l2.043 1.359c1.335.888 3.497.884 4.829-.009 1.332-.892 1.329-2.336-.006-3.223z"
/>
<path
fill="#005482"
d="M143.793 44.169l-1.333-.887c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.329 2.336.006 3.224l1.333.886c1.335.888 3.497.885 4.829-.008 1.332-.892 1.329-2.336-.006-3.224z"
/>
</g>
<g filter="url(#filter4_dd_571_94747)">
<path
fill="url(#paint8_linear_571_94747)"
stroke="url(#paint9_radial_571_94747)"
strokeWidth="1.088"
d="M178.276 104.952l-74.452-49.545c-1.793-1.193-4.877-1.067-6.888.282L73.486 71.41c-2.011 1.349-2.19 3.409-.396 4.602l74.453 49.545c1.791 1.193 4.876 1.067 6.888-.282l23.449-15.721c2.011-1.349 2.189-3.409.396-4.602z"
/>
<path
fill="#005482"
d="M142.102 86.838l-25.588-17.02c-1.335-.887-3.497-.883-4.829.01-1.332.892-1.33 2.335.005 3.223l25.588 17.019c1.335.888 3.497.884 4.829-.009 1.332-.892 1.33-2.336-.005-3.223z"
/>
<path
fill="#1E8DCA"
d="M148.91 91.366l15.883 10.564c1.335.888 1.337 2.331.005 3.223-1.332.893-3.494.897-4.829.009l-15.882-10.564c-1.335-.888-1.338-2.331-.006-3.224 1.332-.892 3.494-.896 4.829-.008z"
/>
<path
fill="#794AA8"
d="M109.148 64.92l-1.473-.98c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.005 3.224l1.474.98c1.335.888 3.497.884 4.829-.009 1.332-.892 1.329-2.335-.006-3.224zM129.606 89.87L115.671 80.6c-1.335-.888-3.497-.884-4.829.009-1.332.892-1.329 2.335.006 3.223l13.935 9.269c1.335.888 3.497.884 4.829-.009 1.332-.892 1.329-2.336-.006-3.224zM141.954 108.69l-30.702-20.42c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.005 3.223l30.703 20.421c1.335.888 3.497.884 4.829-.008 1.331-.893 1.329-2.336-.006-3.224zM99.687 80.577l3.205 2.132c1.335.888 1.338 2.331.006 3.224-1.332.892-3.494.896-4.829.008l-3.205-2.132c-1.335-.888-1.338-2.33-.006-3.223 1.332-.893 3.494-.897 4.83-.009zM151.793 104.627L136.61 94.528c-1.335-.888-3.497-.884-4.829.009-1.332.892-1.33 2.335.005 3.223l15.184 10.099c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224zM92.54 75.823l-1.592-1.058c-1.335-.888-3.497-.884-4.83.008-1.331.893-1.328 2.336.007 3.224l1.59 1.058c1.336.888 3.498.885 4.83-.008 1.332-.892 1.33-2.336-.006-3.224zM108.561 75.873l-9.669-6.431c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.006 3.224l9.669 6.43c1.335.889 3.497.885 4.829-.008 1.332-.892 1.329-2.336-.006-3.224z"
/>
</g>
<path
fill="#A6B6D9"
d="M165.384 105.477a.195.195 0 00-.21.328l.21-.328zm.631.866a.195.195 0 00.269-.058.195.195 0 00-.059-.269l-.21.327zm1.892.75a.195.195 0 00-.21.328l.21-.328zm1.471 1.405a.194.194 0 00.211-.327l-.211.327zm1.892.75a.194.194 0 10-.21.327l.21-.327zm1.472 1.405a.195.195 0 00.21-.328l-.21.328zm1.892.75a.195.195 0 00-.211.327l.211-.327zm1.471 1.405a.195.195 0 00.21-.328l-.21.328zm1.892.75a.195.195 0 00-.269.058.195.195 0 00.059.269l.21-.327zm1.471 1.404a.194.194 0 10.211-.327l-.211.327zm1.892.75a.195.195 0 00-.21.328l.21-.328zm1.472 1.405a.195.195 0 00.21-.328l-.21.328zm1.892.75a.194.194 0 10-.21.327l.21-.327zm1.471 1.404a.194.194 0 10.21-.327l-.21.327zm1.892.75a.195.195 0 00-.21.328l.21-.328zm1.472 1.405c.09.058.211.031.269-.059a.195.195 0 00-.059-.269l-.21.328zm1.891.75a.194.194 0 10-.21.327l.21-.327zm-26.276-16.371l.841.538.21-.327-.841-.539-.21.328zm2.523 1.616l1.681 1.077.211-.327-1.682-1.078-.21.328zm3.363 2.154l1.682 1.078.21-.328-1.682-1.077-.21.327zm3.363 2.155l1.682 1.078.21-.328-1.681-1.077-.211.327zm3.364 2.155l1.681 1.077.211-.327-1.682-1.077-.21.327zm3.363 2.155l1.682 1.077.21-.328-1.682-1.077-.21.328zm3.364 2.154l1.681 1.077.21-.327-1.681-1.077-.21.327zm3.363 2.155l1.682 1.077.21-.328-1.682-1.077-.21.328zm3.363 2.154l.841.539.21-.328-.841-.538-.21.327zM156.144 59.553a.194.194 0 00-.227-.316l.227.316zm-1.055.278a.193.193 0 00-.044.271.193.193 0 00.27.045l-.226-.316zm-1.428 1.503a.196.196 0 00.044-.27.194.194 0 00-.271-.045l.227.316zm-1.883.872a.195.195 0 00-.045.272.195.195 0 00.272.044l-.227-.316zm-1.429 1.504a.194.194 0 10-.226-.316l.226.316zm-1.882.872a.194.194 0 00.227.316l-.227-.316zm-1.428 1.504a.194.194 0 00-.227-.316l.227.316zm-1.883.871a.194.194 0 00.227.316l-.227-.316zm-1.428 1.504a.194.194 0 00-.227-.316l.227.316zm-1.882.872a.194.194 0 10.226.316l-.226-.316zm-1.429 1.504a.195.195 0 00.045-.272.195.195 0 00-.272-.044l.227.316zm-1.882.871a.195.195 0 00-.045.272.195.195 0 00.272.044l-.227-.316zm-1.429 1.504a.194.194 0 10-.226-.316l.226.316zm-1.882.872a.194.194 0 00.226.316l-.226-.316zm-1.428 1.504a.194.194 0 00-.227-.316l.227.316zm-1.883.871a.195.195 0 00-.045.272.196.196 0 00.272.044l-.227-.316zm-1.429 1.504a.194.194 0 10-.226-.316l.226.316zm25.433-18.726l-.828.594.226.316.829-.594-.227-.316zm-2.483 1.782l-1.656 1.187.227.316 1.656-1.188-.227-.315zm-3.311 2.375l-1.656 1.188.227.316 1.655-1.188-.226-.316zm-3.311 2.376l-1.656 1.187.227.316 1.656-1.188-.227-.315zm-3.311 2.375l-1.655 1.188.226.316 1.656-1.188-.227-.316zm-3.311 2.376l-1.655 1.187.227.316 1.655-1.188-.227-.316zm-3.31 2.375l-1.656 1.188.226.316 1.656-1.188-.226-.316zm-3.311 2.376l-1.656 1.187.227.316 1.656-1.188-.227-.316zm-3.311 2.375l-.828.594.227.316.827-.594-.226-.316z"
/>
<g filter="url(#filter5_di_571_94747)">
<path
fill="url(#paint10_linear_571_94747)"
stroke="url(#paint11_radial_571_94747)"
strokeWidth="1"
d="M445.685 235.197l-111.727-74.35c-2.9-1.929-7.598-1.921-10.491.019l-56.671 37.993c-2.894 1.94-2.889 5.078.011 7.008l111.727 74.349c2.901 1.931 7.598 1.923 10.492-.017l56.671-37.994c2.894-1.94 2.888-5.077-.012-7.008z"
/>
<path
fill="#005482"
d="M309.137 189.402l-1.776-1.181c-1.676-1.116-4.392-1.112-6.066.01-1.673 1.122-1.669 2.936.008 4.052l1.775 1.181c1.677 1.116 4.393 1.112 6.066-.01 1.673-1.121 1.67-2.936-.007-4.052zM367.484 228.231l-50.466-33.584c-1.677-1.115-4.392-1.111-6.066.011-1.673 1.121-1.67 2.936.007 4.052l50.466 33.583c1.676 1.116 4.392 1.111 6.066-.011 1.673-1.122 1.669-2.936-.007-4.051z"
/>
<path
fill="#794AA8"
d="M363.487 194.528l-6.863-4.566c-1.676-1.115-4.392-1.111-6.065.01-1.674 1.122-1.67 2.937.006 4.052l6.862 4.566c1.677 1.117 4.393 1.112 6.066-.01 1.674-1.122 1.67-2.935-.006-4.052zM372.531 200.547l7.124 4.741c1.678 1.116 1.68 2.93.007 4.052-1.673 1.122-4.389 1.126-6.066.01l-7.125-4.741c-1.676-1.116-1.68-2.93-.006-4.051 1.673-1.122 4.389-1.127 6.066-.011zM388.05 224.206l-41.406-27.553c-1.676-1.116-4.392-1.112-6.065.01-1.674 1.122-1.67 2.936.006 4.052l41.406 27.553c1.677 1.116 4.392 1.112 6.066-.01 1.673-1.121 1.67-2.936-.007-4.052zM428.16 237.567l-37.753-25.124c-1.677-1.116-4.393-1.112-6.066.011-1.673 1.122-1.67 2.935.007 4.051l37.753 25.124c1.677 1.115 4.393 1.11 6.066-.011 1.673-1.122 1.67-2.936-.007-4.051zM336.919 190.181l-17.209-11.452c-1.677-1.116-4.393-1.112-6.066.01-1.673 1.121-1.67 2.936.007 4.052l17.209 11.452c1.678 1.116 4.394 1.112 6.066-.01 1.673-1.122 1.671-2.936-.007-4.052zM297.249 204.086l-4.364-2.904c-1.677-1.116-4.393-1.112-6.066.01-1.673 1.121-1.67 2.936.007 4.052l4.365 2.904c1.677 1.117 4.393 1.112 6.066-.01s1.67-2.935-.008-4.052z"
/>
<path
fill="#1E8DCA"
d="M323.841 221.923l-17.21-11.452c-1.677-1.116-4.393-1.111-6.066.011-1.674 1.122-1.67 2.936.006 4.051l17.211 11.453c1.676 1.115 4.392 1.111 6.066-.01 1.673-1.122 1.669-2.937-.007-4.053z"
/>
<path
fill="#794AA8"
d="M333.862 228.593l24.983 16.625c1.678 1.116 1.68 2.93.007 4.051-1.673 1.122-4.389 1.127-6.066.011l-24.982-16.625c-1.678-1.116-1.681-2.93-.008-4.052 1.673-1.122 4.389-1.126 6.066-.01z"
/>
<path
fill="#FFF388"
d="M346.898 183.49l-17.209-11.452c-1.677-1.116-4.393-1.111-6.066.011-1.673 1.122-1.67 2.935.007 4.051l17.209 11.452c1.678 1.116 4.394 1.112 6.067-.01 1.673-1.121 1.67-2.936-.008-4.052z"
/>
</g>
</g>
</g>
<defs>
<filter
id="filter0_di_571_94747"
width="200.04"
height="143.694"
x="6.762"
y="164.36"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="4" dy="10" />
<feGaussianBlur stdDeviation="7.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_571_94747" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_571_94747" result="shape" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="10.5" />
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.16 0" />
<feBlend in2="shape" result="effect2_innerShadow_571_94747" />
</filter>
<filter
id="filter1_di_571_94747"
width="200.04"
height="143.693"
x="61.391"
y="125.317"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="4" dy="10" />
<feGaussianBlur stdDeviation="7.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_571_94747" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_571_94747" result="shape" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="10.5" />
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.16 0" />
<feBlend in2="shape" result="effect2_innerShadow_571_94747" />
</filter>
<filter
id="filter2_dd_571_94747"
width="147.33"
height="107.019"
x="170.442"
y="81.08"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="4.856" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.16 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_571_94747" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="1.85" dy="4.625" />
<feGaussianBlur stdDeviation="3.469" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend in2="effect1_dropShadow_571_94747" result="effect2_dropShadow_571_94747" />
<feBlend in="SourceGraphic" in2="effect2_dropShadow_571_94747" result="shape" />
</filter>
<filter
id="filter3_d_571_94747"
width="124.658"
height="88.103"
x="107.138"
y="24.755"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="1.85" dy="4.625" />
<feGaussianBlur stdDeviation="3.469" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_571_94747" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_571_94747" result="shape" />
</filter>
<filter
id="filter4_dd_571_94747"
width="128.172"
height="94.149"
x="61.597"
y="44.333"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="4.856" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.16 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_571_94747" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="1.85" dy="4.625" />
<feGaussianBlur stdDeviation="3.469" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend in2="effect1_dropShadow_571_94747" result="effect2_dropShadow_571_94747" />
<feBlend in="SourceGraphic" in2="effect2_dropShadow_571_94747" result="shape" />
</filter>
<filter
id="filter5_di_571_94747"
width="214.235"
height="153.254"
x="253.129"
y="153.905"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="4" dy="10" />
<feGaussianBlur stdDeviation="7.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_571_94747" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_571_94747" result="shape" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="10.5" />
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.16 0" />
<feBlend in2="shape" result="effect2_innerShadow_571_94747" />
</filter>
<linearGradient
id="paint0_linear_571_94747"
x1="125.627"
x2="97.955"
y1="211.358"
y2="252.942"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" stopOpacity="0.17" />
<stop offset="1" stopColor="#fff" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint1_radial_571_94747"
cx="0"
cy="0"
r="1"
gradientTransform="rotate(111.976 -27.866 125.376) scale(37.7222 126.429)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.007" stopColor="#5E6E8C" />
<stop offset="0.196" stopColor="#5E6E8C" />
<stop offset="1" stopColor="#5E6E8C" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint2_linear_571_94747"
x1="180.256"
x2="152.584"
y1="172.315"
y2="213.899"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" stopOpacity="0.17" />
<stop offset="1" stopColor="#fff" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint3_radial_571_94747"
cx="0"
cy="0"
r="1"
gradientTransform="rotate(111.976 12.622 124.287) scale(37.7222 126.429)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.007" stopColor="#5E6E8C" />
<stop offset="0.196" stopColor="#5E6E8C" />
<stop offset="1" stopColor="#5E6E8C" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint4_linear_571_94747"
x1="272.103"
x2="237.918"
y1="115.17"
y2="166.542"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" stopOpacity="0.17" />
<stop offset="1" stopColor="#fff" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint5_radial_571_94747"
cx="0"
cy="0"
r="1"
gradientTransform="rotate(130.436 91.386 110.983) scale(45.9602 75.3358)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.007" stopColor="#5E6E8C" />
<stop offset="0.196" stopColor="#5E6E8C" />
<stop offset="1" stopColor="#5E6E8C" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint6_linear_571_94747"
x1="183.476"
x2="164.24"
y1="53.846"
y2="82.751"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" stopOpacity="0.17" />
<stop offset="1" stopColor="#fff" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint7_radial_571_94747"
cx="0"
cy="0"
r="1"
gradientTransform="matrix(-10.98 23.53919 -73.33289 -34.2066 152.416 46.03)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.007" stopColor="#5E6E8C" />
<stop offset="0.196" stopColor="#5E6E8C" />
<stop offset="1" stopColor="#5E6E8C" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint8_linear_571_94747"
x1="141.536"
x2="122.301"
y1="80.142"
y2="109.048"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" stopOpacity="0.17" />
<stop offset="1" stopColor="#fff" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint9_radial_571_94747"
cx="0"
cy="0"
r="1"
gradientTransform="rotate(115.807 32.713 71.158) scale(25.9216 79.0405)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.007" stopColor="#5E6E8C" />
<stop offset="0.196" stopColor="#5E6E8C" />
<stop offset="1" stopColor="#5E6E8C" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint10_linear_571_94747"
x1="390.559"
x2="348.755"
y1="197.995"
y2="260.815"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#fff" stopOpacity="0.17" />
<stop offset="1" stopColor="#fff" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint11_radial_571_94747"
cx="0"
cy="0"
r="1"
gradientTransform="matrix(-32.34734 45.5104 -98.19276 -69.79228 338.08 191.004)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.007" stopColor="#5E6E8C" />
<stop offset="0.196" stopColor="#5E6E8C" />
<stop offset="1" stopColor="#5E6E8C" stopOpacity="0" />
</radialGradient>
<clipPath id="clip0_571_94747">
<path fill="#fff" d="M0 0H491.892V280H0z" transform="translate(.054 24.5)" />
</clipPath>
</defs>
</svg>
)
const ContextDiagramLight: FC<Pick<ContextDiagramProps, 'className'>> = ({ className }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="492"
height="329"
fill="none"
viewBox="0 0 492 329"
className={className}
>
<g clipPath="url(#clip0_701_6419)">
<mask id="mask0_701_6419" width="492" height="281" x="0" y="24" maskUnits="userSpaceOnUse">
<path fill="#fff" d="M491.946 24.5H.054v280h491.892v-280z" />
</mask>
<g mask="url(#mask0_701_6419)">
<g filter="url(#filter0_di_701_6419)">
<path
fill="#fff"
stroke="#DBE2F0"
d="M64.379 171.719h0l120.467 80.166c1.348.897 1.954 2.023 1.956 3.085.002 1.062-.601 2.191-1.946 3.092l-33.734 22.616.278.415-.278-.415c-1.349.904-3.144 1.373-4.966 1.376-1.822.003-3.619-.459-4.971-1.358 0 0 0 0 0 0L20.718 200.529h0c-1.347-.896-1.954-2.023-1.956-3.085-.002-1.062.601-2.19 1.945-3.091h0l33.736-22.617c1.348-.904 3.144-1.372 4.965-1.375 1.822-.004 3.62.459 4.97 1.358z"
/>
<path
fill="#A6B6D9"
d="M137.496 230.016l-34.325-22.843c-1.791-1.192-4.692-1.187-6.478.011-1.787 1.199-1.784 3.135.007 4.327l34.326 22.843c1.791 1.191 4.691 1.186 6.478-.011 1.787-1.198 1.783-3.136-.008-4.327zM146.629 236.094l21.307 14.178c1.791 1.192 1.794 3.129.007 4.327-1.787 1.198-4.687 1.203-6.478.011l-21.306-14.178c-1.791-1.191-1.794-3.129-.007-4.327 1.786-1.197 4.687-1.202 6.477-.011zM93.29 200.598l-1.977-1.316c-1.791-1.191-4.691-1.186-6.478.012-1.786 1.197-1.783 3.135.008 4.327l1.976 1.315c1.79 1.192 4.692 1.186 6.478-.012 1.787-1.197 1.784-3.134-.007-4.326z"
/>
<path
fill="#DBE2F0"
d="M120.733 234.085l-18.693-12.439c-1.791-1.192-4.692-1.187-6.478.011-1.787 1.197-1.784 3.135.007 4.327l18.694 12.439c1.79 1.192 4.691 1.188 6.478-.011 1.787-1.198 1.783-3.135-.008-4.327zM137.299 259.346l-41.187-27.408c-1.791-1.192-4.691-1.187-6.478.011-1.787 1.197-1.784 3.134.007 4.327l41.187 27.408c1.791 1.191 4.691 1.186 6.478-.012 1.787-1.197 1.784-3.135-.007-4.326zM80.597 221.613l4.3 2.862c1.79 1.191 1.794 3.128.007 4.327-1.787 1.198-4.687 1.202-6.478.011l-4.3-2.862c-1.79-1.191-1.794-3.129-.007-4.327 1.787-1.197 4.687-1.202 6.478-.011zM150.498 253.893l-20.368-13.554c-1.791-1.193-4.691-1.188-6.478.011-1.787 1.198-1.784 3.134.007 4.327l20.369 13.554c1.791 1.191 4.691 1.186 6.478-.011 1.786-1.198 1.783-3.135-.008-4.327zM71.008 215.233l-2.135-1.42c-1.79-1.193-4.69-1.188-6.478.011-1.787 1.198-1.783 3.134.008 4.327l2.134 1.42c1.791 1.192 4.692 1.187 6.478-.011 1.787-1.198 1.784-3.135-.007-4.327z"
/>
<path
fill="#A6B6D9"
d="M92.503 215.298l-12.971-8.631c-1.791-1.192-4.691-1.187-6.478.011s-1.784 3.135.007 4.327l12.97 8.631c1.792 1.193 4.693 1.188 6.48-.011 1.786-1.198 1.783-3.135-.008-4.327z"
/>
<path
fill="#C5C5EC"
d="M55.67 190.103l3.95 2.629-.008-5.272-3.943 2.643zm-.012-6.71l5.747 3.824.01 5.751-8.6 5.766c-.381.255-.896.399-1.435.399-.539.002-1.055-.14-1.436-.394l-8.62-5.736c-.381-.254-.595-.598-.596-.958 0-.359.213-.705.593-.959l11.467-7.688c.795-.533 2.073-.536 2.87-.005zm-5.002 11.99l1.433-.961-6.465-4.302-1.433.961 6.465 4.302zm5.022-.487l1.434-.962-8.62-5.736-1.434.96 8.62 5.738z"
/>
<path stroke="#fff" strokeLinecap="round" strokeOpacity="0.2" d="M50.253 212.974l32.554-21.968" />
</g>
<g filter="url(#filter1_di_701_6419)">
<path
fill="#fff"
stroke="#DBE2F0"
d="M75.337 155.31v-.001l33.735-22.616s0 0 0 0c1.348-.904 3.144-1.373 4.966-1.376 1.821-.003 3.619.459 4.97 1.358 0 0 0 0 0 0l120.468 80.167c1.347.896 1.953 2.022 1.955 3.084.002 1.062-.601 2.191-1.945 3.092l.278.415-.278-.415-33.735 22.617c-1.348.903-3.144 1.372-4.966 1.375-1.821.003-3.619-.459-4.97-1.359l-.277.417.277-.417-120.468-80.165v-.001c-1.347-.896-1.954-2.023-1.956-3.084-.001-1.062.602-2.19 1.946-3.091z"
/>
<path
fill="#DBE2F0"
d="M192.126 190.972L157.8 168.129c-1.791-1.191-4.692-1.186-6.479.012-1.787 1.197-1.783 3.134.008 4.326l34.325 22.843c1.791 1.192 4.692 1.186 6.479-.012 1.787-1.197 1.784-3.134-.007-4.326zM201.259 197.051l21.306 14.178c1.791 1.191 1.794 3.129.007 4.326-1.786 1.198-4.687 1.203-6.478.012l-21.305-14.179c-1.791-1.192-1.795-3.128-.008-4.327 1.787-1.198 4.687-1.203 6.478-.01zM147.918 161.554l-1.976-1.315c-1.791-1.192-4.691-1.187-6.477.011-1.787 1.198-1.784 3.135.007 4.327l1.976 1.315c1.791 1.192 4.691 1.187 6.478-.011 1.787-1.199 1.783-3.135-.008-4.327z"
/>
<path
fill="#A6B6D9"
d="M175.363 195.042l-18.694-12.44c-1.791-1.192-4.691-1.187-6.478.01-1.787 1.199-1.783 3.136.007 4.327l18.694 12.441c1.791 1.191 4.691 1.186 6.477-.011 1.788-1.198 1.785-3.136-.006-4.327z"
/>
<path
fill="#DBE2F0"
d="M191.928 220.302l-41.187-27.408c-1.791-1.192-4.691-1.187-6.478.01-1.786 1.199-1.783 3.136.008 4.327l41.186 27.408c1.791 1.193 4.692 1.188 6.479-.01 1.786-1.199 1.783-3.135-.008-4.327z"
/>
<path
fill="#A6B6D9"
d="M135.227 182.57l4.299 2.861c1.791 1.191 1.794 3.129.008 4.327-1.787 1.197-4.687 1.202-6.478.011l-4.3-2.862c-1.791-1.191-1.795-3.128-.008-4.326 1.787-1.199 4.688-1.204 6.479-.011z"
/>
<path
fill="#DBE2F0"
d="M205.128 214.85l-20.369-13.556c-1.791-1.191-4.691-1.186-6.478.012-1.787 1.197-1.783 3.135.008 4.327l20.368 13.554c1.791 1.192 4.691 1.187 6.478-.011s1.784-3.135-.007-4.326z"
/>
<path
fill="#A6B6D9"
d="M125.637 176.189l-2.135-1.421c-1.79-1.191-4.691-1.186-6.478.012-1.787 1.197-1.783 3.135.008 4.326l2.135 1.421c1.791 1.192 4.691 1.187 6.478-.011 1.786-1.198 1.783-3.135-.008-4.327z"
/>
<path
fill="#DBE2F0"
d="M147.132 176.255l-12.971-8.632c-1.791-1.192-4.691-1.187-6.478.012-1.787 1.197-1.784 3.134.007 4.326l12.971 8.632c1.791 1.191 4.691 1.186 6.478-.011 1.787-1.198 1.784-3.136-.007-4.327z"
/>
<path
fill="#C5C5EC"
d="M110.298 151.059l3.952 2.629-.01-5.272-3.942 2.643zm-.011-6.71l5.747 3.824.01 5.752-8.601 5.765c-.38.255-.896.399-1.434.4-.539.001-1.055-.141-1.436-.395l-8.62-5.736c-.381-.254-.596-.598-.596-.958 0-.359.213-.704.593-.959l11.467-7.688c.795-.533 2.073-.535 2.87-.005zm-5.002 11.991l1.433-.961-6.465-4.303-1.433.961 6.465 4.303zm5.022-.488l1.433-.961-8.62-5.736-1.433.96 8.62 5.737z"
/>
<path stroke="#fff" strokeLinecap="round" strokeOpacity="0.2" d="M104.883 173.93l32.554-21.968" />
</g>
<g filter="url(#filter2_dd_701_6419)">
<path
fill="#fff"
stroke="#DBE2F0"
d="M306.539 139.328h0c.302.201.444.457.472.769.03.335-.069.775-.35 1.304-.56 1.058-1.761 2.317-3.509 3.489l-41.675 27.939.279.416-.279-.416c-1.742 1.169-3.651 1.998-5.319 2.397-.834.199-1.593.288-2.233.264-.646-.024-1.131-.162-1.452-.375l-.277.416.277-.416-70.798-47.113s0 0 0 0c-.302-.201-.443-.457-.471-.769-.03-.336.069-.776.35-1.305.56-1.058 1.761-2.316 3.51-3.489h0l41.674-27.94h0c1.742-1.167 3.651-1.996 5.319-2.395.833-.2 1.593-.288 2.233-.264.646.024 1.131.161 1.452.375 0 0 0 0 0 0l70.797 47.113z"
/>
<path
fill="#DBE2F0"
d="M271.145 121.311L234.743 97.1c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.329 2.336.006 3.224l36.401 24.211c1.335.888 3.497.884 4.829-.009 1.332-.892 1.33-2.335-.005-3.223zM256.675 123.029l-13.935-9.268c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.329 2.336.006 3.224l13.934 9.268c1.335.888 3.497.885 4.829-.008 1.332-.892 1.33-2.336-.005-3.224z"
/>
<path
fill="#A6B6D9"
d="M259.537 135.54l-24.741-16.455c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.005 3.224l24.742 16.455c1.335.888 3.497.885 4.829-.008 1.332-.892 1.329-2.336-.006-3.224zM286.251 142.7l-22.467-14.942c-1.335-.888-3.497-.884-4.829.008-1.331.893-1.329 2.336.006 3.224l22.466 14.942c1.335.888 3.497.885 4.829-.008 1.332-.892 1.33-2.336-.005-3.224zM227.686 114.356l-9.669-6.431c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.33 2.336.005 3.224l9.669 6.431c1.335.888 3.497.884 4.829-.008 1.332-.893 1.33-2.336-.005-3.224zM206.435 130.13l-9.669-6.431c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.329 2.336.006 3.224l9.669 6.431c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224z"
/>
<path
fill="#DBE2F0"
d="M223.863 142.338l-9.669-6.431c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.329 2.336.006 3.224l9.669 6.431c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224zM235.63 109.033l-9.669-6.431c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.329 2.336.006 3.224l9.669 6.431c1.335.888 3.497.884 4.829-.009 1.332-.892 1.329-2.335-.006-3.223z"
/>
</g>
<g filter="url(#filter3_d_701_6419)">
<path
fill="#fff"
stroke="#DBE2F0"
d="M138.103 29.108h0c.915-.614 2.104-.955 3.288-.993 1.187-.039 2.317.228 3.128.768l76.376 50.825c.8.532 1.108 1.201 1.066 1.832-.044.646-.463 1.38-1.38 1.994l-23.45 15.721.279.415-.279-.415c-.916.614-2.104.955-3.288.993-1.187.039-2.316-.228-3.128-.768l-76.376-50.825c-.8-.532-1.108-1.201-1.066-1.832.043-.646.463-1.38 1.38-1.994h0l23.45-15.721z"
/>
<path
fill="#DBE2F0"
d="M187.645 61.991l-36.401-24.21c-1.335-.889-3.497-.885-4.829.007-1.332.893-1.33 2.336.005 3.224l36.402 24.211c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224z"
/>
<path
fill="#A6B6D9"
d="M173.175 63.71l-22.524-14.98c-1.335-.889-3.497-.885-4.829.008-1.332.892-1.329 2.335.006 3.223l22.523 14.98c1.335.889 3.497.885 4.829-.007 1.332-.893 1.33-2.336-.005-3.224z"
/>
<path
fill="#DBE2F0"
d="M176.588 76.603l-24.741-16.456c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.33 2.336.005 3.224l24.742 16.456c1.335.888 3.497.884 4.829-.008 1.332-.893 1.329-2.336-.006-3.224zM202.75 83.381L180.284 68.44c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.006 3.224l22.466 14.942c1.335.888 3.497.885 4.829-.008 1.332-.892 1.329-2.336-.006-3.224z"
/>
<path
fill="#A6B6D9"
d="M136.559 49.964l-2.043-1.359c-1.335-.888-3.497-.884-4.829.009-1.332.892-1.329 2.335.006 3.223l2.042 1.359c1.335.888 3.497.884 4.829-.008 1.332-.893 1.33-2.336-.005-3.224zM145.003 55.709l-2.042-1.359c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.33 2.336.005 3.224l2.043 1.359c1.335.888 3.497.884 4.829-.009 1.332-.892 1.329-2.336-.006-3.223z"
/>
<path
fill="#DBE2F0"
d="M143.793 44.169l-1.333-.887c-1.335-.888-3.497-.884-4.829.008-1.331.893-1.329 2.336.006 3.224l1.333.886c1.335.888 3.497.885 4.829-.008 1.332-.892 1.329-2.336-.006-3.224z"
/>
</g>
<g filter="url(#filter4_dd_701_6419)">
<path
fill="#fff"
stroke="#DBE2F0"
d="M97.214 56.104h0c.918-.616 2.1-.962 3.268-1.01 1.173-.048 2.279.206 3.065.729l74.452 49.545c.772.514 1.061 1.164 1.008 1.784-.056.639-.485 1.37-1.405 1.986l.278.416-.278-.416-23.45 15.721c-.918.616-2.1.962-3.268 1.01-1.173.048-2.279-.205-3.064-.728L73.367 75.595l-.277.417.277-.416c-.773-.514-1.061-1.164-1.008-1.784.056-.64.485-1.37 1.406-1.987l23.45-15.721z"
/>
<path
fill="#A6B6D9"
d="M142.102 86.838l-25.588-17.02c-1.335-.887-3.497-.883-4.829.01-1.332.892-1.33 2.335.005 3.223l25.588 17.019c1.335.888 3.497.884 4.829-.009 1.332-.892 1.33-2.336-.005-3.223zM148.91 91.366l15.883 10.564c1.335.888 1.337 2.331.005 3.223-1.332.893-3.494.897-4.829.009l-15.882-10.564c-1.335-.888-1.338-2.331-.006-3.224 1.332-.892 3.494-.896 4.829-.008z"
/>
<path
fill="#DBE2F0"
d="M109.148 64.92l-1.473-.98c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.005 3.224l1.474.98c1.335.888 3.497.884 4.829-.009 1.332-.892 1.329-2.335-.006-3.224zM129.606 89.87L115.671 80.6c-1.335-.888-3.497-.884-4.829.009-1.332.892-1.329 2.335.006 3.223l13.935 9.269c1.335.888 3.497.884 4.829-.009 1.332-.892 1.329-2.336-.006-3.224zM141.954 108.69l-30.702-20.42c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.005 3.223l30.703 20.421c1.335.888 3.497.884 4.829-.008 1.331-.893 1.329-2.336-.006-3.224zM99.687 80.577l3.205 2.132c1.335.888 1.338 2.331.006 3.224-1.332.892-3.494.896-4.829.008l-3.205-2.132c-1.335-.888-1.338-2.33-.006-3.223 1.332-.893 3.494-.897 4.83-.009z"
/>
<path
fill="#A6B6D9"
d="M151.794 104.627L136.61 94.528c-1.335-.888-3.497-.884-4.829.009-1.332.892-1.33 2.335.005 3.223l15.184 10.099c1.335.888 3.497.884 4.829-.008 1.332-.893 1.33-2.336-.005-3.224zM92.54 75.823l-1.592-1.058c-1.335-.888-3.497-.884-4.829.008-1.332.893-1.33 2.336.006 3.224l1.59 1.058c1.336.888 3.498.885 4.83-.008 1.332-.892 1.33-2.336-.006-3.224z"
/>
<path
fill="#DBE2F0"
d="M108.561 75.873l-9.669-6.431c-1.335-.888-3.497-.885-4.829.008-1.332.892-1.33 2.336.006 3.224l9.669 6.43c1.335.889 3.497.885 4.829-.008 1.332-.892 1.329-2.336-.006-3.224z"
/>
</g>
<path
fill="#A6B6D9"
d="M165.384 105.477a.195.195 0 00-.21.328l.21-.328zm.631.866a.195.195 0 00.269-.058.195.195 0 00-.059-.269l-.21.327zm1.892.75a.195.195 0 00-.21.328l.21-.328zm1.471 1.405a.194.194 0 00.211-.327l-.211.327zm1.892.75a.194.194 0 10-.21.327l.21-.327zm1.472 1.405c.09.058.211.031.269-.059a.195.195 0 00-.059-.269l-.21.328zm1.892.75a.195.195 0 00-.211.327l.211-.327zm1.471 1.405a.195.195 0 00.21-.328l-.21.328zm1.892.75a.195.195 0 00-.269.058.195.195 0 00.059.269l.21-.327zm1.471 1.404a.194.194 0 00.211-.327l-.211.327zm1.892.75a.195.195 0 00-.21.328l.21-.328zm1.472 1.405a.195.195 0 00.21-.328l-.21.328zm1.892.75a.194.194 0 00-.211.327l.211-.327zm1.471 1.404a.194.194 0 10.21-.327l-.21.327zm1.892.75a.195.195 0 00-.21.328l.21-.328zm1.472 1.405c.09.058.211.031.269-.059a.195.195 0 00-.059-.269l-.21.328zm1.891.75a.194.194 0 10-.21.327l.21-.327zm-26.276-16.371l.841.538.21-.327-.841-.539-.21.328zm2.523 1.616l1.681 1.077.211-.327-1.682-1.078-.21.328zm3.363 2.154l1.682 1.078.21-.328-1.682-1.077-.21.327zm3.363 2.155l1.682 1.078.21-.328-1.681-1.077-.211.327zm3.364 2.155l1.681 1.077.211-.327-1.682-1.077-.21.327zm3.363 2.155l1.682 1.077.21-.328-1.682-1.077-.21.328zm3.363 2.154l1.682 1.077.21-.327-1.681-1.077-.211.327zm3.364 2.155l1.682 1.077.21-.328-1.682-1.077-.21.328zm3.363 2.154l.841.539.21-.328-.841-.538-.21.327zM156.144 59.553a.194.194 0 00-.227-.316l.227.316zm-1.055.278a.194.194 0 00.226.316l-.226-.316zm-1.428 1.503a.196.196 0 00.044-.27.194.194 0 00-.271-.045l.227.316zm-1.883.872a.195.195 0 00-.045.272.195.195 0 00.272.044l-.227-.316zm-1.429 1.504a.194.194 0 10-.226-.316l.226.316zm-1.881.872a.194.194 0 10.226.316l-.226-.316zm-1.429 1.504a.195.195 0 00.045-.272.195.195 0 00-.272-.044l.227.316zm-1.883.871a.194.194 0 00.227.316l-.227-.316zm-1.428 1.504a.194.194 0 00-.226-.316l.226.316zm-1.882.872a.194.194 0 10.226.316l-.226-.316zm-1.429 1.504a.195.195 0 00.045-.272.195.195 0 00-.272-.044l.227.316zm-1.882.871a.195.195 0 00-.045.272.195.195 0 00.272.044l-.227-.316zm-1.429 1.504a.194.194 0 10-.226-.316l.226.316zm-1.882.872a.194.194 0 00.226.316l-.226-.316zm-1.428 1.504a.194.194 0 00-.227-.316l.227.316zm-1.883.871a.195.195 0 00-.045.272.196.196 0 00.272.044l-.227-.316zm-1.429 1.504a.194.194 0 10-.226-.316l.226.316zm25.433-18.726l-.828.594.226.316.829-.594-.227-.316zm-2.483 1.782l-1.656 1.187.227.316 1.656-1.188-.227-.315zm-3.311 2.375l-1.655 1.188.226.316 1.655-1.188-.226-.316zm-3.311 2.376l-1.656 1.187.227.316 1.656-1.188-.227-.315zm-3.31 2.375l-1.656 1.188.226.316 1.656-1.188-.226-.316zm-3.312 2.376l-1.655 1.187.227.316 1.655-1.188-.227-.316zm-3.31 2.375l-1.656 1.188.226.316 1.656-1.188-.226-.316zm-3.311 2.376l-1.656 1.187.227.316 1.656-1.188-.227-.316zm-3.311 2.375l-.828.594.227.316.827-.594-.226-.316z"
/>
<g filter="url(#filter5_di_701_6419)">
<path
fill="#fff"
stroke="#DBE2F0"
d="M267.074 199.275h0l56.671-37.994-.278-.415.278.415c1.348-.904 3.144-1.372 4.966-1.375 1.821-.004 3.619.459 4.97 1.358l111.727 74.349c1.347.897 1.954 2.023 1.956 3.085.002 1.062-.601 2.191-1.945 3.092l-56.671 37.994c-1.349.903-3.145 1.372-4.966 1.375-1.822.003-3.619-.459-4.971-1.359l-.277.416.277-.416-111.727-74.349-.277.416.277-.416c-1.347-.897-1.953-2.023-1.955-3.085-.002-1.062.601-2.19 1.945-3.091z"
/>
<path
fill="#A6B6D9"
d="M309.137 189.402l-1.776-1.181c-1.677-1.116-4.392-1.112-6.066.01-1.673 1.122-1.67 2.936.008 4.052l1.775 1.181c1.676 1.116 4.392 1.112 6.066-.01 1.673-1.121 1.67-2.936-.007-4.052z"
/>
<path
fill="#DBE2F0"
d="M367.484 228.231l-50.466-33.584c-1.677-1.115-4.392-1.111-6.066.011-1.673 1.121-1.67 2.936.007 4.052l50.466 33.583c1.676 1.116 4.392 1.111 6.066-.011 1.673-1.122 1.669-2.936-.007-4.051zM363.486 194.528l-6.862-4.566c-1.676-1.115-4.392-1.111-6.065.01-1.674 1.122-1.67 2.937.006 4.052l6.862 4.566c1.677 1.117 4.393 1.112 6.066-.01s1.67-2.935-.007-4.052zM372.531 200.547l7.124 4.741c1.678 1.116 1.68 2.93.007 4.052-1.673 1.122-4.389 1.126-6.066.01l-7.125-4.741c-1.676-1.116-1.68-2.93-.006-4.051 1.673-1.122 4.389-1.127 6.066-.011z"
/>
<path
fill="#A6B6D9"
d="M388.05 224.206l-41.406-27.553c-1.676-1.116-4.392-1.112-6.065.01-1.674 1.122-1.67 2.936.006 4.052l41.406 27.553c1.676 1.116 4.392 1.112 6.066-.01 1.673-1.121 1.67-2.936-.007-4.052zM428.16 237.567l-37.753-25.124c-1.677-1.116-4.393-1.112-6.066.011-1.673 1.122-1.67 2.935.007 4.051l37.753 25.124c1.677 1.115 4.393 1.11 6.066-.011 1.673-1.122 1.67-2.936-.007-4.051zM336.919 190.181l-17.209-11.452c-1.677-1.116-4.393-1.112-6.066.01-1.673 1.121-1.67 2.936.007 4.052l17.209 11.452c1.678 1.116 4.394 1.112 6.066-.01 1.673-1.122 1.671-2.936-.007-4.052zM297.249 204.086l-4.364-2.904c-1.677-1.116-4.393-1.112-6.066.01-1.673 1.121-1.67 2.936.007 4.052l4.365 2.904c1.677 1.117 4.393 1.112 6.066-.01s1.67-2.935-.008-4.052z"
/>
<path
fill="#DBE2F0"
d="M323.841 221.923l-17.211-11.452c-1.676-1.116-4.392-1.111-6.065.011-1.674 1.122-1.67 2.936.006 4.051l17.211 11.453c1.676 1.115 4.392 1.111 6.066-.01 1.673-1.122 1.669-2.937-.007-4.053zM333.862 228.593l24.983 16.625c1.678 1.116 1.68 2.93.007 4.051-1.673 1.122-4.389 1.127-6.066.011l-24.982-16.625c-1.678-1.116-1.681-2.93-.008-4.052 1.673-1.122 4.389-1.126 6.066-.01zM346.898 183.49l-17.209-11.452c-1.677-1.116-4.393-1.111-6.066.011-1.673 1.122-1.67 2.935.007 4.051l17.209 11.452c1.678 1.116 4.394 1.112 6.067-.01 1.673-1.121 1.67-2.936-.008-4.052z"
/>
</g>
</g>
</g>
<defs>
<filter
id="filter0_di_701_6419"
width="199.04"
height="142.693"
x="7.262"
y="164.861"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="4" dy="10" />
<feGaussianBlur stdDeviation="7.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_701_6419" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_701_6419" result="shape" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="10.5" />
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.04 0" />
<feBlend in2="shape" result="effect2_innerShadow_701_6419" />
</filter>
<filter
id="filter1_di_701_6419"
width="199.04"
height="142.693"
x="61.892"
y="125.817"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="4" dy="10" />
<feGaussianBlur stdDeviation="7.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_701_6419" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_701_6419" result="shape" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="10.5" />
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.04 0" />
<feBlend in2="shape" result="effect2_innerShadow_701_6419" />
</filter>
<filter
id="filter2_dd_701_6419"
width="146.242"
height="105.932"
x="170.986"
y="81.624"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="4.856" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.08 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_701_6419" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="1.85" dy="4.625" />
<feGaussianBlur stdDeviation="3.469" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" />
<feBlend in2="effect1_dropShadow_701_6419" result="effect2_dropShadow_701_6419" />
<feBlend in="SourceGraphic" in2="effect2_dropShadow_701_6419" result="shape" />
</filter>
<filter
id="filter3_d_701_6419"
width="123.57"
height="87.016"
x="107.682"
y="25.299"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="1.85" dy="4.625" />
<feGaussianBlur stdDeviation="3.469" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_701_6419" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_701_6419" result="shape" />
</filter>
<filter
id="filter4_dd_701_6419"
width="127.084"
height="93.061"
x="62.141"
y="44.876"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="4.856" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.08 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_701_6419" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="1.85" dy="4.625" />
<feGaussianBlur stdDeviation="3.469" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" />
<feBlend in2="effect1_dropShadow_701_6419" result="effect2_dropShadow_701_6419" />
<feBlend in="SourceGraphic" in2="effect2_dropShadow_701_6419" result="shape" />
</filter>
<filter
id="filter5_di_701_6419"
width="213.235"
height="152.254"
x="253.629"
y="154.406"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dx="4" dy="10" />
<feGaussianBlur stdDeviation="7.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_701_6419" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_701_6419" result="shape" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="10.5" />
<feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic" />
<feColorMatrix values="0 0 0 0 0.631373 0 0 0 0 0.0705882 0 0 0 0 1 0 0 0 0.04 0" />
<feBlend in2="shape" result="effect2_innerShadow_701_6419" />
</filter>
<clipPath id="clip0_701_6419">
<path fill="#fff" d="M0 0H491.892V280H0z" transform="translate(.054 24.5)" />
</clipPath>
</defs>
</svg>
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,16 +1,16 @@
import { useState, useEffect, useCallback, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { noop } from 'lodash'
import type {
TranscriptJSON,
TranscriptJSONScope,
CodyClient,
CodyClientScope,
CodyClientConfig,
CodyClientEvent,
CodyClientScope,
TranscriptJSON,
TranscriptJSONScope,
} from '@sourcegraph/cody-shared'
import { Transcript, useClient, NoopEditor } from '@sourcegraph/cody-shared'
import { NoopEditor, Transcript, useClient } from '@sourcegraph/cody-shared'
import type { Scalars } from '@sourcegraph/shared/src/graphql-operations'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { EVENT_LOGGER } from '@sourcegraph/shared/src/telemetry/web/eventLogger'
@ -18,8 +18,8 @@ import { useLocalStorage } from '@sourcegraph/wildcard'
import { EventName } from '../util/constants'
import { isEmailVerificationNeededForCody } from './isCodyEnabled'
import { useCodyIgnore } from './useCodyIgnore'
import { currentUserRequiresEmailVerificationForCody } from './util'
export interface CodyChatStore
extends Pick<
@ -179,7 +179,7 @@ export const useCodyChat = ({
accessToken: null,
customHeaders: window.context.xhrHeaders,
debugEnable: false,
needsEmailVerification: isEmailVerificationNeededForCody(),
needsEmailVerification: currentUserRequiresEmailVerificationForCody(),
experimentalLocalSymbols: false,
},
scope: initialScope,

View File

@ -1,8 +1,8 @@
import React, { createContext, useCallback, useMemo, useContext, useEffect, useState } from 'react'
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { type ApolloClient, type ApolloQueryResult, useApolloClient } from '@apollo/client'
import { useApolloClient, type ApolloClient, type ApolloQueryResult } from '@apollo/client'
import { useQuery, gql, getDocumentNode } from '@sourcegraph/http-client'
import { getDocumentNode, gql, useQuery } from '@sourcegraph/http-client'
import type {
CodyIgnoreContentResult,
@ -11,8 +11,6 @@ import type {
ContextFiltersVariables,
} from '../graphql-operations'
import { isCodyEnabled } from './isCodyEnabled'
interface CodyIgnoreFns {
isRepoIgnored(repoName: string): boolean
isFileIgnored(repoName: string, filePath: string): boolean
@ -49,7 +47,7 @@ export const CodyIgnoreProvider: React.FC<React.PropsWithChildren<{ isSourcegrap
children,
}) => {
// Cody is not enabled, return default ignore fns.
if (!isCodyEnabled()) {
if (!window.context?.codyEnabledForCurrentUser) {
return <CodyIgnoreContext.Provider value={defaultCodyIgnoreFns}>{children}</CodyIgnoreContext.Provider>
}

View File

@ -42,3 +42,10 @@ export function isValidEmailAddress(emailAddress: string): boolean {
* and keep in mind that the backend validation has the final say, validation in the web app is only for UX improvement.
*/
const emailRegex = /^[^@]+@[^@]+\.[^@]+$/
/**
* Whether the current user is unable to use Cody because they must verify their email address.
*/
export function currentUserRequiresEmailVerificationForCody(): boolean {
return window.context?.codyRequiresVerifiedEmail && !window.context?.currentUser?.hasVerifiedEmail
}

View File

@ -72,8 +72,4 @@ export const legacyLayoutRouteContextMock = {
...dynamicWebAppConfig,
...legacyRouteComputedContext,
...legacyRouteInjectedContext,
licenseFeatures: {
isCodeSearchEnabled: true,
isCodyEnabled: true,
},
} satisfies LegacyLayoutRouteContext

View File

@ -20,11 +20,6 @@ const config: Meta = {
export default config
const licenseFeatures = {
isCodeSearchEnabled: true,
isCodyEnabled: true,
}
// Moved story under enterprise folder to avoid failing ci linting
// due to importing enterprise path in oss folders.
export const AdminSidebarItems: StoryFn = () => (
@ -44,7 +39,6 @@ export const AdminSidebarItems: StoryFn = () => (
batchChangesWebhookLogsEnabled={true}
codeInsightsEnabled={true}
endUserOnboardingEnabled={false}
license={licenseFeatures}
/>
<SiteAdminSidebar
{...webProps}
@ -55,7 +49,6 @@ export const AdminSidebarItems: StoryFn = () => (
batchChangesWebhookLogsEnabled={true}
codeInsightsEnabled={true}
endUserOnboardingEnabled={false}
license={licenseFeatures}
/>
<SiteAdminSidebar
{...webProps}
@ -66,7 +59,6 @@ export const AdminSidebarItems: StoryFn = () => (
batchChangesWebhookLogsEnabled={false}
codeInsightsEnabled={true}
endUserOnboardingEnabled={false}
license={licenseFeatures}
/>
<SiteAdminSidebar
{...webProps}
@ -77,7 +69,6 @@ export const AdminSidebarItems: StoryFn = () => (
batchChangesWebhookLogsEnabled={true}
codeInsightsEnabled={false}
endUserOnboardingEnabled={false}
license={licenseFeatures}
/>
</Grid>
)}

View File

@ -6,10 +6,10 @@ import { renderMarkdown } from '@sourcegraph/common'
import type { Notice } from '@sourcegraph/shared/src/schema/settings.schema'
import { useSettings } from '@sourcegraph/shared/src/settings/settings'
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { Alert, type AlertProps, Markdown } from '@sourcegraph/wildcard'
import { Alert, Markdown, type AlertProps } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../auth'
import { isEmailVerificationNeededForCody } from '../cody/isCodyEnabled'
import { currentUserRequiresEmailVerificationForCody } from '../cody/util'
import { DismissibleAlert } from '../components/DismissibleAlert'
import styles from './Notices.module.scss'
@ -103,11 +103,11 @@ export const VerifyEmailNotices: React.FunctionComponent<VerifyEmailNoticesProps
telemetryRecorder,
}) => {
useEffect(() => {
if (isEmailVerificationNeededForCody() && authenticatedUser) {
if (currentUserRequiresEmailVerificationForCody() && authenticatedUser) {
telemetryRecorder.recordEvent('alert.verifyEmail', 'view')
}
}, [telemetryRecorder, authenticatedUser])
if (isEmailVerificationNeededForCody() && authenticatedUser) {
if (currentUserRequiresEmailVerificationForCody() && authenticatedUser) {
return (
<div className={classNames(styles.notices, className)}>
<NoticeAlert

View File

@ -31,7 +31,8 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
batchChangesWebhookLogsEnabled: true,
codeInsightsEnabled: true,
executorsEnabled: true,
codyEnabled: true,
codyEnabledOnInstance: true,
codeSearchEnabledOnInstance: true,
codeIntelligenceEnabled: true,
codeMonitoringEnabled: true,
notebooksEnabled: true,
@ -57,10 +58,6 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
maxNumChangesets: -1,
unrestricted: true,
},
features: {
codeSearch: true,
cody: true,
},
},
needServerRestart: false,
needsSiteInit: false,

View File

@ -196,15 +196,22 @@ export interface SourcegraphContext extends Pick<Required<SiteConfiguration>, 'e
batchChangesWebhookLogsEnabled: boolean
/** Whether cody is enabled site-wide. */
codyEnabled: boolean
/**
* Whether Cody is enabled on this instance. Check
* {@link SourcegraphContext.codyEnabledForCurrentUser} to see whether Cody is enabled for the
* current user.
*/
codyEnabledOnInstance: boolean
/** Whether cody is enabled for the user. */
/** Whether Cody is enabled for the user. */
codyEnabledForCurrentUser: boolean
/** Whether the site requires a verified email for cody. */
/** Whether the instance requires a verified email for Cody. */
codyRequiresVerifiedEmail: boolean
/** Whether the code search feature is enabled on the instance. */
codeSearchEnabledOnInstance: boolean
/** Whether executors are enabled on the site. */
executorsEnabled: boolean
@ -285,7 +292,6 @@ export interface SourcegraphContext extends Pick<Required<SiteConfiguration>, 'e
/** Contains information about the product license. */
licenseInfo?: {
batchChanges?: BatchChangesLicenseInfo
features: LicenseFeatures
}
/** sha256 hashed license key */
@ -320,11 +326,3 @@ export interface BrandAssets {
/** The URL to the symbol used as the search icon */
symbol?: string
}
/**
* Defines the license features available.
*/
export interface LicenseFeatures {
codeSearch: boolean
cody: boolean
}

View File

@ -1,8 +1,8 @@
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { type PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import type { AuthenticatedUser } from '../auth'
import type { BatchChangesProps } from '../batches'
import type { UserAreaUserFields, OrgAreaOrganizationFields } from '../graphql-operations'
import type { OrgAreaOrganizationFields, UserAreaUserFields } from '../graphql-operations'
import type { NavItemWithIconDescriptor, RouteV6Descriptor } from '../util/contributions'
/**
@ -13,10 +13,6 @@ export interface NamespaceAreaContext extends PlatformContextProps {
authenticatedUser: AuthenticatedUser | null
isSourcegraphDotCom: boolean
license: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
export interface NamespaceAreaRoute extends RouteV6Descriptor<NamespaceAreaContext> {}

View File

@ -17,20 +17,20 @@ export const namespaceAreaRoutes: readonly NamespaceAreaRoute[] = [
{
path: 'searches',
render: props => <SavedSearchListPage {...props} telemetryRecorder={props.platformContext.telemetryRecorder} />,
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
path: 'searches/add',
render: props => (
<SavedSearchCreateForm {...props} telemetryRecorder={props.platformContext.telemetryRecorder} />
),
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
path: 'searches/:id',
render: props => (
<SavedSearchUpdateForm {...props} telemetryRecorder={props.platformContext.telemetryRecorder} />
),
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
]

View File

@ -87,7 +87,9 @@ const config: Meta<typeof GlobalNavbar> = {
export default config
export const Default: StoryFn<GlobalNavbarProps> = props => {
window.context.licenseInfo = { features: { codeSearch: true, cody: true } }
window.context.codeSearchEnabledOnInstance = true
window.context.codyEnabledOnInstance = true
window.context.codyEnabledForCurrentUser = true
return (
<Grid columnCount={1}>
<div>
@ -115,8 +117,10 @@ export const Default: StoryFn<GlobalNavbarProps> = props => {
)
}
export const CodyOnlyLicense: StoryFn<GlobalNavbarProps> = props => {
window.context.licenseInfo = { features: { codeSearch: false, cody: true } }
export const CodyOnly: StoryFn<GlobalNavbarProps> = props => {
window.context.codeSearchEnabledOnInstance = false
window.context.codyEnabledOnInstance = true
window.context.codyEnabledForCurrentUser = true
return (
<Grid columnCount={1}>
<div>
@ -144,8 +148,41 @@ export const CodyOnlyLicense: StoryFn<GlobalNavbarProps> = props => {
)
}
export const CodeSearchOnlyLicense: StoryFn<GlobalNavbarProps> = props => {
window.context.licenseInfo = { features: { codeSearch: true, cody: false } }
export const UserNotLicensedForCody: StoryFn<GlobalNavbarProps> = props => {
window.context.codeSearchEnabledOnInstance = true
window.context.codyEnabledOnInstance = true
window.context.codyEnabledForCurrentUser = false
return (
<Grid columnCount={1}>
<div>
<H3 className="ml-2">Anonymous viewer</H3>
<GlobalNavbar {...props} />
</div>
<div>
<H3 className="ml-2">Anonymous viewer with all possible nav items</H3>
<GlobalNavbar {...props} {...allNavItemsProps} />
</div>
<div>
<H3 className="ml-2">Authenticated user with all possible nav items</H3>
<GlobalNavbar {...props} {...allNavItemsProps} {...allAuthenticatedNavItemsProps} />
</div>
<div>
<H3 className="ml-2">Authenticated user with all possible nav items and search input</H3>
<GlobalNavbar
{...props}
{...allNavItemsProps}
{...allAuthenticatedNavItemsProps}
showSearchBox={true}
/>
</div>
</Grid>
)
}
export const CodeSearchOnly: StoryFn<GlobalNavbarProps> = props => {
window.context.codeSearchEnabledOnInstance = true
window.context.codyEnabledOnInstance = false
window.context.codyEnabledForCurrentUser = false
return (
<Grid columnCount={1}>
<div>

View File

@ -1,7 +1,8 @@
import React from 'react'
import { describe, expect, test, vi, afterAll } from 'vitest'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
import {
mockFetchSearchContexts,
@ -16,7 +17,7 @@ vi.mock('../search/input/SearchNavbarItem', () => ({ SearchNavbarItem: () => 'Se
vi.mock('../components/branding/BrandLogo', () => ({ BrandLogo: () => 'BrandLogo' }))
const PROPS: React.ComponentProps<typeof GlobalNavbar> = {
authenticatedUser: null,
authenticatedUser: { username: 'alice', organizations: { nodes: [] } } as Partial<AuthenticatedUser> as any,
isSourcegraphDotCom: false,
platformContext: {} as any,
settingsCascade: NOOP_SETTINGS_CASCADE,
@ -38,55 +39,146 @@ const PROPS: React.ComponentProps<typeof GlobalNavbar> = {
codeMonitoringEnabled: true,
ownEnabled: true,
showFeedbackModal: () => undefined,
__testing__isOpen: true,
}
describe('GlobalNavbar', () => {
afterAll(() => {
vi.restoreAllMocks()
})
if (!window.context) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.context = {} as any
}
const origCodeSearchEnabledOnInstance = window.context?.codeSearchEnabledOnInstance ?? true
const origCodyEnabledOnInstance = window.context?.codyEnabledOnInstance ?? true
const origCodyEnabledForCurrentUser = window.context?.codyEnabledForCurrentUser ?? true
const reset = () => {
window.context.codeSearchEnabledOnInstance = origCodeSearchEnabledOnInstance
window.context.codyEnabledOnInstance = origCodyEnabledOnInstance
window.context.codyEnabledForCurrentUser = origCodyEnabledForCurrentUser
}
beforeEach(reset)
afterEach(reset)
test('default', () => {
vi.mock('../util/license', () => ({
isCodeSearchOnlyLicense: () => false,
isCodeSearchPlusCodyLicense: () => true,
isCodyOnlyLicense: () => false,
}))
window.context.codeSearchEnabledOnInstance = true
window.context.codyEnabledOnInstance = true
window.context.codyEnabledForCurrentUser = true
const { asFragment } = renderWithBrandedContext(
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<GlobalNavbar {...PROPS} />
</MockedTestProvider>
)
expect(asFragment()).toMatchSnapshot()
expect(describeNavBar(baseElement)).toEqual<NavBarTestDescription>({
codyItemType: 'dropdown',
codyDropdownItems: ['Dashboard /cody/dashboard', 'Web Chat /cody/chat'],
})
})
test('cody only license', () => {
vi.mock('../util/license', () => ({
isCodeSearchOnlyLicense: () => false,
isCodeSearchPlusCodyLicense: () => false,
isCodyOnlyLicense: () => true,
}))
const { asFragment } = renderWithBrandedContext(
test('dotcom unauthed', () => {
window.context.codyEnabledForCurrentUser = false
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<GlobalNavbar {...PROPS} />
<GlobalNavbar {...PROPS} isSourcegraphDotCom={true} authenticatedUser={null} />
</MockedTestProvider>
)
expect(asFragment()).toMatchSnapshot()
expect(describeNavBar(baseElement)).toEqual<NavBarTestDescription>({
codyItemType: 'link',
codyItemLink: '/cody',
})
})
test('code search only license', () => {
vi.mock('../util/license', () => ({
isCodeSearchOnlyLicense: () => true,
isCodeSearchPlusCodyLicense: () => false,
isCodyOnlyLicense: () => false,
}))
test('dotcom authed', () => {
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<GlobalNavbar {...PROPS} isSourcegraphDotCom={true} />
</MockedTestProvider>
)
expect(describeNavBar(baseElement)).toEqual<NavBarTestDescription>({
codyItemType: 'dropdown',
codyDropdownItems: ['Dashboard /cody/manage', 'Web Chat /cody/chat'],
})
})
const { asFragment } = renderWithBrandedContext(
test('enterprise cody enabled for user', () => {
window.context.codyEnabledForCurrentUser = true
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<GlobalNavbar {...PROPS} />
</MockedTestProvider>
)
expect(asFragment()).toMatchSnapshot()
expect(describeNavBar(baseElement)).toEqual<NavBarTestDescription>({
codyItemType: 'dropdown',
codyDropdownItems: ['Dashboard /cody/dashboard', 'Web Chat /cody/chat'],
})
})
test('enterprise cody disabled for user', () => {
window.context.codyEnabledForCurrentUser = false
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<GlobalNavbar {...PROPS} />
</MockedTestProvider>
)
expect(describeNavBar(baseElement)).toEqual<NavBarTestDescription>({
codyItemType: 'link',
codyItemLink: '/cody/dashboard',
})
})
test('code search disabled on instance', () => {
window.context.codeSearchEnabledOnInstance = false
window.context.codyEnabledOnInstance = true
window.context.codyEnabledForCurrentUser = true
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<GlobalNavbar {...PROPS} />
</MockedTestProvider>
)
expect(describeNavBar(baseElement)).toEqual<NavBarTestDescription>({
codyItemType: 'dropdown',
codyDropdownItems: ['Dashboard /cody/dashboard', 'Web Chat /cody/chat'],
})
})
test('cody disabled on instance', () => {
window.context.codeSearchEnabledOnInstance = true
window.context.codyEnabledOnInstance = false
window.context.codyEnabledForCurrentUser = false
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<GlobalNavbar {...PROPS} />
</MockedTestProvider>
)
expect(baseElement.querySelector('a[href^="/cody"]')).toBeNull()
expect(describeNavBar(baseElement)).toEqual<NavBarTestDescription>({ codyItemType: 'none' })
})
})
interface NavBarTestDescription {
codyItemType: 'none' | 'link' | 'dropdown'
codyItemLink?: string
codyDropdownItems?: string[]
}
function describeNavBar(baseElement: HTMLElement): NavBarTestDescription {
const dropdownButton = baseElement.querySelector('button[aria-label="Show cody menu"]')
if (dropdownButton) {
const popover = baseElement.querySelector('[data-reach-menu-popover]')!
return {
codyItemType: 'dropdown',
codyDropdownItems: [...popover.querySelectorAll('a')].map(
item => `${item.textContent ?? ''} ${item.getAttribute('href') ?? ''}`
),
}
}
const item = baseElement.querySelector<HTMLAnchorElement>('a[href^="/cody"]')
return item
? {
codyItemType: 'link',
codyItemLink: item?.getAttribute('href') ?? '',
}
: { codyItemType: 'none' }
}

View File

@ -1,21 +1,21 @@
import {
type FC,
type MutableRefObject,
type SetStateAction,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
type FC,
type MutableRefObject,
type SetStateAction,
} from 'react'
import classNames from 'classnames'
import BarChartIcon from 'mdi-react/BarChartIcon'
import MagnifyIcon from 'mdi-react/MagnifyIcon'
import { type RouteObject, useLocation } from 'react-router-dom'
import { useLocation, type RouteObject } from 'react-router-dom'
import useResizeObserver from 'use-resize-observer'
import { isMacPlatform } from '@sourcegraph/common'
import { isDefined, isMacPlatform } from '@sourcegraph/common'
import { shortcutDisplayName } from '@sourcegraph/shared/src/keyboardShortcuts'
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import type { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
@ -46,7 +46,6 @@ import { SearchNavbarItem } from '../search/input/SearchNavbarItem'
import { AccessRequestsGlobalNavItem } from '../site-admin/AccessRequestsPage/AccessRequestsGlobalNavItem'
import { useDeveloperSettings, useNavbarQueryState } from '../stores'
import { SvelteKitNavItem } from '../sveltekit/SvelteKitNavItem'
import { isCodyOnlyLicense, isCodeSearchOnlyLicense } from '../util/license'
import { NavAction, NavActions, NavBar, NavGroup, NavItem, NavLink } from '.'
import { NavDropdown, type NavDropdownItem } from './NavBar/NavDropdown'
@ -77,6 +76,8 @@ export interface GlobalNavbarProps
showFeedbackModal: () => void
setFuzzyFinderIsVisible: React.Dispatch<SetStateAction<boolean>>
__testing__isOpen?: boolean
}
/**
@ -133,6 +134,7 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
notebooksEnabled,
ownEnabled,
showFeedbackModal,
__testing__isOpen,
...props
}) => {
const location = useLocation()
@ -141,7 +143,7 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
const onNavbarQueryChange = useNavbarQueryState(state => state.setQueryState)
const isLicensed = !!window.context?.licenseInfo
const disableCodeSearchFeatures = isCodyOnlyLicense()
const disableCodeSearchFeatures = !window.context?.codeSearchEnabledOnInstance
// Search context management is still enabled on .com
// but should not show in the navbar. Users can still
// access this feature via the context dropdown.
@ -196,6 +198,7 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
showCodeInsights={codeInsights}
routeMatch={routeMatch}
isSourcegraphDotCom={isSourcegraphDotCom}
__testing__isOpen={__testing__isOpen}
/>
<NavActions>
@ -287,6 +290,8 @@ export interface InlineNavigationPanelProps {
/** A current react router route match */
routeMatch?: string
className?: string
__testing__isOpen?: boolean
}
export const InlineNavigationPanel: FC<InlineNavigationPanelProps> = props => {
@ -300,12 +305,11 @@ export const InlineNavigationPanel: FC<InlineNavigationPanelProps> = props => {
isSourcegraphDotCom,
routeMatch,
className,
__testing__isOpen,
} = props
const navbarReference = useRef<HTMLDivElement | null>(null)
const navLinkVariant = useCalculatedNavLinkVariant(navbarReference)
const disableCodyFeatures = isCodeSearchOnlyLicense()
const disableCodeSearchFeatures = isCodyOnlyLicense()
const searchNavBarItems = useMemo(() => {
const items: (NavDropdownItem | false)[] = [
@ -350,11 +354,14 @@ export const InlineNavigationPanel: FC<InlineNavigationPanelProps> = props => {
</NavItem>
)
const CodyLogoWrapper = (): JSX.Element => <CodyLogo withColor={routeMatch === `${PageRoutes.Cody}/*`} />
const hideCodyDropdown = disableCodyFeatures || !props.authenticatedUser
const codyNavigation = hideCodyDropdown ? (
const CodyLogoWrapper = (): JSX.Element => <CodyLogo withColor={routeMatch?.startsWith('/cody/')} />
const codyNavigation = !window.context?.codyEnabledOnInstance ? null : !window.context
?.codyEnabledForCurrentUser ? (
<NavItem icon={() => <CodyLogoWrapper />} key="cody">
<NavLink variant={navLinkVariant} to={disableCodyFeatures ? PageRoutes.Cody : PageRoutes.CodyChat}>
<NavLink
variant={navLinkVariant}
to={isSourcegraphDotCom ? PageRoutes.CodyRedirectToMarketingOrDashboard : PageRoutes.CodyDashboard}
>
Cody
</NavLink>
</NavItem>
@ -362,7 +369,7 @@ export const InlineNavigationPanel: FC<InlineNavigationPanelProps> = props => {
<NavDropdown
key="cody"
toggleItem={{
path: isSourcegraphDotCom ? CodyProRoutes.Manage : PageRoutes.Cody,
path: '/cody/*',
icon: () => <CodyLogoWrapper />,
content: 'Cody',
variant: navLinkVariant,
@ -370,7 +377,7 @@ export const InlineNavigationPanel: FC<InlineNavigationPanelProps> = props => {
routeMatch={routeMatch}
items={[
{
path: isSourcegraphDotCom ? CodyProRoutes.Manage : PageRoutes.Cody,
path: isSourcegraphDotCom ? CodyProRoutes.Manage : PageRoutes.CodyDashboard,
content: 'Dashboard',
},
{
@ -379,12 +386,13 @@ export const InlineNavigationPanel: FC<InlineNavigationPanelProps> = props => {
},
]}
name="cody"
__testing__isOpen={__testing__isOpen}
/>
)
let prioritizedLinks: JSX.Element[] = [searchNavigation, codyNavigation]
let prioritizedLinks: JSX.Element[] = [searchNavigation, codyNavigation].filter(isDefined)
if (disableCodeSearchFeatures) {
if (!window.context?.codeSearchEnabledOnInstance) {
// This should be cheap considering there will only be two items in the array.
prioritizedLinks = prioritizedLinks.reverse()
}

View File

@ -2,23 +2,22 @@ import React, { forwardRef, useContext } from 'react'
import { mdiMenu } from '@mdi/js'
import classNames from 'classnames'
import { type LinkProps, NavLink as RouterNavLink } from 'react-router-dom'
import { NavLink as RouterNavLink, type LinkProps } from 'react-router-dom'
import {
Link,
Icon,
H1,
type ForwardReferenceComponent,
VIEWPORT_SM,
Icon,
Link,
Menu,
MenuList,
MenuButton,
MenuLink,
MenuList,
VIEWPORT_SM,
useMatchMedia,
type ForwardReferenceComponent,
} from '@sourcegraph/wildcard'
import { PageRoutes } from '../../routes.constants'
import { isCodyOnlyLicense } from '../../util/license'
import navActionStyles from './NavAction.module.scss'
import navBarStyles from './NavBar.module.scss'
@ -52,7 +51,7 @@ export interface NavLinkProps extends NavItemProps, Pick<LinkProps, 'to'> {
}
export const NavBar = forwardRef(function NavBar({ children, logo }, reference): JSX.Element {
const logoUrl = isCodyOnlyLicense() ? PageRoutes.Cody : PageRoutes.Search
const logoUrl = window.context?.codeSearchEnabledOnInstance ? PageRoutes.Search : PageRoutes.Cody
return (
<nav aria-label="Main" className={navBarStyles.navbar} ref={reference}>
{logo && (

View File

@ -4,7 +4,7 @@ import { mdiChevronDown, mdiChevronUp } from '@mdi/js'
import classNames from 'classnames'
import { useLocation } from 'react-router-dom'
import { Link, Menu, MenuButton, MenuLink, MenuList, EMPTY_RECTANGLE, Icon } from '@sourcegraph/wildcard'
import { EMPTY_RECTANGLE, Icon, Link, Menu, MenuButton, MenuLink, MenuList } from '@sourcegraph/wildcard'
import { MobileNavGroupContext, NavItem, NavLink, type NavLinkProps } from '.'
@ -37,6 +37,8 @@ interface NavDropdownProps {
routeMatch?: string
/** The name of the dropdown to use for accessible labels */
name: string
__testing__isOpen?: boolean
}
export const NavDropdown: React.FunctionComponent<React.PropsWithChildren<NavDropdownProps>> = ({
@ -45,6 +47,7 @@ export const NavDropdown: React.FunctionComponent<React.PropsWithChildren<NavDro
items,
routeMatch,
name,
__testing__isOpen,
}) => {
const location = useLocation()
const isItemSelected = useMemo(
@ -101,7 +104,11 @@ export const NavDropdown: React.FunctionComponent<React.PropsWithChildren<NavDro
</MenuButton>
</div>
<MenuList className={styles.menuList} targetPadding={EMPTY_RECTANGLE}>
<MenuList
className={styles.menuList}
targetPadding={EMPTY_RECTANGLE}
isOpen={__testing__isOpen}
>
{homeItem && (
<MenuLink as={Link} key={toggleItem.path} to={toggleItem.path}>
{homeItem.content}

View File

@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter } from 'react-router-dom'
import sinon from 'sinon'
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
@ -11,12 +11,6 @@ import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
import { UserNavItem, type UserNavItemProps } from './UserNavItem'
vi.mock('../util/license', () => ({
isCodeSearchOnlyLicense: () => false,
isCodeSearchPlusCodyLicense: () => true,
isCodyOnlyLicense: () => false,
}))
describe('UserNavItem', () => {
beforeAll(() => {
setLinkComponent(RouterLink)

View File

@ -1,439 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`GlobalNavbar > code search only license 1`] = `
<DocumentFragment>
<nav
aria-label="Main"
class="navbar"
>
<h1
class="h1 logo"
>
<a
class="anchorLink d-flex align-items-center"
href="/search"
>
BrandLogo
</a>
</h1>
<hr
aria-hidden="true"
class="divider"
/>
<div
class="menu list"
>
<ul
class="list"
>
<li
class="item wrapper"
>
<div
class="link d-flex align-items-center p-0"
data-test-active="false"
data-test-id="/search"
>
<button
aria-controls=""
aria-haspopup="true"
aria-label="Show search menu"
class="btn itemFocusable button"
data-reach-menu-button=""
id="menu-button-test-id"
type="button"
>
<span
class="itemFocusableContent"
>
<svg
aria-hidden="true"
class="mdi-icon mdi-icon iconInline icon"
fill="currentColor"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
>
<path
d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"
/>
</svg>
<span
class="text iconIncluded"
>
Code Search
</span>
<svg
aria-hidden="true"
class="mdi-icon iconInline icon"
fill="currentColor"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
>
<path
d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"
/>
</svg>
</span>
</button>
</div>
</li>
<li
class="item"
>
<a
class="link"
href="/cody"
>
<span
class="linkContent"
>
<svg
fill="none"
height="20"
viewBox="0 0 20 20"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9088 4C14.756 4 15.4429 4.69836 15.4429 5.55983V8.33286C15.4429 9.19433 14.756 9.89269 13.9088 9.89269C13.0615 9.89269 12.3747 9.19433 12.3747 8.33286V5.55983C12.3747 4.69836 13.0615 4 13.9088 4Z"
fill="#a6b6d9"
/>
<path
d="M4.19287 7.63942C4.19287 6.77795 4.87971 6.07959 5.72696 6.07959H8.45423C9.30148 6.07959 9.98832 6.77795 9.98832 7.63942C9.98832 8.50089 9.30148 9.19925 8.45423 9.19925H5.72696C4.87971 9.19925 4.19287 8.50089 4.19287 7.63942Z"
fill="#a6b6d9"
/>
<path
d="M17.5756 12.1801C18.1216 12.7075 18.1437 13.5851 17.625 14.1403L17.1423 14.6569C13.3654 18.6994 6.99777 18.5987 3.34628 14.4387C2.84481 13.8674 2.89377 12.9909 3.45565 12.481C4.01752 11.9711 4.87954 12.0209 5.38102 12.5922C7.97062 15.5424 12.4865 15.6139 15.1651 12.747L15.6477 12.2304C16.1664 11.6752 17.0296 11.6527 17.5756 12.1801Z"
fill="#a6b6d9"
/>
</svg>
<span
class="text iconIncluded"
>
Cody
</span>
</span>
</a>
</li>
</ul>
</div>
<ul
class="actions"
>
<li
class="action"
>
<div>
<a
class="anchorLink btn btnSecondary btnOutline btnSm mr-1"
href="/sign-in?returnTo=/"
>
Sign in
</a>
</div>
</li>
</ul>
</nav>
<div
class="searchNavBar"
>
SearchNavbarItem
</div>
</DocumentFragment>
`;
exports[`GlobalNavbar > cody only license 1`] = `
<DocumentFragment>
<nav
aria-label="Main"
class="navbar"
>
<h1
class="h1 logo"
>
<a
class="anchorLink d-flex align-items-center"
href="/search"
>
BrandLogo
</a>
</h1>
<hr
aria-hidden="true"
class="divider"
/>
<div
class="menu list"
>
<ul
class="list"
>
<li
class="item wrapper"
>
<div
class="link d-flex align-items-center p-0"
data-test-active="false"
data-test-id="/search"
>
<button
aria-controls=""
aria-haspopup="true"
aria-label="Show search menu"
class="btn itemFocusable button"
data-reach-menu-button=""
id="menu-button-test-id"
type="button"
>
<span
class="itemFocusableContent"
>
<svg
aria-hidden="true"
class="mdi-icon mdi-icon iconInline icon"
fill="currentColor"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
>
<path
d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"
/>
</svg>
<span
class="text iconIncluded"
>
Code Search
</span>
<svg
aria-hidden="true"
class="mdi-icon iconInline icon"
fill="currentColor"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
>
<path
d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"
/>
</svg>
</span>
</button>
</div>
</li>
<li
class="item"
>
<a
class="link"
href="/cody"
>
<span
class="linkContent"
>
<svg
fill="none"
height="20"
viewBox="0 0 20 20"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9088 4C14.756 4 15.4429 4.69836 15.4429 5.55983V8.33286C15.4429 9.19433 14.756 9.89269 13.9088 9.89269C13.0615 9.89269 12.3747 9.19433 12.3747 8.33286V5.55983C12.3747 4.69836 13.0615 4 13.9088 4Z"
fill="#a6b6d9"
/>
<path
d="M4.19287 7.63942C4.19287 6.77795 4.87971 6.07959 5.72696 6.07959H8.45423C9.30148 6.07959 9.98832 6.77795 9.98832 7.63942C9.98832 8.50089 9.30148 9.19925 8.45423 9.19925H5.72696C4.87971 9.19925 4.19287 8.50089 4.19287 7.63942Z"
fill="#a6b6d9"
/>
<path
d="M17.5756 12.1801C18.1216 12.7075 18.1437 13.5851 17.625 14.1403L17.1423 14.6569C13.3654 18.6994 6.99777 18.5987 3.34628 14.4387C2.84481 13.8674 2.89377 12.9909 3.45565 12.481C4.01752 11.9711 4.87954 12.0209 5.38102 12.5922C7.97062 15.5424 12.4865 15.6139 15.1651 12.747L15.6477 12.2304C16.1664 11.6752 17.0296 11.6527 17.5756 12.1801Z"
fill="#a6b6d9"
/>
</svg>
<span
class="text iconIncluded"
>
Cody
</span>
</span>
</a>
</li>
</ul>
</div>
<ul
class="actions"
>
<li
class="action"
>
<div>
<a
class="anchorLink btn btnSecondary btnOutline btnSm mr-1"
href="/sign-in?returnTo=/"
>
Sign in
</a>
</div>
</li>
</ul>
</nav>
<div
class="searchNavBar"
>
SearchNavbarItem
</div>
</DocumentFragment>
`;
exports[`GlobalNavbar > default 1`] = `
<DocumentFragment>
<nav
aria-label="Main"
class="navbar"
>
<h1
class="h1 logo"
>
<a
class="anchorLink d-flex align-items-center"
href="/search"
>
BrandLogo
</a>
</h1>
<hr
aria-hidden="true"
class="divider"
/>
<div
class="menu list"
>
<ul
class="list"
>
<li
class="item wrapper"
>
<div
class="link d-flex align-items-center p-0"
data-test-active="false"
data-test-id="/search"
>
<button
aria-controls=""
aria-haspopup="true"
aria-label="Show search menu"
class="btn itemFocusable button"
data-reach-menu-button=""
id="menu-button-test-id"
type="button"
>
<span
class="itemFocusableContent"
>
<svg
aria-hidden="true"
class="mdi-icon mdi-icon iconInline icon"
fill="currentColor"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
>
<path
d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"
/>
</svg>
<span
class="text iconIncluded"
>
Code Search
</span>
<svg
aria-hidden="true"
class="mdi-icon iconInline icon"
fill="currentColor"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
>
<path
d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"
/>
</svg>
</span>
</button>
</div>
</li>
<li
class="item"
>
<a
class="link"
href="/cody"
>
<span
class="linkContent"
>
<svg
fill="none"
height="20"
viewBox="0 0 20 20"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9088 4C14.756 4 15.4429 4.69836 15.4429 5.55983V8.33286C15.4429 9.19433 14.756 9.89269 13.9088 9.89269C13.0615 9.89269 12.3747 9.19433 12.3747 8.33286V5.55983C12.3747 4.69836 13.0615 4 13.9088 4Z"
fill="#a6b6d9"
/>
<path
d="M4.19287 7.63942C4.19287 6.77795 4.87971 6.07959 5.72696 6.07959H8.45423C9.30148 6.07959 9.98832 6.77795 9.98832 7.63942C9.98832 8.50089 9.30148 9.19925 8.45423 9.19925H5.72696C4.87971 9.19925 4.19287 8.50089 4.19287 7.63942Z"
fill="#a6b6d9"
/>
<path
d="M17.5756 12.1801C18.1216 12.7075 18.1437 13.5851 17.625 14.1403L17.1423 14.6569C13.3654 18.6994 6.99777 18.5987 3.34628 14.4387C2.84481 13.8674 2.89377 12.9909 3.45565 12.481C4.01752 11.9711 4.87954 12.0209 5.38102 12.5922C7.97062 15.5424 12.4865 15.6139 15.1651 12.747L15.6477 12.2304C16.1664 11.6752 17.0296 11.6527 17.5756 12.1801Z"
fill="#a6b6d9"
/>
</svg>
<span
class="text iconIncluded"
>
Cody
</span>
</span>
</a>
</li>
</ul>
</div>
<ul
class="actions"
>
<li
class="action"
>
<div>
<a
class="anchorLink btn btnSecondary btnOutline btnSm mr-1"
href="/sign-in?returnTo=/"
>
Sign in
</a>
</div>
</li>
</ul>
</nav>
<div
class="searchNavBar"
>
SearchNavbarItem
</div>
</DocumentFragment>
`;

View File

@ -13,6 +13,10 @@ import { NewGlobalNavigationBar } from './NewGlobalNavigationBar'
const decorator: Decorator<GlobalNavbarProps> = Story => {
updateJSContextBatchChangesLicense('full')
window.context.codeSearchEnabledOnInstance = true
window.context.codyEnabledOnInstance = true
window.context.codyEnabledForCurrentUser = true
return <WebStory>{() => <Story />}</WebStory>
}

View File

@ -0,0 +1,155 @@
import React from 'react'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { AuthenticatedUser } from '@sourcegraph/shared/src/auth'
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
import { NewGlobalNavigationBar } from './NewGlobalNavigationBar'
vi.mock('../search/input/SearchNavbarItem', () => ({ SearchNavbarItem: () => 'SearchNavbarItem' }))
vi.mock('../components/branding/BrandLogo', () => ({ BrandLogo: () => 'BrandLogo' }))
const PROPS: React.ComponentProps<typeof NewGlobalNavigationBar> = {
authenticatedUser: { username: 'alice', organizations: { nodes: [] } } as Partial<AuthenticatedUser> as any,
isSourcegraphDotCom: false,
notebooksEnabled: false,
searchContextsEnabled: true,
codeMonitoringEnabled: true,
batchChangesEnabled: true,
codeInsightsEnabled: true,
showSearchBox: true,
showFeedbackModal: () => {},
routes: [],
__testing__initialSideMenuOpen: true,
telemetryService: {} as any,
telemetryRecorder: {} as any,
}
describe('NewGlobalNavigationBar', () => {
if (!window.context) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.context = {} as any
}
const origCodeSearchEnabledOnInstance = window.context?.codeSearchEnabledOnInstance ?? true
const origCodyEnabledOnInstance = window.context?.codyEnabledOnInstance ?? true
const origCodyEnabledForCurrentUser = window.context?.codyEnabledForCurrentUser ?? true
const reset = () => {
window.context.codeSearchEnabledOnInstance = origCodeSearchEnabledOnInstance
window.context.codyEnabledOnInstance = origCodyEnabledOnInstance
window.context.codyEnabledForCurrentUser = origCodyEnabledForCurrentUser
}
beforeEach(reset)
afterEach(reset)
test('default', () => {
window.context.codeSearchEnabledOnInstance = true
window.context.codyEnabledOnInstance = true
window.context.codyEnabledForCurrentUser = true
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<NewGlobalNavigationBar {...PROPS} />
</MockedTestProvider>
)
const sidebarElement = baseElement.querySelector<HTMLElement>('[data-reach-dialog-overlay]')!
expect(describeNavBarSideMenu(sidebarElement)).toEqual<NavBarTestDescription>({
codyItems: ['Cody /cody/dashboard', 'Web Chat /cody/chat'],
})
})
test('dotcom unauthed', () => {
window.context.codyEnabledForCurrentUser = false
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<NewGlobalNavigationBar {...PROPS} isSourcegraphDotCom={true} authenticatedUser={null} />
</MockedTestProvider>
)
const sidebarElement = baseElement.querySelector<HTMLElement>('[data-reach-dialog-overlay]')!
expect(describeNavBarSideMenu(sidebarElement)).toEqual<NavBarTestDescription>({
codyItems: ['Cody /cody'],
})
})
test('dotcom authed', () => {
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<NewGlobalNavigationBar {...PROPS} isSourcegraphDotCom={true} />
</MockedTestProvider>
)
const sidebarElement = baseElement.querySelector<HTMLElement>('[data-reach-dialog-overlay]')!
expect(describeNavBarSideMenu(sidebarElement)).toEqual<NavBarTestDescription>({
codyItems: ['Cody /cody/manage', 'Web Chat /cody/chat'],
})
})
test('enterprise cody enabled for user', () => {
window.context.codyEnabledForCurrentUser = true
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<NewGlobalNavigationBar {...PROPS} />
</MockedTestProvider>
)
const sidebarElement = baseElement.querySelector<HTMLElement>('[data-reach-dialog-overlay]')!
expect(describeNavBarSideMenu(sidebarElement)).toEqual<NavBarTestDescription>({
codyItems: ['Cody /cody/dashboard', 'Web Chat /cody/chat'],
})
})
test('enterprise cody disabled for user', () => {
window.context.codyEnabledForCurrentUser = false
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<NewGlobalNavigationBar {...PROPS} />
</MockedTestProvider>
)
const sidebarElement = baseElement.querySelector<HTMLElement>('[data-reach-dialog-overlay]')!
expect(describeNavBarSideMenu(sidebarElement)).toEqual<NavBarTestDescription>({
codyItems: ['Cody /cody/dashboard'],
})
})
test('code search disabled on instance', () => {
window.context.codeSearchEnabledOnInstance = false
window.context.codyEnabledOnInstance = true
window.context.codyEnabledForCurrentUser = true
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<NewGlobalNavigationBar {...PROPS} />
</MockedTestProvider>
)
const sidebarElement = baseElement.querySelector<HTMLElement>('[data-reach-dialog-overlay]')!
expect(describeNavBarSideMenu(sidebarElement)).toEqual<NavBarTestDescription>({
codyItems: ['Cody /cody/dashboard', 'Web Chat /cody/chat'],
})
})
test('cody disabled on instance', () => {
window.context.codeSearchEnabledOnInstance = true
window.context.codyEnabledOnInstance = false
window.context.codyEnabledForCurrentUser = false
const { baseElement } = renderWithBrandedContext(
<MockedTestProvider>
<NewGlobalNavigationBar {...PROPS} />
</MockedTestProvider>
)
const sidebarElement = baseElement.querySelector<HTMLElement>('[data-reach-dialog-overlay]')!
expect(sidebarElement.querySelector('a[href*="/cody"]')).toBeNull()
expect(describeNavBarSideMenu(sidebarElement)).toEqual<NavBarTestDescription>({ codyItems: [] })
})
})
interface NavBarTestDescription {
codyItems: string[]
}
function describeNavBarSideMenu(sidebarElement: HTMLElement): NavBarTestDescription {
return {
codyItems: Array.from(sidebarElement.querySelectorAll<HTMLAnchorElement>('a[href^="/cody"]')).map(
a => `${a.textContent?.trim() ?? ''} ${a.getAttribute('href') ?? ''}`
),
}
}

View File

@ -17,6 +17,7 @@ import { Button, ButtonLink, Icon, Link, Modal, ProductStatusBadge, Text } from
import type { AuthenticatedUser } from '../../auth'
import { BatchChangesIconNav } from '../../batches/icons'
import { CodyProRoutes } from '../../cody/codyProRoutes'
import { CodyLogo } from '../../cody/components/CodyLogo'
import { BrandLogo } from '../../components/branding/BrandLogo'
import { DeveloperSettingsGlobalNavItem } from '../../devsettings/DeveloperSettingsGlobalNavItem'
@ -49,7 +50,11 @@ interface NewGlobalNavigationBar extends TelemetryProps, TelemetryV2Props {
* New experimental global navigation bar with inline search bar and
* dynamic navigation items.
*/
export const NewGlobalNavigationBar: FC<NewGlobalNavigationBar> = props => {
export const NewGlobalNavigationBar: FC<
NewGlobalNavigationBar & {
__testing__initialSideMenuOpen?: boolean
}
> = props => {
const {
isSourcegraphDotCom,
notebooksEnabled,
@ -63,11 +68,12 @@ export const NewGlobalNavigationBar: FC<NewGlobalNavigationBar> = props => {
showFeedbackModal,
telemetryService,
telemetryRecorder,
__testing__initialSideMenuOpen,
} = props
const isLightTheme = useIsLightTheme()
const [params] = useSearchParams()
const [isSideMenuOpen, setSideMenuOpen] = useState(false)
const [isSideMenuOpen, setSideMenuOpen] = useState(__testing__initialSideMenuOpen ?? false)
const routeMatch = useRoutesMatch(props.routes)
// Features enablement flags and conditions
@ -146,7 +152,6 @@ export const NewGlobalNavigationBar: FC<NewGlobalNavigationBar> = props => {
showBatchChanges={showBatchChanges}
showCodeInsights={showCodeInsights}
isSourcegraphDotCom={isSourcegraphDotCom}
authenticatedUser={authenticatedUser}
onClose={() => setSideMenuOpen(false)}
/>
)}
@ -311,7 +316,6 @@ interface SidebarNavigationProps {
showBatchChanges: boolean
showCodeInsights: boolean
onClose: () => void
authenticatedUser: AuthenticatedUser | null
}
const SidebarNavigation: FC<SidebarNavigationProps> = props => {
@ -323,7 +327,6 @@ const SidebarNavigation: FC<SidebarNavigationProps> = props => {
showBatchChanges,
showCodeInsights,
isSourcegraphDotCom,
authenticatedUser,
onClose,
} = props
@ -382,11 +385,23 @@ const SidebarNavigation: FC<SidebarNavigationProps> = props => {
</ul>
</li>
<NavItemLink url={PageRoutes.Cody} icon={CodyLogo} onClick={handleNavigationClick}>
Cody
</NavItemLink>
{window.context?.codyEnabledOnInstance && (
<NavItemLink
url={
isSourcegraphDotCom
? window.context.codyEnabledForCurrentUser
? CodyProRoutes.Manage
: PageRoutes.CodyRedirectToMarketingOrDashboard
: PageRoutes.CodyDashboard
}
icon={CodyLogo}
onClick={handleNavigationClick}
>
Cody
</NavItemLink>
)}
{authenticatedUser && (
{window.context?.codyEnabledForCurrentUser && (
<ul className={classNames(styles.sidebarNavigationList, styles.sidebarNavigationListNested)}>
<NavItemLink url={PageRoutes.CodyChat} onClick={handleNavigationClick}>
Web Chat

View File

@ -4,31 +4,31 @@ import type * as H from 'history'
import AlertCircleIcon from 'mdi-react/AlertCircleIcon'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import { Route, Routes, type NavigateFunction } from 'react-router-dom'
import { combineLatest, merge, type Observable, of, Subject, Subscription } from 'rxjs'
import { Subject, Subscription, combineLatest, merge, of, type Observable } from 'rxjs'
import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'
import { type ErrorLike, isErrorLike, asError, logger } from '@sourcegraph/common'
import { gql, dataOrThrowErrors } from '@sourcegraph/http-client'
import { asError, isErrorLike, logger, type ErrorLike } from '@sourcegraph/common'
import { dataOrThrowErrors, gql } from '@sourcegraph/http-client'
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { type TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { LoadingSpinner, ErrorMessage } from '@sourcegraph/wildcard'
import { ErrorMessage, LoadingSpinner } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import { requestGraphQL } from '../../backend/graphql'
import type { BatchChangesProps } from '../../batches'
import type { BreadcrumbsProps, BreadcrumbSetters } from '../../components/Breadcrumbs'
import type { BreadcrumbSetters, BreadcrumbsProps } from '../../components/Breadcrumbs'
import { RouteError } from '../../components/ErrorBoundary'
import { HeroPage } from '../../components/HeroPage'
import { Page } from '../../components/Page'
import type { OrganizationResult, OrganizationVariables, OrgAreaOrganizationFields } from '../../graphql-operations'
import type { OrgAreaOrganizationFields, OrganizationResult, OrganizationVariables } from '../../graphql-operations'
import type { NamespaceProps } from '../../namespaces'
import type { RouteV6Descriptor } from '../../util/contributions'
import type { OrgSettingsAreaRoute } from '../settings/OrgSettingsArea'
import type { OrgSettingsSidebarItems } from '../settings/OrgSettingsSidebar'
import { type OrgAreaHeaderNavItem, OrgHeader } from './OrgHeader'
import { OrgHeader, type OrgAreaHeaderNavItem } from './OrgHeader'
import { OrgInvitationPageLegacy } from './OrgInvitationPageLegacy'
function queryOrganization(args: { name: string }): Observable<OrgAreaOrganizationFields> {
@ -138,11 +138,6 @@ export interface OrgAreaRouteContext
orgSettingsSideBarItems: OrgSettingsSidebarItems
orgSettingsAreaRoutes: readonly OrgSettingsAreaRoute[]
license: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
/**
@ -249,10 +244,6 @@ export class OrgArea extends React.Component<OrgAreaProps> {
useBreadcrumb: this.state.useBreadcrumb,
orgSettingsAreaRoutes: this.props.orgSettingsAreaRoutes,
orgSettingsSideBarItems: this.props.orgSettingsSideBarItems,
license: {
isCodeSearchEnabled: Boolean(window.context.licenseInfo?.features.codeSearch),
isCodyEnabled: Boolean(window.context.licenseInfo?.features.cody),
},
}
if (this.props.location.pathname === `/organizations/${this.props.orgName}/invitation`) {

View File

@ -2,11 +2,10 @@ import React from 'react'
import { NavLink } from 'react-router-dom'
import { PageHeader, Button, Link, Icon } from '@sourcegraph/wildcard'
import { Button, Icon, Link, PageHeader } from '@sourcegraph/wildcard'
import type { BatchChangesProps } from '../../batches'
import type { NavItemWithIconDescriptor } from '../../util/contributions'
import { getLicenseFeatures } from '../../util/license'
import { OrgAvatar } from '../OrgAvatar'
import type { OrgAreaRouteContext } from './OrgArea'
@ -24,11 +23,6 @@ export interface OrgSummary {
export interface OrgAreaHeaderContext extends BatchChangesProps, Pick<Props, 'org'> {
isSourcegraphDotCom: boolean
license: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
export interface OrgAreaHeaderNavItem extends NavItemWithIconDescriptor<OrgAreaHeaderContext> {}
@ -51,7 +45,6 @@ export const OrgHeader: React.FunctionComponent<React.PropsWithChildren<Props>>
batchChangesWebhookLogsEnabled,
org,
isSourcegraphDotCom,
license: getLicenseFeatures(),
}
const url = `/organizations/${org.name}`

View File

@ -16,7 +16,8 @@ export const orgAreaHeaderNavItems: readonly OrgAreaHeaderNavItem[] = [
to: '/searches',
label: 'Saved searches',
icon: FeatureSearchOutlineIcon,
condition: ({ org: { viewerCanAdminister }, license }) => license.isCodeSearchEnabled && viewerCanAdminister,
condition: ({ org: { viewerCanAdminister } }) =>
viewerCanAdminister && window.context?.codeSearchEnabledOnInstance,
},
...namespaceAreaHeaderNavItems,
]

View File

@ -1,7 +1,7 @@
import React, { type FC } from 'react'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import { Routes, Route } from 'react-router-dom'
import { Route, Routes } from 'react-router-dom'
import { LoadingSpinner } from '@sourcegraph/wildcard'
@ -11,7 +11,6 @@ import { RouteError } from '../../components/ErrorBoundary'
import { HeroPage } from '../../components/HeroPage'
import type { OrgAreaOrganizationFields } from '../../graphql-operations'
import type { RouteV6Descriptor } from '../../util/contributions'
import { getLicenseFeatures } from '../../util/license'
import type { OrgAreaRouteContext } from '../area/OrgArea'
import { OrgSettingsSidebar, type OrgSettingsSidebarItems } from './OrgSettingsSidebar'
@ -35,11 +34,6 @@ export interface OrgSettingsAreaProps extends OrgAreaRouteContext {
export interface OrgSettingsAreaRouteContext extends OrgSettingsAreaProps {
org: OrgAreaOrganizationFields
license: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
/**
@ -49,7 +43,6 @@ export interface OrgSettingsAreaRouteContext extends OrgSettingsAreaProps {
const AuthenticatedOrgSettingsArea: FC<OrgSettingsAreaProps> = props => {
const context: OrgSettingsAreaRouteContext = {
...props,
license: getLicenseFeatures(),
}
return (

View File

@ -38,7 +38,8 @@ export const orgSettingsAreaRoutes: readonly OrgSettingsAreaRoute[] = [
telemetryRecorder={props.platformContext.telemetryRecorder}
/>
),
condition: ({ org: { viewerCanAdminister }, license }) => license.isCodeSearchEnabled && viewerCanAdminister,
condition: ({ org: { viewerCanAdminister } }) =>
viewerCanAdminister && window.context?.codeSearchEnabledOnInstance,
},
]

View File

@ -1,25 +1,25 @@
import React, {
createContext,
type FC,
type PropsWithChildren,
type RefObject,
Suspense,
useContext,
useEffect,
useMemo,
useRef,
useState,
type FC,
type PropsWithChildren,
type RefObject,
} from 'react'
import classNames from 'classnames'
import { escapeRegExp } from 'lodash'
import { createPortal } from 'react-dom'
import { type Location, useLocation, Route, Routes } from 'react-router-dom'
import { Route, Routes, useLocation, type Location } from 'react-router-dom'
import { NEVER, of } from 'rxjs'
import { catchError, switchMap } from 'rxjs/operators'
import type { StreamingSearchResultsListProps } from '@sourcegraph/branded'
import { asError, type ErrorLike, isErrorLike, repeatUntil } from '@sourcegraph/common'
import { asError, isErrorLike, repeatUntil, type ErrorLike } from '@sourcegraph/common'
import {
isCloneInProgressErrorLike,
isRepoSeeOtherErrorLike,
@ -42,7 +42,7 @@ import type { BatchChangesProps } from '../batches'
import type { CodeIntelligenceProps } from '../codeintel'
import { RepoContainerEditor } from '../cody/components/RepoContainerEditor'
import { CodySidebar } from '../cody/sidebar'
import { useCodySidebar, useSidebarSize, CODY_SIDEBAR_SIZES } from '../cody/sidebar/Provider'
import { CODY_SIDEBAR_SIZES, useCodySidebar, useSidebarSize } from '../cody/sidebar/Provider'
import { useCodyIgnore } from '../cody/useCodyIgnore'
import type { BreadcrumbSetters, BreadcrumbsProps } from '../components/Breadcrumbs'
import { RouteError } from '../components/ErrorBoundary'
@ -56,11 +56,10 @@ import { useV2QueryInput } from '../search/useV2QueryInput'
import { useNavbarQueryState } from '../stores'
import { EventName } from '../util/constants'
import type { RouteV6Descriptor } from '../util/contributions'
import { getLicenseFeatures } from '../util/license'
import { parseBrowserRepoURL } from '../util/url'
import { GoToCodeHostAction } from './actions/GoToCodeHostAction'
import { fetchFileExternalLinks, type ResolvedRevision, resolveRepoRevision, type Repo } from './backend'
import { fetchFileExternalLinks, resolveRepoRevision, type Repo, type ResolvedRevision } from './backend'
import { AskCodyButton } from './cody/AskCodyButton'
import { RepoContainerError } from './RepoContainerError'
import { RepoHeader, type RepoHeaderContributionsLifecycleProps } from './RepoHeader'
@ -436,8 +435,7 @@ const RepoUserContainer: FC<RepoUserContainerProps> = ({
// must exactly match how the revision was encoded in the URL
const repoNameAndRevision = `${repoName}${typeof rawRevision === 'string' ? `@${rawRevision}` : ''}`
const licenseFeatures = getLicenseFeatures()
const showAskCodyBtn = licenseFeatures.isCodyEnabled && !isRepoIgnored(repoName) && !isCodySidebarOpen
const showAskCodyBtn = window.context?.codyEnabledForCurrentUser && !isRepoIgnored(repoName) && !isCodySidebarOpen
return (
<>

View File

@ -14,7 +14,7 @@ import type { Optional } from 'utility-types'
import type { StreamingSearchResultsListProps } from '@sourcegraph/branded'
import { TabbedPanelContent } from '@sourcegraph/branded/src/components/panel/TabbedPanelContent'
import { NoopEditor } from '@sourcegraph/cody-shared'
import { asError, type ErrorLike, isErrorLike, basename, SourcegraphURL } from '@sourcegraph/common'
import { asError, basename, isErrorLike, SourcegraphURL, type ErrorLike } from '@sourcegraph/common'
import {
createActiveSpan,
reactManualTracer,
@ -25,7 +25,7 @@ import type { FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
import { HighlightResponseFormat } from '@sourcegraph/shared/src/graphql-operations'
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import type { SearchContextProps } from '@sourcegraph/shared/src/search'
import { type SettingsCascadeProps, useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settings'
import { useExperimentalFeatures, type SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
@ -49,7 +49,6 @@ import {
import type { AuthenticatedUser } from '../../auth'
import type { CodeIntelligenceProps } from '../../codeintel'
import { FileContentEditor } from '../../cody/components/FileContentEditor'
import { isCodyEnabled } from '../../cody/isCodyEnabled'
import { useCodySidebar } from '../../cody/sidebar/Provider'
import type { BreadcrumbSetters } from '../../components/Breadcrumbs'
import { HeroPage } from '../../components/HeroPage'
@ -68,7 +67,6 @@ import { parseBrowserRepoURL, toTreeURL } from '../../util/url'
import { serviceKindDisplayNameAndIcon } from '../actions/GoToCodeHostAction'
import { ToggleBlameAction } from '../actions/ToggleBlameAction'
import { useBlameHunks, useBlameVisibility } from '../blame/hooks'
import { TryCodyWidget } from '../components/TryCodyWidget/TryCodyWidget'
import { FilePathBreadcrumbs } from '../FilePathBreadcrumbs'
import { isPackageServiceType } from '../packages/isPackageServiceType'
import type { HoverThresholdProps } from '../RepoContainer'
@ -371,16 +369,6 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
const alwaysRender = (
<>
<PageTitle title={getPageTitle()} />
{(props.isSourcegraphDotCom || isCodyEnabled()) && (
<TryCodyWidget
telemetryService={props.telemetryService}
telemetryRecorder={props.telemetryRecorder}
type="blob"
authenticatedUser={props.authenticatedUser}
context={context}
isSourcegraphDotCom={props.isSourcegraphDotCom}
/>
)}
{window.context.isAuthenticatedUser && (
<RepoHeaderContributionPortal
position="right"

View File

@ -22,7 +22,7 @@ import { useKeyboardShortcut } from '@sourcegraph/shared/src/keyboardShortcuts/u
import { Shortcut } from '@sourcegraph/shared/src/react-shortcuts'
import { useSettings } from '@sourcegraph/shared/src/settings/settings'
import type { TemporarySettingsSchema } from '@sourcegraph/shared/src/settings/temporary/TemporarySettings'
import { type TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import { codeCopiedEvent } from '@sourcegraph/shared/src/tracking/event-log-creators'
@ -35,7 +35,6 @@ import {
import { useLocalStorage } from '@sourcegraph/wildcard'
import { CodeMirrorEditor } from '../../cody/components/CodeMirrorEditor'
import { isCodyEnabled } from '../../cody/isCodyEnabled'
import { useCodySidebar } from '../../cody/sidebar/Provider'
import { useCodyIgnore } from '../../cody/useCodyIgnore'
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
@ -340,7 +339,8 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = props => {
)
const { isFileIgnored } = useCodyIgnore()
const isCodyEnabledForFile = isCodyEnabled() && !isFileIgnored(blobInfo.repoName, blobInfo.filePath)
const isCodyEnabledForFile =
window.context?.codyEnabledForCurrentUser && !isFileIgnored(blobInfo.repoName, blobInfo.filePath)
const extensions = useMemo(
() => [

View File

@ -1,129 +0,0 @@
@import 'wildcard/src/global-styles/breakpoints';
:global(.theme-dark) {
--cody-blob-bg-gradient: linear-gradient(339.95deg, #15171e -44.65%, #49169d 15.56%, #5e398c 88.23%);
--cody-repo-bg-gradient: linear-gradient(339.95deg, #15171e -44.65%, #49169d -16.82%, #1e212e 88.23%), #ffffff;
--cody-repo-border: var(--gray-09);
}
:global(.theme-light) {
--cody-blob-bg-gradient: linear-gradient(339.95deg, #ffffff -44.65%, #d0b9f5 -16.82%, rgba(255, 255, 255, 0) 88.23%),
#ffffff;
--cody-repo-bg-gradient: var(--cody-blob-bg-gradient);
--cody-repo-border: var(--gray-03);
}
.close-button {
top: 0;
right: 0;
}
.repo-card-wrapper {
padding: 0.065rem !important;
background: var(--cody-repo-border);
.card {
background: var(--cody-repo-bg-gradient);
}
}
.blob-card-wrapper {
padding: 0.065rem !important;
.card {
background: var(--cody-blob-bg-gradient);
}
}
.card {
padding-top: 0.5rem;
gap: 1rem;
}
.card-title {
color: var(--body-color);
font-weight: 600;
font-size: 1.25rem;
line-height: 1.5rem;
margin-bottom: 0.6875rem;
}
.card-list {
font-size: 1rem;
line-height: 1.25rem;
color: var(--body-color);
width: max-content;
}
.card-images {
width: 100%;
align-items: self-end;
@media (--md-breakpoint-down) {
width: unset;
}
}
.card-image {
max-width: 35.688rem;
max-height: 7.438rem;
}
.no-auth-card {
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
background: var(--cody-bg-gradient);
padding-right: 0.75rem;
padding-top: 1.25rem;
padding-bottom: 1rem;
padding-left: 1.875rem;
gap: 1.5rem;
}
.card-description {
margin-bottom: 0.75rem;
margin-top: 0.125rem;
}
.auth-buttons-wrap {
display: flex;
flex-wrap: wrap;
gap: 0.813rem;
}
.terms-link {
color: var(--cody-link-color);
text-decoration: underline;
}
.auth-button,
.email-auth-button {
text-align: center;
border: none;
max-width: 11.938rem;
width: 100%;
padding: 0.375rem 0.75rem;
border-radius: 0.188rem;
margin-bottom: 0.313rem;
margin-top: 0;
}
.auth-button {
background-color: var(--white) !important;
color: var(--black) !important;
}
.email-auth-button {
background-color: var(--white);
color: var(--body-color) !important;
border: 1px solid var(--border-color);
svg {
color: var(--email-icon-color);
height: 1.125rem;
}
:global(.theme-dark) & {
color: var(--gray-08) !important;
}
}

View File

@ -1,242 +0,0 @@
import React, { useCallback, useEffect } from 'react'
import { mdiClose } from '@mdi/js'
import classNames from 'classnames'
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import { Button, H2, H4, Icon, Link, Text } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../../auth'
import { ExternalsAuth } from '../../../auth/components/ExternalsAuth'
import { MarketingBlock } from '../../../components/MarketingBlock'
import type { SourcegraphContext } from '../../../jscontext'
import { EventName } from '../../../util/constants'
import { GlowingCodySVG, MeetCodySVG } from './WidgetIcons'
import styles from './TryCodyWidget.module.scss'
const AUTO_DISMISS_ON_EVENTS = new Set([EventName.CODY_SIDEBAR_CHAT_OPENED, EventName.CODY_CHAT_SUBMIT])
interface WidgetContentProps extends TelemetryProps, TelemetryV2Props {
type: 'blob' | 'repo'
theme?: 'light' | 'dark'
isSourcegraphDotCom: boolean
}
interface NoAuhWidgetContentProps extends WidgetContentProps {
context: Pick<SourcegraphContext, 'externalURL'>
}
function useTryCodyWidget(telemetryService: TelemetryProps['telemetryService']): {
isDismissed: boolean | undefined
onDismiss: () => void
} {
// `isDismissed = true` maintain the initial concealment of the CTA when loading the settings
const [isDismissed = true, setIsDismissed] = useTemporarySetting('cody.blobPageCta.dismissed', false)
const onDismiss = useCallback(() => {
setIsDismissed(true)
}, [setIsDismissed])
// Listen for telemetry events to auto dismiss the widget
useEffect(() => {
if (isDismissed) {
return
}
return telemetryService.addEventLogListener?.(eventName => {
if (AUTO_DISMISS_ON_EVENTS.has(eventName as EventName)) {
onDismiss()
}
})
}, [telemetryService, isDismissed, onDismiss])
return { isDismissed, onDismiss }
}
const NoAuthWidgetContent: React.FC<NoAuhWidgetContentProps> = ({
type,
telemetryService,
telemetryRecorder,
context,
}) => {
const title = type === 'blob' ? 'Sign up to get Cody, our AI assistant, free' : 'Meet Cody, your AI assistant'
const eventPage = type === 'blob' ? 'try-cody-widget-blob' : 'try-cody-widget-repo'
return (
<>
<MeetCodySVG />
<div className="flex-grow-1">
<H2 className={styles.cardTitle}>{title}</H2>
<Text className={styles.cardDescription}>
Cody combines an LLM with the context of Sourcegraph's code graph on public code or your code at
work.{' '}
</Text>
<div className={styles.authButtonsWrap}>
<ExternalsAuth
page={eventPage}
context={context}
githubLabel="GitHub"
gitlabLabel="GitLab"
googleLabel="Google"
withCenteredText={true}
onClick={() => {}}
ctaClassName={styles.authButton}
telemetryRecorder={telemetryRecorder}
telemetryService={telemetryService}
/>
</div>
<Text className="mb-2 mt-2">
By registering, you agree to our{' '}
<Link
to="https://sourcegraph.com/terms"
className={styles.termsLink}
target="_blank"
rel="noopener"
>
Terms of Service
</Link>{' '}
and{' '}
<Link
to="https://sourcegraph.com/terms/privacy"
className={styles.termsLink}
target="_blank"
rel="noopener"
>
Privacy Policy
</Link>
</Text>
</div>
</>
)
}
const AuthUserWidgetContent: React.FC<WidgetContentProps> = ({ type, theme, isSourcegraphDotCom }) => {
const { title, useCases, image } = isSourcegraphDotCom
? type === 'blob'
? {
title: 'Try Cody on public code',
useCases: ['Select code in the file below', 'Select an action with Cody widget'],
image: `https://storage.googleapis.com/sourcegraph-assets/app-images/cody-action-bar-${theme}.png`,
}
: {
title: 'Try Cody on this repository',
useCases: [
'Click the Ask Cody button above and to the right of this banner',
'Ask Cody a question like “Explain the structure of this repository”',
],
image: `https://storage.googleapis.com/sourcegraph-assets/app-images/cody-chat-banner-image-${theme}.png`,
}
: type === 'blob'
? {
title: 'Try Cody on this file',
useCases: ['Select code in the file below', 'Select an action with Cody widget'],
image: `https://storage.googleapis.com/sourcegraph-assets/app-images/cody-action-bar-${theme}.png`,
}
: {
title: 'Try Cody on this repository',
useCases: [
'Click the Ask Cody button above and to the right of this banner',
'Ask Cody a question like “Explain the structure of this repository”',
],
image: `https://storage.googleapis.com/sourcegraph-assets/app-images/cody-chat-banner-image-${theme}.png`,
}
return (
<>
<div className="d-flex pb-3">
<GlowingCodySVG />
<div className="d-flex flex-column flex-grow-1 justify-content-center flex-shrink-0">
<H4 as="h2" className={styles.cardTitle}>
{title}
</H4>
<ol className={classNames('m-0 pl-4', styles.cardList)}>
{useCases.map(useCase => (
<Text key={useCase} as="li">
{useCase}
</Text>
))}
</ol>
</div>
</div>
<div className={classNames('d-flex justify-content-center', styles.cardImages)}>
<img src={image} alt="Cody" className={classNames(styles.cardImage, 'percy-hide')} />
</div>
</>
)
}
interface TryCodyWidgetProps extends TelemetryProps, TelemetryV2Props {
className?: string
type: 'blob' | 'repo'
authenticatedUser: AuthenticatedUser | null
context: Pick<SourcegraphContext, 'externalURL'>
isSourcegraphDotCom: boolean
}
export const TryCodyWidget: React.FC<TryCodyWidgetProps> = ({
className,
telemetryService,
telemetryRecorder,
authenticatedUser,
context,
type,
isSourcegraphDotCom,
}) => {
const isLightTheme = useIsLightTheme()
const { isDismissed, onDismiss } = useTryCodyWidget(telemetryService)
useEffect(() => {
if (isDismissed) {
return
}
const eventPage = type === 'blob' ? 'BlobPage' : 'RepoPage'
telemetryService.log(EventName.TRY_CODY_WEB_ONBOARDING_DISPLAYED, { type: eventPage }, { type: eventPage })
const v2EventPage = type === 'blob' ? 0 : 1
telemetryRecorder.recordEvent('cta.tryCodyWebOnboarding', 'view', { metadata: { page: v2EventPage } })
}, [isDismissed, telemetryService, telemetryRecorder, type])
if (isDismissed) {
return null
}
return (
<MarketingBlock
wrapperClassName={classNames(
className,
type === 'blob' ? styles.blobCardWrapper : styles.repoCardWrapper,
'mb-2'
)}
contentClassName={classNames(
'd-flex position-relative pb-0 overflow-auto justify-content-between',
styles.card,
!authenticatedUser && styles.noAuthCard
)}
variant="thin"
>
{authenticatedUser ? (
<AuthUserWidgetContent
type={type}
theme={isLightTheme ? 'light' : 'dark'}
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
isSourcegraphDotCom={isSourcegraphDotCom}
/>
) : (
<NoAuthWidgetContent
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
type={type}
context={context}
isSourcegraphDotCom={isSourcegraphDotCom}
/>
)}
<Button className={classNames(styles.closeButton, 'position-absolute mt-2')} onClick={onDismiss}>
<Icon svgPath={mdiClose} aria-label="Close try Cody widget" />
</Button>
</MarketingBlock>
)
}

File diff suppressed because one or more lines are too long

View File

@ -8,10 +8,9 @@ import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
import type { AuthenticatedUser } from '../../auth'
import { type RepositoryFields, RepositoryType } from '../../graphql-operations'
import { RepositoryType, type RepositoryFields } from '../../graphql-operations'
import { type Props, TreePage } from './TreePage'
import { TreePage, type Props } from './TreePage'
// TreePage has a dependency on the `perforceChangelistMapping` experimental feature
// in order to build an appropriately-worded Commits button.
@ -156,32 +155,5 @@ describe('TreePage', () => {
)
expect(result.queryByTestId('repo-fork-badge')).toHaveTextContent('Fork')
})
it('Should display cody CTA', () => {
const repo = repoDefaults()
const props = treePagePropsDefaults(repo)
window.context = window.context || {}
window.context.codyEnabled = true
window.context.codyEnabledForCurrentUser = true
const mockUser = {
id: 'userID',
username: 'username',
emails: [{ email: 'user@me.com', isPrimary: true, verified: true }],
siteAdmin: true,
} as AuthenticatedUser
renderWithBrandedContext(
<MockedProvider>
<TreePage {...{ ...props, isSourcegraphDotCom: true, authenticatedUser: mockUser }} />
</MockedProvider>
)
expect(screen.getByText('Try Cody on this repository')).toBeVisible()
expect(screen.getByText('Click the Ask Cody button above and to the right of this banner')).toBeVisible()
expect(
screen.getByText('Ask Cody a question like “Explain the structure of this repository”')
).toBeVisible()
})
})
})

View File

@ -46,13 +46,11 @@ import type { AuthenticatedUser } from '../../auth'
import type { BatchChangesProps } from '../../batches'
import { RepoBatchChangesButton } from '../../batches/RepoBatchChangesButton'
import type { CodeIntelligenceProps } from '../../codeintel'
import { isCodyEnabled } from '../../cody/isCodyEnabled'
import type { BreadcrumbSetters } from '../../components/Breadcrumbs'
import { PageTitle } from '../../components/PageTitle'
import type { FileCommitsResult, FileCommitsVariables, RepositoryFields } from '../../graphql-operations'
import type { SourcegraphContext } from '../../jscontext'
import type { OwnConfigProps } from '../../own/OwnConfigProps'
import { TryCodyWidget } from '../components/TryCodyWidget/TryCodyWidget'
import { FilePathBreadcrumbs } from '../FilePathBreadcrumbs'
import { isPackageServiceType } from '../packages/isPackageServiceType'
import { RepoCommitsButton } from '../utils'
@ -350,17 +348,6 @@ export const TreePage: FC<Props> = ({
return (
<div className={classNames(styles.treePage, className)}>
{(isSourcegraphDotCom || isCodyEnabled()) && (
<TryCodyWidget
className="mb-2"
telemetryService={props.telemetryService}
telemetryRecorder={props.telemetryRecorder}
type="repo"
authenticatedUser={authenticatedUser}
context={context}
isSourcegraphDotCom={isSourcegraphDotCom}
/>
)}
<Container className={styles.container}>
<div className={classNames(styles.header)}>
<PageTitle title={getPageTitle()} />

View File

@ -35,6 +35,8 @@ export enum PageRoutes {
Notebooks = '/notebooks',
SearchNotebook = '/search/notebook',
Cody = '/cody',
CodyDashboard = '/cody/dashboard',
CodyRedirectToMarketingOrDashboard = '/cody',
CodyChat = '/cody/chat',
CodySwitchAccount = '/cody/switch-account/:username',
Own = '/own',

View File

@ -1,10 +1,11 @@
import { useEffect } from 'react'
import { Navigate, useNavigate, type RouteObject } from 'react-router-dom'
import { Navigate, type RouteObject } from 'react-router-dom'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { codyProRoutes } from './cody/codyProRoutes'
import { codyRoutes } from './cody/codyRoutes'
import { communitySearchContextsRoutes } from './communitySearchContexts/routes'
import { LegacyRoute, type LegacyLayoutRouteContext } from './LegacyRouteContext'
import { PageRoutes } from './routes.constants'
@ -60,13 +61,6 @@ const SearchContextPage = lazyComponent(
)
const SearchUpsellPage = lazyComponent(() => import('./search/upsell/SearchUpsellPage'), 'SearchUpsellPage')
const SearchPageWrapper = lazyComponent(() => import('./search/SearchPageWrapper'), 'SearchPageWrapper')
const CodyChatPage = lazyComponent(() => import('./cody/chat/CodyChatPage'), 'CodyChatPage')
const CodySwitchAccountPage = lazyComponent(
() => import('./cody/switch-account/CodySwitchAccountPage'),
'CodySwitchAccountPage'
)
const CodyUpsellPage = lazyComponent(() => import('./cody/upsell/CodyUpsellPage'), 'CodyUpsellPage')
const CodyDashboardPage = lazyComponent(() => import('./cody/dashboard/CodyDashboardPage'), 'CodyDashboardPage')
const SearchJob = lazyComponent(() => import('./enterprise/search-jobs/SearchJobsPage'), 'SearchJobsPage')
const Index = lazyComponent(() => import('./Index'), 'IndexPage')
@ -153,8 +147,8 @@ export const routes: RouteObject[] = [
element: (
<LegacyRoute
render={props => <GlobalCodeMonitoringArea {...props} />}
condition={({ isSourcegraphDotCom, licenseFeatures }) =>
!isSourcegraphDotCom && licenseFeatures.isCodeSearchEnabled
condition={({ isSourcegraphDotCom }) =>
!isSourcegraphDotCom && window.context?.codeSearchEnabledOnInstance
}
/>
),
@ -190,7 +184,7 @@ export const routes: RouteObject[] = [
element: (
<LegacyRoute
render={props => <SearchContextsListPage {...props} />}
condition={({ licenseFeatures }) => licenseFeatures.isCodeSearchEnabled}
condition={() => window.context?.codeSearchEnabledOnInstance}
/>
),
},
@ -199,7 +193,7 @@ export const routes: RouteObject[] = [
element: (
<LegacyRoute
render={props => <CreateSearchContextPage {...props} />}
condition={({ licenseFeatures }) => licenseFeatures.isCodeSearchEnabled}
condition={() => window.context?.codeSearchEnabledOnInstance}
/>
),
},
@ -208,7 +202,7 @@ export const routes: RouteObject[] = [
element: (
<LegacyRoute
render={props => <EditSearchContextPage {...props} />}
condition={({ licenseFeatures }) => licenseFeatures.isCodeSearchEnabled}
condition={() => window.context?.codeSearchEnabledOnInstance}
/>
),
},
@ -217,7 +211,7 @@ export const routes: RouteObject[] = [
element: (
<LegacyRoute
render={props => <SearchContextPage {...props} />}
condition={({ licenseFeatures }) => licenseFeatures.isCodeSearchEnabled}
condition={() => window.context?.codeSearchEnabledOnInstance}
/>
),
},
@ -232,7 +226,7 @@ export const routes: RouteObject[] = [
render={props => (
<GlobalNotebooksArea {...props} telemetryRecorder={props.platformContext.telemetryRecorder} />
)}
condition={({ licenseFeatures }) => licenseFeatures.isCodeSearchEnabled}
condition={() => window.context?.codeSearchEnabledOnInstance}
/>
),
},
@ -321,61 +315,9 @@ export const routes: RouteObject[] = [
path: PageRoutes.Debug,
element: <PassThroughToServer />,
},
// TODO: [TEMPORARY] remove this redirect route when the marketing page is added.
{
path: `${PageRoutes.Cody}/*`,
element: (
<LegacyRoute
render={() => {
const chatID = window.location.pathname.split('/').pop()
const navigate = useNavigate()
useEffect(() => {
navigate(`/cody/chat/${chatID}`)
}, [navigate, chatID])
return <div />
}}
condition={({ licenseFeatures }) =>
!window.location.pathname.startsWith('/cody/chat') && licenseFeatures.isCodyEnabled
}
/>
),
},
{
path: PageRoutes.CodyChat + '/*',
element: (
<LegacyRoute
render={props => (
<CodyIgnoreProvider isSourcegraphDotCom={props.isSourcegraphDotCom}>
<CodyChatPage
{...props}
context={window.context}
telemetryRecorder={props.platformContext.telemetryRecorder}
/>
</CodyIgnoreProvider>
)}
condition={({ licenseFeatures }) => licenseFeatures.isCodyEnabled}
/>
),
},
{
path: PageRoutes.CodySwitchAccount,
element: (
<LegacyRoute
render={props => (
<CodySwitchAccountPage {...props} telemetryRecorder={props.platformContext.telemetryRecorder} />
)}
condition={({ licenseFeatures }) => licenseFeatures.isCodyEnabled}
/>
),
},
...codyProRoutes,
...codyRoutes,
...communitySearchContextsRoutes,
{
path: PageRoutes.Cody,
element: <LegacyRoute render={props => <CodyDashboardOrUpsellPage {...props} />} />,
},
// this should be the last route to be regustered because it's a catch all route
// when the instance has the code search feature.
{
@ -392,7 +334,7 @@ export const routes: RouteObject[] = [
</CodySidebarStoreProvider>
</CodyIgnoreProvider>
)}
condition={({ licenseFeatures }) => licenseFeatures.isCodeSearchEnabled}
condition={() => window.context?.codeSearchEnabledOnInstance}
/>
),
// In RR6, the useMatches hook will only give you the location that is matched
@ -404,17 +346,9 @@ export const routes: RouteObject[] = [
]
function SearchPageOrUpsellPage(props: LegacyLayoutRouteContext): JSX.Element {
const { isCodeSearchEnabled } = props.licenseFeatures
if (!isCodeSearchEnabled) {
return <SearchUpsellPage telemetryRecorder={props.platformContext.telemetryRecorder} />
}
return <SearchPageWrapper {...props} />
}
function CodyDashboardOrUpsellPage(props: LegacyLayoutRouteContext): JSX.Element {
const { isCodyEnabled } = props.licenseFeatures
if (!isCodyEnabled) {
return <CodyUpsellPage />
}
return <CodyDashboardPage {...props} telemetryRecorder={props.platformContext.telemetryRecorder} />
return window.context?.codeSearchEnabledOnInstance ? (
<SearchPageWrapper {...props} />
) : (
<SearchUpsellPage telemetryRecorder={props.platformContext.telemetryRecorder} />
)
}

View File

@ -2,14 +2,14 @@ import React, { useMemo, useRef } from 'react'
import classNames from 'classnames'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import { Routes, Route } from 'react-router-dom'
import { Route, Routes } from 'react-router-dom'
import type { SiteSettingFields } from '@sourcegraph/shared/src/graphql-operations'
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { PageHeader, LoadingSpinner } from '@sourcegraph/wildcard'
import { LoadingSpinner, PageHeader } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../auth'
import { withAuthenticatedUser } from '../auth/withAuthenticatedUser'
@ -20,15 +20,14 @@ import { Page } from '../components/Page'
import { useFeatureFlag } from '../featureFlags/useFeatureFlag'
import { useUserExternalAccounts } from '../hooks/useUserExternalAccounts'
import type { RouteV6Descriptor } from '../util/contributions'
import { getLicenseFeatures } from '../util/license'
import {
maintenanceGroupHeaderLabel,
maintenanceGroupInstrumentationItemLabel,
maintenanceGroupMonitoringItemLabel,
maintenanceGroupMigrationsItemLabel,
maintenanceGroupUpdatesItemLabel,
maintenanceGroupMonitoringItemLabel,
maintenanceGroupTracingItemLabel,
maintenanceGroupUpdatesItemLabel,
} from './sidebaritems'
import { SiteAdminSidebar, type SiteAdminSideBarGroups } from './SiteAdminSidebar'
@ -62,11 +61,6 @@ export interface SiteAdminAreaRouteContext
codeInsightsEnabled: boolean
endUserOnboardingEnabled: boolean
license: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
export interface SiteAdminAreaRoute extends RouteV6Descriptor<SiteAdminAreaRouteContext> {}
@ -149,7 +143,6 @@ const AuthenticatedSiteAdminArea: React.FunctionComponent<React.PropsWithChildre
telemetryRecorder: props.telemetryRecorder,
codeInsightsEnabled: props.codeInsightsEnabled,
endUserOnboardingEnabled,
license: getLicenseFeatures(),
}
return (
@ -161,7 +154,6 @@ const AuthenticatedSiteAdminArea: React.FunctionComponent<React.PropsWithChildre
</PageHeader>
<div className="d-flex my-3 flex-column flex-sm-row" ref={reference}>
<SiteAdminSidebar
license={context.license}
className={classNames('flex-0 mr-3 mb-4', styles.sidebar)}
groups={adminSideBarGroups}
isSourcegraphDotCom={props.isSourcegraphDotCom}

View File

@ -15,11 +15,6 @@ export interface SiteAdminSideBarGroupContext extends BatchChangesProps {
isSourcegraphDotCom: boolean
codeInsightsEnabled: boolean
endUserOnboardingEnabled: boolean
license: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
export interface SiteAdminSideBarGroup extends NavGroupDescriptor<SiteAdminSideBarGroupContext> {}

View File

@ -1,18 +1,17 @@
import React from 'react'
import { mdiMagnify, mdiSitemap, mdiBookOutline, mdiPuzzleOutline, mdiPoll } from '@mdi/js'
import { mdiBookOutline, mdiMagnify, mdiPoll, mdiPuzzleOutline, mdiSitemap } from '@mdi/js'
import classNames from 'classnames'
import { useQuery } from '@sourcegraph/http-client'
import { H2, H3, Text, LoadingSpinner, Link, Icon, Tooltip } from '@sourcegraph/wildcard'
import { H2, H3, Icon, Link, LoadingSpinner, Text, Tooltip } from '@sourcegraph/wildcard'
import { BatchChangesIconNav } from '../../../batches/icons'
import {
AnalyticsDateRange,
type OverviewDevTimeSavedResult,
type OverviewDevTimeSavedVariables,
AnalyticsDateRange,
} from '../../../graphql-operations'
import { isCodyOnlyLicense } from '../../../util/license'
import { ValueLegendItem } from '../components/ValueLegendList'
import { formatNumber } from '../utils'
@ -115,7 +114,7 @@ export const DevTimeSaved: React.FunctionComponent<DevTimeSavedProps> = ({ showA
return totalHoursSaved
})()
const disableCodeSearchItems = isCodyOnlyLicense()
const disableCodeSearchItems = !window.context?.codeSearchEnabledOnInstance
return (
<div>

View File

@ -191,12 +191,12 @@ export const otherSiteAdminRoutes: readonly SiteAdminAreaRoute[] = [
{
path: '/analytics/search',
render: props => <AnalyticsSearchPage {...props} />,
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
path: '/analytics/code-intel',
render: props => <AnalyticsCodeIntelPage {...props} />,
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
path: '/analytics/extensions',
@ -209,7 +209,7 @@ export const otherSiteAdminRoutes: readonly SiteAdminAreaRoute[] = [
{
path: '/analytics/cody',
render: props => <AnalyticsCodyPage {...props} />,
condition: ({ license }) => license.isCodyEnabled,
condition: () => window.context?.codyEnabledOnInstance,
},
{
path: '/analytics/code-insights',
@ -224,7 +224,7 @@ export const otherSiteAdminRoutes: readonly SiteAdminAreaRoute[] = [
{
path: '/analytics/notebooks',
render: props => <AnalyticsNotebooksPage {...props} />,
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
path: '/configuration',
@ -457,13 +457,13 @@ export const otherSiteAdminRoutes: readonly SiteAdminAreaRoute[] = [
{
path: '/code-intelligence/*',
render: () => <NavigateToCodeGraph />,
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
// Code graph routes
{
path: '/code-graph/*',
render: props => <AdminCodeIntelArea {...props} />,
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
path: '/lsif-uploads/:id',

View File

@ -29,17 +29,17 @@ const analyticsGroup: SiteAdminSideBarGroup = {
{
label: 'Search',
to: '/site-admin/analytics/search',
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
label: 'Cody',
to: '/site-admin/analytics/cody',
condition: ({ license }) => license.isCodyEnabled,
condition: () => window.context?.codyEnabledOnInstance,
},
{
label: 'Code navigation',
to: '/site-admin/analytics/code-intel',
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
label: 'Users',
@ -58,7 +58,7 @@ const analyticsGroup: SiteAdminSideBarGroup = {
{
label: 'Notebooks',
to: '/site-admin/analytics/notebooks',
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
label: 'Search extensions',
@ -67,7 +67,7 @@ const analyticsGroup: SiteAdminSideBarGroup = {
{
label: 'Code ownership',
to: '/site-admin/analytics/own',
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
},
{
label: 'Feedback survey',
@ -277,7 +277,7 @@ const codeIntelGroup: SiteAdminSideBarGroup = {
to: '/site-admin/own-signal-page',
},
],
condition: ({ license }) => license.isCodeSearchEnabled,
condition: () => window.context?.codeSearchEnabledOnInstance,
}
const usersGroup: SiteAdminSideBarGroup = {

View File

@ -1,51 +0,0 @@
@import 'wildcard/src/global-styles/breakpoints';
.upsell {
padding: 1.75rem 2.5rem;
padding-right: 1rem;
margin-top: 4rem;
max-width: 60rem;
border-radius: 0.5rem;
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 1rem;
@media (--sm-breakpoint-down) {
grid-template-columns: 1fr;
}
&-logo {
width: 2.5rem;
height: 2.5rem;
margin-bottom: 1rem;
}
&-image {
filter: drop-shadow(-7px -16px 32px #a112ff24);
width: 100%;
}
&-meta {
align-self: center;
}
&-title {
color: var(--text-title);
font-size: 1.5rem;
font-weight: 500;
margin-bottom: 0.75rem;
}
&-description {
color: var(--text-body);
font-size: 0.9375rem;
margin-bottom: 1.25rem;
}
&-link {
&-icon {
width: 1rem;
height: 1rem;
}
}
}

View File

@ -1,34 +0,0 @@
import type { FC } from 'react'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import { Link, Text } from '@sourcegraph/wildcard'
import { CodyLogo } from '../../../cody/components/CodyLogo'
import { MultiLineCompletion } from './MultilineCompletion'
import styles from './CodyUpsell.module.scss'
interface CodyUpsellProps {
isSourcegraphDotCom: boolean
}
export const CodyUpsell: FC<CodyUpsellProps> = ({ isSourcegraphDotCom }) => {
const isLightTheme = useIsLightTheme()
// On DotCom, we want to redirect to the PLG page. On Enterprise instances, we redirect to their Cody dashboard page.
const exploreCodyLink = isSourcegraphDotCom ? 'https://sourcegraph.com/cody' : '/cody'
return (
<section className={styles.upsell}>
<section className={styles.upsellMeta}>
<CodyLogo withColor={true} className={styles.upsellLogo} />
<Text className={styles.upsellTitle}>Introducing Cody: your new AI coding assistant.</Text>
<Text className={styles.upsellDescription}>
Cody autocompletes single lines, or entire code blocks, in any programming language, keeping all of
your companys codebase in mind.
</Text>
<Link to={exploreCodyLink}>Explore Cody</Link>
</section>
<MultiLineCompletion isLightTheme={isLightTheme} className={styles.upsellImage} />
</section>
)
}

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { type FC, useEffect, useState } from 'react'
import { useEffect, useState, type FC } from 'react'
import classNames from 'classnames'
@ -19,7 +19,6 @@ import { GettingStartedTour } from '../../../tour/GettingStartedTour'
import { useShowOnboardingTour } from '../../../tour/hooks'
import { AddCodeHostWidget } from './AddCodeHostWidget'
import { CodyUpsell } from './CodyUpsell'
import { KeywordSearchCtaSection } from './KeywordSearchCtaSection'
import { SearchPageFooter } from './SearchPageFooter'
import { SearchPageInput } from './SearchPageInput'
@ -158,7 +157,6 @@ export const SearchPageContent: FC<SearchPageContentProps> = props => {
)}
</div>
)}
<CodyUpsell isSourcegraphDotCom={isSourcegraphDotCom} />
<SearchPageFooter />
</div>
)

View File

@ -1,6 +1,6 @@
import { type FC, useMemo, Suspense } from 'react'
import { Suspense, useMemo, type FC } from 'react'
import { useParams, Routes, Route } from 'react-router-dom'
import { Route, Routes, useParams } from 'react-router-dom'
import { gql, useQuery } from '@sourcegraph/http-client'
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
@ -10,7 +10,7 @@ import { LoadingSpinner } from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
import type { BatchChangesProps } from '../../batches'
import type { BreadcrumbsProps, BreadcrumbSetters } from '../../components/Breadcrumbs'
import type { BreadcrumbSetters, BreadcrumbsProps } from '../../components/Breadcrumbs'
import { RouteError } from '../../components/ErrorBoundary'
import { NotFoundPage } from '../../components/HeroPage'
import { Page } from '../../components/Page'
@ -21,7 +21,6 @@ import type {
} from '../../graphql-operations'
import type { NamespaceProps } from '../../namespaces'
import type { RouteV6Descriptor } from '../../util/contributions'
import { getLicenseFeatures } from '../../util/license'
import { isAccessTokenCallbackPage } from '../settings/accessTokens/UserSettingsCreateAccessTokenCallbackPage'
import type { UserSettingsAreaRoute } from '../settings/UserSettingsArea'
import type { UserSettingsSidebarItems } from '../settings/UserSettingsSidebar'
@ -127,12 +126,6 @@ export interface UserAreaRouteContext
userSettingsAreaRoutes: readonly UserSettingsAreaRoute[]
isSourcegraphDotCom: boolean
// license related properties
license: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
/**
@ -193,7 +186,6 @@ export const UserArea: FC<UserAreaProps> = ({ useBreadcrumb, userAreaRoutes, isS
namespace: user,
...childBreadcrumbSetters,
isSourcegraphDotCom,
license: getLicenseFeatures(),
telemetryRecorder: props.platformContext.telemetryRecorder,
}

View File

@ -20,11 +20,6 @@ interface Props extends UserAreaRouteContext {
export interface UserAreaHeaderContext extends BatchChangesProps, Pick<Props, 'user'> {
isSourcegraphDotCom: boolean
license: {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
}
export interface UserAreaHeaderNavItem extends NavItemWithIconDescriptor<UserAreaHeaderContext> {}

View File

@ -22,7 +22,8 @@ export const userAreaHeaderNavItems: readonly UserAreaHeaderNavItem[] = [
to: '/searches',
label: 'Saved searches',
icon: FeatureSearchOutlineIcon,
condition: ({ user: { viewerCanAdminister }, license }) => viewerCanAdminister && license.isCodeSearchEnabled,
condition: ({ user: { viewerCanAdminister } }) =>
viewerCanAdminister && window.context?.codeSearchEnabledOnInstance,
},
...namespaceAreaHeaderNavItems,
]

View File

@ -1,55 +0,0 @@
import { describe, expect, it, afterEach } from 'vitest'
import { isCodyOnlyLicense, isCodeSearchOnlyLicense, isCodeSearchPlusCodyLicense } from './license'
describe('licensing utils', () => {
const origContext = window.context
afterEach(() => {
window.context = origContext
})
it('Cody only license', () => {
window.context = {
licenseInfo: {
features: {
cody: true,
codeSearch: false,
},
},
} as any
expect(isCodyOnlyLicense()).toEqual(true)
expect(isCodeSearchOnlyLicense()).toEqual(false)
expect(isCodeSearchPlusCodyLicense()).toEqual(false)
})
it('Code Search only license', () => {
window.context = {
licenseInfo: {
features: {
cody: false,
codeSearch: true,
},
},
} as any
expect(isCodyOnlyLicense()).toEqual(false)
expect(isCodeSearchOnlyLicense()).toEqual(true)
expect(isCodeSearchPlusCodyLicense()).toEqual(false)
})
it('Code Search plus Cody license', () => {
window.context = {
licenseInfo: {
features: {
cody: true,
codeSearch: true,
},
},
} as any
expect(isCodyOnlyLicense()).toEqual(false)
expect(isCodeSearchOnlyLicense()).toEqual(false)
expect(isCodeSearchPlusCodyLicense()).toEqual(true)
})
})

View File

@ -1,30 +0,0 @@
export const isCodyOnlyLicense = (): boolean =>
Boolean(
typeof window !== 'undefined' &&
!window.context.licenseInfo?.features.codeSearch &&
window.context.licenseInfo?.features.cody
)
export const isCodeSearchOnlyLicense = (): boolean =>
Boolean(
typeof window !== 'undefined' &&
window.context.licenseInfo?.features.codeSearch &&
!window.context.licenseInfo?.features.cody
)
export const isCodeSearchPlusCodyLicense = (): boolean =>
Boolean(
typeof window !== 'undefined' &&
window.context.licenseInfo?.features.codeSearch &&
window.context.licenseInfo?.features.cody
)
interface LicenseFeatures {
isCodeSearchEnabled: boolean
isCodyEnabled: boolean
}
export const getLicenseFeatures = (): LicenseFeatures => ({
isCodeSearchEnabled: Boolean(window.context.licenseInfo?.features.codeSearch),
isCodyEnabled: Boolean(window.context.licenseInfo?.features.cody),
})

View File

@ -132,19 +132,9 @@ type FeatureBatchChanges struct {
MaxNumChangesets int `json:"maxNumChangesets"`
}
// LicenseFeatures contains information about licensed features that are
// enabled/disabled on the current license.
type LicenseFeatures struct {
CodeSearch bool `json:"codeSearch"`
Cody bool `json:"cody"`
}
// LicenseInfo contains non-sensitive information about the legitimate usage of the
// current license on the instance. It is technically accessible to all users, so only
// include information that is safe to be seen by others.
// LicenseInfo contains non-sensitive information about the current license on the instance.
type LicenseInfo struct {
BatchChanges *FeatureBatchChanges `json:"batchChanges"`
Features LicenseFeatures `json:"features"`
}
// FrontendCodyProConfig is the configuration data for Cody Pro that needs to be passed
@ -221,15 +211,22 @@ type JSContext struct {
BatchChangesDisableWebhooksWarning bool `json:"batchChangesDisableWebhooksWarning"`
BatchChangesWebhookLogsEnabled bool `json:"batchChangesWebhookLogsEnabled"`
// CodyEnabled is true `cody.enabled` is not false in site-config
CodyEnabled bool `json:"codyEnabled"`
// CodyEnabledForCurrentUser is true if CodyEnabled is true and current
// CodyEnabledOnInstance is true `cody.enabled` is not false in site config. Check
// CodyEnabledForCurrentUser to see if the current user has access to Cody.
CodyEnabledOnInstance bool `json:"codyEnabledOnInstance"`
// CodyEnabledForCurrentUser is true if CodyEnabled is true and the current
// user has access to Cody.
CodyEnabledForCurrentUser bool `json:"codyEnabledForCurrentUser"`
// CodyRequiresVerifiedEmail is true if usage of Cody requires the current
// user to have a verified email.
CodyRequiresVerifiedEmail bool `json:"codyRequiresVerifiedEmail"`
// CodeSearchEnabledOnInstance is true if code search is licensed. (There is currently no
// separate config to disable it if licensed.)
CodeSearchEnabledOnInstance bool `json:"codeSearchEnabledOnInstance"`
ExecutorsEnabled bool `json:"executorsEnabled"`
CodeIntelAutoIndexingEnabled bool `json:"codeIntelAutoIndexingEnabled"`
CodeIntelAutoIndexingAllowGlobalPolicies bool `json:"codeIntelAutoIndexingAllowGlobalPolicies"`
@ -374,6 +371,8 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext {
isDotComMode := dotcom.SourcegraphDotComMode()
licenseInfo, codeSearchLicensed, codyLicensed := licenseInfo()
// 🚨 SECURITY: This struct is sent to all users regardless of whether or
// not they are logged in, for example on an auth.public=false private
// server. Including secret fields here is OK if it is based on the user's
@ -432,10 +431,12 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext {
BatchChangesDisableWebhooksWarning: conf.Get().BatchChangesDisableWebhooksWarning,
BatchChangesWebhookLogsEnabled: webhooks.LoggingEnabled(conf.Get()),
CodyEnabled: conf.CodyEnabled(),
CodyEnabledOnInstance: conf.CodyEnabled(),
CodyEnabledForCurrentUser: codyEnabled,
CodyRequiresVerifiedEmail: siteResolver.RequiresVerifiedEmailForCody(ctx),
CodeSearchEnabledOnInstance: codeSearchLicensed,
ExecutorsEnabled: conf.ExecutorsEnabled(),
CodeIntelAutoIndexingEnabled: conf.CodeIntelAutoIndexingEnabled(),
CodeIntelAutoIndexingAllowGlobalPolicies: conf.CodeIntelAutoIndexingAllowGlobalPolicies(),
@ -457,7 +458,7 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext {
ExperimentalFeatures: conf.ExperimentalFeatures(),
LicenseInfo: licenseInfo(),
LicenseInfo: licenseInfo,
HashedLicenseKey: conf.HashedCurrentLicenseKeyForAnalytics(),
@ -483,7 +484,8 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext {
// If the license a Sourcegraph instance is running under does not support Code Search features
// we force disable related features (executors, batch-changes, executors, code-insights).
if !context.LicenseInfo.Features.CodeSearch {
if !codeSearchLicensed {
context.CodeSearchEnabledOnInstance = false
context.BatchChangesEnabled = false
context.CodeInsightsEnabled = false
context.ExecutorsEnabled = false
@ -500,8 +502,8 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext {
// If the license a Sourcegraph instance is running under does not support Cody features,
// we force disable related features.
if !context.LicenseInfo.Features.Cody {
context.CodyEnabled = false
if !codyLicensed {
context.CodyEnabledOnInstance = false
context.CodyEnabledForCurrentUser = false
}
@ -668,7 +670,7 @@ func isBot(userAgent string) bool {
return isBotPat.MatchString(userAgent)
}
func licenseInfo() (info LicenseInfo) {
func licenseInfo() (info LicenseInfo, codeSearchLicensed, codyLicensed bool) {
if !dotcom.SourcegraphDotComMode() {
bcFeature := &licensing.FeatureBatchChanges{}
if err := licensing.Check(bcFeature); err == nil {
@ -687,12 +689,10 @@ func licenseInfo() (info LicenseInfo) {
}
}
info.Features = LicenseFeatures{
CodeSearch: licensing.Check(licensing.FeatureCodeSearch) == nil,
Cody: licensing.Check(licensing.FeatureCody) == nil,
}
codeSearchLicensed = licensing.Check(licensing.FeatureCodeSearch) == nil
codyLicensed = licensing.Check(licensing.FeatureCody) == nil
return info
return info, codeSearchLicensed, codyLicensed
}
func makeFrontendCodyProConfig(config *schema.CodyProConfig) *FrontendCodyProConfig {

View File

@ -367,8 +367,7 @@ func serveHome(db database.DB) handlerFunc {
// On non-Sourcegraph.com instances, there is no separate homepage, so redirect to /search.
// except if the instance is on a Cody-Only license.
redirectURL := "/search"
features := common.Context.LicenseInfo.Features
if !features.CodeSearch && features.Cody && !dotcom.SourcegraphDotComMode() {
if !common.Context.CodeSearchEnabledOnInstance && common.Context.CodyEnabledOnInstance && !dotcom.SourcegraphDotComMode() {
redirectURL = "/cody"
}

View File

@ -34,8 +34,8 @@ export const defaultProjectConfig: UserWorkspaceConfig = {
],
css: { modules: { classNameStrategy: 'non-scoped' } },
hideSkippedTests: true,
setupFiles: [path.join(process.cwd(), `client/testing/src/perTestSetup.${TS_EXT}`)],
globalSetup: [path.join(process.cwd(), `client/testing/src/globalTestSetup.${TS_EXT}`)],
setupFiles: [path.join(BAZEL ? process.cwd() : __dirname, `client/testing/src/perTestSetup.${TS_EXT}`)],
globalSetup: [path.join(BAZEL ? process.cwd() : __dirname, `client/testing/src/globalTestSetup.${TS_EXT}`)],
},
plugins: BAZEL
? [