Skip to content

Commit d91fbff

Browse files
authored
[Create WS] Various fixes (#17135)
* [server] normalize contextURL * [create workspace] only create one workspace - respect referrer context - useWorkspaceContext returns null instead of being disabled when contextUrl is undefined * [dashboard] immediately show running workspaces
1 parent 049e14a commit d91fbff

File tree

6 files changed

+112
-114
lines changed

6 files changed

+112
-114
lines changed

components/dashboard/src/data/workspaces/resolve-context-query.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,11 @@ import { useQuery } from "@tanstack/react-query";
99
import { getGitpodService } from "../../service/service";
1010

1111
export function useWorkspaceContext(contextUrl?: string) {
12-
const query = useQuery<WorkspaceContext, Error>(
13-
["workspace-context", contextUrl],
14-
() => {
15-
if (!contextUrl) {
16-
throw new Error("no contextURL. Query should be disabled.");
17-
}
18-
return getGitpodService().server.resolveContext(contextUrl);
19-
},
20-
{
21-
enabled: !!contextUrl,
22-
},
23-
);
12+
const query = useQuery<WorkspaceContext | null, Error>(["workspace-context", contextUrl], () => {
13+
if (!contextUrl) {
14+
return null;
15+
}
16+
return getGitpodService().server.resolveContext(contextUrl);
17+
});
2418
return query;
2519
}

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
521521
<div className="flex flex-col text-center m-auto text-sm w-72 text-gray-400">
522522
{client.installationSteps.map((step) => (
523523
<div
524+
key={step}
524525
dangerouslySetInnerHTML={{
525526
// eslint-disable-next-line no-template-curly-in-string
526527
__html: step.replaceAll("${OPEN_LINK_LABEL}", openLinkLabel),

components/dashboard/src/workspaces/CreateWorkspacePage.tsx

Lines changed: 68 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { CommitContext, ContextURL, GitpodServer, WorkspaceInfo } from "@gitpod/gitpod-protocol";
7+
import { CommitContext, GitpodServer, WithReferrerContext } from "@gitpod/gitpod-protocol";
88
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
99
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1010
import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
1111
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from "react";
1212
import { useHistory, useLocation } from "react-router";
1313
import { Button } from "../components/Button";
14-
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
1514
import RepositoryFinder from "../components/RepositoryFinder";
1615
import SelectIDEComponent from "../components/SelectIDEComponent";
1716
import SelectWorkspaceClassComponent from "../components/SelectWorkspaceClassComponent";
@@ -21,6 +20,7 @@ import { useFeatureFlags } from "../contexts/FeatureFlagContext";
2120
import { useCurrentOrg } from "../data/organizations/orgs-query";
2221
import { useListProjectsQuery } from "../data/projects/list-projects-query";
2322
import { useCreateWorkspaceMutation } from "../data/workspaces/create-workspace-mutation";
23+
import { useListWorkspacesQuery } from "../data/workspaces/list-workspaces-query";
2424
import { useWorkspaceContext } from "../data/workspaces/resolve-context-query";
2525
import { openAuthorizeWindow } from "../provider-utils";
2626
import { gitpodHostUrl } from "../service/service";
@@ -29,6 +29,7 @@ import { StartWorkspaceOptions } from "../start/start-workspace-options";
2929
import { StartWorkspaceError } from "../start/StartPage";
3030
import { useCurrentUser } from "../user-context";
3131
import { SelectAccountModal } from "../user-settings/SelectAccountModal";
32+
import { WorkspaceEntry } from "./WorkspaceEntry";
3233

3334
export const useNewCreateWorkspacePage = () => {
3435
const { startWithOptions } = useFeatureFlags();
@@ -40,6 +41,7 @@ export function CreateWorkspacePage() {
4041
const user = useCurrentUser();
4142
const currentOrg = useCurrentOrg().data;
4243
const projects = useListProjectsQuery();
44+
const workspaces = useListWorkspacesQuery({ limit: 50, orgId: currentOrg?.id });
4345
const location = useLocation();
4446
const history = useHistory();
4547
const props = StartWorkspaceOptions.parseSearchParams(location.search);
@@ -101,14 +103,29 @@ export function CreateWorkspacePage() {
101103
);
102104
const [errorIde, setErrorIde] = useState<string | undefined>(undefined);
103105

104-
const [existingWorkspaces, setExistingWorkspaces] = useState<WorkspaceInfo[]>([]);
106+
const existingWorkspaces = useMemo(() => {
107+
if (!workspaces.data || !CommitContext.is(workspaceContext.data)) {
108+
return [];
109+
}
110+
return workspaces.data.filter(
111+
(ws) =>
112+
ws.latestInstance?.status?.phase === "running" &&
113+
CommitContext.is(ws.workspace.context) &&
114+
CommitContext.is(workspaceContext.data) &&
115+
ws.workspace.context.repository.cloneUrl === workspaceContext.data.repository.cloneUrl &&
116+
ws.workspace.context.revision === workspaceContext.data.revision,
117+
);
118+
}, [workspaces.data, workspaceContext.data]);
105119
const [selectAccountError, setSelectAccountError] = useState<SelectAccountPayload | undefined>(undefined);
106120

107121
const createWorkspace = useCallback(
108122
async (options?: Omit<GitpodServer.CreateWorkspaceOptions, "contextUrl">) => {
109123
// add options from search params
110124
const opts = options || {};
111125

126+
// we already have shown running workspaces to the user
127+
opts.ignoreRunningWorkspaceOnSameCommit = true;
128+
112129
if (!opts.workspaceClass) {
113130
opts.workspaceClass = selectedWsClass;
114131
}
@@ -123,14 +140,17 @@ export function CreateWorkspacePage() {
123140
}
124141

125142
const organizationId = currentOrg?.id;
126-
console.log("organizationId: " + JSON.stringify(organizationId));
127143
if (!organizationId && !!user?.additionalData?.isMigratedToTeamOnlyAttribution) {
128144
// We need an organizationId for this group of users
129145
console.warn("Skipping createWorkspace");
130146
return;
131147
}
132148

133149
try {
150+
if (createWorkspaceMutation.isLoading || createWorkspaceMutation.isSuccess) {
151+
console.log("Skipping duplicate createWorkspace call.");
152+
return;
153+
}
134154
const result = await createWorkspaceMutation.mutateAsync({
135155
contextUrl: contextURL,
136156
organizationId,
@@ -140,19 +160,42 @@ export function CreateWorkspacePage() {
140160
window.location.href = result.workspaceURL;
141161
} else if (result.createdWorkspaceId) {
142162
history.push(`/start/#${result.createdWorkspaceId}`);
143-
} else if (result.existingWorkspaces && result.existingWorkspaces.length > 0) {
144-
setExistingWorkspaces(result.existingWorkspaces);
145163
}
146164
} catch (error) {
147165
console.log(error);
148166
}
149167
},
150-
[createWorkspaceMutation, history, contextURL, selectedIde, selectedWsClass, currentOrg?.id, user?.additionalData?.isMigratedToTeamOnlyAttribution, useLatestIde],
168+
[
169+
createWorkspaceMutation,
170+
history,
171+
contextURL,
172+
selectedIde,
173+
selectedWsClass,
174+
currentOrg?.id,
175+
user?.additionalData?.isMigratedToTeamOnlyAttribution,
176+
useLatestIde,
177+
],
151178
);
152179

153180
// Need a wrapper here so we call createWorkspace w/o any arguments
154181
const onClickCreate = useCallback(() => createWorkspace(), [createWorkspace]);
155182

183+
// if the context URL has a referrer prefix, we set the referrerIde as the selected IDE and immediately start a workspace.
184+
useEffect(() => {
185+
if (workspaceContext.data && WithReferrerContext.is(workspaceContext.data)) {
186+
let options: Omit<GitpodServer.CreateWorkspaceOptions, "contextUrl"> | undefined;
187+
if (workspaceContext.data.referrerIde) {
188+
setSelectedIde(workspaceContext.data.referrerIde);
189+
options = {
190+
ideSettings: {
191+
defaultIde: workspaceContext.data.referrerIde,
192+
},
193+
};
194+
}
195+
createWorkspace(options);
196+
}
197+
}, [workspaceContext.data, createWorkspace]);
198+
156199
if (SelectAccountPayload.is(selectAccountError)) {
157200
return (
158201
<SelectAccountModal
@@ -214,6 +257,24 @@ export function CreateWorkspacePage() {
214257
{isLoading ? "Loading ..." : isStarting ? "Creating Workspace ..." : "New Workspace"}
215258
</Button>
216259
</div>
260+
{existingWorkspaces.length > 0 && (
261+
<div className="w-full flex flex-col justify-end px-6">
262+
<p className="mt-6 text-center text-base">Running workspaces on this revision</p>
263+
<>
264+
{existingWorkspaces.map((w) => {
265+
return (
266+
<a
267+
key={w.workspace.id}
268+
href={w.latestInstance?.ideUrl || `/start/${w.workspace.id}}`}
269+
className="rounded-xl group hover:bg-gray-100 dark:hover:bg-gray-800 flex"
270+
>
271+
<WorkspaceEntry info={w} shortVersion={true} />
272+
</a>
273+
);
274+
})}
275+
</>
276+
</div>
277+
)}
217278
<div>
218279
<StatusMessage
219280
error={createWorkspaceMutation.error as StartWorkspaceError}
@@ -225,14 +286,6 @@ export function CreateWorkspacePage() {
225286
/>
226287
</div>
227288
</div>
228-
{existingWorkspaces.length > 0 && (
229-
<ExistingWorkspaceModal
230-
existingWorkspaces={existingWorkspaces}
231-
createWorkspace={createWorkspace}
232-
isStarting={isStarting}
233-
onClose={() => setExistingWorkspaces([])}
234-
/>
235-
)}
236289
</div>
237290
);
238291
}
@@ -351,62 +404,3 @@ const StatusMessage: FunctionComponent<StatusMessageProps> = ({
351404
return <p className="text-base text-gitpod-red w-96">Unknown Error: {JSON.stringify(error, null, 2)}</p>;
352405
}
353406
};
354-
355-
interface ExistingWorkspaceModalProps {
356-
existingWorkspaces: WorkspaceInfo[];
357-
onClose: () => void;
358-
isStarting: boolean;
359-
createWorkspace: (opts: Omit<GitpodServer.CreateWorkspaceOptions, "contextUrl">) => void;
360-
}
361-
362-
const ExistingWorkspaceModal: FunctionComponent<ExistingWorkspaceModalProps> = ({
363-
existingWorkspaces,
364-
onClose,
365-
isStarting,
366-
createWorkspace,
367-
}) => {
368-
return (
369-
<Modal visible={true} closeable={true} onClose={onClose}>
370-
<ModalHeader>Running Workspaces</ModalHeader>
371-
<ModalBody>
372-
<p className="mt-1 mb-2 text-base">
373-
You already have running workspaces with the same context. You can open an existing one or open a
374-
new workspace.
375-
</p>
376-
<>
377-
{existingWorkspaces.map((w) => {
378-
const normalizedContextUrl =
379-
ContextURL.getNormalizedURL(w.workspace)?.toString() || "undefined";
380-
return (
381-
<a
382-
key={w.workspace.id}
383-
href={w.latestInstance?.ideUrl || `/start/${w.workspace.id}}`}
384-
className="rounded-xl group hover:bg-gray-100 dark:hover:bg-gray-800 flex p-3 my-1"
385-
>
386-
<div className="w-full">
387-
<p className="text-base text-black dark:text-gray-100 font-bold">
388-
{w.workspace.id}
389-
</p>
390-
<p className="truncate" title={normalizedContextUrl}>
391-
{normalizedContextUrl}
392-
</p>
393-
</div>
394-
</a>
395-
);
396-
})}
397-
</>
398-
</ModalBody>
399-
<ModalFooter>
400-
<button className="secondary" onClick={onClose}>
401-
Cancel
402-
</button>
403-
<Button
404-
loading={isStarting}
405-
onClick={() => createWorkspace({ ignoreRunningWorkspaceOnSameCommit: true })}
406-
>
407-
{isStarting ? "Creating Workspace ..." : "New Workspace"}
408-
</Button>
409-
</ModalFooter>
410-
</Modal>
411-
);
412-
};

components/dashboard/src/workspaces/WorkspaceEntry.tsx

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import Tooltip from "../components/Tooltip";
1313
import dayjs from "dayjs";
1414
import { WorkspaceEntryOverflowMenu } from "./WorkspaceOverflowMenu";
1515
import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";
16+
import { projectsPathInstallGitHubApp } from "../projects/projects.routes";
1617

1718
type Props = {
1819
info: WorkspaceInfo;
20+
shortVersion?: boolean;
1921
};
2022

21-
export const WorkspaceEntry: FunctionComponent<Props> = ({ info }) => {
23+
export const WorkspaceEntry: FunctionComponent<Props> = ({ info, shortVersion }) => {
2224
const [menuActive, setMenuActive] = useState(false);
2325

2426
const workspace = info.workspace;
@@ -63,32 +65,36 @@ export const WorkspaceEntry: FunctionComponent<Props> = ({ info }) => {
6365
</a>
6466
</Tooltip>
6567
</ItemField>
66-
<ItemField className="w-4/12 flex flex-col my-auto">
67-
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">
68-
{workspace.description}
69-
</div>
70-
<a href={normalizedContextUrl}>
71-
<div className="text-sm text-gray-400 dark:text-gray-500 overflow-ellipsis truncate hover:text-blue-600 dark:hover:text-blue-400">
72-
{normalizedContextUrlDescription}
73-
</div>
74-
</a>
75-
</ItemField>
76-
<ItemField className="w-2/12 flex flex-col my-auto">
77-
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">
78-
<Tooltip content={currentBranch}>{currentBranch}</Tooltip>
79-
</div>
80-
<div className="mr-auto">
81-
<PendingChangesDropdown workspaceInstance={info.latestInstance} />
82-
</div>
83-
</ItemField>
84-
<ItemField className="w-2/12 flex my-auto">
85-
<Tooltip content={`Created ${dayjs(info.workspace.creationTime).fromNow()}`}>
86-
<div className="text-sm w-full text-gray-400 overflow-ellipsis truncate">
87-
{dayjs(WorkspaceInfo.lastActiveISODate(info)).fromNow()}
88-
</div>
89-
</Tooltip>
90-
</ItemField>
91-
<WorkspaceEntryOverflowMenu changeMenuState={changeMenuState} info={info} />
68+
{!shortVersion && (
69+
<>
70+
<ItemField className="w-4/12 flex flex-col my-auto">
71+
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">
72+
{workspace.description}
73+
</div>
74+
<a href={normalizedContextUrl}>
75+
<div className="text-sm text-gray-400 dark:text-gray-500 overflow-ellipsis truncate hover:text-blue-600 dark:hover:text-blue-400">
76+
{normalizedContextUrlDescription}
77+
</div>
78+
</a>
79+
</ItemField>
80+
<ItemField className="w-2/12 flex flex-col my-auto">
81+
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">
82+
<Tooltip content={currentBranch}>{currentBranch}</Tooltip>
83+
</div>
84+
<div className="mr-auto">
85+
<PendingChangesDropdown workspaceInstance={info.latestInstance} />
86+
</div>
87+
</ItemField>
88+
<ItemField className="w-2/12 flex my-auto">
89+
<Tooltip content={`Created ${dayjs(info.workspace.creationTime).fromNow()}`}>
90+
<div className="text-sm w-full text-gray-400 overflow-ellipsis truncate">
91+
{dayjs(WorkspaceInfo.lastActiveISODate(info)).fromNow()}
92+
</div>
93+
</Tooltip>
94+
</ItemField>
95+
<WorkspaceEntryOverflowMenu changeMenuState={changeMenuState} info={info} />
96+
</>
97+
)}
9298
</Item>
9399
);
94100
};

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ export namespace GitpodServer {
482482
organizationId?: string;
483483

484484
// whether running workspaces on the same context should be ignored. If false (default) users will be asked.
485+
//TODO(se) remove this option and let clients do that check if they like. The new create workspace page does it already
485486
ignoreRunningWorkspaceOnSameCommit?: boolean;
486487
ignoreRunningPrebuild?: boolean;
487488
allowUsingPreviousPrebuilds?: boolean;

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
11361136

11371137
logContext = { userId: user.id };
11381138

1139+
//TODO(se) remove this implicit check and let instead clients do the checking.
11391140
// Credit check runs in parallel with the other operations up until we start consuming resources.
11401141
// Make sure to await for the creditCheck promise in the right places.
11411142
const runningInstancesPromise = this.workspaceDb.trace(ctx).findRegularRunningInstances(user.id);
@@ -3669,6 +3670,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
36693670
async getIDToken(): Promise<void> {}
36703671
public async resolveContext(ctx: TraceContextWithSpan, contextUrl: string): Promise<WorkspaceContext> {
36713672
const user = this.checkAndBlockUser("resolveContext");
3672-
return this.contextParser.handle(ctx, user, contextUrl);
3673+
const normalizedCtxURL = this.contextParser.normalizeContextURL(contextUrl);
3674+
return this.contextParser.handle(ctx, user, normalizedCtxURL);
36733675
}
36743676
}

0 commit comments

Comments
 (0)