diff --git a/components/dashboard/src/start/start-workspace-options.ts b/components/dashboard/src/start/start-workspace-options.ts
index 69e56e25d900d6..6ab52011dcefbf 100644
--- a/components/dashboard/src/start/start-workspace-options.ts
+++ b/components/dashboard/src/start/start-workspace-options.ts
@@ -4,8 +4,13 @@
* See License.AGPL.txt in the project root for license information.
*/
-import { GitpodServer } from "@gitpod/gitpod-protocol";
+import { IDESettings } from "@gitpod/gitpod-protocol";
+export interface StartWorkspaceOptions {
+ workspaceClass?: string;
+ ideSettings?: IDESettings;
+ autostart?: boolean;
+}
export namespace StartWorkspaceOptions {
// The workspace class to use for the workspace. If not specified, the default workspace class is used.
export const WORKSPACE_CLASS = "workspaceClass";
@@ -13,9 +18,12 @@ export namespace StartWorkspaceOptions {
// The editor to use for the workspace. If not specified, the default editor is used.
export const EDITOR = "editor";
- export function parseSearchParams(search: string): GitpodServer.StartWorkspaceOptions {
+ // whether the workspace should automatically start
+ export const AUTOSTART = "autostart";
+
+ export function parseSearchParams(search: string): StartWorkspaceOptions {
const params = new URLSearchParams(search);
- const options: GitpodServer.StartWorkspaceOptions = {};
+ const options: StartWorkspaceOptions = {};
const workspaceClass = params.get(StartWorkspaceOptions.WORKSPACE_CLASS);
if (workspaceClass) {
options.workspaceClass = workspaceClass;
@@ -34,10 +42,13 @@ export namespace StartWorkspaceOptions {
};
}
}
+ if (params.get(StartWorkspaceOptions.AUTOSTART) === "true") {
+ options.autostart = true;
+ }
return options;
}
- export function toSearchParams(options: GitpodServer.StartWorkspaceOptions): string {
+ export function toSearchParams(options: StartWorkspaceOptions): string {
const params = new URLSearchParams();
if (options.workspaceClass) {
params.set(StartWorkspaceOptions.WORKSPACE_CLASS, options.workspaceClass);
@@ -47,6 +58,9 @@ export namespace StartWorkspaceOptions {
const latest = options.ideSettings.useLatestVersion;
params.set(StartWorkspaceOptions.EDITOR, latest ? ide + "-latest" : ide);
}
+ if (options.autostart) {
+ params.set(StartWorkspaceOptions.AUTOSTART, "true");
+ }
return params.toString();
}
diff --git a/components/dashboard/src/user-settings/Preferences.tsx b/components/dashboard/src/user-settings/Preferences.tsx
index da3633ce03e957..46e252c36f76f3 100644
--- a/components/dashboard/src/user-settings/Preferences.tsx
+++ b/components/dashboard/src/user-settings/Preferences.tsx
@@ -11,7 +11,7 @@ import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";
import { ThemeSelector } from "../components/ThemeSelector";
import Alert from "../components/Alert";
import { Link } from "react-router-dom";
-import { Heading2, Subheading } from "../components/typography/headings";
+import { Heading2, Heading3, Subheading } from "../components/typography/headings";
import { useUserMaySetTimeout } from "../data/current-user/may-set-timeout-query";
import { Button } from "../components/Button";
import SelectIDE from "./SelectIDE";
@@ -19,6 +19,7 @@ import { InputField } from "../components/forms/InputField";
import { TextInput } from "../components/forms/TextInputField";
import { useToast } from "../components/toasts/Toasts";
import { useUpdateCurrentUserDotfileRepoMutation } from "../data/current-user/update-mutation";
+import { AdditionalUserData } from "@gitpod/gitpod-protocol";
export type IDEChangedTrackLocation = "workspace_list" | "workspace_start" | "preferences";
@@ -67,12 +68,22 @@ export default function Preferences() {
[toast, setUser, workspaceTimeout],
);
+ const clearAutostartWorkspaceOptions = useCallback(async () => {
+ if (!user) {
+ return;
+ }
+ AdditionalUserData.set(user, { workspaceAutostartOptions: [] });
+ setUser(user);
+ await getGitpodService().server.updateLoggedInUser(user);
+ toast("Your autostart options were cleared.");
+ }, [setUser, toast, user]);
+
return (
- Editor
+ New Workspaces
- Choose the editor for opening workspaces.{" "}
+ Choose your default editor.{" "}
+ Autostart Options
+ Forget any saved autostart options for all repositories.
+
diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx
index 317ab6e7c4bd17..9e5fad29744585 100644
--- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx
+++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx
@@ -4,34 +4,37 @@
* See License.AGPL.txt in the project root for license information.
*/
-import { CommitContext, GitpodServer, WithReferrerContext } from "@gitpod/gitpod-protocol";
+import { AdditionalUserData, CommitContext, GitpodServer, WithReferrerContext } 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, useEffect, useMemo, useState } from "react";
+import { FunctionComponent, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useHistory, useLocation } from "react-router";
+import { Link } from "react-router-dom";
import { Button } from "../components/Button";
+import Modal from "../components/Modal";
import RepositoryFinder from "../components/RepositoryFinder";
import SelectIDEComponent from "../components/SelectIDEComponent";
import SelectWorkspaceClassComponent from "../components/SelectWorkspaceClassComponent";
-import { Heading1 } from "../components/typography/headings";
import { UsageLimitReachedModal } from "../components/UsageLimitReachedModal";
-import { useCurrentOrg } from "../data/organizations/orgs-query";
+import { CheckboxInputField } from "../components/forms/CheckboxInputField";
+import { Heading1 } from "../components/typography/headings";
+import { useAuthProviders } from "../data/auth-providers/auth-provider-query";
+import { useFeatureFlag } from "../data/featureflag-query";
+import { useCurrentOrg, useOrganizations } from "../data/organizations/orgs-query";
import { useListProjectsQuery } from "../data/projects/list-projects-query";
import { useCreateWorkspaceMutation } from "../data/workspaces/create-workspace-mutation";
import { useListWorkspacesQuery } from "../data/workspaces/list-workspaces-query";
import { useWorkspaceContext } from "../data/workspaces/resolve-context-query";
import { openAuthorizeWindow } from "../provider-utils";
-import { gitpodHostUrl } from "../service/service";
-import { StartWorkspaceOptions } from "../start/start-workspace-options";
+import { getGitpodService, gitpodHostUrl } from "../service/service";
import { StartWorkspaceError } from "../start/StartPage";
-import { useCurrentUser } from "../user-context";
+import { VerifyModal } from "../start/VerifyModal";
+import { StartWorkspaceOptions } from "../start/start-workspace-options";
+import { UserContext, useCurrentUser } from "../user-context";
import { SelectAccountModal } from "../user-settings/SelectAccountModal";
+import { settingsPathPreferences } from "../user-settings/settings.routes";
import { WorkspaceEntry } from "./WorkspaceEntry";
-import { useAuthProviders } from "../data/auth-providers/auth-provider-query";
-import { VerifyModal } from "../start/VerifyModal";
-import { useFeatureFlag } from "../data/featureflag-query";
-import Modal from "../components/Modal";
export const useNewCreateWorkspacePage = () => {
const startWithOptions = useFeatureFlag("start_with_options");
@@ -40,8 +43,9 @@ export const useNewCreateWorkspacePage = () => {
};
export function CreateWorkspacePage() {
- const user = useCurrentUser();
+ const { user, setUser } = useContext(UserContext);
const currentOrg = useCurrentOrg().data;
+ const organizations = useOrganizations();
const projects = useListProjectsQuery();
const workspaces = useListWorkspacesQuery({ limit: 50 });
const location = useLocation();
@@ -69,6 +73,42 @@ export function CreateWorkspacePage() {
StartWorkspaceOptions.parseContextUrl(location.hash),
);
const workspaceContext = useWorkspaceContext(contextURL);
+ const [rememberOptions, setRememberOptions] = useState(false);
+ const [autostart, setAutostart] = useState(props.autostart);
+
+ const storeAutoStartOptions = useCallback(() => {
+ if (!workspaceContext.data || !user || !currentOrg) {
+ return;
+ }
+ const cloneURL = CommitContext.is(workspaceContext.data) && workspaceContext.data.repository.cloneUrl;
+ if (!cloneURL) {
+ return;
+ }
+ let workspaceAutoStartOptions = (user.additionalData?.workspaceAutostartOptions || []).filter(
+ (e) => e.cloneURL !== cloneURL,
+ );
+
+ // we only keep the last 20 options
+ workspaceAutoStartOptions = workspaceAutoStartOptions.slice(-20);
+
+ if (rememberOptions) {
+ workspaceAutoStartOptions.push({
+ cloneURL,
+ organizationId: currentOrg.id,
+ ideSettings: {
+ defaultIde: selectedIde,
+ useLatestVersion: useLatestIde,
+ },
+ workspaceClass: selectedWsClass,
+ });
+
+ AdditionalUserData.set(user, {
+ workspaceAutostartOptions: workspaceAutoStartOptions,
+ });
+ }
+ setUser(user);
+ getGitpodService().server.updateLoggedInUser(user).catch(console.error);
+ }, [currentOrg, rememberOptions, selectedIde, selectedWsClass, setUser, useLatestIde, user, workspaceContext.data]);
// see if we have a matching project based on context url and project's repo url
const project = useMemo(() => {
@@ -99,6 +139,8 @@ export function CreateWorkspacePage() {
// This allows the contextURL to persist if user changes orgs, or copies/shares url
const handleContextURLChange = useCallback(
(newContextURL: string) => {
+ // we disable auto start if the user changes the context URL
+ setAutostart(false);
setContextURL(newContextURL);
history.replace(`#${newContextURL}`);
},
@@ -163,11 +205,17 @@ export function CreateWorkspacePage() {
console.log("Skipping duplicate createWorkspace call.");
return;
}
+ if (rememberOptions) {
+ storeAutoStartOptions();
+ }
+ // we wait at least 5 secs
+ const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
const result = await createWorkspaceMutation.mutateAsync({
contextUrl: contextURL,
organizationId,
...opts,
});
+ await timeout;
if (result.workspaceURL) {
window.location.href = result.workspaceURL;
} else if (result.createdWorkspaceId) {
@@ -175,38 +223,88 @@ export function CreateWorkspacePage() {
}
} catch (error) {
console.log(error);
+ } finally {
+ // we only auto start once, so we don't run into endless start loops on errors
+ if (autostart) {
+ setAutostart(false);
+ }
}
},
[
- createWorkspaceMutation,
- history,
contextURL,
- selectedIde,
- selectedWsClass,
currentOrg?.id,
user?.additionalData?.isMigratedToTeamOnlyAttribution,
+ selectedWsClass,
+ selectedIde,
useLatestIde,
+ createWorkspaceMutation,
+ rememberOptions,
+ autostart,
+ storeAutoStartOptions,
+ history,
],
);
+ // listen on auto start changes
+ useEffect(() => {
+ if (!autostart) {
+ return;
+ }
+ createWorkspace();
+ }, [autostart, createWorkspace]);
+
+ // when workspaceContext is available, we look up if options are remembered
+ useEffect(() => {
+ if (!organizations.data) {
+ return;
+ }
+ const cloneURL = CommitContext.is(workspaceContext.data) && workspaceContext.data.repository.cloneUrl;
+ if (!cloneURL || autostart) {
+ return undefined;
+ }
+ const rememberedOptions = (user?.additionalData?.workspaceAutostartOptions || []).find(
+ (e) => e.cloneURL === cloneURL,
+ );
+ setRememberOptions(!!rememberedOptions);
+ if (rememberedOptions) {
+ // if it's another org, we simply redirect using the same hash and let the reloaded page handle everything again.
+ if (rememberedOptions.organizationId !== currentOrg?.id) {
+ const org = organizations.data.find((o) => o.id === rememberedOptions.organizationId);
+ if (org) {
+ const redirect = `${location.pathname}?org=${encodeURIComponent(rememberedOptions.organizationId)}${
+ location.hash
+ }`;
+ window.location.href = redirect;
+ } else {
+ console.warn("Could not find organization", rememberedOptions.organizationId);
+ }
+ }
+ if (rememberedOptions.ideSettings?.defaultIde) {
+ setSelectedIde(rememberedOptions.ideSettings?.defaultIde);
+ }
+ setUseLatestIde(!!rememberedOptions.ideSettings?.useLatestVersion);
+ if (rememberedOptions.workspaceClass) {
+ setSelectedWsClass(rememberedOptions.workspaceClass);
+ }
+ if (autostart === undefined) {
+ setAutostart(true);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [workspaceContext.data]);
+
// Need a wrapper here so we call createWorkspace w/o any arguments
const onClickCreate = useCallback(() => createWorkspace(), [createWorkspace]);
- // if the context URL has a referrer prefix, we set the referrerIde as the selected IDE and immediately start a workspace.
+ // if the context URL has a referrer prefix, we set the referrerIde as the selected IDE and autostart the workspace.
useEffect(() => {
if (workspaceContext.data && WithReferrerContext.is(workspaceContext.data)) {
- let options: Omit | undefined;
if (workspaceContext.data.referrerIde) {
setSelectedIde(workspaceContext.data.referrerIde);
- options = {
- ideSettings: {
- defaultIde: workspaceContext.data.referrerIde,
- },
- };
}
- createWorkspace(options);
+ setAutostart(true);
}
- }, [workspaceContext.data, createWorkspace]);
+ }, [workspaceContext.data]);
if (SelectAccountPayload.is(selectAccountError)) {
return (
@@ -259,7 +357,7 @@ export function CreateWorkspacePage() {
{errorWsClass && {errorWsClass}
}
-
+
- {existingWorkspaces.length > 0 && (
+ {workspaceContext.data && (
+
+ )}
+ {existingWorkspaces.length > 0 && !isStarting && (
Running workspaces on this revision
<>
@@ -299,6 +404,33 @@ export function CreateWorkspacePage() {
);
}
+function RememberOptions(params: { disabled?: boolean; checked: boolean; onChange: (checked: boolean) => void }) {
+ const { disabled, checked, onChange } = params;
+
+ return (
+ <>
+
+
+
+
+
+ Don't worry, you can reset this anytime in your{" "}
+
+ preferences
+
+ .
+
+
+ >
+ );
+}
+
function tryAuthorize(host: string, scopes?: string[]): Promise
{
const result = new Deferred();
openAuthorizeWindow({
diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts
index 5d76e53f75279d..6e41cfbc77fc2d 100644
--- a/components/gitpod-protocol/src/protocol.ts
+++ b/components/gitpod-protocol/src/protocol.ts
@@ -9,6 +9,7 @@ import { RoleOrPermission } from "./permission";
import { Project } from "./teams-projects-protocol";
import { createHash } from "crypto";
import { AttributionId } from "./attribution";
+import { WorkspaceRegion } from "./workspace-cluster";
export interface UserInfo {
name?: string;
@@ -273,7 +274,19 @@ export interface AdditionalUserData extends Partial {
// whether the user has been migrated to team attribution.
// a corresponding feature flag (team_only_attribution) triggers the migration.
isMigratedToTeamOnlyAttribution?: boolean;
+
+ // remembered workspace auto start options
+ workspaceAutostartOptions?: WorkspaceAutostartOption[];
+}
+
+interface WorkspaceAutostartOption {
+ cloneURL: string;
+ organizationId: string;
+ workspaceClass?: string;
+ ideSettings?: IDESettings;
+ region?: WorkspaceRegion;
}
+
export namespace AdditionalUserData {
export function set(user: User, partialData: Partial): User {
if (!user.additionalData) {