diff --git a/components/dashboard/src/App.tsx b/components/dashboard/src/App.tsx index 513b1001692636..a4db7352e6283f 100644 --- a/components/dashboard/src/App.tsx +++ b/components/dashboard/src/App.tsx @@ -4,7 +4,7 @@ * See License.AGPL.txt in the project root for license information. */ -import React, { FunctionComponent, Suspense } from "react"; +import React, { FunctionComponent, Suspense, useEffect } from "react"; import * as GitpodCookie from "@gitpod/gitpod-protocol/lib/util/gitpod-cookie"; import { Login } from "./Login"; import { isGitpodIo } from "./utils"; @@ -13,6 +13,7 @@ import { useAnalyticsTracking } from "./hooks/use-analytics-tracking"; import { AppLoading } from "./app/AppLoading"; import { AppRoutes } from "./app/AppRoutes"; import { useCurrentTeam } from "./teams/teams-context"; +import { useHistory } from "react-router"; const Setup = React.lazy(() => import(/* webpackPrefetch: true */ "./Setup")); @@ -20,6 +21,12 @@ const Setup = React.lazy(() => import(/* webpackPrefetch: true */ "./Setup")); const App: FunctionComponent = () => { const { user, teams, isSetupRequired, loading } = useUserAndTeamsLoader(); const currentOrg = useCurrentTeam(); + const history = useHistory(); + useEffect(() => { + return history.listen((location, action) => { + console.log(location, action); + }); + }, [history]); // Setup analytics/tracking useAnalyticsTracking(); diff --git a/components/dashboard/src/app/AppRoutes.tsx b/components/dashboard/src/app/AppRoutes.tsx index 3a2a3e5a7e4e8c..5cf926240b56e5 100644 --- a/components/dashboard/src/app/AppRoutes.tsx +++ b/components/dashboard/src/app/AppRoutes.tsx @@ -4,46 +4,47 @@ * See License.AGPL.txt in the project root for license information. */ +import { ContextURL, Team, User } from "@gitpod/gitpod-protocol"; import React, { FunctionComponent, useContext, useState } from "react"; -import { ContextURL, User, Team } from "@gitpod/gitpod-protocol"; -import SelectIDEModal from "../user-settings/SelectIDEModal"; -import { StartPage, StartPhase } from "../start/StartPage"; -import { getURLHash, isGitpodIo, isLocalPreview } from "../utils"; -import { shouldSeeWhatsNew, WhatsNew } from "../whatsnew/WhatsNew"; -import { Redirect, Route, Switch } from "react-router"; +import { Redirect, Route, Switch, useLocation } from "react-router"; +import { AppNotifications } from "../AppNotifications"; import Menu from "../menu/Menu"; +import OAuthClientApproval from "../OauthClientApproval"; +import { projectsPathInstallGitHubApp, projectsPathNew } from "../projects/projects.routes"; +import { StartPage, StartPhase } from "../start/StartPage"; import { parseProps } from "../start/StartWorkspace"; -import { AppNotifications } from "../AppNotifications"; -import { AdminRoute } from "./AdminRoute"; -import { StartWorkspaceModal } from "../workspaces/StartWorkspaceModal"; +import SelectIDEModal from "../user-settings/SelectIDEModal"; import { settingsPathAccount, settingsPathBilling, settingsPathIntegrations, settingsPathMain, settingsPathNotifications, + settingsPathPersonalAccessTokenCreate, + settingsPathPersonalAccessTokenEdit, + settingsPathPersonalAccessTokens, settingsPathPlans, settingsPathPreferences, - settingsPathVariables, settingsPathSSHKeys, + settingsPathVariables, usagePathMain, - settingsPathPersonalAccessTokens, - settingsPathPersonalAccessTokenCreate, - settingsPathPersonalAccessTokenEdit, } from "../user-settings/settings.routes"; -import { projectsPathInstallGitHubApp, projectsPathNew } from "../projects/projects.routes"; +import { getURLHash, isGitpodIo, isLocalPreview } from "../utils"; +import { shouldSeeWhatsNew, WhatsNew } from "../whatsnew/WhatsNew"; +import { StartWorkspaceModal } from "../workspaces/StartWorkspaceModal"; import { workspacesPathMain } from "../workspaces/workspaces.routes"; -import { LocalPreviewAlert } from "./LocalPreviewAlert"; -import OAuthClientApproval from "../OauthClientApproval"; +import { AdminRoute } from "./AdminRoute"; import { Blocked } from "./Blocked"; +import { LocalPreviewAlert } from "./LocalPreviewAlert"; // TODO: Can we bundle-split/lazy load these like other pages? import { BlockedRepositories } from "../admin/BlockedRepositories"; import PersonalAccessTokenCreateView from "../user-settings/PersonalAccessTokensCreateView"; +import { CreateWorkspacePage, useNewCreateWorkspacePage } from "../workspaces/CreateWorkspacePage"; import { StartWorkspaceModalContext } from "../workspaces/start-workspace-modal-context"; -import { StartWorkspaceOptions } from "../start/start-workspace-options"; -import { WebsocketClients } from "./WebsocketClients"; import { OrgRequiredRoute } from "./OrgRequiredRoute"; +import { WebsocketClients } from "./WebsocketClients"; +import { StartWorkspaceOptions } from "../start/start-workspace-options"; const Setup = React.lazy(() => import(/* webpackPrefetch: true */ "../Setup")); const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "../workspaces/Workspaces")); @@ -61,7 +62,6 @@ const Preferences = React.lazy(() => import(/* webpackPrefetch: true */ "../user const PersonalAccessTokens = React.lazy( () => import(/* webpackPrefetch: true */ "../user-settings/PersonalAccessTokens"), ); -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")); @@ -97,6 +97,8 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => const hash = getURLHash(); const { startWorkspaceModalProps, setStartWorkspaceModalProps } = useContext(StartWorkspaceModalContext); const [isWhatsNewShown, setWhatsNewShown] = useState(shouldSeeWhatsNew(user)); + const newCreateWsPage = useNewCreateWorkspacePage(); + const location = useLocation(); // Prefix with `/#referrer` will specify an IDE for workspace // We don't need to show IDE preference in this case @@ -105,12 +107,12 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => ); // TODO: Add a Route for this instead of inspecting location manually - if (window.location.pathname.startsWith("/blocked")) { + if (location.pathname.startsWith("/blocked")) { return ; } // TODO: Add a Route for this instead of inspecting location manually - if (window.location.pathname.startsWith("/oauth-approval")) { + if (location.pathname.startsWith("/oauth-approval")) { return ; } @@ -119,7 +121,7 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => } // TODO: Try and encapsulate this in a route for "/" (check for hash in route component, render or redirect accordingly) - const isCreation = window.location.pathname === "/" && hash !== ""; + const isCreation = location.pathname === "/" && hash !== ""; if (isCreation) { if (showUserIdePreference) { return ( @@ -127,19 +129,8 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => setShowUserIdePreference(false)} /> ); - } else if (new URLSearchParams(window.location.search).has("showOptions")) { - const props = StartWorkspaceOptions.parseSearchParams(window.location.search); - return ( - - ); + } else if (new URLSearchParams(location.search).has("showOptions") || newCreateWsPage) { + return ; } else { return ; } @@ -160,6 +151,19 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => return
; } + if (newCreateWsPage && startWorkspaceModalProps) { + const search = StartWorkspaceOptions.toSearchParams({ + ideSettings: { + defaultIde: startWorkspaceModalProps.ide, + useLatestVersion: startWorkspaceModalProps.uselatestIde, + }, + workspaceClass: startWorkspaceModalProps.workspaceClass, + }); + const hash = startWorkspaceModalProps.contextUrl ? "#" + startWorkspaceModalProps.contextUrl : ""; + setStartWorkspaceModalProps(undefined); + return ; + } + return (
@@ -167,8 +171,11 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => {isLocalPreview() && } + - + + + @@ -297,7 +304,7 @@ export const AppRoutes: FunctionComponent = ({ user, teams }) => }} > - {startWorkspaceModalProps && ( + {startWorkspaceModalProps && !newCreateWsPage && ( setStartWorkspaceModalProps(undefined))} diff --git a/components/dashboard/src/contexts/FeatureFlagContext.tsx b/components/dashboard/src/contexts/FeatureFlagContext.tsx index 767bde5c881fec..2157e1db1cc1ba 100644 --- a/components/dashboard/src/contexts/FeatureFlagContext.tsx +++ b/components/dashboard/src/contexts/FeatureFlagContext.tsx @@ -15,6 +15,7 @@ interface FeatureFlagConfig { } const FeatureFlagContext = createContext<{ + startWithOptions: boolean; showUsageView: boolean; isUsageBasedBillingEnabled: boolean; showUseLastSuccessfulPrebuild: boolean; @@ -23,6 +24,7 @@ const FeatureFlagContext = createContext<{ oidcServiceEnabled: boolean; orgGitAuthProviders: boolean; }>({ + startWithOptions: false, showUsageView: false, isUsageBasedBillingEnabled: false, showUseLastSuccessfulPrebuild: false, @@ -37,6 +39,7 @@ const FeatureFlagContextProvider: React.FC = ({ children }) => { const teams = useTeams(); const { project } = useContext(ProjectContext); const team = useCurrentTeam(); + const [startWithOptions, setStartWithOptions] = useState(false); const [showUsageView, setShowUsageView] = useState(false); const [isUsageBasedBillingEnabled, setIsUsageBasedBillingEnabled] = useState(false); const [showUseLastSuccessfulPrebuild, setShowUseLastSuccessfulPrebuild] = useState(false); @@ -49,6 +52,7 @@ const FeatureFlagContextProvider: React.FC = ({ children }) => { if (!user) return; (async () => { const featureFlags: FeatureFlagConfig = { + start_with_options: { defaultValue: false, setter: setStartWithOptions }, usage_view: { defaultValue: false, setter: setShowUsageView }, isUsageBasedBillingEnabled: { defaultValue: false, setter: setIsUsageBasedBillingEnabled }, showUseLastSuccessfulPrebuild: { defaultValue: false, setter: setShowUseLastSuccessfulPrebuild }, @@ -98,6 +102,7 @@ const FeatureFlagContextProvider: React.FC = ({ children }) => { return ( void }) { const { account, onSuccess } = params; - const state = btoa(JSON.stringify({ from: "/reconfigure", next: "/new" })); + const state = btoa(JSON.stringify({ from: "/reconfigure", next: projectsPathNew })); const url = gitpodHostUrl .withApi({ pathname: "/apps/github/reconfigure", diff --git a/components/dashboard/src/projects/Project.tsx b/components/dashboard/src/projects/Project.tsx index d913a2b00517b6..ece0b5859c0aa8 100644 --- a/components/dashboard/src/projects/Project.tsx +++ b/components/dashboard/src/projects/Project.tsx @@ -16,6 +16,7 @@ import NoAccess from "../icons/NoAccess.svg"; import { ReactComponent as Spinner } from "../icons/Spinner.svg"; import { openAuthorizeWindow } from "../provider-utils"; import { getGitpodService, gitpodHostUrl } from "../service/service"; +import { useNewCreateWorkspacePage } from "../workspaces/CreateWorkspacePage"; import { StartWorkspaceModalContext } from "../workspaces/start-workspace-modal-context"; import { prebuildStatusIcon, prebuildStatusLabel } from "./Prebuilds"; import { useCurrentProject } from "./project-context"; @@ -39,6 +40,8 @@ export default function ProjectsPage() { const [showAuthBanner, setShowAuthBanner] = useState<{ host: string } | undefined>(undefined); + const isNewCreateWsPage = useNewCreateWorkspacePage(); + useEffect(() => { // project changed, reset state setBranches([]); @@ -375,15 +378,19 @@ export default function ProjectsPage() { - setStartWorkspaceModalProps({ - contextUrl: branch.url, - allowContextUrlChange: true, - }), - separator: true, - }, + ...(isNewCreateWsPage + ? [] + : [ + { + title: "New Workspace ...", + onClick: () => + setStartWorkspaceModalProps({ + contextUrl: branch.url, + allowContextUrlChange: true, + }), + separator: true, + }, + ]), prebuild?.status === "queued" || prebuild?.status === "building" ? { diff --git a/components/dashboard/src/projects/ProjectListItem.tsx b/components/dashboard/src/projects/ProjectListItem.tsx index e9313446bd2b09..f11505f18ab6bb 100644 --- a/components/dashboard/src/projects/ProjectListItem.tsx +++ b/components/dashboard/src/projects/ProjectListItem.tsx @@ -15,6 +15,7 @@ import { prebuildStatusIcon } from "./Prebuilds"; import { gitpodHostUrl } from "../service/service"; import { useLatestProjectPrebuildQuery } from "../data/prebuilds/latest-project-prebuild-query"; import { StartWorkspaceModalContext } from "../workspaces/start-workspace-modal-context"; +import { useNewCreateWorkspacePage } from "../workspaces/CreateWorkspacePage"; type ProjectListItemProps = { project: Project; @@ -25,6 +26,7 @@ export const ProjectListItem: FunctionComponent = ({ proje const [showRemoveModal, setShowRemoveModal] = useState(false); const { data: prebuild, isLoading } = useLatestProjectPrebuildQuery({ projectId: project.id }); const { setStartWorkspaceModalProps } = useContext(StartWorkspaceModalContext); + const isNewCreateWsPage = useNewCreateWorkspacePage(); return (
@@ -41,15 +43,19 @@ export const ProjectListItem: FunctionComponent = ({ proje href: gitpodHostUrl.withContext(`${project.cloneUrl}`).toString(), separator: true, }, - { - title: "New Workspace ...", - onClick: () => - setStartWorkspaceModalProps({ - contextUrl: project.cloneUrl, - allowContextUrlChange: true, - }), - separator: true, - }, + ...(isNewCreateWsPage + ? [] + : [ + { + title: "New Workspace ...", + onClick: () => + setStartWorkspaceModalProps({ + contextUrl: project.cloneUrl, + allowContextUrlChange: true, + }), + separator: true, + }, + ]), { title: "Remove Project", customFontStyle: diff --git a/components/dashboard/src/projects/Projects.tsx b/components/dashboard/src/projects/Projects.tsx index 4793a614d46515..356f7e928c5fe7 100644 --- a/components/dashboard/src/projects/Projects.tsx +++ b/components/dashboard/src/projects/Projects.tsx @@ -17,6 +17,7 @@ import Alert from "../components/Alert"; import { ProjectListItem } from "./ProjectListItem"; import { SpinnerLoader } from "../components/Loader"; import { useListProjectsQuery } from "../data/projects/list-projects-query"; +import { projectsPathNew } from "./projects.routes"; export default function ProjectsPage() { const history = useHistory(); @@ -24,11 +25,10 @@ export default function ProjectsPage() { const { data, isLoading, isError, refetch } = useListProjectsQuery(); const { isDark } = useContext(ThemeContext); const [searchFilter, setSearchFilter] = useState(); - const newProjectUrl = `/new`; const onNewProject = useCallback(() => { - history.push(newProjectUrl); - }, [history, newProjectUrl]); + history.push(projectsPathNew); + }, [history]); const filteredProjects = useMemo(() => { const filter = (project: Project) => { @@ -80,7 +80,7 @@ export default function ProjectsPage() {

- + {team && ( @@ -135,7 +135,7 @@ export default function ProjectsPage() { key="new-project" className="h-52 border-dashed border-2 border-gray-100 dark:border-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl focus:bg-gitpod-kumquat-light transition ease-in-out group" > - +
New Project
diff --git a/components/dashboard/src/projects/projects.routes.ts b/components/dashboard/src/projects/projects.routes.ts index 43adfa8ea0ca3e..dcf9eda5bfd057 100644 --- a/components/dashboard/src/projects/projects.routes.ts +++ b/components/dashboard/src/projects/projects.routes.ts @@ -11,7 +11,7 @@ export const projectsPathMain = "/projects"; export const projectsPathMainWithParams = [projectsPathMain, ":projectName", ":resourceOrPrebuild?"].join("/"); export const projectsPathInstallGitHubApp = "/install-github-app"; -export const projectsPathNew = "/new"; +export const projectsPathNew = "/projects/new"; export function getProjectTabs(project: Project | undefined): TabEntry[] { if (!project) { diff --git a/components/dashboard/src/start/CreateWorkspace.tsx b/components/dashboard/src/start/CreateWorkspace.tsx index 7432de59f8ad30..3ad05523cd51d3 100644 --- a/components/dashboard/src/start/CreateWorkspace.tsx +++ b/components/dashboard/src/start/CreateWorkspace.tsx @@ -325,7 +325,7 @@ export function CreateWorkspace({ contextUrl }: CreateWorkspaceProps) { ); } -function SelectCostCenterModal(props: { onSelected?: () => void }) { +export function SelectCostCenterModal(props: { onSelected?: () => void }) { return ( {}}>

Choose Billing Organization

@@ -334,7 +334,7 @@ function SelectCostCenterModal(props: { onSelected?: () => void }) { ); } -function LimitReachedModal(p: { children: React.ReactNode }) { +export function LimitReachedModal(p: { children: React.ReactNode }) { const { user } = useContext(UserContext); return ( // TODO: Use title and buttons props @@ -358,7 +358,7 @@ function LimitReachedModal(p: { children: React.ReactNode }) { ); } -function LimitReachedParallelWorkspacesModal() { +export function LimitReachedParallelWorkspacesModal() { return (

@@ -369,7 +369,7 @@ function LimitReachedParallelWorkspacesModal() { ); } -function LimitReachedOutOfHours() { +export function LimitReachedOutOfHours() { return (

@@ -380,7 +380,7 @@ function LimitReachedOutOfHours() { ); } -function RepositoryNotFoundView(p: { error: StartWorkspaceError }) { +export function RepositoryNotFoundView(p: { error: StartWorkspaceError }) { const [statusMessage, setStatusMessage] = useState(); const { host, owner, repoName, userIsOwner, userScopes, lastUpdate } = p.error.data; const repoFullName = owner && repoName ? `${owner}/${repoName}` : ""; diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx new file mode 100644 index 00000000000000..41b3b578313a0a --- /dev/null +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -0,0 +1,346 @@ +/** + * Copyright (c) 2023 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 { ContextURL, GitpodServer, WorkspaceInfo } from "@gitpod/gitpod-protocol"; +import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth"; +import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred"; +import { FunctionComponent, useCallback, useState } from "react"; +import { useHistory, useLocation } from "react-router"; +import Modal from "../components/Modal"; +import RepositoryFinder from "../components/RepositoryFinder"; +import SelectIDEComponent from "../components/SelectIDEComponent"; +import SelectWorkspaceClassComponent from "../components/SelectWorkspaceClassComponent"; +import { UsageLimitReachedModal } from "../components/UsageLimitReachedModal"; +import { openAuthorizeWindow } from "../provider-utils"; +import { getGitpodService, gitpodHostUrl } from "../service/service"; +import { LimitReachedOutOfHours, LimitReachedParallelWorkspacesModal } from "../start/CreateWorkspace"; +import { StartWorkspaceOptions } from "../start/start-workspace-options"; +import { StartWorkspaceError } from "../start/StartPage"; +import { useCurrentUser } from "../user-context"; +import { SelectAccountModal } from "../user-settings/SelectAccountModal"; +import Spinner from "../icons/Spinner.svg"; +import { useFeatureFlags } from "../contexts/FeatureFlagContext"; +import { useCurrentTeam } from "../teams/teams-context"; + +export const useNewCreateWorkspacePage = () => { + const { startWithOptions } = useFeatureFlags(); + const user = useCurrentUser(); + return !!startWithOptions || !!user?.additionalData?.isMigratedToTeamOnlyAttribution; +}; + +export function CreateWorkspacePage() { + const user = useCurrentUser(); + const team = useCurrentTeam(); + const location = useLocation(); + const history = useHistory(); + const props = StartWorkspaceOptions.parseSearchParams(location.search); + + const [useLatestIde, setUseLatestIde] = useState( + props.ideSettings?.useLatestVersion !== undefined + ? props.ideSettings.useLatestVersion + : !!user?.additionalData?.ideSettings?.useLatestVersion, + ); + const [selectedIde, setSelectedIde] = useState( + props.ideSettings?.defaultIde !== undefined + ? props.ideSettings.defaultIde + : user?.additionalData?.ideSettings?.defaultIde, + ); + const [selectedWsClass, setSelectedWsClass] = useState(props.workspaceClass); + const [errorWsClass, setErrorWsClass] = useState(undefined); + const [repo, setRepo] = useState(location.hash.substring(1)); + const onSelectEditorChange = useCallback( + (ide: string, useLatest: boolean) => { + setSelectedIde(ide); + setUseLatestIde(useLatest); + }, + [setSelectedIde, setUseLatestIde], + ); + const [errorIde, setErrorIde] = useState(undefined); + const [creating, setCreating] = useState(false); + + const [existingWorkspaces, setExistingWorkspaces] = useState([]); + const [createError, setCreateError] = useState(undefined); + const [selectAccountError, setSelectAccountError] = useState(undefined); + + const createWorkspace = useCallback( + async (options?: Omit) => { + // add options from search params + const opts = options || {}; + + if (!opts.workspaceClass) { + opts.workspaceClass = selectedWsClass; + } + if (!opts.ideSettings) { + opts.ideSettings = { + defaultIde: selectedIde, + useLatestVersion: useLatestIde, + }; + } + if (!repo) { + return; + } + + try { + setCreating(true); + const result = await getGitpodService().server.createWorkspace({ + contextUrl: repo, + organizationId: team?.id, + ...opts, + }); + if (result.workspaceURL) { + window.location.href = result.workspaceURL; + } else if (result.createdWorkspaceId) { + history.push(`/start/${result.createdWorkspaceId}`); + } else if (result.existingWorkspaces && result.existingWorkspaces.length > 0) { + setExistingWorkspaces(result.existingWorkspaces); + } + } catch (error) { + setCreateError(error); + } finally { + setCreating(false); + } + }, + [history, repo, selectedIde, selectedWsClass, team?.id, useLatestIde], + ); + + const onClickCreate = useCallback(() => createWorkspace(), [createWorkspace]); + + if (SelectAccountPayload.is(selectAccountError)) { + return ( + { + window.location.href = gitpodHostUrl.asAccessControl().toString(); + }} + /> + ); + } + if (existingWorkspaces.length > 0) { + return ( + {}} + /> + ); + } + + return ( +

+
+

New Workspace

+
+ Start a new workspace with the following options. +
+
+
+ +
+
+ {errorIde &&
{errorIde}
} + +
+
+ {errorWsClass &&
{errorWsClass}
} + +
+
+
+ +
+
+ +
+
+
+ ); +} + +function tryAuthorize(host: string, scopes?: string[]): Promise { + const result = new Deferred(); + openAuthorizeWindow({ + host, + scopes, + onSuccess: () => { + result.resolve(); + }, + onError: (error) => { + if (typeof error === "string") { + try { + const payload = JSON.parse(error); + if (SelectAccountPayload.is(payload)) { + result.resolve(payload); + } + } catch (error) { + console.log(error); + } + } + }, + }).catch((error) => { + console.log(error); + }); + return result.promise; +} + +interface StatusMessageProps { + error?: StartWorkspaceError; + setSelectAccountError: (error?: SelectAccountPayload) => void; + createWorkspace: (opts: Omit) => void; +} +const StatusMessage: FunctionComponent = ({ error, setSelectAccountError, createWorkspace }) => { + if (!error) { + return <>; + } + switch (error.code) { + case ErrorCodes.CONTEXT_PARSE_ERROR: + return ( +
+

+ Are you trying to open a Git repository from a self-managed git hoster?{" "} + + Add integration + +

+
+ ); + case ErrorCodes.INVALID_GITPOD_YML: + return ( +
+ +
+ ); + case ErrorCodes.NOT_AUTHENTICATED: + return ( +
+ +
+ ); + case ErrorCodes.PERMISSION_DENIED: + return

Access is not allowed

; + case ErrorCodes.USER_BLOCKED: + window.location.href = "/blocked"; + return <>; + case ErrorCodes.NOT_FOUND: + return

{error.message}

; + case ErrorCodes.TOO_MANY_RUNNING_WORKSPACES: + return ; + case ErrorCodes.NOT_ENOUGH_CREDIT: + return ; + case ErrorCodes.INVALID_COST_CENTER: + return ( +
+

+ The organization {error.data} is not valid. +

+
+ ); + case ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED: + return ; + case ErrorCodes.PROJECT_REQUIRED: + return ( +

+ + Learn more about projects + +

+ ); + default: + return

Unknown Error: {JSON.stringify(error, null, 2)}

; + } +}; + +interface ExistingWorkspaceModalProps { + existingWorkspaces: WorkspaceInfo[]; + onClose: () => void; + createWorkspace: (opts: Omit) => void; +} + +const ExistingWorkspaceModal: FunctionComponent = ({ + existingWorkspaces, + onClose, + createWorkspace, +}) => { + return ( + +

Running Workspaces

+
+

+ You already have running workspaces with the same context. You can open an existing one or open a + new workspace. +

+ <> + {existingWorkspaces.map((w) => { + const normalizedContextUrl = + ContextURL.getNormalizedURL(w.workspace)?.toString() || "undefined"; + return ( + +
+

+ {w.workspace.id} +

+

+ {normalizedContextUrl} +

+
+
+ ); + })} + +
+
+ +
+
+ ); +}; diff --git a/components/dashboard/src/workspaces/start-workspace-modal-context.tsx b/components/dashboard/src/workspaces/start-workspace-modal-context.tsx index 339554c00727db..07aeaa857df4ef 100644 --- a/components/dashboard/src/workspaces/start-workspace-modal-context.tsx +++ b/components/dashboard/src/workspaces/start-workspace-modal-context.tsx @@ -5,6 +5,8 @@ */ import React, { createContext, useEffect, useState } from "react"; +import { useHistory } from "react-router"; +import { useNewCreateWorkspacePage } from "./CreateWorkspacePage"; import { StartWorkspaceModalProps } from "./StartWorkspaceModal"; export const StartWorkspaceModalContext = createContext<{ @@ -18,11 +20,17 @@ export const StartWorkspaceModalContextProvider: React.FC = ({ children }) => { const [startWorkspaceModalProps, setStartWorkspaceModalProps] = useState( undefined, ); + const isNewCreateWorkspacePage = useNewCreateWorkspacePage(); + const history = useHistory(); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key === "o") { event.preventDefault(); + if (isNewCreateWorkspacePage) { + history.push("/new"); + return; + } setStartWorkspaceModalProps({ onClose: () => setStartWorkspaceModalProps(undefined), }); @@ -32,7 +40,7 @@ export const StartWorkspaceModalContextProvider: React.FC = ({ children }) => { return () => { window.removeEventListener("keydown", onKeyDown); }; - }, []); + }, [history, isNewCreateWorkspacePage]); return ( diff --git a/components/server/src/gitlab/gitlab-file-provider.spec.ts b/components/server/src/gitlab/gitlab-file-provider.spec.ts index cab81892b0f4b1..0a98af188fbed6 100644 --- a/components/server/src/gitlab/gitlab-file-provider.spec.ts +++ b/components/server/src/gitlab/gitlab-file-provider.spec.ts @@ -80,7 +80,7 @@ class TestFileProvider { path: `test_project_${i}`, namespace_id: 57982169, initialize_with_readme: true, - description: "generated project to test pagination in gitpod.io/new", + description: "generated project to test pagination", }), ); if (GitLab.ApiError.is(project)) {