New welcome page (homepage) for Sourcegraph.com (#1975)

* remove scrolling effects

* remove forced dark theme and custom colors

* move MainPage and related to enterprise/dotcom/welcome

* rename /start to /welcome

The name /welcome better conveys that it is for first-time users, not the page where you start each session.

* create WelcomeArea with common header

* extract company logos

* factor out welcome footer

* clean up HTML for welcome main page

* redirect self-hosted instance /welcome to sourcegraph.com/welcome

* update copy on main page
This commit is contained in:
Quinn Slack 2019-02-11 16:26:22 +13:00 committed by GitHub
parent 3a8c80f6eb
commit 1cc218f8de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1054 additions and 3091 deletions

View File

@ -215,9 +215,9 @@ func serveHome(w http.ResponseWriter, r *http.Request) error {
}
if envvar.SourcegraphDotComMode() && !actor.FromContext(r.Context()).IsAuthenticated() {
// The user is not signed in and tried to access our main site at sourcegraph.com.
// Redirect to sourcegraph.com/start so they see general info.
http.Redirect(w, r, "/start", http.StatusTemporaryRedirect)
// The user is not signed in and tried to access Sourcegraph.com. Redirect to /welcome so
// they see the welcome page.
http.Redirect(w, r, "/welcome", http.StatusTemporaryRedirect)
return nil
}
// sourcegraph.com (not about) homepage. There is none, redirect them to /search.
@ -248,7 +248,7 @@ func serveSignIn(w http.ResponseWriter, r *http.Request) error {
return renderTemplate(w, "app.html", common)
}
func serveStart(w http.ResponseWriter, r *http.Request) error {
func serveWelcome(w http.ResponseWriter, r *http.Request) error {
common, err := newCommon(w, r, "Sourcegraph", serveError)
if err != nil {
return err
@ -258,8 +258,7 @@ func serveStart(w http.ResponseWriter, r *http.Request) error {
}
if !envvar.SourcegraphDotComMode() {
// The user is signed in and tried to access sourcegraph.com/start,
// this page should be a 404 under that situation.
// The welcome page only exists on Sourcegraph.com.
w.WriteHeader(http.StatusNotFound)
}
return renderTemplate(w, "app.html", common)

View File

@ -64,6 +64,7 @@ const (
routeExtensions = "extensions"
routeHelp = "help"
routeExplore = "explore"
routeWelcome = "welcome"
// Legacy redirects
routeLegacyLogin = "login"
@ -113,6 +114,7 @@ func newRouter() *mux.Router {
r.Path("/").Methods("GET").Name(routeHome)
r.PathPrefix("/threads").Methods("GET").Name(routeThreads)
r.Path("/start").Methods("GET").Name(routeStart)
r.PathPrefix("/welcome").Methods("GET").Name(routeWelcome)
r.Path("/search").Methods("GET").Name(routeSearch)
r.Path("/search/badge").Methods("GET").Name(routeSearchBadge)
r.Path("/search/searches").Methods("GET").Name(routeSearchSearches)
@ -185,7 +187,8 @@ func initRouter() {
router := newRouter()
uirouter.Router = router // make accessible to other packages
router.Get(routeHome).Handler(handler(serveHome))
router.Get(routeStart).Handler(handler(serveStart))
router.Get(routeStart).Handler(staticRedirectHandler("/welcome", http.StatusMovedPermanently))
router.Get(routeWelcome).Handler(handler(serveWelcome))
router.Get(routeThreads).Handler(handler(serveBasicPageString("Threads - Sourcegraph")))
router.Get(uirouter.RouteSignIn).Handler(handler(serveSignIn))
router.Get(uirouter.RouteSignUp).Handler(handler(serveBasicPageString("Sign up - Sourcegraph")))

4
go.mod
View File

@ -10,11 +10,9 @@ require (
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
github.com/coreos/go-semver v0.2.0
github.com/cosiner/argv v0.0.1 // indirect
github.com/crewjam/saml v0.0.0-20180831135026-ebc5f787b786
github.com/davecgh/go-spew v1.1.1
github.com/daviddengcn/go-colortext v0.0.0-20171126034257-17e75f6184bc
github.com/derekparker/delve v1.1.0 // indirect
github.com/dghubble/gologin v1.0.2-0.20181013174641-0e442dd5bb73
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/die-net/lrucache v0.0.0-20190123005519-19a39ef22a11
@ -82,7 +80,6 @@ require (
github.com/neelance/parallel v0.0.0-20160708114440-4de9ce63d14c
github.com/opentracing-contrib/go-stdlib v0.0.0-20190104202730-77df8e8e70b4
github.com/opentracing/opentracing-go v1.0.2
github.com/peterh/liner v1.1.0 // indirect
github.com/peterhellberg/link v1.0.0
github.com/pkg/errors v0.8.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
@ -118,7 +115,6 @@ require (
github.com/xeonx/timeago v1.0.0-rc3
github.com/zenazn/goji v0.9.0 // indirect
go.uber.org/atomic v1.3.2 // indirect
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 // indirect
golang.org/x/crypto v0.0.0-20190104202753-ff983b9c42bc
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e
golang.org/x/oauth2 v0.0.0-20190201180606-99b60b757ec1

10
go.sum
View File

@ -43,8 +43,6 @@ github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmq
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cosiner/argv v0.0.1 h1:2iAFN+sWPktbZ4tvxm33Ei8VY66FPCxdOxpncUGpAXE=
github.com/cosiner/argv v0.0.1/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
github.com/crewjam/saml v0.0.0-20180831135026-ebc5f787b786 h1:8OVABJfT9iJh/uHeYlk1HWugxt7j50JPwW2uLOV9Yqs=
github.com/crewjam/saml v0.0.0-20180831135026-ebc5f787b786/go.mod h1:w5eu+HNtubx+kRpQL6QFT2F3yIFfYVe6+EzOFVU7Hko=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
@ -62,8 +60,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/daviddengcn/go-colortext v0.0.0-20171126034257-17e75f6184bc h1:nqMZEdowWmtK9ysqvFibHJ56mTprkyE5c/6q8ZHwLc0=
github.com/daviddengcn/go-colortext v0.0.0-20171126034257-17e75f6184bc/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
github.com/derekparker/delve v1.1.0 h1:icd65nMp7s2HiLz6y/6RCVXBdoED3xxYLwX09EMaRCc=
github.com/derekparker/delve v1.1.0/go.mod h1:pMSZMfp0Nhbm8qdZJkuE/yPGOkLpGXLS1I4poXQpuJU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I=
@ -328,8 +324,6 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -367,8 +361,6 @@ github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM=
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterh/liner v1.1.0 h1:f+aAedNJA6uk7+6rXsYBnhdo4Xux7ESLe+kcuVUF5os=
github.com/peterh/liner v1.1.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/peterhellberg/link v1.0.0 h1:mUWkiegowUXEcmlb+ybF75Q/8D2Y0BjZtR8cxoKhaQo=
github.com/peterhellberg/link v1.0.0/go.mod h1:gtSlOT4jmkY8P47hbTc8PTgiDDWpdPbFYl75keYyBB8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -504,8 +496,6 @@ go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 h1:Pn8fQdvx+z1avAi7fdM2kRYWQNxGlavNDSyzrQg2SsU=
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20180505025534-4ec37c66abab/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

View File

@ -54,6 +54,7 @@
"@types/babel__core": "7.0.4",
"@types/chai": "4.1.7",
"@types/chai-as-promised": "7.1.0",
"@types/classnames": "^2.2.7",
"@types/cpy": "5.1.0",
"@types/d3-axis": "1.0.11",
"@types/d3-scale": "2.1.0",
@ -174,6 +175,7 @@
"@sqs/jsonc-parser": "^1.0.3",
"abortable-rx": "^1.0.9",
"bootstrap": "^4.1.3",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.0.7",
"d3-axis": "^1.0.7",
"d3-scale": "^2.0.0",

View File

@ -10,7 +10,9 @@ import { PlatformContextProps } from '../platform/context'
import { ActionItem, ActionItemProps } from './ActionItem'
import { ActionsState } from './actions'
export interface ActionsProps extends ExtensionsControllerProps, PlatformContextProps {
export interface ActionsProps
extends ExtensionsControllerProps<'executeCommand' | 'services'>,
PlatformContextProps<'forceUpdateTooltip'> {
menu: ContributableMenu
scope?: ContributionScope
actionItemClass?: string

View File

@ -14,7 +14,9 @@ import { getContributedActionItems } from '../contributions/contributions'
import { ExtensionsControllerProps } from '../extensions/controller'
import { PlatformContextProps } from '../platform/context'
interface Props extends ExtensionsControllerProps, PlatformContextProps {
interface Props
extends ExtensionsControllerProps<'services' | 'executeCommand'>,
PlatformContextProps<'forceUpdateTooltip'> {
/** The menu whose commands to display. */
menu: ContributableMenu

View File

@ -64,8 +64,6 @@ export interface LayoutProps
isLightTheme: boolean
onThemeChange: () => void
onMainPage: (mainPage: boolean) => void
isMainPage: boolean
navbarSearchQuery: string
onNavbarQueryChange: (query: string) => void

View File

@ -240,7 +240,6 @@ hr {
@import './global/GlobalAlerts';
@import './global/GlobalDebug';
@import './docSite/DocSitePage';
@import './search/input/MainPage';
@import './search/input/ScopePage';
@import './search/input/SearchPage';
@import './search/results/SearchResults';

View File

@ -73,11 +73,6 @@ interface SourcegraphWebAppState extends PlatformContextProps, SettingsCascadePr
*/
isLightTheme: boolean
/**
* Whether the user is on MainPage and therefore not logged in
*/
isMainPage: boolean
/**
* The current search query in the navbar.
*/
@ -108,7 +103,6 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
extensionsController: createExtensionsController(platformContext),
settingsCascade: EMPTY_SETTINGS_CASCADE,
viewerSubject: SITE_SUBJECT_NO_ADMIN,
isMainPage: false,
}
}
@ -161,15 +155,9 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
}
public componentDidUpdate(): void {
// Always show MainPage in dark theme look
if (this.state.isMainPage && this.state.isLightTheme) {
document.body.classList.remove('theme-light')
document.body.classList.add('theme-dark')
} else {
localStorage.setItem(LIGHT_THEME_LOCAL_STORAGE_KEY, this.state.isLightTheme + '')
document.body.classList.toggle('theme-light', this.state.isLightTheme)
document.body.classList.toggle('theme-dark', !this.state.isLightTheme)
}
localStorage.setItem(LIGHT_THEME_LOCAL_STORAGE_KEY, this.state.isLightTheme + '')
document.body.classList.toggle('theme-light', this.state.isLightTheme)
document.body.classList.toggle('theme-dark', !this.state.isLightTheme)
}
public render(): React.ReactFragment | null {
@ -222,8 +210,6 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
// Theme
isLightTheme={this.state.isLightTheme}
onThemeChange={this.onThemeChange}
isMainPage={this.state.isMainPage}
onMainPage={this.onMainPage}
// Search query
navbarSearchQuery={this.state.navbarSearchQuery}
onNavbarQueryChange={this.onNavbarQueryChange}
@ -246,10 +232,6 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
this.setState(state => ({ isLightTheme: !state.isLightTheme }))
}
private onMainPage = (mainPage: boolean) => {
this.setState(() => ({ isMainPage: mainPage }))
}
private onNavbarQueryChange = (navbarSearchQuery: string) => {
this.setState({ navbarSearchQuery })
}

View File

@ -53,7 +53,7 @@ export function refreshAuthenticatedUser(): Observable<never> {
)
}
const initialSiteConfigAuthPublic = window.context.critical['auth.public']
const initialSiteConfigAuthPublic = window.context ? window.context.critical['auth.public'] : false // default to false in tests
/**
* Whether auth is required to perform any action.
@ -67,7 +67,7 @@ const initialSiteConfigAuthPublic = window.context.critical['auth.public']
export const authRequired = authenticatedUser.pipe(map(user => user === null && !initialSiteConfigAuthPublic))
// Populate authenticatedUser.
if (window.context.isAuthenticatedUser) {
if (window.context && window.context.isAuthenticatedUser) {
refreshAuthenticatedUser()
.toPromise()
.then(() => void 0, err => console.error(err))

View File

@ -3,6 +3,7 @@
@import './SourcegraphWebApp.scss'; // Need .scss extension because of https://github.com/webpack-contrib/sass-loader/issues/556
// Enterprise styles
@import './enterprise/dotcom/welcome/WelcomeArea';
@import './enterprise/extensions/registry/RegistryArea';
@import './enterprise/extensions/registry/RegistryNewExtensionPage';
@import './enterprise/extensions/extension/RegistryExtensionManagePage';

View File

@ -0,0 +1 @@
@import './WelcomeMainPage';

View File

@ -0,0 +1,91 @@
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import H from 'history'
import ChevronLeftIcon from 'mdi-react/ChevronLeftIcon'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import React from 'react'
import { Route, RouteComponentProps, Switch } from 'react-router'
import { Link } from 'react-router-dom'
import { ExtensionsControllerProps } from '../../../../../shared/src/extensions/controller'
import * as GQL from '../../../../../shared/src/graphql/schema'
import { PlatformContextProps } from '../../../../../shared/src/platform/context'
import { ErrorBoundary } from '../../../components/ErrorBoundary'
import { HeroPage } from '../../../components/HeroPage'
import { RouteDescriptor } from '../../../util/contributions'
import { WelcomeAreaFooter } from './WelcomeAreaFooter'
const NotFoundPage = () => <HeroPage icon={MapSearchIcon} title="404: Not Found" />
export interface WelcomeAreaRoute extends RouteDescriptor<WelcomeAreaRouteContext> {}
export interface WelcomeAreaProps extends ExtensionsControllerProps, PlatformContextProps, RouteComponentProps<{}> {
authenticatedUser: GQL.IUser | null
isLightTheme: boolean
routes: ReadonlyArray<WelcomeAreaRoute>
location: H.Location
history: H.History
}
export interface WelcomeAreaRouteContext extends WelcomeAreaProps {}
/**
* The welcome area, which contains general product information.
*/
export class WelcomeArea extends React.PureComponent<WelcomeAreaProps> {
public render(): JSX.Element | null {
if (!window.context.sourcegraphDotComMode) {
return (
<HeroPage
icon={MapSearchIcon}
title="No welcome page"
detail={
<p>
Visit{' '}
<a href="https://sourcegraph.com/welcome" target="_blank">
sourcegraph.com/welcome
</a>{' '}
instead.
</p>
}
/>
)
}
const { children, ...context } = this.props
return (
<div className="welcome-area container">
{this.props.location.pathname !== '/welcome' && (
<nav className="d-block my-2">
<Link to="/welcome" className="py-2 pr-2">
<ChevronLeftIcon className="icon-inline" />
Welcome
</Link>
</nav>
)}
<ErrorBoundary location={this.props.location}>
<React.Suspense fallback={<LoadingSpinner className="icon-inline my-2 d-block mx-auto" />}>
<Switch>
{this.props.routes.map(
({ path, exact, render, condition = () => true }) =>
condition(context) && (
<Route
path={this.props.match.url + path}
key="hardcoded-key" // see https://github.com/ReactTraining/react-router/issues/4578#issuecomment-334489490
exact={exact}
// tslint:disable-next-line:jsx-no-lambda
render={routeComponentProps => (
<>
{render({ ...context, ...routeComponentProps })}
<WelcomeAreaFooter isLightTheme={this.props.isLightTheme} />
</>
)}
/>
)
)}
<Route component={NotFoundPage} key="hardcoded-key" />
</Switch>
</React.Suspense>
</ErrorBoundary>
</div>
)
}
}

View File

@ -0,0 +1,135 @@
import React from 'react'
import { Link } from 'react-router-dom'
export const WelcomeAreaFooter: React.FunctionComponent<{ isLightTheme: boolean }> = ({ isLightTheme }) => (
<>
<div className="row mt-5 pt-4 border-top">
<div className="col-sm-4 col-m-4 col-lg-4">
<img
className="mb-2"
src={
isLightTheme
? 'https://about.sourcegraph.com/sourcegraph/logo.svg'
: 'https://about.sourcegraph.com/sourcegraph/logo--light.svg'
}
/>
<p>
<a href="mailto:hi@sourcegraph.com" target="_blank">
hi@sourcegraph.com
</a>
<br />
142 Minna St, 2nd Floor
<br />
San Francisco, CA 94105 (USA)
</p>
</div>
<div className="col-xs-12 col-sm-12 col-md-2 col-lg-2">
<h3 className="mb-0">Features</h3>
<ul className="list-unstyled">
<li>
<Link to="/welcome/search">Code search</Link>
</li>
<li>
<Link to="/welcome/code-intelligence">Code intelligence</Link>
</li>
<li>
<Link to="/welcome/integrations">Integrations</Link>
</li>
<li>
<a href="https://about.sourcegraph.com/pricing" target="_blank">
Enterprise
</a>
</li>
</ul>
</div>
<div className="col-xs-12 col-sm-12 col-md-2 col-lg-2">
<h3 className="mb-0">Community</h3>
<ul className="list-unstyled">
<li>
<a href="https://github.com/sourcegraph/sourcegraph" target="_blank">
GitHub
</a>
</li>
<li>
<a href="https://about.sourcegraph.com/blog" target="_blank">
Blog
</a>
</li>
<li>
<a href="https://twitter.com/srcgraph" target="_blank">
Twitter
</a>
</li>
<li>
<a href="https://www.linkedin.com/company/4803356/" target="_blank">
LinkedIn
</a>
</li>
</ul>
</div>
<div className="col-xs-12 col-sm-12 col-md-2 col-lg-2">
<h3 className="mb-0">Company</h3>
<ul className="list-unstyled">
<li>
<a href="https://about.sourcegraph.com/plan" target="_blank">
Master plan
</a>
</li>
<li>
<a href="https://about.sourcegraph.com/about" target="_blank">
About
</a>
</li>
<li>
<a href="https://about.sourcegraph.com/contact" target="_blank">
Contact
</a>
</li>
<li>
<a href="https://about.sourcegraph.com/jobs" target="_blank">
Careers
</a>
</li>
</ul>
</div>
<div className="col-xs-12 col-sm-12 col-md-2 col-lg-2">
<h3 className="mb-0">Resources</h3>
<ul className="list-unstyled">
<li>
<a href="https://docs.sourcegraph.com" target="_blank">
Documentation
</a>
</li>
<li>
<a
href="https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/CHANGELOG.md"
target="_blank"
>
Changelog
</a>
</li>
<li>
<a href="https://about.sourcegraph.com/pricing" target="_blank">
Pricing
</a>
</li>
<li>
<a href="https://about.sourcegraph.com/security" target="_blank">
Security
</a>
</li>
</ul>
</div>
</div>
<p className="text-muted mt-3 pb-2">
<a href="https://about.sourcegraph.com/terms" target="_blank">
Terms
</a>{' '}
-{' '}
<a href="https://about.sourcegraph.com/privacy" target="_blank">
Privacy
</a>{' '}
- Copyright © 2018 Sourcegraph, Inc.
</p>
</>
)

View File

@ -0,0 +1,23 @@
@import './WelcomeMainPageDemos';
@import './WelcomeMainPageLogos';
.welcome-main-page {
&__logo-mark {
width: 1.5rem;
height: 1.5rem;
position: absolute;
top: -1.5rem; // make CTAs flush with header text
}
&__header {
font-size: 24px;
}
&__demo {
min-height: 540px; // reduce jitter while page is loading
}
}
.theme-dark .welcome-main-page {
&__sign-up {
color: #ffffff; // otherwise the signup CTA link text is indistinguishable from text-muted
}
}

View File

@ -0,0 +1,157 @@
import * as H from 'history'
import CloudUploadIcon from 'mdi-react/CloudUploadIcon'
import * as React from 'react'
import { Link } from 'react-router-dom'
import { ExtensionsControllerProps } from '../../../../../shared/src/extensions/controller'
import * as GQL from '../../../../../shared/src/graphql/schema'
import { PlatformContextProps } from '../../../../../shared/src/platform/context'
import { eventLogger } from '../../../tracking/eventLogger'
import { WelcomeMainPageDemos } from './WelcomeMainPageDemos'
import { WelcomeMainPageLogos } from './WelcomeMainPageLogos'
// Lambdas are OK in this component because it is not performance sensitive and using them
// simplifies the code.
//
// tslint:disable:jsx-no-lambda
interface Props extends ExtensionsControllerProps, PlatformContextProps {
authenticatedUser: GQL.IUser | null
isLightTheme: boolean
location: H.Location
history: H.History
}
/**
* The welcome main page, which describes Sourcegraph functionality and other general information.
*/
export class WelcomeMainPage extends React.Component<Props> {
public render(): JSX.Element | null {
return (
<div className="welcome-main-page">
<section className="hero-section">
<div className="container hero-container mt-5 pt-3">
<div className="row justify-content-md-center">
<div className="col-md-7 col-lg-6 mr-lg-4 mb-4">
<img
className="welcome-main-page__logo-mark mb-1"
src="/.assets/img/sourcegraph-mark.svg"
/>
<h2 className="welcome-main-page__header mt-2">
<span className="font-weight-normal">
Search,&nbsp;navigate, and review&nbsp;code.
</span>{' '}
Find&nbsp;answers.
</h2>
<p>Sourcegraph is a web-based code search and navigation tool for dev teams.</p>
<ul className="pl-3">
<li>
<strong>Code search:</strong> fast, cross-repository, on any commit/branch (no
indexing delay), with support for regexps, diffs, and{' '}
<a href="https://docs.sourcegraph.com/user/search/queries" target="_blank">
filters
</a>
</li>
<li>
<strong>Code navigation:</strong> go-to-definition and find-references for{' '}
<a
href="https://sourcegraph.com/extensions?query=category%3A%22Programming+languages%22"
target="_blank"
>
all major languages
</a>
</li>
<li>
<strong>Deep integrations</strong> with GitHub, GitHub Enterprise, GitLab,
Bitbucket Server, Phabricator, etc., plus a{' '}
<a href="https://docs.sourcegraph.com/extensions" target="_blank">
powerful extension API
</a>
</li>
<li>
<a href="https://github.com/sourcegraph/sourcegraph" target="_blank">
Open-source
</a>
, self-hosted, and free (
<a href="https://about.sourcegraph.com/pricing" target="_blank">
Enterprise
</a>{' '}
upgrade available)
</li>
</ul>
<p className="mb-1">
<a href="https://docs.sourcegraph.com/user/tour" target="_blank">
See how it's used
</a>{' '}
to build better software faster at:
</p>
<div className="welcome-main-page__customer-logos d-flex align-items-center pl-2">
<WelcomeMainPageLogos isLightTheme={this.props.isLightTheme} />
<span className="small text-muted">
&hellip;and thousands of other organizations.
</span>
</div>
</div>
<div className="col-md-5 col-lg-4 mb-4">
<div className="mt-3">
<a
className="btn btn-primary btn-lg font-weight-bold mb-1 d-inline-flex align-items-center text-nowrap flex-wrap justify-content-center"
href="https://docs.sourcegraph.com/#quickstart"
onClick={() => eventLogger.log('WelcomeDeploySelfHosted')}
>
<CloudUploadIcon className="icon-inline mr-2" /> Deploy self-hosted Sourcegraph
</a>
<small className="text-muted d-block">
For use with your organization's private code. Runs securely on your infra (in a
single Docker container or on a cluster).
</small>
</div>
<div className="mt-4">
<a
className="btn btn-secondary mb-1 d-inline-flex align-items-center"
href="https://docs.sourcegraph.com/integration/browser_extension"
onClick={() => eventLogger.log('WelcomeInstallBrowserExtension')}
>
Install browser extension
</a>
<small className="text-muted d-block">
Adds go-to-definition and find-references to GitHub and other code hosts. For
private code, connect it to your self-hosted Sourcegraph instance.
</small>
</div>
{!this.props.authenticatedUser && (
<div className="mt-4">
<Link
to="/sign-up"
target="_blank"
className="welcome-main-page__sign-up"
onClick={() => eventLogger.log('WelcomeSignUpForSourcegraphDotCom')}
>
Sign up on Sourcegraph.com
</Link>
<small className="text-muted d-block">
A public Sourcegraph instance for public code only.
</small>
</div>
)}
</div>
</div>
<div className="row justify-content-md-center mt-3 pt-5 border-top">
<WelcomeMainPageDemos
className="col-md-12 col-lg-10"
location={this.props.location}
history={this.props.history}
/>
<iframe
className="welcome-main-page__demo col-md-12 col-lg-10 d-none"
src="https://www.youtube.com/embed/Pfy2CjeJ2N4"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen={true}
/>
</div>
</div>
</section>
</div>
)
}
}

View File

@ -0,0 +1,13 @@
.welcome-main-page-demos {
&__item {
touch-action: manipulation; // remove iOS tap delay
}
&__video {
// To avoid jitter during loading/switching, calculate height of video based on page
// dimensions and video aspect ratio.
min-height: calc((100vw - (12px * 4)) * (1596 / 2535));
@media screen and (min-width: 1000px) {
min-height: 550px;
}
}
}

View File

@ -0,0 +1,83 @@
import classnames from 'classnames'
import H from 'history'
import PlayCircleOutlineIcon from 'mdi-react/PlayCircleOutlineIcon'
import React from 'react'
import { Link } from 'react-router-dom'
import { eventLogger } from '../../../tracking/eventLogger'
interface Props {
className: string
location: H.Location
history: H.History
}
const BASE_VIDEO_URL = 'https://storage.googleapis.com/sourcegraph-assets/video/welcome/video'
// The mp4 videos are 2535x1596 at 20fps.
//
// To convert videos from mp4 (from kazam) to m4v (for iPhone):
//
// ffmpeg -i INPUT_FILE -pix_fmt yuv420p -vf "scale=-2:720:flags=lanczos" -vcodec libx264 -level 3.2 -profile:v main -preset medium -crf 23 -x264-params ref=4 -movflags +faststart OUTPUT_FILE
//
// To upload files:
//
// gsutil cp -a public-read -r INPUT_FILES gs://sourcegraph-assets/video/welcome/video/
const VIDEOS: { title: string; hash: string; filename: string }[] = [
{
title: 'Code navigation',
hash: 'code-navigation',
filename: 'Welcome-CodeNavigation',
},
{
title: 'Code search',
hash: 'code-search',
filename: 'Welcome-Search',
},
{
title: 'GitHub integration',
hash: 'github-integration',
filename: 'Welcome-GitHub',
},
]
export class WelcomeMainPageDemos extends React.PureComponent<Props> {
public render(): JSX.Element | null {
const activeTab = this.props.location.hash.replace(/^#/, '') || VIDEOS[0].hash
const video = VIDEOS.find(v => v.hash === activeTab) || VIDEOS[0]
return (
<div className={`welcome-main-page-demos ${this.props.className}`}>
<ul className="nav nav-pills text-nowrap justify-content-md-center">
<li className="nav-item disabled text-muted d-flex align-items-center mr-2 mb-2">
<PlayCircleOutlineIcon className="mr-2" /> Demos:
</li>
{VIDEOS.map(({ title, hash }, i) => (
<li className="nav-item" key={i}>
<Link
to={{ hash }}
className={classnames('welcome-main-page-demos__item nav-link border mx-1 mb-2', {
active: activeTab === hash,
})}
// tslint:disable-next-line:jsx-no-lambda
onClick={() => eventLogger.log('WelcomeMainPageDemosVideo', { hash })}
>
{title}
</Link>
</li>
))}
</ul>
<video
key={video.hash}
autoPlay={true}
muted={true}
loop={true}
playsInline={true}
className="welcome-main-page-demos__video w-100 h-auto"
>
<source src={`${BASE_VIDEO_URL}/${video.filename}.mp4`} type="video/mp4" />
<source src={`${BASE_VIDEO_URL}/${video.filename}.m4v`} type="video/x-m4v" />
Demo video playback is not supported on your browser.
</video>
</div>
)
}
}

View File

@ -0,0 +1,18 @@
.welcome-main-page-logos {
&__logo {
flex: 0 0 auto;
&-1 {
height: 1.5rem;
}
&-2 {
height: 2rem;
// stylelint-disable-next-line declaration-property-unit-whitelist
margin-top: 5px; // equalize perceived text baseline of customer logos
}
&-3 {
height: 3.5rem;
// stylelint-disable-next-line declaration-property-unit-whitelist
margin-top: 6px; // equalize perceived text baseline of customer logos
}
}
}

View File

@ -0,0 +1,33 @@
import { shuffle } from 'lodash'
import React from 'react'
import { Logo1, Logo2, Logo3 } from './logos'
// Shuffle logos because we love all of them infinitely. :)
const LOGOS: {
component: React.ComponentType<{ className: string; isLightTheme: boolean }>
className: string
}[] = shuffle([
{
component: Logo1,
className: 'welcome-main-page-logos__logo-1 mr-3',
},
{
component: Logo2,
className: 'welcome-main-page-logos__logo-2 mr-3',
},
{
component: Logo3,
className: 'welcome-main-page-logos__logo-3 mr-3',
},
])
/**
* The logos for the welcome main page.
*/
export const WelcomeMainPageLogos: React.FunctionComponent<{ isLightTheme: boolean }> = ({ isLightTheme }) => (
<>
{LOGOS.map(({ component: C, className }, i) => (
<C key={i} className={`welcome-main-page-logos__logo ${className}`} isLightTheme={isLightTheme} />
))}
</>
)

View File

@ -0,0 +1,133 @@
import React from 'react'
interface Props {
isLightTheme: boolean
className: string
}
// These components are intentionally not named after the companies, to avoid people reusing them
// and thinking they are the official SVGs for these company logos.
export const Logo1: React.FunctionComponent<Props> = ({ isLightTheme, className }) => {
const fill = isLightTheme ? '#010202' : 'white'
return (
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="68px"
height="36px"
viewBox="0 0 926.906 321.777"
className={className}
>
<g>
<path
fill={fill}
d="M53.328,229.809c3.917,10.395,9.34,19.283,16.27,26.664c6.93,7.382,15.14,13.031,24.63,16.948
c9.491,3.917,19.81,5.875,30.958,5.875c10.847,0,21.015-2.034,30.506-6.102s17.776-9.792,24.856-17.173
c7.08-7.382,12.579-16.194,16.496-26.438c3.917-10.244,5.875-21.692,5.875-34.347V0h47.453v316.354h-47.001v-29.376
c-10.545,11.147-22.974,19.734-37.285,25.761c-14.312,6.025-29.752,9.038-46.323,9.038c-16.873,0-32.615-2.938-47.228-8.813
c-14.612-5.875-27.267-14.235-37.962-25.082S15.441,264.006,9.265,248.79C3.088,233.575,0,216.628,0,197.947V0h47.453v195.236
C47.453,207.891,49.411,219.414,53.328,229.809z"
/>
<path
fill={fill}
d="M332.168,0v115.243c10.545-10.545,22.748-18.905,36.607-25.082s28.924-9.265,45.193-9.265
c16.873,0,32.689,3.163,47.453,9.49c14.763,6.327,27.567,14.914,38.414,25.761s19.434,23.651,25.761,38.414
c6.327,14.764,9.49,30.431,9.49,47.002c0,16.57-3.163,32.162-9.49,46.774c-6.327,14.613-14.914,27.343-25.761,38.188
c-10.847,10.847-23.651,19.434-38.414,25.761c-14.764,6.327-30.581,9.49-47.453,9.49c-16.27,0-31.409-3.088-45.419-9.265
c-14.01-6.176-26.288-14.537-36.833-25.082v28.924h-45.193V0H332.168z M337.365,232.746c4.067,9.642,9.717,18.078,16.948,25.309
c7.231,7.231,15.667,12.956,25.308,17.174c9.642,4.218,20.036,6.327,31.184,6.327c10.847,0,21.09-2.109,30.731-6.327
s18.001-9.942,25.083-17.174c7.08-7.23,12.729-15.667,16.947-25.309c4.218-9.641,6.327-20.035,6.327-31.183
c0-11.148-2.109-21.618-6.327-31.41s-9.867-18.303-16.947-25.534c-7.081-7.23-15.441-12.88-25.083-16.947
s-19.885-6.102-30.731-6.102c-10.846,0-21.09,2.034-30.731,6.102s-18.077,9.717-25.309,16.947
c-7.23,7.231-12.955,15.742-17.173,25.534c-4.218,9.792-6.327,20.262-6.327,31.41C331.264,212.711,333.298,223.105,337.365,232.746
z"
/>
<path
fill={fill}
d="M560.842,155.014c6.025-14.462,14.312-27.191,24.856-38.188s23.049-19.659,37.511-25.986
s30.129-9.49,47.001-9.49c16.571,0,31.937,3.013,46.098,9.038c14.16,6.026,26.362,14.387,36.606,25.083
c10.244,10.695,18.229,23.35,23.952,37.962c5.725,14.613,8.587,30.506,8.587,47.68v14.914H597.901
c1.507,9.34,4.52,18.002,9.039,25.985c4.52,7.984,10.168,14.914,16.947,20.789c6.779,5.876,14.462,10.471,23.049,13.784
c8.587,3.314,17.7,4.972,27.342,4.972c27.418,0,49.563-11.299,66.435-33.896l32.991,24.404
c-11.449,15.366-25.609,27.418-42.481,36.155c-16.873,8.737-35.854,13.106-56.944,13.106c-17.174,0-33.217-3.014-48.131-9.039
s-27.869-14.462-38.866-25.309s-19.659-23.576-25.986-38.188s-9.491-30.506-9.491-47.679
C551.803,184.842,554.817,169.476,560.842,155.014z M624.339,137.162c-12.805,10.696-21.316,24.932-25.534,42.708h140.552
c-3.917-17.776-12.278-32.012-25.083-42.708c-12.805-10.695-27.794-16.043-44.967-16.043
C652.133,121.119,637.144,126.467,624.339,137.162z"
/>
<path
fill={fill}
d="M870.866,142.359c-9.641,10.545-14.462,24.856-14.462,42.934v131.062h-45.646V85.868h45.193v28.472
c5.725-9.34,13.182-16.722,22.371-22.145c9.189-5.424,20.111-8.136,32.766-8.136h15.817v42.482h-18.981
C892.86,126.542,880.507,131.814,870.866,142.359z"
/>
</g>
</svg>
)
}
export const Logo2: React.FunctionComponent<Props> = ({ isLightTheme, className }) => {
const fill = isLightTheme ? '#EA0B8C' : 'white'
return (
<svg
width="49px"
height="47.7px"
viewBox="0 0 65.8 141"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g transform="translate(-177.000000, -79.000000)" fill={fill}>
<path d="M112.746,82.343 L142.806,82.343 L142.806,163.017 C142.806,175.784 148.646,183.389 153.264,186.649 C148.375,190.995 133.435,194.798 122.298,185.562 C115.732,180.118 112.746,171.166 112.746,162.745 L112.746,82.343 Z M300.609,149.357 L300.609,140.879 L309.777,140.879 L309.777,111.136 L299.709,111.136 C295.844,93.279 279.946,79.898 260.933,79.898 C239.03,79.898 221.275,97.654 221.275,119.556 L221.275,189.015 C227.515,189.892 234.949,188.906 241.274,183.661 C247.839,178.216 250.825,169.265 250.825,160.844 L250.825,158.285 L265.838,158.285 L265.838,128.542 L250.825,128.542 L250.825,119.556 L250.861,119.556 C250.861,113.994 255.37,109.485 260.933,109.485 C266.495,109.485 271.022,113.994 271.022,119.556 L271.022,149.357 C271.022,171.259 288.796,189.015 310.698,189.015 L310.698,159.428 C305.136,159.428 300.609,154.919 300.609,149.357 Z M186.761,111.136 L186.761,154.787 C186.761,157.265 184.705,159.274 182.168,159.274 C179.631,159.274 177.575,157.265 177.575,154.787 L177.575,111.136 L147.831,111.136 L147.831,162.474 C147.831,171.709 150.963,183.389 165.216,187.192 C179.484,190.999 187.761,183.118 187.761,183.118 C187.007,188.31 182.117,192.113 174.24,192.928 C168.28,193.544 160.658,191.57 156.856,189.94 L156.856,217.139 C166.544,219.996 176.791,220.919 186.819,218.973 C205.018,215.442 216.504,200.231 216.504,179.994 L216.504,111.136 L186.761,111.136 Z" />
</g>
</g>
</svg>
)
}
export const Logo3: React.FunctionComponent<Props> = ({ isLightTheme, className }) => {
// From https://www.yelp.com/brand: "If you need to display the Yelp logo on a white background
// be sure to use the version with the grey stroke."
const outlineColor = isLightTheme ? '#cccccc' : 'transparent'
return (
<svg width="90px" height="56px" viewBox="43 59 248 130" version="1.1" className={className}>
<defs id="defs6" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,213.33333)" id="g10">
<g transform="scale(0.1)" id="g12">
<g fillRule="nonzero" stroke="none" fillOpacity="1">
<path
fill="#ffffff"
d="m 2152.1,634.496 c 12.27,25.715 12.86,55.481 1.61,81.672 -11.25,26.199 -33.26,46.281 -60.37,55.094 l -3.22,1.047 c 0,0 45.25,9.554 65.46,53.781 11.85,25.941 12.66,55.598 1.31,81.785 -19.3,44.551 -47.58,83.449 -84.04,115.615 -18.07,15.94 -41.29,24.62 -65.37,24.72 -0.16,0 -0.33,0 -0.48,0 -58.28,0 -83.56,-46.48 -83.56,-46.48 v 161.4 c 0,54.58 -43.57,98.69 -98.87,99 -0.2,0 -0.39,0 -0.58,0 -23.47,0 -66.74,-7.81 -95.43,-15.5 -28.9,-7.74 -57.16,-18.15 -83.98,-30.94 -24.79,-11.83 -43.69,-33.67 -51.84,-59.92 -8.17,-26.3 -4.95,-55.05 8.81,-78.9 l 80.69,-139.765 c 0,0 -19.39,11.524 -46.44,11.524 -42.58,0 -80.28,-27.082 -93.82,-67.379 -1,-2.965 -1.95,-5.941 -2.86,-8.922 -12.87,12.391 -28.44,23.516 -47.4,32.524 -25.81,12.257 -54.7,18.738 -83.56,18.738 -22.03,0 -43.58,-3.738 -63.62,-10.942 -14.91,7.86 -31.86,12.239 -49.69,12.239 -25.28,0 -48.51,-8.867 -66.79,-23.633 v 19.492 c 0,58.711 -47.77,106.474 -106.48,106.474 -58.71,0 -106.47,-47.763 -106.47,-106.474 V 861.98 c -40.122,42.02 -93.606,59.27 -141.887,59.27 -36.2,0 -84.336,-11.781 -124.825,-45.094 -13.57,18.965 -32.839,32.942 -55.503,39.953 -10.329,3.196 -21.028,4.817 -31.805,4.817 -47.399,0 -88.66,-30.434 -102.676,-75.731 l -4.527,-14.625 -14.633,29.985 c -14.668,32.023 -52.141,60.629 -96.383,60.633 -15.211,0 -31.219,-3.379 -47.379,-11.165 -25.875,-12.46 -45.168,-34.484 -54.508,-61.636 -9.343,-27.157 -7.55,-56.321 5.043,-82.125 L 447.941,557.441 c -30.183,-11.793 -52.617,-36.07 -62,-68.992 -9.023,-31.668 -3.308,-64.629 15.688,-90.429 14.914,-20.258 44.305,-44.7 99.605,-46.032 2.008,-0.047 3.985,-0.078 5.957,-0.078 77.582,0.012 134.344,38.371 155.735,105.25 8.801,27.52 19,59.899 29.461,93.36 1.91,-4.258 3.918,-8.45 6.062,-12.52 33.598,-63.68 94.957,-98.762 172.774,-98.762 73.343,0 124.808,24.762 156.627,56.973 18.02,-33.32 53.27,-56 93.73,-56 25.28,0 48.51,8.859 66.79,23.629 v -19.488 c 0,-58.723 47.77,-106.481 106.48,-106.481 57.78,0 104.96,46.258 106.44,103.688 0.07,0 1.75,-0.09 4.72,-0.09 18.4,0 86.61,3.5 133.77,53.781 5.65,6.02 15.7,16.422 25.97,33.75 13.04,-7.18 28.03,-11.27 43.97,-11.27 12.91,0 25.18,2.692 36.31,7.52 6.13,-7.82 13.45,-14.77 21.81,-20.512 39.45,-27.097 85.17,-45.597 132.22,-53.5 5.43,-0.91 10.97,-1.367 16.45,-1.367 53.6,0 97.04,41.969 98.91,95.551 l 0.12,3.418 1.81,-2.899 c 18.2,-29.14 49.6,-46.543 83.99,-46.543 22.92,0 45.3,8.051 63,22.661 36.79,30.363 67.14,69.242 87.76,112.437"
/>
<path
fill={outlineColor}
d="m 2001.34,499.398 c -34.39,0 -65.79,17.403 -83.99,46.543 l -1.81,2.899 -0.12,-3.418 c -1.87,-53.582 -45.31,-95.551 -98.91,-95.551 -5.48,0 -11.02,0.457 -16.45,1.367 -47.05,7.903 -92.77,26.403 -132.22,53.5 -8.36,5.742 -15.68,12.692 -21.81,20.512 -11.13,-4.828 -23.4,-7.52 -36.31,-7.52 -15.94,0 -30.93,4.09 -43.97,11.27 -10.27,-17.328 -20.32,-27.73 -25.97,-33.75 -47.16,-50.281 -115.37,-53.781 -133.77,-53.781 -2.97,0 -4.65,0.09 -4.72,0.09 -1.48,-57.43 -48.66,-103.688 -106.44,-103.688 -58.71,0 -106.48,47.758 -106.48,106.481 v 19.488 c -18.28,-14.77 -41.51,-23.629 -66.79,-23.629 -40.46,0 -75.71,22.68 -93.73,56 C 996.031,464 944.566,439.238 871.223,439.238 c -77.817,0 -139.176,35.082 -172.774,98.762 -2.144,4.07 -4.152,8.262 -6.062,12.52 -10.461,-33.461 -20.66,-65.84 -29.461,-93.36 -21.391,-66.879 -78.153,-105.238 -155.735,-105.25 -1.972,0 -3.949,0.031 -5.957,0.078 -55.3,1.332 -84.691,25.774 -99.605,46.032 -18.996,25.8 -24.711,58.761 -15.688,90.429 9.383,32.922 31.817,57.199 62,68.992 L 346.027,766.262 c -12.593,25.804 -14.386,54.968 -5.043,82.125 9.34,27.152 28.633,49.176 54.508,61.636 16.16,7.786 32.168,11.165 47.379,11.165 44.242,-0.004 81.715,-28.61 96.383,-60.633 l 14.633,-29.985 4.527,14.625 c 14.016,45.297 55.277,75.731 102.676,75.731 10.777,0 21.476,-1.621 31.805,-4.817 22.664,-7.011 41.933,-20.988 55.503,-39.953 40.489,33.313 88.625,45.094 124.825,45.094 48.281,0 101.765,-17.25 141.887,-59.27 v 58.766 c 0,58.711 47.76,106.474 106.47,106.474 58.71,0 106.48,-47.763 106.48,-106.474 v -19.492 c 18.28,14.766 41.51,23.633 66.79,23.633 17.83,0 34.78,-4.379 49.69,-12.239 20.04,7.204 41.59,10.942 63.62,10.942 28.86,0 57.75,-6.481 83.56,-18.738 18.96,-9.008 34.53,-20.133 47.4,-32.524 0.91,2.981 1.86,5.957 2.86,8.922 13.54,40.297 51.24,67.379 93.82,67.379 27.05,0 46.44,-11.524 46.44,-11.524 l -80.69,139.765 c -13.76,23.85 -16.98,52.6 -8.81,78.9 8.15,26.25 27.05,48.09 51.84,59.92 26.82,12.79 55.08,23.2 83.98,30.94 28.69,7.69 71.96,15.5 95.43,15.5 0.19,0 0.38,0 0.58,0 55.3,-0.31 98.87,-44.42 98.87,-99 v -161.4 c 0,0 25.28,46.48 83.56,46.48 0.15,0 0.32,0 0.48,0 24.08,-0.1 47.3,-8.78 65.37,-24.72 36.46,-32.166 64.74,-71.064 84.04,-115.615 11.35,-26.187 10.54,-55.844 -1.31,-81.785 -20.21,-44.227 -65.46,-53.781 -65.46,-53.781 l 3.22,-1.047 c 27.11,-8.813 49.12,-28.895 60.37,-55.094 11.25,-26.191 10.66,-55.957 -1.61,-81.672 -20.62,-43.195 -50.97,-82.074 -87.76,-112.437 -17.7,-14.61 -40.08,-22.661 -63,-22.661 z m 104.06,272.645 c 15.79,6.582 40.81,21.348 54.79,51.937 12.59,27.547 13.08,58.86 1.36,85.911 -19.6,45.242 -48.32,84.742 -85.35,117.409 -18.87,16.65 -43.27,25.88 -68.7,25.99 h -0.5 c -40.73,0 -65.97,-21.29 -78.48,-35.86 v 145.7 c 0,27.8 -10.72,53.88 -30.18,73.43 -19.51,19.61 -45.7,30.49 -73.75,30.65 h -0.6 c -23.94,0 -67.99,-7.97 -96.74,-15.67 -29.2,-7.83 -57.75,-18.35 -84.86,-31.27 -26.06,-12.43 -45.93,-35.39 -54.5,-63 -8.58,-27.64 -5.21,-57.87 9.27,-82.93 l 72.98,-126.426 c -8.61,2.945 -20.5,5.789 -34.34,5.789 -44.76,0 -84.4,-28.465 -98.63,-70.836 -0.16,-0.469 -0.32,-0.941 -0.48,-1.418 -12.64,11.086 -27,20.485 -42.79,27.985 -26.49,12.582 -56.14,19.23 -85.74,19.23 -21.85,0 -43.11,-3.531 -63.24,-10.5 -15.43,7.727 -32.69,11.801 -50.07,11.801 -21.94,0 -43.5,-6.582 -61.71,-18.699 v 9.48 c 0,61.512 -50.05,111.554 -111.56,111.554 -61.51,0 -111.55,-50.042 -111.55,-111.554 v -46.68 c -36.296,33.348 -85.467,52.258 -136.807,52.258 -21.012,0 -74.321,-4.261 -123.91,-42.937 -14.075,17.836 -32.954,30.773 -54.918,37.57 -10.817,3.348 -22.024,5.047 -33.305,5.047 -49.637,0 -92.848,-31.871 -107.527,-79.309 l -0.739,-2.382 -9.011,18.468 c -13.973,30.516 -51.559,63.481 -100.942,63.485 -16.824,0 -33.508,-3.926 -49.582,-11.668 -26.973,-12.993 -47.254,-35.922 -57.105,-64.563 -9.786,-28.433 -7.911,-58.976 5.281,-86 l 99.558,-203.996 c -29.582,-13.23 -50.777,-37.961 -59.964,-70.199 -9.457,-33.192 -3.454,-67.75 16.48,-94.828 15.59,-21.18 46.215,-46.711 103.57,-48.102 2.047,-0.051 4.071,-0.07 6.082,-0.07 79.938,0 138.461,39.648 160.567,108.769 7.168,22.411 15.773,49.653 25.633,81.121 0.187,-0.371 0.379,-0.73 0.57,-1.089 34.516,-65.442 97.469,-101.481 177.262,-101.481 79.859,0 128.156,28.281 155.507,53.871 20.31,-32.793 56.07,-52.902 94.85,-52.902 21.94,0 43.5,6.582 61.71,18.703 v -9.48 c 0,-61.512 50.05,-111.563 111.56,-111.563 29.24,0 56.9,11.281 77.88,31.762 19.72,19.258 31.47,44.609 33.39,71.847 20.21,0.012 89.25,4.09 137.36,55.383 l 0.76,0.801 c 5.41,5.758 14.16,15.066 23.38,29.738 13.05,-6.34 27.51,-9.672 42.1,-9.672 11.99,0 23.67,2.184 34.75,6.473 6.01,-7.109 12.89,-13.34 20.5,-18.57 40.05,-27.512 86.47,-46.289 134.25,-54.321 5.71,-0.949 11.52,-1.441 17.29,-1.441 52.55,0 95.81,38.371 103,89.242 19.63,-25.051 49.47,-39.711 81.83,-39.711 24.1,0 47.62,8.461 66.23,23.821 37.36,30.828 68.18,70.308 89.11,114.168 12.9,27.035 13.52,58.328 1.7,85.859 -10.34,24.078 -29.32,43.242 -52.98,53.875"
/>
</g>
<path
fillRule="evenodd"
stroke="none"
fillOpacity="1"
fill="#050505"
d="m 1159.17,546.68 c 0,-20.758 -16.83,-37.59 -37.59,-37.59 -20.76,0 -37.58,16.832 -37.58,37.59 v 374.066 c 0,20.762 16.82,37.59 37.58,37.59 20.76,0 37.59,-16.828 37.59,-37.59 V 546.68"
/>
<g fillRule="nonzero" stroke="none" fillOpacity="1" fill="#050505">
<path d="m 817.418,718.516 c 2.512,51.925 41.84,60.464 59.527,60.464 17.782,0 57.258,-8.648 59.36,-61.347 0,-2.789 -1.492,-4.434 -3.141,-4.434 H 820.688 c -1.817,0 -3.454,1.832 -3.27,5.317 z m 16.348,-67.184 135.035,-0.141 c 0.933,0 1.863,0.071 2.793,0.137 18.734,1.418 36.496,16.961 36.566,41.856 0,0.046 0.01,0.093 0.01,0.14 -0.02,0.656 -0.09,1.262 -0.13,1.906 -6.7,137.254 -94.841,157.129 -134.817,157.129 -41.567,0 -135.7,-24.535 -135.7,-177.152 0.715,-57.391 13.008,-167.078 133.7,-167.078 104.254,0 127.593,60.211 127.593,75.57 0,22.656 -18.871,41.875 -40.183,34.645 -17.297,-5.867 -41.016,-40.313 -79.238,-40.313 -54.688,0 -61.77,50.004 -61.77,60.653 0,8.789 4.586,12.211 16.141,12.648" />
<path d="M 525.379,555.84 407.938,796.477 c -9.356,19.164 -1.403,42.285 17.765,51.636 19.164,9.36 42.285,1.403 51.637,-17.765 l 90.926,-186.317 55.957,180.797 c 6.304,20.371 27.918,31.781 48.304,25.473 20.371,-6.305 31.774,-27.934 25.469,-48.305 0,0 -62.891,-205.676 -100.687,-323.855 -12.504,-39.102 -45.274,-58.493 -94.387,-57.282 -48.578,1.172 -56.113,29.821 -50.731,48.711 5.922,20.77 23.262,27.051 40.235,26.961 43.887,-0.23 49.476,26.36 32.953,59.309" />
<path d="m 1605.27,606.609 h 4.16 c 3.38,0 6.53,0.176 6.53,3.993 0,3.363 -2.87,3.765 -5.48,3.765 h -5.21 z m -3.39,10.793 h 8.9 c 5.79,0 8.53,-2.136 8.53,-6.98 0,-4.32 -2.78,-6.168 -6.41,-6.563 l 6.99,-10.711 h -3.66 l -6.62,10.438 h -4.34 v -10.438 h -3.39 z m 7.83,-29.843 c 9.84,0 17.72,7.722 17.72,17.972 0,10.043 -7.88,17.77 -17.72,17.77 -9.84,0 -17.72,-7.727 -17.72,-17.985 0,-10.035 7.88,-17.757 17.72,-17.757 z m 0,38.777 c 11.47,0 21.09,-8.992 21.09,-20.805 0,-12.019 -9.62,-21.019 -21.09,-21.019 -11.47,0 -21.07,9 -21.07,20.804 0,12.028 9.6,21.02 21.07,21.02" />
<path d="m 1456.07,667.563 c 0,-29.711 0,-83.051 -54.02,-83.051 -54.18,0 -54.02,53.34 -54.02,83.051 v 27.14 c 0,29.711 0,83.051 54.02,83.051 54.17,0 54.02,-53.34 54.02,-83.051 z m 74.45,28.542 c 0,49.329 -6.06,116.926 -68.35,146.516 -35.14,16.688 -78.42,16.395 -111.7,-2.84 -6.67,-3.859 -13.28,-8.129 -19.13,-12.496 -4,16.469 -18.8,28.715 -36.49,28.715 -20.76,0 -37.59,-16.828 -37.59,-37.59 V 444.352 c 0,-20.762 16.83,-37.59 37.59,-37.59 20.76,0 37.59,16.828 37.59,37.59 v 98.168 c 0,0 25.25,-30.961 65.45,-32.008 142.76,-3.723 133.41,151.551 132.63,185.593" />
</g>
<g fillRule="evenodd" stroke="none" fillOpacity="1" fill="#d32323">
<path d="m 1648.99,876.68 131.26,-64.012 c 25.25,-12.313 21.34,-49.445 -5.91,-56.238 l -141.69,-35.332 c -17.68,-4.407 -35.13,7.789 -37.21,25.882 -4.46,38.704 -0.06,76.93 11.84,112.317 5.77,17.187 25.41,25.332 41.71,17.383" />
<path d="m 1701.54,606.473 97.7,108.511 c 18.79,20.879 53.41,6.895 52.43,-21.179 l -5.1,-145.985 c -0.63,-18.191 -17.15,-31.66 -35.1,-28.64 -37.75,6.34 -73.46,20.929 -104.63,42.34 -14.98,10.289 -17.46,31.441 -5.3,44.953" />
<path d="m 1933.18,750.875 138.87,-45.129 c 17.28,-5.617 25.71,-25.176 17.88,-41.578 -16.29,-34.125 -39.92,-64.617 -69.44,-88.977 -14.04,-11.589 -35.07,-8.191 -44.72,7.25 l -77.41,123.875 c -14.88,23.825 8.1,53.243 34.82,44.559" />
<path d="m 2074.4,839.465 -140.38,-40.25 c -27,-7.742 -48.94,22.461 -33.23,45.75 l 81.67,121.066 c 10.14,15.032 31.21,17.797 44.81,5.805 27.99,-24.699 50.92,-55.602 66.41,-91.352 7.24,-16.711 -1.77,-36 -19.28,-41.019" />
<path d="m 1746.39,1180.09 c -25.26,-6.77 -49.36,-15.71 -72.15,-26.58 -15.82,-7.55 -21.79,-27.02 -13.03,-42.19 l 137.2,-237.64 c 15.39,-26.66 56.14,-15.742 56.14,15.043 v 274.397 c 0,17.52 -14.91,31.4 -32.38,30.03 -25.17,-1.99 -50.51,-6.29 -75.78,-13.06" />
</g>
</g>
</g>
</svg>
)
}

View File

@ -0,0 +1,16 @@
import React from 'react'
import { WelcomeAreaRoute } from './WelcomeArea'
const WelcomeMainPage = React.lazy(async () => ({
default: (await import('./WelcomeMainPage')).WelcomeMainPage,
}))
export const welcomeAreaRoutes: ReadonlyArray<WelcomeAreaRoute> = [
{
path: '/',
exact: true,
// tslint:disable-next-line:jsx-no-lambda
render: props => <WelcomeMainPage {...props} />,
},
// We will add more pages here soon. The other pages (search, code intel, integrations) were
// removed to avoid blocking shipping of the new main welcome page.
]

View File

@ -1,5 +1,10 @@
import React from 'react'
import { Redirect } from 'react-router'
import { LayoutRouteProps, routes } from '../routes'
import { welcomeAreaRoutes } from './dotcom/welcome/routes'
const WelcomeArea = React.lazy(async () => ({
default: (await import('./dotcom/welcome/WelcomeArea')).WelcomeArea,
}))
const NewProductSubscriptionPageOrRedirectUser = React.lazy(async () => ({
default: (await import('./user/productSubscriptions/NewProductSubscriptionPageOrRedirectUser'))
.NewProductSubscriptionPageOrRedirectUser,
@ -13,5 +18,14 @@ export const enterpriseRoutes: ReadonlyArray<LayoutRouteProps> = [
exact: true,
render: props => <NewProductSubscriptionPageOrRedirectUser {...props} />,
},
{
path: '/start',
render: () => <Redirect to="/welcome" />,
exact: true,
},
{
path: '/welcome',
render: props => <WelcomeArea {...props} routes={welcomeAreaRoutes} />,
},
...routes,
]

View File

@ -22,19 +22,26 @@
}
&__logo {
width: 1.5rem;
height: 1.5rem;
&:hover {
animation: spin 0.5s ease-in-out 1;
&--full {
// stylelint-disable-next-line declaration-property-unit-whitelist
width: 150px;
margin-right: 0.25rem; // full logo image has slightly more spacing on left side
}
&:not(&--full) {
width: 1.5rem;
&:hover {
animation: spin 0.5s ease-in-out 1;
@keyframes spin {
50% {
transform: rotate(180deg) scale(1.2);
}
@keyframes spin {
50% {
transform: rotate(180deg) scale(1.2);
}
100% {
transform: rotate(180deg) scale(1);
100% {
transform: rotate(180deg) scale(1);
}
}
}
}

View File

@ -10,6 +10,7 @@ import { authRequired } from '../auth'
import { KeybindingsProps } from '../keybindings'
import { parseSearchURLQuery } from '../search'
import { SearchNavbarItem } from '../search/input/SearchNavbarItem'
import { showDotComMarketing } from '../util/features'
import { NavLinks } from './NavLinks'
interface Props extends SettingsCascadeProps, PlatformContextProps, ExtensionsControllerProps, KeybindingsProps {
@ -70,7 +71,19 @@ export class GlobalNavbar extends React.PureComponent<Props, State> {
}
public render(): JSX.Element | null {
const logo = <img className="global-navbar__logo" src="/.assets/img/sourcegraph-mark.svg" />
let logoSrc: string
const showFullLogo = this.props.location.pathname === '/welcome'
if (showFullLogo) {
logoSrc = this.props.isLightTheme
? '/.assets/img/sourcegraph-light-head-logo.svg'
: '/.assets/img/sourcegraph-head-logo.svg'
} else {
logoSrc = '/.assets/img/sourcegraph-mark.svg'
}
const logo = (
<img className={`global-navbar__logo ${showFullLogo ? 'global-navbar__logo--full' : ''}`} src={logoSrc} />
)
return (
<div className={`global-navbar ${this.props.lowProfile ? '' : 'global-navbar--bg'}`}>
{this.props.lowProfile ? (
@ -84,8 +97,8 @@ export class GlobalNavbar extends React.PureComponent<Props, State> {
{logo}
</Link>
)}
{!this.state.authRequired && (
<div className="global-navbar__search-box-container">
{!this.state.authRequired && this.props.location.pathname !== '/welcome' && (
<div className="global-navbar__search-box-container d-none d-sm-flex">
<SearchNavbarItem
{...this.props}
navbarSearchQuery={this.props.navbarSearchQuery}
@ -95,7 +108,7 @@ export class GlobalNavbar extends React.PureComponent<Props, State> {
)}
</>
)}
{!this.state.authRequired && <NavLinks {...this.props} />}
{!this.state.authRequired && <NavLinks {...this.props} showDotComMarketing={showDotComMarketing} />}
</div>
)
}

View File

@ -0,0 +1,84 @@
import * as H from 'history'
import { flatten } from 'lodash'
import React from 'react'
import { createRenderer } from 'react-test-renderer/shallow'
import { setLinkComponent } from '../../../shared/src/components/Link'
import { ExtensionsControllerProps } from '../../../shared/src/extensions/controller'
import * as GQL from '../../../shared/src/graphql/schema'
import { SettingsCascadeProps } from '../../../shared/src/settings/settings'
import { KeybindingsProps } from '../keybindings'
import { NavLinks } from './NavLinks'
// Renders a human-readable list of the NavLinks' contents so that humans can more easily diff
// snapshots to see what actually changed.
const renderShallow = (element: React.ReactElement<NavLinks['props']>): any => {
const renderer = createRenderer()
renderer.render(element)
const getDisplayName = (element: React.ReactChild): string | string[] => {
if (element === null) {
return []
} else if (typeof element === 'string' || typeof element === 'number') {
return element.toString()
} else if (element.type === 'li' && (element.props.children.props.href || element.props.children.props.to)) {
return `${element.props.children.props.children} ${element.props.children.props.href ||
element.props.children.props.to}`
} else if (typeof element.type === 'symbol' || typeof element.type === 'string') {
return flatten(React.Children.map(element.props.children, element => getDisplayName(element)))
} else {
return element.type.displayName || element.type.name || 'Unknown'
}
}
return flatten(
React.Children.map(renderer.getRenderOutput().props.children, e => getDisplayName(e)).filter(e => !!e)
)
}
describe('NavLinks', () => {
setLinkComponent((props: any) => <a {...props} />)
afterAll(() => setLinkComponent(null as any)) // reset global env for other tests
const NOOP_EXTENSIONS_CONTROLLER: ExtensionsControllerProps<
'executeCommand' | 'services'
>['extensionsController'] = { executeCommand: async () => void 0, services: {} as any }
const NOOP_PLATFORM_CONTEXT = { forceUpdateTooltip: () => void 0 }
const KEYBINDINGS: KeybindingsProps['keybindings'] = { commandPalette: [] }
const SETTINGS_CASCADE: SettingsCascadeProps['settingsCascade'] = { final: null, subjects: null }
// tslint:disable-next-line:no-object-literal-type-assertion
const USER = { username: 'u' } as GQL.IUser
const history = H.createMemoryHistory({ keyLength: 0 })
const commonProps = {
extensionsController: NOOP_EXTENSIONS_CONTROLLER,
platformContext: NOOP_PLATFORM_CONTEXT,
isLightTheme: true,
onThemeChange: () => void 0,
keybindings: KEYBINDINGS,
settingsCascade: SETTINGS_CASCADE,
}
// The 3 main props that affect the desired contents of NavLinks are whether the user is signed
// in, whether we're on Sourcegraph.com, and the path. Create snapshots of all permutations.
for (const authenticatedUser of [null, USER]) {
for (const showDotComMarketing of [false, true]) {
for (const path of ['/foo', '/search', '/welcome']) {
const name = [
authenticatedUser ? 'authed' : 'unauthed',
showDotComMarketing ? 'Sourcegraph.com' : 'self-hosted',
path,
].join(' ')
test(name, () => {
expect(
renderShallow(
<NavLinks
{...commonProps}
authenticatedUser={authenticatedUser}
showDotComMarketing={showDotComMarketing}
location={H.createLocation(path, history.location)}
/>
)
).toMatchSnapshot()
})
}
}
}
})

View File

@ -1,27 +1,28 @@
import * as H from 'history'
import * as React from 'react'
import { Link } from 'react-router-dom'
import { Subscription } from 'rxjs'
import { ActionsNavItems } from '../../../shared/src/actions/ActionsNavItems'
import { ContributableMenu } from '../../../shared/src/api/protocol'
import { CommandListPopoverButton } from '../../../shared/src/commandPalette/CommandList'
import { Link } from '../../../shared/src/components/Link'
import { ExtensionsControllerProps } from '../../../shared/src/extensions/controller'
import * as GQL from '../../../shared/src/graphql/schema'
import { PlatformContextProps } from '../../../shared/src/platform/context'
import { SettingsCascadeProps } from '../../../shared/src/settings/settings'
import { isDiscussionsEnabled } from '../discussions'
import { KeybindingsProps } from '../keybindings'
import { eventLogger } from '../tracking/eventLogger'
import { showDotComMarketing } from '../util/features'
import { UserNavItem } from './UserNavItem'
interface Props extends SettingsCascadeProps, PlatformContextProps, ExtensionsControllerProps, KeybindingsProps {
interface Props
extends SettingsCascadeProps,
KeybindingsProps,
ExtensionsControllerProps<'executeCommand' | 'services'>,
PlatformContextProps<'forceUpdateTooltip'> {
location: H.Location
history: H.History
authenticatedUser: GQL.IUser | null
isLightTheme: boolean
onThemeChange: () => void
isMainPage?: boolean
showDotComMarketing: boolean
}
export class NavLinks extends React.PureComponent<Props> {
@ -31,24 +32,28 @@ export class NavLinks extends React.PureComponent<Props> {
this.subscriptions.unsubscribe()
}
private onClickInstall = (): void => {
eventLogger.log('InstallSourcegraphServerCTAClicked', {
location_on_page: 'Navbar',
})
}
public render(): JSX.Element | null {
return (
<ul className="nav-links nav align-items-center pl-2 pr-1">
{showDotComMarketing && (
{/* Show "Search" link on small screens when GlobalNavbar hides the SearchNavbarItem. */}
{this.props.location.pathname !== '/search' && this.props.location.pathname !== '/welcome' && (
<li className="nav-item d-sm-none">
<Link className="nav-link" to="/search">
Search
</Link>
</li>
)}
{this.props.showDotComMarketing && this.props.location.pathname !== '/welcome' && (
<li className="nav-item">
<a
href="https://docs.sourcegraph.com/#quickstart"
className="nav-link text-truncate"
onClick={this.onClickInstall}
title="Install self-hosted Sourcegraph to search your own (private) code"
>
Install Sourcegraph
<Link to="/welcome" className="nav-link">
Welcome
</Link>
</li>
)}
{this.props.showDotComMarketing && this.props.location.pathname === '/welcome' && (
<li className="nav-item">
<a href="https://docs.sourcegraph.com" className="nav-link" target="_blank">
Docs
</a>
</li>
)}
@ -59,18 +64,24 @@ export class NavLinks extends React.PureComponent<Props> {
platformContext={this.props.platformContext}
location={this.props.location}
/>
<li className="nav-item">
<Link to="/explore" className="nav-link">
Explore
</Link>
</li>
{(!this.props.showDotComMarketing ||
!!this.props.authenticatedUser ||
this.props.location.pathname !== '/welcome') && (
<li className="nav-item">
<Link to="/explore" className="nav-link">
Explore
</Link>
</li>
)}
{!this.props.authenticatedUser && (
<>
<li className="nav-item">
<Link to="/extensions" className="nav-link">
Extensions
</Link>
</li>
{this.props.location.pathname !== '/welcome' && (
<li className="nav-item">
<Link to="/extensions" className="nav-link">
Extensions
</Link>
</li>
)}
{this.props.location.pathname !== '/sign-in' && (
<li className="nav-item mx-1">
<Link className="nav-link btn btn-primary" to="/sign-in">
@ -78,33 +89,37 @@ export class NavLinks extends React.PureComponent<Props> {
</Link>
</li>
)}
{showDotComMarketing && (
{this.props.showDotComMarketing && (
<li className="nav-item">
<a href="https://about.sourcegraph.com" className="nav-link">
About
</a>
</li>
)}
<li className="nav-item">
<Link to="/help" className="nav-link">
Help
</Link>
</li>
{this.props.location.pathname !== '/welcome' && (
<li className="nav-item">
<Link to="/help" className="nav-link">
Help
</Link>
</li>
)}
</>
)}
<CommandListPopoverButton
menu={ContributableMenu.CommandPalette}
extensionsController={this.props.extensionsController}
platformContext={this.props.platformContext}
toggleVisibilityKeybinding={this.props.keybindings.commandPalette}
location={this.props.location}
/>
{this.props.location.pathname !== '/welcome' && (
<CommandListPopoverButton
menu={ContributableMenu.CommandPalette}
extensionsController={this.props.extensionsController}
platformContext={this.props.platformContext}
toggleVisibilityKeybinding={this.props.keybindings.commandPalette}
location={this.props.location}
/>
)}
{this.props.authenticatedUser && (
<li className="nav-item">
<UserNavItem
{...this.props}
authenticatedUser={this.props.authenticatedUser}
showAbout={showDotComMarketing}
showAbout={this.props.showDotComMarketing}
showDiscussions={isDiscussionsEnabled(this.props.settingsCascade)}
/>
</li>

View File

@ -11,7 +11,6 @@ interface Props {
authenticatedUser: GQL.IUser
isLightTheme: boolean
onThemeChange: () => void
isMainPage?: boolean
showAbout: boolean
showDiscussions: boolean
}

View File

@ -0,0 +1,125 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NavLinks authed Sourcegraph.com /foo 1`] = `
Array [
"Search /search",
"Welcome /welcome",
"ActionsNavItems",
"Explore /explore",
"CommandListPopoverButton",
"UserNavItem",
]
`;
exports[`NavLinks authed Sourcegraph.com /search 1`] = `
Array [
"Welcome /welcome",
"ActionsNavItems",
"Explore /explore",
"CommandListPopoverButton",
"UserNavItem",
]
`;
exports[`NavLinks authed Sourcegraph.com /welcome 1`] = `
Array [
"Docs https://docs.sourcegraph.com",
"ActionsNavItems",
"Explore /explore",
"UserNavItem",
]
`;
exports[`NavLinks authed self-hosted /foo 1`] = `
Array [
"Search /search",
"ActionsNavItems",
"Explore /explore",
"CommandListPopoverButton",
"UserNavItem",
]
`;
exports[`NavLinks authed self-hosted /search 1`] = `
Array [
"ActionsNavItems",
"Explore /explore",
"CommandListPopoverButton",
"UserNavItem",
]
`;
exports[`NavLinks authed self-hosted /welcome 1`] = `
Array [
"ActionsNavItems",
"Explore /explore",
"UserNavItem",
]
`;
exports[`NavLinks unauthed Sourcegraph.com /foo 1`] = `
Array [
"Search /search",
"Welcome /welcome",
"ActionsNavItems",
"Explore /explore",
"Extensions /extensions",
"Sign in /sign-in",
"About https://about.sourcegraph.com",
"Help /help",
"CommandListPopoverButton",
]
`;
exports[`NavLinks unauthed Sourcegraph.com /search 1`] = `
Array [
"Welcome /welcome",
"ActionsNavItems",
"Explore /explore",
"Extensions /extensions",
"Sign in /sign-in",
"About https://about.sourcegraph.com",
"Help /help",
"CommandListPopoverButton",
]
`;
exports[`NavLinks unauthed Sourcegraph.com /welcome 1`] = `
Array [
"Docs https://docs.sourcegraph.com",
"ActionsNavItems",
"Sign in /sign-in",
"About https://about.sourcegraph.com",
]
`;
exports[`NavLinks unauthed self-hosted /foo 1`] = `
Array [
"Search /search",
"ActionsNavItems",
"Explore /explore",
"Extensions /extensions",
"Sign in /sign-in",
"Help /help",
"CommandListPopoverButton",
]
`;
exports[`NavLinks unauthed self-hosted /search 1`] = `
Array [
"ActionsNavItems",
"Explore /explore",
"Extensions /extensions",
"Sign in /sign-in",
"Help /help",
"CommandListPopoverButton",
]
`;
exports[`NavLinks unauthed self-hosted /welcome 1`] = `
Array [
"ActionsNavItems",
"Explore /explore",
"Sign in /sign-in",
]
`;

View File

@ -2,9 +2,6 @@ import * as React from 'react'
import { Redirect, RouteComponentProps } from 'react-router'
import { LayoutProps } from './Layout'
import { parseSearchURLQuery } from './search'
const MainPage = React.lazy(async () => ({
default: (await import('./search/input/MainPage')).MainPage,
}))
const SearchPage = React.lazy(async () => ({
default: (await import('./search/input/SearchPage')).SearchPage,
}))
@ -78,12 +75,11 @@ export const routes: ReadonlyArray<LayoutRouteProps> = [
{
path: '/',
render: (props: any) =>
window.context.sourcegraphDotComMode && !props.user ? <Redirect to="/start" /> : <Redirect to="/search" />,
exact: true,
},
{
path: '/start',
render: (props: any) => <MainPage {...props} />,
window.context.sourcegraphDotComMode && !props.user ? (
<Redirect to="/welcome" />
) : (
<Redirect to="/search" />
),
exact: true,
},
{

View File

@ -1,353 +0,0 @@
import { createHoverifier, findPositionsFromEvents, HoveredToken, HoverState } from '@sourcegraph/codeintellify'
import { getTokenAtPosition } from '@sourcegraph/codeintellify/lib/token_position'
import { Position } from '@sourcegraph/extension-api-types'
import * as H from 'history'
import * as React from 'react'
import { Subject, Subscription } from 'rxjs'
import { catchError, filter, map, withLatestFrom } from 'rxjs/operators'
import { ActionItemProps } from '../../../../shared/src/actions/ActionItem'
import { HoverMerged } from '../../../../shared/src/api/client/types/hover'
import { ExtensionsControllerProps } from '../../../../shared/src/extensions/controller'
import * as GQL from '../../../../shared/src/graphql/schema'
import { getHoverActions } from '../../../../shared/src/hover/actions'
import { HoverContext, HoverOverlay } from '../../../../shared/src/hover/HoverOverlay'
import { getModeFromPath } from '../../../../shared/src/languages'
import { PlatformContextProps } from '../../../../shared/src/platform/context'
import { ErrorLike, isErrorLike } from '../../../../shared/src/util/errors'
import { isDefined, propertyIsDefined } from '../../../../shared/src/util/types'
import { FileSpec, ModeSpec, PositionSpec, RepoSpec, ResolvedRevSpec, RevSpec } from '../../../../shared/src/util/url'
import { getHover } from '../../backend/features'
import { fetchBlob } from '../../repo/blob/BlobPage'
interface Props extends ExtensionsControllerProps, PlatformContextProps {
history: H.History
location: H.Location
className: string
startLine: number
endLine: number
parentElement: string
overlayPortal?: HTMLElement
tooltipClass: string
defaultHoverPosition: Position
}
interface State extends HoverState<HoverContext, HoverMerged, ActionItemProps> {
/**
* The blob data or error that happened.
* undefined while loading.
*/
blobOrError?: GQL.IGitBlob | ErrorLike
target?: EventTarget
}
const domFunctions = {
getCodeElementFromTarget: (target: HTMLElement): HTMLTableCellElement | null => {
// If the target is part of the decoration, return null.
if (
target.classList.contains('line-decoration-attachment') ||
target.classList.contains('line-decoration-attachment__contents')
) {
return null
}
const row = target.closest('tr')
if (!row) {
return null
}
return row.cells[1]
},
getCodeElementFromLineNumber: (codeView: HTMLElement, line: number): HTMLElement | null => {
const lineNumberElement = codeView.querySelector(`td[data-line="${line}"]`)
if (!lineNumberElement) {
return null
}
return lineNumberElement.nextElementSibling as HTMLElement | null
},
getLineNumberFromCodeElement: (codeCell: HTMLElement): number => {
const row = codeCell.closest('tr')
if (!row) {
throw new Error('Could not find closest row for codeCell')
}
const numberCell = row.cells[0]
if (!numberCell || !numberCell.dataset.line) {
throw new Error('Could not find line number')
}
return parseInt(numberCell.dataset.line, 10)
},
}
const REPO_NAME = 'github.com/gorilla/mux'
const COMMIT_ID = '9e1f5955c0d22b55d9e20d6faa28589f83b2faca'
const REV = undefined
const FILE_PATH = 'mux.go'
export class CodeIntellifyBlob extends React.Component<Props, State> {
/** Emits whenever the ref callback for the code element is called */
private codeViewElements = new Subject<HTMLElement | null>()
private nextCodeViewElement = (element: HTMLElement | null) => this.codeViewElements.next(element)
/** Emits whenever the ref callback for the hover element is called */
private hoverOverlayElements = new Subject<HTMLElement | null>()
private nextOverlayElement = (element: HTMLElement | null) => this.hoverOverlayElements.next(element)
/** Emits whenever the ref callback for the demo file element is called */
private codeIntellifyBlobElements = new Subject<HTMLElement | null>()
private nextCodeIntellifyBlobElements = (element: HTMLElement | null) =>
this.codeIntellifyBlobElements.next(element)
/** Emits when the close button was clicked */
private closeButtonClicks = new Subject<MouseEvent>()
private nextCloseButtonClick = (event: MouseEvent) => this.closeButtonClicks.next(event)
private subscriptions = new Subscription()
private componentUpdates = new Subject<void>()
private target: EventTarget | null = null
constructor(props: Props) {
super(props)
this.state = {}
const hoverifier = createHoverifier<
RepoSpec & RevSpec & FileSpec & ResolvedRevSpec,
HoverMerged,
ActionItemProps
>({
closeButtonClicks: this.closeButtonClicks,
hoverOverlayElements: this.hoverOverlayElements,
hoverOverlayRerenders: this.componentUpdates.pipe(
withLatestFrom(this.hoverOverlayElements, this.codeIntellifyBlobElements),
map(([, hoverOverlayElement, codeIntellifyBlobElement]) => ({
hoverOverlayElement,
codeIntellifyBlobElement,
})),
filter(propertyIsDefined('codeIntellifyBlobElement')),
map(({ hoverOverlayElement, codeIntellifyBlobElement }) => ({
hoverOverlayElement,
relativeElement: codeIntellifyBlobElement.closest(this.props.parentElement) as HTMLElement | null,
})),
// Can't reposition HoverOverlay or file weren't rendered
filter(propertyIsDefined('relativeElement')),
filter(propertyIsDefined('hoverOverlayElement'))
),
getHover: hoveredToken => getHover(this.getLSPTextDocumentPositionParams(hoveredToken), this.props),
getActions: context => getHoverActions(this.props, context),
})
this.subscriptions.add(hoverifier)
const positionEvents = this.codeViewElements.pipe(
filter(isDefined),
findPositionsFromEvents(domFunctions)
)
const targets = positionEvents.pipe(map(({ event: { target } }) => target))
targets.subscribe(target => (this.target = target))
this.subscriptions.add(
hoverifier.hoverify({
positionEvents,
resolveContext: () => ({
repoName: REPO_NAME,
commitID: COMMIT_ID,
rev: REV || '',
filePath: FILE_PATH,
}),
dom: domFunctions,
})
)
this.subscriptions.add(hoverifier.hoverStateUpdates.subscribe(update => this.setState(update)))
this.subscriptions.add(
this.codeViewElements
.pipe(
filter(isDefined),
map(codeView => getTokenAtPosition(codeView, props.defaultHoverPosition, domFunctions)),
filter(isDefined)
)
.subscribe(token => {
const showOnHomepage = props.className === 'code-intellify-container' && window.innerWidth >= 1393
const showOnModal =
props.className === 'code-intellify-container-modal' && window.innerWidth >= 1275
if (showOnHomepage || showOnModal) {
token.click()
}
})
)
}
public componentDidMount(): void {
// Fetch repository revision.
fetchBlob({
repoName: REPO_NAME,
commitID: COMMIT_ID,
filePath: FILE_PATH,
isLightTheme: false,
disableTimeout: false,
})
.pipe(
catchError(error => {
console.error(error)
return [error]
})
)
.subscribe(blobOrError => this.setState({ blobOrError }), err => console.error(err))
this.componentUpdates.next()
this.subscriptions.add(
this.props.extensionsController.services.model.model.next({
...this.props.extensionsController.services.model.model.value,
visibleViewComponents: [
{
type: 'textEditor',
item: {
uri: `git://github.com/gorilla/mux?9e1f5955c0d22b55d9e20d6faa28589f83b2faca#mux.go`,
languageId: 'go',
text: '',
},
selections: [],
isActive: true,
},
],
})
)
}
public componentDidUpdate(): void {
this.componentUpdates.next()
}
private getLSPTextDocumentPositionParams(
position: HoveredToken & RepoSpec & RevSpec & FileSpec & ResolvedRevSpec
): RepoSpec & RevSpec & ResolvedRevSpec & FileSpec & PositionSpec & ModeSpec {
return {
repoName: position.repoName,
filePath: position.filePath,
commitID: position.commitID,
rev: position.rev,
mode: getModeFromPath(FILE_PATH),
position,
}
}
public render(): JSX.Element {
if (!this.state.blobOrError) {
// Render placeholder for layout before content is fetched.
return <div className="blob-page__placeholder">Loading...</div>
}
const hoverOverlayProps = this.adjustHoverOverlayPosition(this.target)
return (
<div className={this.props.className} ref={this.nextCodeIntellifyBlobElements}>
<div className="code-header">
<span className="code-header__title">github.com/gorilla/mux/mux.go</span>
<span className="code-header__link">
<a href="https://sourcegraph.com/github.com/gorilla/mux/-/blob/mux.go">View full file</a>
</span>
</div>
{!isErrorLike(this.state.blobOrError) && (
<code
className={`blob__code blob__code--wrapped e2e-blob`}
ref={this.nextCodeViewElement}
dangerouslySetInnerHTML={{
__html: trimHTMLString(
this.state.blobOrError.highlight.html,
this.props.startLine - 1,
this.props.endLine + 1
),
}}
/>
)}
{this.state.hoverOverlayProps && (
<HoverOverlay
{...hoverOverlayProps}
hoverRef={this.nextOverlayElement}
extensionsController={this.props.extensionsController}
platformContext={this.props.platformContext}
location={this.props.location}
onCloseButtonClick={this.nextCloseButtonClick}
showCloseButton={false}
className={this.props.tooltipClass}
/>
)}
</div>
)
}
/**
* This function adjusts the position of the hoverOverlay so that it does not overflow on the right side
* of the viewport. If a hoverOverlay will exceed the viewport, this function will adjust the position
* so that it aligns the right side of the hover overlay with the right side of the target element.
*
*/
private adjustHoverOverlayPosition(
target: EventTarget | null
): HoverState<HoverContext, HoverMerged, ActionItemProps>['hoverOverlayProps'] {
const viewPortEdge = window.innerWidth
if (!this.state.hoverOverlayProps) {
return undefined
}
if (!target) {
return this.state.hoverOverlayProps
}
const { overlayPosition, ...rest } = this.state.hoverOverlayProps
const targetBounds = (target as HTMLElement).getBoundingClientRect()
let newOverlayPosition: { top: number; left: number } = overlayPosition!
if (overlayPosition && viewPortEdge < targetBounds.left + 512 && targetBounds.right - 512 >= 0) {
const containerWidth = (document.querySelector(
this.props.parentElement
) as HTMLElement).parentElement!.getBoundingClientRect().width
const parentWidth = (document.querySelector(
this.props.parentElement
) as HTMLElement).getBoundingClientRect().width
// One side of the total horizontal margin.
const halfMarginWidth = (viewPortEdge - containerWidth) / 2
// The difference between the viewport width and parent width. We need to subtract this because
// `left` is relative to the parent, whereas targetBounds.right is relative to the viewport.
const relativeElementDifference = viewPortEdge - parentWidth
newOverlayPosition = {
top: overlayPosition.top,
// 512 is the width of a hoverOverlay.
left: targetBounds.right - 512 - relativeElementDifference + halfMarginWidth,
}
}
return { ...rest, overlayPosition: newOverlayPosition }
}
}
/**
* We can only fetch blobs as an entire file. For demo purposes, we only want to show part of the file.
* This function trims the HTML string of the file that will be code-intellfied on the homepage to only show
* the lines that we specify. It makes some assumptions for this specific use case, such as the presence
* of a single table and tbody element in the html, so be careful when changing.
*/
function trimHTMLString(html: string, startLine: number, endLine: number): string {
const domParser = new DOMParser()
const doc = domParser.parseFromString(html, 'text/html')
const startToRemove = doc.querySelectorAll(`tr:nth-child(n + 0):nth-child(-n + ${startLine})`)
const endToRemove = doc.querySelectorAll(`tr:nth-child(n + ${endLine})`)
const elementsToRemove = [...startToRemove, ...endToRemove]
const tableEl = doc.querySelector('tbody')! // assume a single tbody element will exist in blob HTML
for (const el of elementsToRemove) {
tableEl.removeChild(el)
}
const xmlSerializer = new XMLSerializer()
const tbl = doc.querySelector('table')! // assume a single table element will exist in blob HTML
const trimmedHTMLString = xmlSerializer.serializeToString(tbl)
return trimmedHTMLString
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,76 +0,0 @@
@import './Suggestion';
.query-input2-for-modal {
width: 100%;
position: relative;
&__input {
// Right side is flush with SearchButton.
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&__suggestions {
position: absolute;
width: 100%;
z-index: 1;
max-height: 25rem;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
overflow-y: overlay;
background-color: #000000;
border: solid 1px #1b548a;
border-top: none;
padding: 0;
.theme-light & {
background-color: $color-light-bg-2;
border-top: none;
}
// Custom scrollbar
&::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
}
&::-webkit-scrollbar-corner,
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #2a3a51;
}
.theme-light &::-webkit-scrollbar-thumb {
background-color: $color-light-bg-3;
}
* {
display: inline-block;
}
li {
display: block;
position: relative;
}
}
&__loading-notifier {
position: absolute;
z-index: 1;
right: 0.5rem;
padding-left: 0.25rem;
bottom: 0.375rem;
background-color: $color-bg-4;
}
}
.theme-light {
.query-input2 {
&__loading-notifier {
background-color: $color-light-bg-1;
}
}
}

View File

@ -1,425 +0,0 @@
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import * as H from 'history'
import * as React from 'react'
import { fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs'
import {
catchError,
debounceTime,
delay,
distinctUntilChanged,
filter,
map,
publishReplay,
refCount,
repeat,
startWith,
switchMap,
takeUntil,
tap,
toArray,
} from 'rxjs/operators'
import { Key } from 'ts-key-enum'
import { eventLogger } from '../../tracking/eventLogger'
import { scrollIntoView } from '../../util'
import { fetchSuggestions } from '../backend'
import { createSuggestion, Suggestion, SuggestionItem } from './Suggestion'
/**
* The query input field is clobbered and updated to contain this subject's values, as
* they are received. This is used to trigger an update; the source of truth is still the URL.
*
* This file is mostly the same as Queryinput.tsx but differs for the use on the homepage on line
* 326-335. It also does not need onInputFocus function.
*
*/
export const queryUpdates = new Subject<string>()
interface Props {
location: H.Location
history: H.History
/** The value of the query input */
value: string
/** Called when the value changes */
onChange: (newValue: string) => void
/**
* A string that is appended to the query input's query before
* fetching suggestions.
*/
prependQueryForSuggestions?: string
/** Whether the input should be autofocused (and the behavior thereof) */
autoFocus?: true | 'cursor-at-end'
/** The input placeholder, if different from the default is desired. */
placeholder?: string
/**
* Whether this input should behave like the global query input: (1)
* pressing the '/' key focuses it and (2) other components contribute a
* query to it with their context (such as the repository area contributing
* 'repo:foo@bar' for the current repository and revision).
*
* At most one query input per page should have this behavior.
*/
hasGlobalQueryBehavior?: boolean
}
interface State {
/** Whether the query input is focused */
inputFocused: boolean
/** Whether suggestions are shown or not */
hideSuggestions: boolean
/** The suggestions shown to the user */
suggestions: Suggestion[]
/** Index of the currently selected suggestion (-1 if none selected) */
selectedSuggestion: number
/** Whether suggestions are currently being fetched */
loading: boolean
}
export class QueryInputForModal extends React.Component<Props, State> {
private static SUGGESTIONS_QUERY_MIN_LENGTH = 2
private componentUpdates = new Subject<Props>()
/** Subscriptions to unsubscribe from on component unmount */
private subscriptions = new Subscription()
/** Emits on keydown events in the input field */
private inputKeyDowns = new Subject<React.KeyboardEvent<HTMLInputElement>>()
/** Emits new input values */
private inputValues = new Subject<string>()
/** Emits when the input field is clicked */
private inputFocuses = new Subject<void>()
/** Emits when the suggestions are hidden */
private suggestionsHidden = new Subject<void>()
/** Only used for selection and focus management */
private inputElement?: HTMLInputElement
/** Only used for scroll state management */
private suggestionListElement?: HTMLElement
/** Only used for scroll state management */
private selectedSuggestionElement?: HTMLElement
/** Only used to keep track if the user has typed a single character into the input field so we can log an event once. */
private hasLoggedFirstInput = false
constructor(props: Props) {
super(props)
this.state = {
hideSuggestions: false,
inputFocused: false,
loading: false,
selectedSuggestion: -1,
suggestions: [],
}
this.subscriptions.add(
// Trigger new suggestions every time the input field is typed into
this.inputValues
.pipe(
tap(query => this.props.onChange(query)),
distinctUntilChanged(),
debounceTime(200),
switchMap(query => {
if (query.length < QueryInputForModal.SUGGESTIONS_QUERY_MIN_LENGTH) {
return [{ suggestions: [], selectedSuggestion: -1, loading: false }]
}
const fullQuery = [this.props.prependQueryForSuggestions, this.props.value]
.filter(s => !!s)
.join(' ')
const suggestionsFetch = fetchSuggestions(fullQuery).pipe(
map(createSuggestion),
toArray(),
map((suggestions: Suggestion[]) => ({
suggestions,
selectedSuggestion: -1,
hideSuggestions: false,
loading: false,
})),
catchError((err: Error) => {
console.error(err)
this.setState({ loading: false })
// HACK: if we catchError before 100ms, then the loader will display over us.
// This is not a good fix.
setTimeout(() => this.setState({ loading: false }), 120)
return [{}]
}),
publishReplay(),
refCount()
)
return merge(
suggestionsFetch,
// Show a loader if the fetch takes longer than 100ms
of({ loading: true }).pipe(
delay(100),
takeUntil(suggestionsFetch)
)
)
}),
// Abort suggestion display on route change or suggestion hiding
takeUntil(this.suggestionsHidden),
// But resubscribe afterwards
repeat()
)
.subscribe(
state => {
this.setState(state as State)
},
err => {
this.setState({ loading: false })
console.error(err)
}
)
)
if (this.props.hasGlobalQueryBehavior) {
// Quick-Open hotkeys
this.subscriptions.add(
fromEvent<KeyboardEvent>(window, 'keydown')
.pipe(
filter(
event =>
// Slash shortcut (if no input element is focused)
(event.key === '/' &&
!!document.activeElement &&
!['INPUT', 'TEXTAREA'].includes(document.activeElement.nodeName)) ||
// Cmd/Ctrl+P shortcut
((event.metaKey || event.ctrlKey) && event.key === 'p') ||
// Cmd/Ctrl+Shift+F shortcut
((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'f')
),
switchMap(event => {
event.preventDefault()
// Use selection as query
const selection = window.getSelection().toString()
if (selection) {
return new Observable<void>(observer =>
this.setState(
{
// query: selection, TODO(sqs): add back this behavior
suggestions: [],
selectedSuggestion: -1,
},
() => {
observer.next()
observer.complete()
}
)
)
}
return [undefined]
})
)
.subscribe(() => {
if (this.inputElement) {
// Select all input
this.inputElement.focus()
this.inputElement.setSelectionRange(0, this.inputElement.value.length)
}
})
)
// Allow other components to update the query (e.g., to be relevant to what the user is
// currently viewing).
this.subscriptions.add(
queryUpdates.pipe(distinctUntilChanged()).subscribe(query => this.props.onChange(query))
)
/** Whenever the URL query has a "focus" property, remove it and focus the query input. */
this.subscriptions.add(
this.componentUpdates.pipe(startWith(props)).subscribe(props => {
if (this.inputElement) {
const value = this.inputElement.value
if (value !== props.value) {
this.inputElement.value = props.value
this.focusInputAndPositionCursorAtEnd()
}
}
})
)
}
}
public componentDidMount(): void {
switch (this.props.autoFocus) {
case 'cursor-at-end':
this.focusInputAndPositionCursorAtEnd()
break
}
}
public componentWillReceiveProps(newProps: Props): void {
this.componentUpdates.next(newProps)
}
public componentWillUnmount(): void {
this.subscriptions.unsubscribe()
}
public componentDidUpdate(prevProps: Props, prevState: State): void {
// Check if selected suggestion is out of view
scrollIntoView(this.suggestionListElement, this.selectedSuggestionElement)
}
public render(): JSX.Element | null {
const showSuggestions =
this.props.value.length >= QueryInputForModal.SUGGESTIONS_QUERY_MIN_LENGTH &&
this.state.inputFocused &&
!this.state.hideSuggestions &&
this.state.suggestions.length !== 0
return (
<div className="query-input2-for-modal">
<input
className="form-control query-input2-for-modal__input"
defaultValue={this.props.value}
autoFocus={this.props.autoFocus === true}
onChange={this.onInputChange}
onKeyDown={this.onInputKeyDown}
onFocus={this.changeCursor}
onBlur={this.onInputBlur}
spellCheck={false}
autoCapitalize="off"
placeholder={this.props.placeholder === undefined ? 'Search code...' : this.props.placeholder}
ref={ref => (this.inputElement = ref!)}
/>
{this.state.loading && <LoadingSpinner className="icon-inline query-input2__loading-notifier" />}
{showSuggestions && (
<ul className="query-input2-for-modal__suggestions" ref={this.setSuggestionListElement}>
{this.state.suggestions.map((suggestion, i) => {
const isSelected = this.state.selectedSuggestion === i
const onRef = (ref: HTMLLIElement | null) => {
if (isSelected) {
this.selectedSuggestionElement = ref || undefined
}
}
return (
<SuggestionItem
key={i}
suggestion={suggestion}
isSelected={isSelected}
// tslint:disable-next-line:jsx-no-lambda
onClick={() => this.selectSuggestion(suggestion)}
liRef={onRef}
/>
)
})}
</ul>
)}
</div>
)
}
// Handle the cursor changing in the input field and
// allow the user to type after the string changes.
// Used in modal on homepage.
private changeCursor: React.FocusEventHandler<HTMLInputElement> = e => {
setTimeout(() => {
this.inputFocuses.next()
this.setState({ inputFocused: true })
}, 100)
}
private setSuggestionListElement = (ref: HTMLElement | null): void => {
this.suggestionListElement = ref || undefined
}
private selectSuggestion = (suggestion: Suggestion): void => {
// 🚨 PRIVACY: never provide any private data in { code_search: { suggestion: { type } } }.
eventLogger.log('SearchSuggestionSelected', {
code_search: {
suggestion: {
type: suggestion.type,
url: suggestion.url,
},
},
})
this.props.history.push(suggestion.url)
this.suggestionsHidden.next()
this.setState({ hideSuggestions: true, selectedSuggestion: -1 })
}
private focusInputAndPositionCursorAtEnd(): void {
if (this.inputElement) {
// Focus the input element and set cursor to the end
this.inputElement.focus()
this.inputElement.setSelectionRange(this.inputElement.value.length, this.inputElement.value.length)
}
}
private onInputChange: React.ChangeEventHandler<HTMLInputElement> = event => {
if (!this.hasLoggedFirstInput) {
eventLogger.log('SearchInitiated')
this.hasLoggedFirstInput = true
}
this.inputValues.next(event.currentTarget.value)
}
private onInputBlur: React.FocusEventHandler<HTMLInputElement> = event => {
this.suggestionsHidden.next()
this.setState({ inputFocused: false, loading: false, hideSuggestions: true })
}
private onInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = event => {
event.persist()
this.inputKeyDowns.next(event)
switch (event.key) {
case Key.Escape: {
this.suggestionsHidden.next()
this.setState({ loading: false, hideSuggestions: true, selectedSuggestion: -1 })
break
}
case Key.ArrowDown: {
event.preventDefault()
this.moveSelection(1)
break
}
case Key.ArrowUp: {
event.preventDefault()
this.moveSelection(-1)
break
}
case Key.Enter: {
if (this.state.selectedSuggestion === -1) {
// Submit form and hide suggestions
this.suggestionsHidden.next()
this.setState({ hideSuggestions: true })
break
}
// Select suggestion
event.preventDefault()
if (this.state.suggestions.length === 0) {
break
}
this.selectSuggestion(this.state.suggestions[Math.max(this.state.selectedSuggestion, 0)])
this.setState({ hideSuggestions: true })
break
}
}
}
private moveSelection(steps: number): void {
this.setState({
selectedSuggestion: Math.max(
Math.min(this.state.selectedSuggestion + steps, this.state.suggestions.length - 1),
-1
),
})
}
}

View File

@ -1 +0,0 @@
Components for areas in the app that expose a search input

View File

@ -10,7 +10,7 @@ class ServerAdminWrapper {
private isAuthenicated = false
constructor() {
if (!window.context.sourcegraphDotComMode) {
if (window.context && !window.context.sourcegraphDotComMode) {
authenticatedUser.subscribe(user => {
if (user) {
this.isAuthenicated = true

View File

@ -11,7 +11,7 @@ class TelligentWrapper {
constructor() {
// Never log anything in self-hosted Sourcegraph instances.
if (!window.context.sourcegraphDotComMode) {
if (!window.context || !window.context.sourcegraphDotComMode) {
return
}

View File

@ -2046,6 +2046,11 @@
"@types/events" "*"
"@types/node" "*"
"@types/classnames@^2.2.7":
version "2.2.7"
resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.7.tgz#fb68cc9be8487e6ea5b13700e759bfbab7e0fefd"
integrity sha512-rzOhiQ55WzAiFgXRtitP/ZUT8iVNyllEpylJ5zHzR4vArUvMB39GTk+Zon/uAM0JxEFAWnwsxC2gH8s+tZ3Myg==
"@types/cli-color@^0.3.29":
version "0.3.29"
resolved "https://registry.npmjs.org/@types/cli-color/-/cli-color-0.3.29.tgz#c83a71fe02c8c7e1ccec048dd6a2458d1f6c96ea"
@ -4663,7 +4668,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.3, classnames@^2.2.5:
classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==