Skip to content

Commit 2e7e3b2

Browse files
committed
[dashbard] Persist configurationId for selected projects on new workspace page
Tool: gitpod/catfood.gitpod.cloud
1 parent cdd79f3 commit 2e7e3b2

File tree

6 files changed

+87
-27
lines changed

6 files changed

+87
-27
lines changed

components/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"lite-youtube-embed": "^0.3.2",
4343
"lodash": "^4.17.21",
4444
"lucide-react": "^0.474.0",
45+
"nuqs": "^2.3.2",
4546
"pretty-bytes": "^6.1.0",
4647
"process": "^0.11.10",
4748
"query-string": "^7.1.1",

components/dashboard/src/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { PaymentContextProvider } from "./payment-context";
2626
import { ThemeContextProvider } from "./theme-context";
2727
import { UserContextProvider } from "./user-context";
2828
import { getURLHash, isGitpodIo, isWebsiteSlug } from "./utils";
29+
import { NuqsAdapter } from "nuqs/adapters/react";
2930

3031
const bootApp = () => {
3132
// gitpod.io specific boot logic
@@ -68,7 +69,9 @@ const bootApp = () => {
6869
<ToastContextProvider>
6970
<UserContextProvider>
7071
<PaymentContextProvider>
71-
<RootAppRouter />
72+
<NuqsAdapter>
73+
<RootAppRouter />
74+
</NuqsAdapter>
7275
</PaymentContextProvider>
7376
</UserContextProvider>
7477
</ToastContextProvider>

components/dashboard/src/teams/sso/SSOConfigForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const SSOConfigForm: FC<Props> = ({ config, readOnly = false, onChange })
8181
/>
8282

8383
<Subheading className="mt-8">
84-
<strong>3.</strong> Restrict available accounts in your Identity Providers.
84+
<strong>3.</strong> Restrict available accounts in your Identity Providers.{" "}
8585
<a
8686
href="https://www.gitpod.io/docs/enterprise/setup-gitpod/configure-sso#restrict-available-accounts-in-your-identity-providers"
8787
target="_blank"

components/dashboard/src/workspaces/CreateWorkspacePage.tsx

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ import Menu from "../menu/Menu";
5454
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
5555
import { useAllowedWorkspaceEditorsMemo } from "../data/ide-options/ide-options-query";
5656
import { isGitpodIo } from "../utils";
57-
import { useListConfigurations } from "../data/configurations/configuration-queries";
57+
import { useConfiguration, useListConfigurations } from "../data/configurations/configuration-queries";
5858
import { flattenPagedConfigurations } from "../data/git-providers/unified-repositories-search-query";
5959
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
6060
import { useMemberRole } from "../data/organizations/members-query";
6161
import { OrganizationPermission } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
62+
import { createParser, useQueryState } from "nuqs";
63+
import { validate as validateUUID } from "uuid";
6264
import { useInstallationConfiguration } from "../data/installation/installation-config-query";
6365

6466
type NextLoadOption = "searchParams" | "autoStart" | "allDone";
@@ -76,10 +78,9 @@ export function CreateWorkspacePage() {
7678
const [autostart, setAutostart] = useState<boolean | undefined>(props.autostart);
7779
const createWorkspaceMutation = useCreateWorkspaceMutation();
7880

79-
// Currently this tracks if the user has selected a project from the dropdown
80-
// Need to make sure we initialize this to a project if the url hash value maps to a project's repo url
81-
// Will need to handle multiple projects w/ same repo url
82-
const [selectedProjectID, setSelectedProjectID] = useState<string | undefined>(undefined);
81+
// This stores the configurationId corresponding to a context URL
82+
// it can either be resolved from the context URL [lossy context URL -> configuration conversion] or it can itself resolve the context URL when specified [lossless configurationId -> context URL conversion]
83+
const [selectedProjectID, setSelectedProjectID] = useQueryState("configurationId", parseAsUUIDv4);
8384

8485
const defaultLatestIde =
8586
props.ideSettings?.useLatestVersion !== undefined
@@ -91,12 +92,12 @@ export function CreateWorkspacePage() {
9192
// Note: it has data fetching and UI rendering race between the updating of `selectedProjectId` and `selectedIde`
9293
// We have to stored the using repositoryId locally so that we can know selectedIde is updated because if which repo
9394
// so that it doesn't show ide error messages in middle state
94-
const [defaultIdeSource, setDefaultIdeSource] = useState<string | undefined>(selectedProjectID);
95+
const [defaultIdeSource, setDefaultIdeSource] = useState<string | undefined>(selectedProjectID ?? undefined);
9596
const {
9697
computedDefault: computedDefaultEditor,
9798
usingConfigurationId,
9899
availableOptions: availableEditorOptions,
99-
} = useAllowedWorkspaceEditorsMemo(selectedProjectID, {
100+
} = useAllowedWorkspaceEditorsMemo(selectedProjectID ?? undefined, {
100101
userDefault: user?.editorSettings?.name,
101102
filterOutDisabled: true,
102103
});
@@ -106,7 +107,7 @@ export function CreateWorkspacePage() {
106107
computedDefaultClass,
107108
data: allowedWorkspaceClasses,
108109
isLoading: isLoadingWorkspaceClasses,
109-
} = useAllowedWorkspaceClassesMemo(selectedProjectID);
110+
} = useAllowedWorkspaceClassesMemo(selectedProjectID ?? undefined);
110111
const defaultWorkspaceClass = props.workspaceClass ?? computedDefaultClass;
111112
const showExamples = props.showExamples ?? false;
112113
const { data: orgSettings } = useOrgSettingsQuery();
@@ -123,9 +124,13 @@ export function CreateWorkspacePage() {
123124
const needsGitAuthorization = useNeedsGitAuthorization();
124125

125126
useEffect(() => {
126-
setContextURL(StartWorkspaceOptions.parseContextUrl(location.hash));
127-
setSelectedProjectID(undefined);
128-
setNextLoadOption("searchParams");
127+
// if we have a context URL in the hash, we can proceed with resolving info based on it.
128+
if (location.hash) {
129+
setContextURL(StartWorkspaceOptions.parseContextUrl(location.hash));
130+
setNextLoadOption("searchParams");
131+
}
132+
133+
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to depend on setSelectedProjectID
129134
}, [location.hash]);
130135

131136
const cloneURL = workspaceContext.data?.cloneUrl;
@@ -189,24 +194,43 @@ export function CreateWorkspacePage() {
189194
setUser,
190195
]);
191196

192-
// see if we have a matching configuration based on context url and configuration's repo url
193-
const configuration = useMemo(() => {
197+
// see if we have a matching configuration based on configuration id / context url and configuration's repo url
198+
const configurationFromURL = useMemo(() => {
194199
if (!workspaceContext.data || configurations.length === 0) {
195200
return undefined;
196201
}
197202
if (!cloneURL) {
198203
return;
199204
}
200-
// TODO: Account for multiple configurations w/ the same cloneUrl
205+
201206
return configurations.find((p) => p.cloneUrl === cloneURL);
202207
}, [workspaceContext.data, configurations, cloneURL]);
208+
const { data: configurationFromId, isLoading: isLoadingConfigurationFromId } = useConfiguration(
209+
selectedProjectID ?? undefined,
210+
);
211+
const configuration = useMemo(
212+
() => {
213+
if (configurationFromId && (!cloneURL || configurationFromId.cloneUrl === cloneURL)) {
214+
return configurationFromId;
215+
}
216+
if (configurationFromURL && !isLoadingConfigurationFromId) {
217+
setSelectedProjectID(configurationFromURL.id); // idk about this
218+
219+
return configurationFromURL;
220+
}
203221

204-
// Handle the case where the context url in the hash matches a project and we don't have that project selected yet
222+
return undefined;
223+
},
224+
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to depend on setSelectedProjectID
225+
[configurationFromId, configurationFromURL, selectedProjectID],
226+
);
227+
228+
// when the user comes in with a configuration id in the search params, we want to set the context URL to the configuration's clone URL
205229
useEffect(() => {
206-
if (configuration && !selectedProjectID) {
207-
setSelectedProjectID(configuration.id);
230+
if (configuration && !contextURL) {
231+
setContextURL(configuration.cloneUrl);
208232
}
209-
}, [configuration, selectedProjectID]);
233+
}, [configuration, contextURL]);
210234

211235
// In addition to updating state, we want to update the url hash as well
212236
// This allows the contextURL to persist if user changes orgs, or copies/shares url
@@ -222,6 +246,8 @@ export function CreateWorkspacePage() {
222246
// reset load options
223247
setNextLoadOption("searchParams");
224248
},
249+
250+
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to depend on setSelectedProjectID
225251
[history],
226252
);
227253

@@ -291,7 +317,7 @@ export function CreateWorkspacePage() {
291317
opts.metadata = {};
292318
}
293319
opts.metadata.organizationId = organizationId;
294-
opts.metadata.configurationId = selectedProjectID;
320+
opts.metadata.configurationId = selectedProjectID ?? undefined;
295321

296322
const contextUrlSource: PartialMessage<CreateAndStartWorkspaceRequest_ContextURL> =
297323
opts.source?.case === "contextUrl" ? opts.source?.value ?? {} : {};
@@ -563,8 +589,8 @@ export function CreateWorkspacePage() {
563589
<RepositoryFinder
564590
onChange={handleContextURLChange}
565591
selectedContextURL={contextURL}
566-
selectedConfigurationId={selectedProjectID}
567-
expanded={!contextURL}
592+
selectedConfigurationId={selectedProjectID ?? undefined}
593+
expanded={!contextURL && !selectedProjectID}
568594
onlyConfigurations={
569595
orgSettings?.roleRestrictions.some(
570596
(roleRestriction) =>
@@ -583,12 +609,14 @@ export function CreateWorkspacePage() {
583609
<SelectIDEComponent
584610
onSelectionChange={onSelectEditorChange}
585611
availableOptions={
586-
defaultIdeSource === selectedProjectID ? availableEditorOptions : undefined
612+
defaultIdeSource === (selectedProjectID ?? undefined)
613+
? availableEditorOptions
614+
: undefined
587615
}
588616
setError={setErrorIde}
589617
setWarning={setWarningIde}
590618
selectedIdeOption={selectedIde}
591-
selectedConfigurationId={selectedProjectID}
619+
selectedConfigurationId={selectedProjectID ?? undefined}
592620
pinnedEditorVersions={
593621
orgSettings?.pinnedEditorVersions &&
594622
new Map<string, string>(Object.entries(orgSettings.pinnedEditorVersions))
@@ -602,7 +630,7 @@ export function CreateWorkspacePage() {
602630

603631
<InputField error={errorWsClass}>
604632
<SelectWorkspaceClassComponent
605-
selectedConfigurationId={selectedProjectID}
633+
selectedConfigurationId={selectedProjectID ?? undefined}
606634
onSelectionChange={setSelectedWsClass}
607635
setError={setErrorWsClass}
608636
selectedWorkspaceClass={selectedWsClass}
@@ -886,3 +914,15 @@ export function LimitReachedModal(p: { children: ReactNode }) {
886914
</Modal>
887915
);
888916
}
917+
918+
export const parseAsUUIDv4 = createParser({
919+
parse(queryValue) {
920+
if (validateUUID(queryValue)) {
921+
return queryValue;
922+
}
923+
return null;
924+
},
925+
serialize(value) {
926+
return value;
927+
},
928+
});

components/dashboard/src/workspaces/Workspaces.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,11 @@ const WorkspacesPage: FunctionComponent = () => {
275275
return (
276276
<Card
277277
key={repo.url}
278-
href={`/new#${repo.url}`}
278+
href={
279+
repo.configurationId
280+
? `/new?configurationId=${repo.configurationId}`
281+
: `/new#${repo.url}`
282+
}
279283
className={cn(
280284
"border-[0.5px] hover:bg-pk-surface-tertiary transition-colors w-full",
281285
{

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11198,6 +11198,11 @@ minizlib@^2.1.1:
1119811198
minipass "^3.0.0"
1119911199
yallist "^4.0.0"
1120011200

11201+
mitt@^3.0.1:
11202+
version "3.0.1"
11203+
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
11204+
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
11205+
1120111206
mkdirp@^1.0.3, mkdirp@^1.0.4:
1120211207
version "1.0.4"
1120311208
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
@@ -11487,6 +11492,13 @@ nth-check@^2.0.0:
1148711492
dependencies:
1148811493
boolbase "^1.0.0"
1148911494

11495+
nuqs@^2.3.2:
11496+
version "2.3.2"
11497+
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.3.2.tgz#0a38ff772f20cadf48caac486527ffad5ca99c7e"
11498+
integrity sha512-WeG78r8e3a30JY3P8npldvNiAZwGIk499lnpeRs3UYA3PpSvs2/PLunKGgjuF/JMw4BOowD3K2xgGEOZ3PeODA==
11499+
dependencies:
11500+
mitt "^3.0.1"
11501+
1149011502
nwsapi@^2.2.0:
1149111503
version "2.2.0"
1149211504
resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz"

0 commit comments

Comments
 (0)