Skip to content

Commit 04f590d

Browse files
Organization onboarding welcome message (#20577)
* squashed (- oidc/newUser) Tool: gitpod/catfood.gitpod.cloud * [server, db] Cleanup UpdateOrgSettings API handling Tool: gitpod/catfood.gitpod.cloud * [dashboard] Render WelcomeMessage based on a) user.createdAt and b) localStorage Tool: gitpod/catfood.gitpod.cloud * [api, server] Add missing update_allowed_workspace_classes field Tool: gitpod/catfood.gitpod.cloud * [dashboard] Fix updateOrgSettings API usage Tool: gitpod/catfood.gitpod.cloud * [dashboard, server] Fix duration handling/conversion Tool: gitpod/catfood.gitpod.cloud --------- Co-authored-by: Gero Posmyk-Leinemann <[email protected]>
1 parent fff49d6 commit 04f590d

33 files changed

+4666
-1102
lines changed

components/dashboard/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"react-focus-on": "^3.8.1",
5454
"react-intl-tel-input": "^8.2.0",
5555
"react-linkedin-login-oauth2": "^2.0.1",
56+
"react-markdown": "^9.0.3",
5657
"react-popper": "^2.3.0",
5758
"react-portal": "^4.2.2",
5859
"react-router-dom": "^5.2.0",

components/dashboard/src/components/forms/InputField.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ type Props = {
1616
topMargin?: boolean;
1717
className?: string;
1818
disabled?: boolean;
19+
labelHidden?: boolean;
1920
};
2021

