Skip to content

Commit 803baa6

Browse files
committed
[dashboard] Improve new workspace start experience ⚡
1 parent 9a9ff73 commit 803baa6

File tree

4 files changed

+86
-36
lines changed

4 files changed

+86
-36
lines changed

components/dashboard/src/start/CreateWorkspace.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { Suspense } from "react";
22
import { CreateWorkspaceMode, WorkspaceCreationResult } from "@gitpod/gitpod-protocol";
33
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
44
import Modal from "../components/Modal";
5-
import { getGitpodService } from "../service/service";
5+
import { getGitpodService, gitpodHostUrl } from "../service/service";
66
import { StartPage, StartPhase } from "./StartPage";
77
import StartWorkspace from "./StartWorkspace";
88

@@ -15,6 +15,7 @@ export interface CreateWorkspaceProps {
1515
export interface CreateWorkspaceState {
1616
result?: WorkspaceCreationResult;
1717
error?: CreateWorkspaceError;
18+
stillParsing: boolean;
1819
}
1920

2021
export interface CreateWorkspaceError {
@@ -27,13 +28,23 @@ export class CreateWorkspace extends React.Component<CreateWorkspaceProps, Creat
2728

2829
constructor(props: CreateWorkspaceProps) {
2930
super(props);
31+
this.state = { stillParsing: true };
3032
}
3133

3234
componentDidMount() {
3335
this.createWorkspace();
3436
}
3537

3638
async createWorkspace(mode = CreateWorkspaceMode.SelectIfRunning) {
39+
// Invalidate any previous result.
40+
this.setState({
41+
result: undefined,
42+
stillParsing: true,
43+
});
44+
45+
// We assume anything longer than 3 seconds is no longer just parsing the context URL (i.e. it's now creating a workspace).
46+
let timeout = setTimeout(() => this.setState({ stillParsing: false }), 3000);
47+
3748
try {
3849
const result = await getGitpodService().server.createWorkspace({
3950
contextUrl: this.props.contextUrl,
@@ -43,16 +54,24 @@ export class CreateWorkspace extends React.Component<CreateWorkspaceProps, Creat
4354
window.location.href = result.workspaceURL;
4455
return;
4556
}
46-
this.setState({ result });
57+
clearTimeout(timeout);
58+
this.setState({
59+
result,
60+
stillParsing: false,
61+
});
4762
} catch (error) {
48-
this.setState({ error });
63+
clearTimeout(timeout);
64+
this.setState({
65+
error,
66+
stillParsing: false,
67+
});
4968
}
5069
}
5170

5271
render() {
5372
const { contextUrl } = this.props;
5473
let phase = StartPhase.Checking;
55-
let statusMessage = <p className="text-base text-gray-400">Checking Context …</p>;
74+
let statusMessage = <p className="text-base text-gray-400">{this.state.stillParsing ? 'Parsing context …' : 'Preparing workspace …'}</p>;
5675
let logsView = undefined;
5776

5877
const error = this.state?.error;
@@ -106,7 +125,6 @@ export class CreateWorkspace extends React.Component<CreateWorkspaceProps, Creat
106125
}
107126

108127
else if (result?.runningWorkspacePrebuild) {
109-
phase = StartPhase.Building;
110128
statusMessage = <p className="text-base text-gray-400">⚡Prebuild in progress</p>;
111129
logsView = <Suspense fallback={<div className="m-6 p-4 h-60 w-11/12 lg:w-3/5 flex-shrink-0 rounded-lg" style={{ color: '#8E8787', background: '#ECE7E5' }}>Loading...</div>}>
112130
<WorkspaceLogs />
@@ -116,16 +134,16 @@ export class CreateWorkspace extends React.Component<CreateWorkspaceProps, Creat
116134
return <StartPage phase={phase} error={!!error}>
117135
{statusMessage}
118136
{logsView}
119-
{error && <>
120-
<button className="mt-8">Go back to dashboard</button>
121-
<p className="mt-10 text-base text-gray-400 flex space-x-2">
137+
{error && <div>
138+
<a href={gitpodHostUrl.asDashboard().toString()}><button className="mt-8 px-4 py-2 text-gray-500 bg-white font-semibold border-gray-500">Go back to dashboard</button></a>
139+
<p className="mt-14 text-base text-gray-400 flex space-x-2">
122140
<a href="https://www.gitpod.io/docs/">Docs</a>
123141
<span></span>
124142
<a href="https://status.gitpod.io/">Status</a>
125143
<span></span>
126144
<a href="https://www.gitpod.io/blog/">Blog</a>
127145
</p>
128-
</>}
146+
</div>}
129147
</StartPage>;
130148
}
131149

components/dashboard/src/start/StartPage.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
export enum StartPhase {
22
Checking = 0,
3-
Building = 1,
4-
Preparing = 2,
3+
Preparing = 1,
4+
Creating = 2,
55
Starting = 3,
66
Running = 4,
7+
Stopping = 5,
8+
Stopped = 6,
79
};
810

911
function ProgressBar(props: { phase: number, error: boolean }) {
12+
const { phase, error } = props;
1013
return <div className="flex mt-4 mb-6">
1114
{[1, 2, 3].map(i => {
1215
let classes = 'h-2 w-10 mx-1 my-2 rounded-full';
13-
if (i < props.phase) {
16+
if (i < phase) {
1417
// Already passed this phase successfully
1518
classes += ' bg-green-400';
16-
} else if (i > props.phase) {
19+
} else if (i > phase) {
1720
// Haven't reached this phase yet
1821
classes += ' bg-gray-200';
19-
} else if (props.error) {
22+
} else if (error) {
2023
// This phase has failed
2124
classes += ' bg-red';
2225
} else {
@@ -36,32 +39,40 @@ export interface StartPageProps {
3639

3740
export function StartPage(props: StartPageProps) {
3841
let title = "";
39-
switch (props.phase) {
42+
const { phase, error } = props;
43+
switch (phase) {
4044
case StartPhase.Checking:
41-
if (props.error) {
45+
title = "Checking";
46+
if (error) {
4247
// Pre-check error
4348
title = "Oh, no! Something went wrong!1";
4449
}
4550
break;
46-
case StartPhase.Building:
47-
title = "Building";
48-
break;
4951
case StartPhase.Preparing:
5052
title = "Preparing";
5153
break;
54+
case StartPhase.Creating:
55+
title = "Creating";
56+
break;
5257
case StartPhase.Starting:
5358
title = "Starting";
5459
break;
5560
case StartPhase.Running:
5661
title = "Starting";
5762
break;
63+
case StartPhase.Stopping:
64+
title = "Stopping";
65+
break;
66+
case StartPhase.Stopped:
67+
title = "Stopped";
68+
break;
5869
}
5970
return <div className="w-screen h-screen bg-white align-middle">
6071
<div className="flex flex-col mx-auto items-center h-screen">
6172
<div className="h-1/3"></div>
62-
<img src="/gitpod.svg" className="h-16 flex-shrink-0" />
73+
<img src="/gitpod.svg" className={`h-16 flex-shrink-0 ${(error || phase === StartPhase.Stopped) ? '' : 'animate-bounce'}`} />
6374
<h3 className="mt-8 text-xl">{title}</h3>
64-
<ProgressBar phase={props.phase} error={!!props.error} />
75+
{(phase < StartPhase.Stopping) && <ProgressBar phase={phase} error={!!error} />}
6576
{props.children}
6677
</div>
6778
</div>;

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from "react";
22
import { DisposableCollection, WorkspaceInstance } from "@gitpod/gitpod-protocol";
3-
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
4-
import { getGitpodService } from "../service/service";
3+
import { getGitpodService, gitpodHostUrl } from "../service/service";
54
import { StartPage, StartPhase } from "./StartPage";
65

76
export interface StartWorkspaceProps {
@@ -117,7 +116,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
117116

118117
async ensureWorkspaceAuth(instanceID: string) {
119118
if (!document.cookie.includes(`${instanceID}_owner_`)) {
120-
const authURL = new GitpodHostUrl(window.location.toString()).asWorkspaceAuth(instanceID);
119+
const authURL = gitpodHostUrl.asWorkspaceAuth(instanceID);
121120
const response = await fetch(authURL.toString());
122121
if (response.redirected) {
123122
this.redirectTo(response.url);
@@ -144,8 +143,8 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
144143
}
145144

146145
render() {
147-
let phase = StartPhase.Checking;
148-
let statusMessage = undefined;
146+
let phase = StartPhase.Preparing;
147+
let statusMessage = <p className="text-base text-gray-400">Preparing workspace …</p>;
149148

150149
switch (this.state?.workspaceInstance?.status.phase) {
151150
// unknown indicates an issue within the system in that it cannot determine the actual phase of
@@ -156,31 +155,31 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
156155
// Preparing means that we haven't actually started the workspace instance just yet, but rather
157156
// are still preparing for launch. This means we're building the Docker image for the workspace.
158157
case "preparing":
159-
phase = StartPhase.Building;
160-
statusMessage = <p className="text-base text-gray-400">Building Image</p>;
158+
phase = StartPhase.Preparing;
159+
statusMessage = <p className="text-base text-gray-400">Building image</p>;
161160
break;
162161

163162
// Pending means the workspace does not yet consume resources in the cluster, but rather is looking for
164163
// some space within the cluster. If for example the cluster needs to scale up to accomodate the
165164
// workspace, the workspace will be in Pending state until that happened.
166165
case "pending":
167166
phase = StartPhase.Preparing;
168-
statusMessage = <p className="text-base text-gray-400">Allocating Resources</p>;
167+
statusMessage = <p className="text-base text-gray-400">Allocating resources</p>;
169168
break;
170169

171170
// Creating means the workspace is currently being created. That includes downloading the images required
172171
// to run the workspace over the network. The time spent in this phase varies widely and depends on the current
173172
// network speed, image size and cache states.
174173
case "creating":
175-
phase = StartPhase.Preparing;
176-
statusMessage = <p className="text-base text-gray-400">Pulling Container Image</p>;
174+
phase = StartPhase.Creating;
175+
statusMessage = <p className="text-base text-gray-400">Pulling container image</p>;
177176
break;
178177

179178
// Initializing is the phase in which the workspace is executing the appropriate workspace initializer (e.g. Git
180179
// clone or backup download). After this phase one can expect the workspace to either be Running or Failed.
181180
case "initializing":
182181
phase = StartPhase.Starting;
183-
statusMessage = <p className="text-base text-gray-400">Cloning Repository</p>; // TODO Loading Prebuild ...
182+
statusMessage = <p className="text-base text-gray-400">Cloning repository</p>; // TODO Loading prebuild ...
184183
break;
185184

186185
// Running means the workspace is able to actively perform work, either by serving a user through Theia,
@@ -194,17 +193,39 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
194193
// When in this state, we expect it to become running or stopping anytime soon.
195194
case "interrupted":
196195
phase = StartPhase.Running;
197-
statusMessage = <p className="text-base text-gray-400">Checking On Workspace</p>;
196+
statusMessage = <p className="text-base text-gray-400">Checking workspace</p>;
198197
break;
199198

200199
// Stopping means that the workspace is currently shutting down. It could go to stopped every moment.
201200
case "stopping":
202-
statusMessage = <p className="text-base text-gray-400">Stopping …</p>;
201+
phase = StartPhase.Stopping;
202+
statusMessage = <div>
203+
<div className="flex space-x-3 items-center rounded-xl py-2 px-4 h-16 w-72 mt-4 bg-gray-100">
204+
<div className="rounded-full w-3 h-3 text-sm bg-gitpod-kumquat">&nbsp;</div>
205+
<div>
206+
<p className="text-gray-700 font-semibold">{this.state.workspaceInstance.workspaceId}</p>
207+
<p>{this.state.contextUrl}</p>
208+
</div>
209+
</div>
210+
</div>;
203211
break;
204212

205213
// Stopped means the workspace ended regularly because it was shut down.
206214
case "stopped":
207-
statusMessage = <p className="text-base text-gray-400">Stopped</p>;
215+
phase = StartPhase.Stopped;
216+
statusMessage = <div>
217+
<div className="flex space-x-3 items-center rounded-xl py-2 px-4 h-16 w-72 mt-4 bg-gray-100">
218+
<div className="rounded-full w-3 h-3 text-sm bg-gray-300">&nbsp;</div>
219+
<div>
220+
<p className="text-gray-700 font-semibold">{this.state.workspaceInstance.workspaceId}</p>
221+
<p>{this.state.contextUrl}</p>
222+
</div>
223+
</div>
224+
<div className="mt-10 flex space-x-2">
225+
<button className="px-4 py-2 text-gray-500 bg-white font-semibold border-gray-500" onClick={() => this.redirectTo(gitpodHostUrl.asDashboard().toString())}>Go to Dashboard</button>
226+
<button className="px-4 py-2 text-gray-50 bg-green-600 font-semibold border-green-800" onClick={() => this.redirectTo(gitpodHostUrl.asStart(this.state.workspaceInstance?.workspaceId).toString())}>Open Workspace</button>
227+
</div>
228+
</div>;
208229
break;
209230
}
210231

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export default function WorkspaceLogs() {
2-
return <pre className="m-6 p-4 h-60 w-11/12 lg:w-3/5 flex-shrink-0 rounded-lg" style={{ color: '#8E8787', background: '#ECE7E5' }}>
2+
return <pre className="m-6 p-4 h-72 w-11/12 lg:w-3/5 flex-shrink-0 rounded-xl bg-gray-100">
33
... logs ...
44
</pre>;
55
}

0 commit comments

Comments
 (0)