diff --git a/components/dashboard/package.json b/components/dashboard/package.json index 85e18012274d84..964659327f970b 100644 --- a/components/dashboard/package.json +++ b/components/dashboard/package.json @@ -9,6 +9,7 @@ "js-cookie": "^3.0.1", "moment": "^2.29.1", "monaco-editor": "^0.25.2", + "query-string": "^7.1.1", "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", diff --git a/components/dashboard/src/App.tsx b/components/dashboard/src/App.tsx index 7c0cf3aa5857d8..348e39944fcb7e 100644 --- a/components/dashboard/src/App.tsx +++ b/components/dashboard/src/App.tsx @@ -27,6 +27,7 @@ import { settingsPathAccount, settingsPathIntegrations, settingsPathMain, settin import { projectsPathInstallGitHubApp, projectsPathMain, projectsPathMainWithParams, projectsPathNew } from './projects/projects.routes'; import { refreshSearchData } from './components/RepositoryFinder'; import { StartWorkspaceModal } from './workspaces/StartWorkspaceModal'; +import { parseProps } from './start/StartWorkspace'; const Setup = React.lazy(() => import(/* webpackPrefetch: true */ './Setup')); const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ './workspaces/Workspaces')); @@ -412,7 +413,7 @@ function App() { } else if (isCreation) { toRender = ; } else if (isWsStart) { - toRender = ; + toRender = ; } else if (/^(github|gitlab)\.com\/.+?/i.test(window.location.pathname)) { let url = new URL(window.location.href) url.hash = url.pathname diff --git a/components/dashboard/src/start/CreateWorkspace.tsx b/components/dashboard/src/start/CreateWorkspace.tsx index 072c1cf08a6ab9..b8fc5ff75a4956 100644 --- a/components/dashboard/src/start/CreateWorkspace.tsx +++ b/components/dashboard/src/start/CreateWorkspace.tsx @@ -12,7 +12,7 @@ import Modal from "../components/Modal"; import { getGitpodService, gitpodHostUrl } from "../service/service"; import { UserContext } from "../user-context"; import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage"; -import StartWorkspace from "./StartWorkspace"; +import StartWorkspace, { parseProps } from "./StartWorkspace"; import { openAuthorizeWindow } from "../provider-utils"; import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth"; import { SelectAccountModal } from "../settings/SelectAccountModal"; @@ -154,7 +154,7 @@ export default class CreateWorkspace extends React.Component; + return ; } else if (result?.existingWorkspaces) { diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx index 7739818f192e65..79f1892bc1311c 100644 --- a/components/dashboard/src/start/StartWorkspace.tsx +++ b/components/dashboard/src/start/StartWorkspace.tsx @@ -8,6 +8,7 @@ import { ContextURL, DisposableCollection, GitpodServer, RateLimiterError, Start import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import EventEmitter from "events"; +import * as queryString from "query-string"; import React, { Suspense, useEffect } from "react"; import { v4 } from 'uuid'; import Arrow from "../components/Arrow"; @@ -21,10 +22,54 @@ const sessionId = v4(); const WorkspaceLogs = React.lazy(() => import('../components/WorkspaceLogs')); export interface StartWorkspaceProps { - workspaceId: string; + workspaceId: string, + runsInIFrame: boolean, + /** + * This flag is used to break the autostart-cycle explained in https://github.com/gitpod-io/gitpod/issues/8043 + */ + dontAutostart: boolean, +} + +export function parseProps(workspaceId: string, search?: string): StartWorkspaceProps { + const params = parseParameters(search); + const runsInIFrame = window.top !== window.self; + return { + workspaceId, + runsInIFrame: window.top !== window.self, + // Either: + // - not_found: we were sent back from a workspace cluster/IDE URL where we expected a workspace to be running but it wasn't because either: + // - this is a (very) old tab and the workspace already timed out + // - due to a start error our workspace terminated very quickly between: + // a) us being redirected to that IDEUrl (based on the first ws-manager update) and + // b) our requests being validated by ws-proxy + // - runsInIFrame (IDE case): + // - we assume the workspace has already been started for us + // - we don't know it's instanceId + dontAutostart: params.notFound || runsInIFrame, + } +} + +function parseParameters(search?: string): { notFound?: boolean } { + try { + if (search === undefined) { + return {}; + } + const params = queryString.parse(search, {parseBooleans: true}); + const notFound = !!(params && params["not_found"]); + return { + notFound, + }; + } catch (err) { + console.error("/start: error parsing search params", err); + return {}; + } } export interface StartWorkspaceState { + /** + * This is set to the istanceId we started (think we started on). + * We only receive updates for this particular instance, or none if not set. + */ startedInstanceId?: string; workspaceInstance?: WorkspaceInstance; workspace?: Workspace; @@ -34,8 +79,8 @@ export interface StartWorkspaceState { link: string label: string clientID?: string - } - ideOptions?: IDEOptions + }; + ideOptions?: IDEOptions; } export default class StartWorkspace extends React.Component { @@ -47,7 +92,7 @@ export default class StartWorkspace extends React.Component { if (event.data.type === 'setState' && 'state' in event.data && typeof event.data['state'] === 'object') { @@ -75,8 +120,15 @@ export default class StartWorkspace extends React.Component this.setState({ ideOptions })) + // query IDE options so we can show them if necessary once the workspace is running + getGitpodService().server.getIDEOptions().then(ideOptions => this.setState({ ideOptions })); } componentWillUnmount() { @@ -130,14 +182,13 @@ export default class StartWorkspace extends React.Component ({ + workspace: info.workspace, + startedInstanceId: s.startedInstanceId || instance.id, // note: here's a potential mismatch between startedInstanceId and instance.id. TODO(gpl) How to handle this? + })); + this.onInstanceUpdate(instance); } } catch (error) { console.error(error); @@ -197,20 +261,35 @@ export default class StartWorkspace extends React.ComponentPreparing workspace …

; const contextURL = ContextURL.getNormalizedURL(this.state.workspace)?.toString(); @@ -317,7 +392,29 @@ export default class StartWorkspace extends React.ComponentOpening Workspace …

; + + if (this.props.dontAutostart) { + // hide the progress bar, as we're already running + phase = undefined; + title = 'Running'; + + // in case we dontAutostart the IDE we have to provide controls to do so + statusMessage =
+
+
 
+
+

{this.state.workspaceInstance.workspaceId}

+

{contextURL}

+
+
+ +
; + } else { + statusMessage =

Opening Workspace …

; + } } else { phase = StartPhase.IdeReady; const openLink = this.state.desktopIde.link; diff --git a/components/supervisor/frontend/src/index.ts b/components/supervisor/frontend/src/index.ts index ffa333f2c497d3..d96ba1908455ac 100644 --- a/components/supervisor/frontend/src/index.ts +++ b/components/supervisor/frontend/src/index.ts @@ -106,7 +106,6 @@ const toStop = new DisposableCollection(); //#region current-frame let current: HTMLElement = loading.frame; - let stopped = false; let desktopRedirected = false; const nextFrame = () => { const instance = gitpodServiceClient.info.latestInstance; @@ -142,19 +141,6 @@ const toStop = new DisposableCollection(); return document.body; } } - if (instance.status.phase === 'stopped') { - stopped = true; - } - if (stopped && ( - instance.status.phase === 'preparing' || - instance.status.phase === 'pending' || - instance.status.phase === 'creating' || - instance.status.phase === 'initializing')) { - // reload the page if the workspace was restarted to ensure: - // - graceful reconnection of IDEs - // - new owner token is set - window.location.href = startUrl.toString(); - } } return loading.frame; } diff --git a/components/ws-proxy/pkg/proxy/routes.go b/components/ws-proxy/pkg/proxy/routes.go index 0d576258929f1c..1ea27970246a3f 100644 --- a/components/ws-proxy/pkg/proxy/routes.go +++ b/components/ws-proxy/pkg/proxy/routes.go @@ -550,7 +550,7 @@ func workspaceMustExistHandler(config *Config, infoProvider WorkspaceInfoProvide info := infoProvider.WorkspaceInfo(coords.ID) if info == nil { log.WithFields(log.OWI("", coords.ID, "")).Info("no workspace info found - redirecting to start") - redirectURL := fmt.Sprintf("%s://%s/start/#%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName, coords.ID) + redirectURL := fmt.Sprintf("%s://%s/start/?not_found=true#%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName, coords.ID) http.Redirect(resp, req, redirectURL, http.StatusFound) return } diff --git a/components/ws-proxy/pkg/proxy/routes_test.go b/components/ws-proxy/pkg/proxy/routes_test.go index 763c4f5e80a3fc..f506c21baae6e7 100644 --- a/components/ws-proxy/pkg/proxy/routes_test.go +++ b/components/ws-proxy/pkg/proxy/routes_test.go @@ -414,10 +414,10 @@ func TestRoutes(t *testing.T) { Status: http.StatusFound, Header: http.Header{ "Content-Type": {"text/html; charset=utf-8"}, - "Location": {"https://test-domain.com/start/#blabla-smelt-9ba20cc1"}, + "Location": {"https://test-domain.com/start/?not_found=true#blabla-smelt-9ba20cc1"}, "Vary": {"Accept-Encoding"}, }, - Body: ("Found.\n\n"), + Body: ("Found.\n\n"), }, }, { @@ -429,10 +429,10 @@ func TestRoutes(t *testing.T) { Status: http.StatusFound, Header: http.Header{ "Content-Type": {"text/html; charset=utf-8"}, - "Location": {"https://test-domain.com/start/#blabla-smelt-9ba20cc1"}, + "Location": {"https://test-domain.com/start/?not_found=true#blabla-smelt-9ba20cc1"}, "Vary": {"Accept-Encoding"}, }, - Body: ("Found.\n\n"), + Body: ("Found.\n\n"), }, }, { diff --git a/yarn.lock b/yarn.lock index 5e395feecdcd9b..6faa6a3cf4591f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14397,6 +14397,16 @@ query-string@^6.13.3: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" +query-string@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1" + integrity sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"