/** * Copyright (c) 2021 Gitpod GmbH. All rights reserved. * Licensed under the GNU Affero General Public License (AGPL). * See License-AGPL.txt in the project root for license information. */ import React, { Suspense, useContext, useEffect, useState } from "react"; import Menu from "./Menu"; import { Redirect, Route, Switch } from "react-router"; import { Login } from "./Login"; import { UserContext } from "./user-context"; import { getSelectedTeamSlug, TeamsContext } from "./teams/teams-context"; import { ThemeContext } from "./theme-context"; import { getGitpodService } from "./service/service"; import { shouldSeeWhatsNew, WhatsNew } from "./whatsnew/WhatsNew"; import gitpodIcon from "./icons/gitpod.svg"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { useHistory } from "react-router-dom"; import { trackButtonOrAnchor, trackPathChange, trackLocation } from "./Analytics"; import { ContextURL, User } from "@gitpod/gitpod-protocol"; import * as GitpodCookie from "@gitpod/gitpod-protocol/lib/util/gitpod-cookie"; import { Experiment } from "./experiments"; import { workspacesPathMain } from "./workspaces/workspaces.routes"; import { settingsPathAccount, settingsPathBilling, settingsPathIntegrations, settingsPathMain, settingsPathNotifications, settingsPathPlans, settingsPathPreferences, settingsPathTeams, settingsPathTeamsJoin, settingsPathTeamsNew, settingsPathVariables, settingsPathSSHKeys, usagePathMain, settingsPathPersonalAccessTokens, } from "./settings/settings.routes"; import { projectsPathInstallGitHubApp, projectsPathMain, projectsPathMainWithParams, projectsPathNew, } from "./projects/projects.routes"; import { refreshSearchData } from "./components/RepositoryFinder"; import { StartWorkspaceModal } from "./workspaces/StartWorkspaceModal"; import { parseProps } from "./start/StartWorkspace"; import SelectIDEModal from "./settings/SelectIDEModal"; import { StartPage, StartPhase } from "./start/StartPage"; import { isGitpodIo, isLocalPreview } from "./utils"; import Alert from "./components/Alert"; import { BlockedRepositories } from "./admin/BlockedRepositories"; import { AppNotifications } from "./AppNotifications"; import { publicApiTeamsToProtocol, teamsService } from "./service/public-api"; import { FeatureFlagContext } from "./contexts/FeatureFlagContext"; import PersonalAccessTokens from "./settings/PersonalAccessTokens"; const Setup = React.lazy(() => import(/* webpackPrefetch: true */ "./Setup")); const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "./workspaces/Workspaces")); const Account = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Account")); const Notifications = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Notifications")); const Billing = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Billing")); const Plans = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Plans")); const Teams = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Teams")); const EnvironmentVariables = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/EnvironmentVariables")); const SSHKeys = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/SSHKeys")); const Integrations = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Integrations")); const Preferences = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Preferences")); const Open = React.lazy(() => import(/* webpackPrefetch: true */ "./start/Open")); const StartWorkspace = React.lazy(() => import(/* webpackPrefetch: true */ "./start/StartWorkspace")); const CreateWorkspace = React.lazy(() => import(/* webpackPrefetch: true */ "./start/CreateWorkspace")); const NewTeam = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/NewTeam")); const JoinTeam = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/JoinTeam")); const Members = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/Members")); const TeamSettings = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamSettings")); const TeamBilling = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamBilling")); const TeamUsage = React.lazy(() => import(/* webpackPrefetch: true */ "./teams/TeamUsage")); const NewProject = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/NewProject")); const Projects = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/Projects")); const Project = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/Project")); const Events = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/Events")); const ProjectSettings = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/ProjectSettings")); const ProjectVariables = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/ProjectVariables")); const Prebuilds = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/Prebuilds")); const Prebuild = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/Prebuild")); const InstallGitHubApp = React.lazy(() => import(/* webpackPrefetch: true */ "./projects/InstallGitHubApp")); const FromReferrer = React.lazy(() => import(/* webpackPrefetch: true */ "./FromReferrer")); const UserSearch = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/UserSearch")); const WorkspacesSearch = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/WorkspacesSearch")); const AdminSettings = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/Settings")); const ProjectsSearch = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/ProjectsSearch")); const TeamsSearch = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/TeamsSearch")); const OAuthClientApproval = React.lazy(() => import(/* webpackPrefetch: true */ "./OauthClientApproval")); const License = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/License")); const Usage = React.lazy(() => import(/* webpackPrefetch: true */ "./Usage")); function Loading() { return <>; } function isWebsiteSlug(pathName: string) { const slugs = [ "about", "blog", "careers", "cde", "changelog", "chat", "code-of-conduct", "contact", "docs", "features", "for", "gitpod-vs-github-codespaces", "imprint", "media-kit", "memes", "pricing", "privacy", "security", "screencasts", "self-hosted", "support", "terms", "values", ]; return slugs.some((slug) => pathName.startsWith("/" + slug + "/") || pathName === "/" + slug); } // A wrapper for that redirects to the workspaces screen if the user isn't a admin. // This wrapper only accepts the component property function AdminRoute({ component }: any) { const { user } = useContext(UserContext); return ( user?.rolesOrPermissions?.includes("admin") ? ( ) : ( ) } /> ); } export function getURLHash() { return window.location.hash.replace(/^[#/]+/, ""); } function App() { const { user, setUser, refreshUserBillingMode } = useContext(UserContext); const { teams, setTeams } = useContext(TeamsContext); const { setIsDark } = useContext(ThemeContext); const { usePublicApiTeamsService } = useContext(FeatureFlagContext); const [loading, setLoading] = useState(true); const [isWhatsNewShown, setWhatsNewShown] = useState(false); const [showUserIdePreference, setShowUserIdePreference] = useState(false); const [isSetupRequired, setSetupRequired] = useState(false); const history = useHistory(); useEffect(() => { (async () => { var user: User | undefined; try { user = await getGitpodService().server.getLoggedInUser(); setUser(user); const teams = usePublicApiTeamsService ? publicApiTeamsToProtocol((await teamsService.listTeams({})).teams) : await getGitpodService().server.getTeams(); { // if a team was selected previously and we call the root URL (e.g. "gitpod.io"), // let's continue with the team page const hash = getURLHash(); const isRoot = window.location.pathname === "/" && hash === ""; if (isRoot) { try { const teamSlug = getSelectedTeamSlug(); if (teams.some((t) => t.slug === teamSlug)) { history.push(`/t/${teamSlug}`); } } catch {} } } setTeams(teams); } catch (error) { console.error(error); if (error && "code" in error) { if (error.code === ErrorCodes.SETUP_REQUIRED) { setSetupRequired(true); } } } finally { trackLocation(!!user); } setLoading(false); (window as any)._gp.path = window.location.pathname; //store current path to have access to previous when path changes })(); }, []); useEffect(() => { const updateTheme = () => { const isDark = localStorage.theme === "dark" || (localStorage.theme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches); setIsDark(isDark); }; updateTheme(); const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); if (mediaQuery instanceof EventTarget) { mediaQuery.addEventListener("change", updateTheme); } else { // backward compatibility for Safari < 14 (mediaQuery as MediaQueryList).addListener(updateTheme); } window.addEventListener("storage", updateTheme); return function cleanup() { if (mediaQuery instanceof EventTarget) { mediaQuery.removeEventListener("change", updateTheme); } else { // backward compatibility for Safari < 14 (mediaQuery as MediaQueryList).removeListener(updateTheme); } window.removeEventListener("storage", updateTheme); }; }, []); // listen and notify Segment of client-side path updates useEffect(() => { if (isGitpodIo()) { // Choose which experiments to run for this session/user Experiment.set(Experiment.seed(true)); } }, []); useEffect(() => { return history.listen((location: any) => { const path = window.location.pathname; trackPathChange({ prev: (window as any)._gp.path, path: path, }); (window as any)._gp.path = path; }); }, [history]); useEffect(() => { const handleButtonOrAnchorTracking = (props: MouseEvent) => { var curr = props.target as HTMLElement; //check if current target or any ancestor up to document is button or anchor while (!(curr instanceof Document)) { if ( curr instanceof HTMLButtonElement || curr instanceof HTMLAnchorElement || (curr instanceof HTMLDivElement && curr.onclick) ) { trackButtonOrAnchor(curr); break; //finding first ancestor is sufficient } curr = curr.parentNode as HTMLElement; } }; window.addEventListener("click", handleButtonOrAnchorTracking, true); return () => window.removeEventListener("click", handleButtonOrAnchorTracking, true); }, []); useEffect(() => { if (user) { refreshSearchData("", user); } }, [user]); useEffect(() => { if (!teams) { return; } // Refresh billing mode (side effect on other components per UserContext!) refreshUserBillingMode(); }, [teams]); // redirect to website for any website slugs if (isGitpodIo() && isWebsiteSlug(window.location.pathname)) { window.location.host = "www.gitpod.io"; return
; } if (isGitpodIo() && window.location.pathname === "/" && window.location.hash === "" && !loading && !user) { if (!GitpodCookie.isPresent(document.cookie)) { window.location.href = `https://www.gitpod.io`; return
; } else { // explicitly render the Login page when the session is out-of-sync with the Gitpod cookie return ; } } if (loading) { return ; } if (isSetupRequired) { return ( }> ); } if (!user) { return ; } if (window.location.pathname.startsWith("/blocked")) { return (
Gitpod's logo

Your account has been blocked.

Please contact support if you think this is an error. See also{" "} terms of service .

); } const shouldWhatsNewShown = shouldSeeWhatsNew(user); if (shouldWhatsNewShown !== isWhatsNewShown) { setWhatsNewShown(shouldWhatsNewShown); } if (window.location.pathname.startsWith("/oauth-approval")) { return ( }> ); } window.addEventListener( "hashchange", () => { // Refresh on hash change if the path is '/' (new context URL) if (window.location.pathname === "/") { window.location.reload(); } }, false, ); let toRender: React.ReactElement = (
{isLocalPreview() && (
You are using a local preview installation, intended for exploring the product on a single machine without requiring a Kubernetes cluster.{" "} Request a community license {" "} or{" "} contact sales {" "} to get a professional license for running Gitpod in production.
)}

Oh, no! Something went wrong!

{decodeURIComponent(getURLHash())}

{ const { resourceOrPrebuild } = match.params; switch (resourceOrPrebuild) { case "events": return ; case "prebuilds": return ; case "settings": return ; case "variables": return ; default: return resourceOrPrebuild ? : ; } }} /> {(teams || []).map((team) => ( { const { maybeProject, resourceOrPrebuild } = match.params; switch (maybeProject) { case "projects": return ; case "workspaces": return ; case "members": return ; case "settings": return ; case "billing": return ; case "usage": return ; default: break; } switch (resourceOrPrebuild) { case "events": return ; case "prebuilds": return ; case "settings": return ; case "variables": return ; default: return resourceOrPrebuild ? : ; } }} /> ))} { return isGitpodIo() ? ( // delegate to our website to handle the request (window.location.host = "www.gitpod.io") ) : (

404

Page not found.

); }} >
); const hash = getURLHash(); if (/^(https:\/\/)?github\.dev\//i.test(hash)) { window.location.hash = hash.replace(/^(https:\/\/)?github\.dev\//i, "https://github.com/"); return
; } else if (/^([^\/]+?=[^\/]*?|prebuild)\/(https:\/\/)?github\.dev\//i.test(hash)) { window.location.hash = hash.replace( /^([^\/]+?=[^\/]*?|prebuild)\/(https:\/\/)?github\.dev\//i, "$1/https://github.com/", ); return
; } // Prefix with `/#referrer` will specify an IDE for workspace // We don't need to show IDE preference in this case const shouldUserIdePreferenceShown = User.isOnboardingUser(user) && !hash.startsWith(ContextURL.REFERRER_PREFIX); if (shouldUserIdePreferenceShown !== showUserIdePreference) { setShowUserIdePreference(shouldUserIdePreferenceShown); } const isCreation = window.location.pathname === "/" && hash !== ""; const isWsStart = /\/start\/?/.test(window.location.pathname) && hash !== ""; if (isWhatsNewShown) { toRender = setWhatsNewShown(false)} />; } else if (isCreation) { if (showUserIdePreference) { toRender = ( setShowUserIdePreference(false)} /> ); } else { toRender = ; } } else if (isWsStart) { toRender = ; } else if (/^(github|gitlab)\.com\/.+?/i.test(window.location.pathname)) { let url = new URL(window.location.href); url.hash = url.pathname; url.pathname = "/"; window.location.replace(url); return
; } return }>{toRender}; } export default App;