2122
export const InputField: FunctionComponent<Props> = memo(
22-
({ label, id, hint, error, topMargin = true, className, children, disabled = false }) => {
23+
({ label, id, hint, error, topMargin = true, className, children, disabled = false, labelHidden = false }) => {
2324
return (
2425
<div className={cn("flex flex-col space-y-2", { "mt-4": topMargin }, className)}>
2526
{label && (
2627
<label
2728
className={cn(
2829
"text-md font-semibold",
30+
{ "sr-only": labelHidden },
2931
disabled
3032
? "text-gray-400 dark:text-gray-400"
3133
: error
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright (c) 2025 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import * as React from "react";
8+
9+
import { cn } from "@podkit/lib/cn";
10+
11+
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
12+
({ className, ...props }, ref) => {
13+
return (
14+
<textarea
15+
className={cn(
16+
"flex min-h-[80px] w-full rounded-md border border-input bg-pk-surface-primary px-3 py-2 text-base ring-offset-background placeholder:text-pk-content-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
17+
className,
18+
)}
19+
ref={ref}
20+
{...props}
21+
/>
22+
);
23+
},
24+
);
25+
Textarea.displayName = "Textarea";
26+
27+
export { Textarea };

components/dashboard/src/data/organizations/update-org-settings-mutation.ts

+24-48
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,16 @@ import { useMutation } from "@tanstack/react-query";
88
import { useOrgSettingsQueryInvalidator } from "./org-settings-query";
99
import { useCurrentOrg } from "./orgs-query";
1010
import { organizationClient } from "../../service/public-api";
11-
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
11+
import {
12+
OrganizationSettings,
13+
UpdateOrganizationSettingsRequest,
14+
} from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
1215
import { ErrorCode } from "@gitpod/gitpod-protocol/lib/messaging/error";
1316
import { useOrgWorkspaceClassesQueryInvalidator } from "./org-workspace-classes-query";
14-
import { PlainMessage } from "@bufbuild/protobuf";
1517
import { useOrgRepoSuggestionsInvalidator } from "./suggested-repositories-query";
18+
import { PartialMessage } from "@bufbuild/protobuf";
1619

17-
type UpdateOrganizationSettingsArgs = Partial<
18-
Pick<
19-
PlainMessage<OrganizationSettings>,
20-
| "workspaceSharingDisabled"
21-
| "defaultWorkspaceImage"
22-
| "allowedWorkspaceClasses"
23-
| "pinnedEditorVersions"
24-
| "restrictedEditorNames"
25-
| "defaultRole"
26-
| "timeoutSettings"
27-
| "roleRestrictions"
28-
| "maxParallelRunningWorkspaces"
29-
| "onboardingSettings"
30-
| "annotateGitCommits"
31-
>
32-
>;
20+
export type UpdateOrganizationSettingsArgs = PartialMessage<UpdateOrganizationSettingsRequest>;
3321

3422
export const useUpdateOrgSettingsMutation = () => {
3523
const org = useCurrentOrg().data;
@@ -39,36 +27,24 @@ export const useUpdateOrgSettingsMutation = () => {
3927
const organizationId = org?.id ?? "";
4028

4129
return useMutation<OrganizationSettings, Error, UpdateOrganizationSettingsArgs>({
42-
mutationFn: async ({
43-
workspaceSharingDisabled,
44-
defaultWorkspaceImage,
45-
allowedWorkspaceClasses,
46-
pinnedEditorVersions,
47-
restrictedEditorNames,
48-
defaultRole,
49-
timeoutSettings,
50-
roleRestrictions,
51-
maxParallelRunningWorkspaces,
52-
onboardingSettings,
53-
annotateGitCommits,
54-
}) => {
55-
const settings = await organizationClient.updateOrganizationSettings({
56-
organizationId,
57-
workspaceSharingDisabled: workspaceSharingDisabled ?? false,
58-
defaultWorkspaceImage,
59-
allowedWorkspaceClasses,
60-
updatePinnedEditorVersions: !!pinnedEditorVersions,
61-
pinnedEditorVersions,
62-
restrictedEditorNames,
63-
updateRestrictedEditorNames: !!restrictedEditorNames,
64-
defaultRole,
65-
timeoutSettings,
66-
roleRestrictions,
67-
updateRoleRestrictions: !!roleRestrictions,
68-
maxParallelRunningWorkspaces,
69-
onboardingSettings,
70-
annotateGitCommits,
71-
});
30+
mutationFn: async (partialUpdate) => {
31+
const update: PartialMessage<UpdateOrganizationSettingsRequest> = {
32+
...partialUpdate,
33+
};
34+
update.organizationId = organizationId;
35+
update.updatePinnedEditorVersions = update.pinnedEditorVersions !== undefined;
36+
update.updateRestrictedEditorNames = update.restrictedEditorNames !== undefined;
37+
update.updateRoleRestrictions = update.roleRestrictions !== undefined;
38+
update.updateAllowedWorkspaceClasses = update.allowedWorkspaceClasses !== undefined;
39+
if (update.onboardingSettings) {
40+
update.onboardingSettings.updateRecommendedRepositories =
41+
!!update.onboardingSettings.recommendedRepositories;
42+
if (update.onboardingSettings.welcomeMessage) {
43+
update.onboardingSettings.welcomeMessage.featuredMemberResolvedAvatarUrl = undefined; // This field is not allowed to be set in the request.
44+
}
45+
}
46+
47+
const settings = await organizationClient.updateOrganizationSettings(update);
7248
return settings.settings!;
7349
},
7450
onSuccess: () => {

components/dashboard/src/index.css

+2-2
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
margin: 0 auto !important;
9696
}
9797
p {
98-
@apply text-sm text-gray-400 dark:text-gray-600;
98+
@apply text-sm text-pk-content-secondary;
9999
}
100100

101101
.app-container {
@@ -118,7 +118,7 @@
118118
}
119119

120120
code {
121-
@apply bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded-md text-sm font-mono font-medium;
121+
@apply bg-gray-100 dark:bg-gray-800 text-pk-content-primary px-1.5 py-0.5 rounded-md text-sm font-mono font-medium;
122122
}
123123

124124
textarea,

components/dashboard/src/login/SSOLoginForm.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ import { useOnboardingState } from "../dedicated-setup/use-needs-setup";
1616
import { getOrgSlugFromQuery } from "../data/organizations/orgs-query";
1717
import { storageAvailable } from "../utils";
1818

19-
type Props = {
20-
onSuccess: () => void;
21-
};
22-
2319
function getOrgSlugFromPath(path: string) {
2420
// '/login/acme' => ['', 'login', 'acme']
2521
const pathSegments = path.split("/");
@@ -29,6 +25,9 @@ function getOrgSlugFromPath(path: string) {
2925
return pathSegments[2];
3026
}
3127

28+
type Props = {
29+
onSuccess: () => void;
30+
};
3231
export const SSOLoginForm: FC<Props> = ({ onSuccess }) => {
3332
const location = useLocation();
3433
const { data: onboardingState } = useOnboardingState();

components/dashboard/src/repositories/detail/general/ManageRepoSuggestion.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export const ManageRepoSuggestion: FC<Props> = ({ configuration }) => {
3636
await updateTeamSettings.mutateAsync(
3737
{
3838
onboardingSettings: {
39-
...orgSettings?.onboardingSettings,
4039
recommendedRepositories: [...newRepositories],
4140
},
4241
},
@@ -47,7 +46,7 @@ export const ManageRepoSuggestion: FC<Props> = ({ configuration }) => {
4746
},
4847
);
4948
},
50-
[orgSettings?.onboardingSettings, toast, updateTeamSettings],
49+
[orgSettings?.onboardingSettings?.recommendedRepositories, toast, updateTeamSettings],
5150
);
5251

5352
const isSuggested = orgSettings?.onboardingSettings?.recommendedRepositories?.includes(configuration.id);

components/dashboard/src/service/json-rpc-organization-client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export class JsonRpcOrganizationClient implements PromiseClient<typeof Organizat
273273
...update,
274274
defaultRole: request.defaultRole as OrgMemberRole,
275275
timeoutSettings: {
276-
inactivity: converter.toDurationString(request.timeoutSettings?.inactivity),
276+
inactivity: converter.toDurationStringOpt(request.timeoutSettings?.inactivity),
277277
denyUserTimeouts: request.timeoutSettings?.denyUserTimeouts,
278278
},
279279
roleRestrictions,

components/dashboard/src/start/StartWorkspace.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ function parseParameters(search?: string): { notFound?: boolean } {
8888

8989
export interface StartWorkspaceState {
9090
/**
91-
* This is set to the istanceId we started (think we started on).
91+
* This is set to the instanceId we started (think we started on).
9292
* We only receive updates for this particular instance, or none if not set.
9393
*/
9494
startedInstanceId?: string;

components/dashboard/src/teams/TeamOnboarding.tsx

+15-8
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
87
import { FormEvent, useCallback, useEffect, useState } from "react";
98
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
109
import { useIsOwner } from "../data/organizations/members-query";
1110
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
1211
import { useCurrentOrg } from "../data/organizations/orgs-query";
13-
import { useUpdateOrgSettingsMutation } from "../data/organizations/update-org-settings-mutation";
12+
import {
13+
UpdateOrganizationSettingsArgs,
14+
useUpdateOrgSettingsMutation,
15+
} from "../data/organizations/update-org-settings-mutation";
1416
import { OrgSettingsPage } from "./OrgSettingsPage";
1517
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
1618
import { useDocumentTitle } from "../hooks/use-document-title";
1719
import { useToast } from "../components/toasts/Toasts";
18-
import type { PlainMessage } from "@bufbuild/protobuf";
1920
import { InputField } from "../components/forms/InputField";
2021
import { TextInput } from "../components/forms/TextInputField";
2122
import { LoadingButton } from "@podkit/buttons/LoadingButton";
@@ -24,11 +25,16 @@ import { useOrgSuggestedRepos } from "../data/organizations/suggested-repositori
2425
import { RepositoryListItem } from "../repositories/list/RepoListItem";
2526
import { LoadingState } from "@podkit/loading/LoadingState";
2627
import { Table, TableHeader, TableRow, TableHead, TableBody } from "@podkit/tables/Table";
28+
import { WelcomeMessageConfigurationField } from "./onboarding/WelcomeMessageConfigurationField";
29+
30+
export type UpdateTeamSettingsOptions = {
31+
throwMutateError?: boolean;
32+
};
2733

2834
export default function TeamOnboardingPage() {
2935
useDocumentTitle("Organization Settings - Onboarding");
3036
const { toast } = useToast();
31-
const org = useCurrentOrg().data;
37+
const { data: org } = useCurrentOrg();
3238
const isOwner = useIsOwner();
3339

3440
const { data: settings } = useOrgSettingsQuery();
@@ -39,7 +45,7 @@ export default function TeamOnboardingPage() {
3945
const [internalLink, setInternalLink] = useState<string | undefined>(undefined);
4046

4147
const handleUpdateTeamSettings = useCallback(
42-
async (newSettings: Partial<PlainMessage<OrganizationSettings>>, options?: { throwMutateError?: boolean }) => {
48+
async (newSettings: UpdateOrganizationSettingsArgs, options?: UpdateTeamSettingsOptions) => {
4349
if (!org?.id) {
4450
throw new Error("no organization selected");
4551
}
@@ -70,11 +76,10 @@ export default function TeamOnboardingPage() {
7076
await handleUpdateTeamSettings({
7177
onboardingSettings: {
7278
internalLink,
73-
recommendedRepositories: settings?.onboardingSettings?.recommendedRepositories ?? [],
7479
},
7580
});
7681
},
77-
[handleUpdateTeamSettings, internalLink, settings?.onboardingSettings?.recommendedRepositories],
82+
[handleUpdateTeamSettings, internalLink],
7883
);
7984

8085
useEffect(() => {
@@ -146,7 +151,7 @@ export default function TeamOnboardingPage() {
146151
</TableRow>
147152
</TableHeader>
148153
<TableBody>
149-
{(suggestedRepos ?? []).map((repo) => (
154+
{suggestedRepos?.map((repo) => (
150155
<RepositoryListItem
151156
key={repo.configurationId}
152157
configuration={repo.configuration}
@@ -157,6 +162,8 @@ export default function TeamOnboardingPage() {
157162
</Table>
158163
)}
159164
</ConfigurationSettingsField>
165+
166+
<WelcomeMessageConfigurationField handleUpdateTeamSettings={handleUpdateTeamSettings} />
160167
</div>
161168
</OrgSettingsPage>
162169
);

components/dashboard/src/teams/TeamPolicies.tsx

+4-7
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,7 @@ export default function TeamPoliciesPage() {
5555
throw new Error("no organization settings change permission");
5656
}
5757
try {
58-
await updateTeamSettings.mutateAsync({
59-
...settings,
60-
...newSettings,
61-
});
58+
await updateTeamSettings.mutateAsync(newSettings);
6259
setWorkspaceTimeoutSettingError(undefined);
6360
toast("Organization settings updated");
6461
} catch (error) {
@@ -69,7 +66,7 @@ export default function TeamPoliciesPage() {
6966
console.error(error);
7067
}
7168
},
72-
[updateTeamSettings, org?.id, isOwner, settings, toast],
69+
[updateTeamSettings, org?.id, isOwner, toast],
7370
);
7471

7572
useEffect(() => {
@@ -100,7 +97,7 @@ export default function TeamPoliciesPage() {
10097

10198
handleUpdateTeamSettings({
10299
timeoutSettings: {
103-
inactivity: workspaceTimeout ? converter.toDuration(workspaceTimeout) : undefined,
100+
inactivity: converter.toDurationOpt(workspaceTimeout),
104101
denyUserTimeouts: !allowTimeoutChangeByMembers,
105102
},
106103
});
@@ -185,7 +182,7 @@ export default function TeamPoliciesPage() {
185182
!isOwner ||
186183
!isPaidOrDedicated ||
187184
(workspaceTimeout ===
188-
converter.toDurationString(settings?.timeoutSettings?.inactivity) &&
185+
converter.toDurationStringOpt(settings?.timeoutSettings?.inactivity) &&
189186
allowTimeoutChangeByMembers === !settings?.timeoutSettings?.denyUserTimeouts)
190187
}
191188
>

0 commit comments

Comments
 (0)