Skip to content

Commit

Permalink
Organization onboarding welcome message (#20577)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
filiptronicek and geropl authored Feb 14, 2025
1 parent fff49d6 commit 04f590d
Show file tree
Hide file tree
Showing 33 changed files with 4,666 additions and 1,102 deletions.
1 change: 1 addition & 0 deletions components/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"react-focus-on": "^3.8.1",
"react-intl-tel-input": "^8.2.0",
"react-linkedin-login-oauth2": "^2.0.1",
"react-markdown": "^9.0.3",
"react-popper": "^2.3.0",
"react-portal": "^4.2.2",
"react-router-dom": "^5.2.0",
Expand Down
4 changes: 3 additions & 1 deletion components/dashboard/src/components/forms/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ type Props = {
topMargin?: boolean;
className?: string;
disabled?: boolean;
labelHidden?: boolean;
};

export const InputField: FunctionComponent<Props> = memo(
({ label, id, hint, error, topMargin = true, className, children, disabled = false }) => {
({ label, id, hint, error, topMargin = true, className, children, disabled = false, labelHidden = false }) => {
return (
<div className={cn("flex flex-col space-y-2", { "mt-4": topMargin }, className)}>
{label && (
<label
className={cn(
"text-md font-semibold",
{ "sr-only": labelHidden },
disabled
? "text-gray-400 dark:text-gray-400"
: error
Expand Down
27 changes: 27 additions & 0 deletions components/dashboard/src/components/podkit/forms/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2025 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import * as React from "react";

import { cn } from "@podkit/lib/cn";

const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"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",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";

export { Textarea };
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,16 @@ import { useMutation } from "@tanstack/react-query";
import { useOrgSettingsQueryInvalidator } from "./org-settings-query";
import { useCurrentOrg } from "./orgs-query";
import { organizationClient } from "../../service/public-api";
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
import {
OrganizationSettings,
UpdateOrganizationSettingsRequest,
} from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
import { ErrorCode } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { useOrgWorkspaceClassesQueryInvalidator } from "./org-workspace-classes-query";
import { PlainMessage } from "@bufbuild/protobuf";
import { useOrgRepoSuggestionsInvalidator } from "./suggested-repositories-query";
import { PartialMessage } from "@bufbuild/protobuf";

type UpdateOrganizationSettingsArgs = Partial<
Pick<
PlainMessage<OrganizationSettings>,
| "workspaceSharingDisabled"
| "defaultWorkspaceImage"
| "allowedWorkspaceClasses"
| "pinnedEditorVersions"
| "restrictedEditorNames"
| "defaultRole"
| "timeoutSettings"
| "roleRestrictions"
| "maxParallelRunningWorkspaces"
| "onboardingSettings"
| "annotateGitCommits"
>
>;
export type UpdateOrganizationSettingsArgs = PartialMessage<UpdateOrganizationSettingsRequest>;

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

return useMutation<OrganizationSettings, Error, UpdateOrganizationSettingsArgs>({
mutationFn: async ({
workspaceSharingDisabled,
defaultWorkspaceImage,
allowedWorkspaceClasses,
pinnedEditorVersions,
restrictedEditorNames,
defaultRole,
timeoutSettings,
roleRestrictions,
maxParallelRunningWorkspaces,
onboardingSettings,
annotateGitCommits,
}) => {
const settings = await organizationClient.updateOrganizationSettings({
organizationId,
workspaceSharingDisabled: workspaceSharingDisabled ?? false,
defaultWorkspaceImage,
allowedWorkspaceClasses,
updatePinnedEditorVersions: !!pinnedEditorVersions,
pinnedEditorVersions,
restrictedEditorNames,
updateRestrictedEditorNames: !!restrictedEditorNames,
defaultRole,
timeoutSettings,
roleRestrictions,
updateRoleRestrictions: !!roleRestrictions,
maxParallelRunningWorkspaces,
onboardingSettings,
annotateGitCommits,
});
mutationFn: async (partialUpdate) => {
const update: PartialMessage<UpdateOrganizationSettingsRequest> = {
...partialUpdate,
};
update.organizationId = organizationId;
update.updatePinnedEditorVersions = update.pinnedEditorVersions !== undefined;
update.updateRestrictedEditorNames = update.restrictedEditorNames !== undefined;
update.updateRoleRestrictions = update.roleRestrictions !== undefined;
update.updateAllowedWorkspaceClasses = update.allowedWorkspaceClasses !== undefined;
if (update.onboardingSettings) {
update.onboardingSettings.updateRecommendedRepositories =
!!update.onboardingSettings.recommendedRepositories;
if (update.onboardingSettings.welcomeMessage) {
update.onboardingSettings.welcomeMessage.featuredMemberResolvedAvatarUrl = undefined; // This field is not allowed to be set in the request.
}
}

const settings = await organizationClient.updateOrganizationSettings(update);
return settings.settings!;
},
onSuccess: () => {
Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
margin: 0 auto !important;
}
p {
@apply text-sm text-gray-400 dark:text-gray-600;
@apply text-sm text-pk-content-secondary;
}

.app-container {
Expand All @@ -118,7 +118,7 @@
}

code {
@apply bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded-md text-sm font-mono font-medium;
@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;
}

textarea,
Expand Down
7 changes: 3 additions & 4 deletions components/dashboard/src/login/SSOLoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ import { useOnboardingState } from "../dedicated-setup/use-needs-setup";
import { getOrgSlugFromQuery } from "../data/organizations/orgs-query";
import { storageAvailable } from "../utils";

type Props = {
onSuccess: () => void;
};

function getOrgSlugFromPath(path: string) {
// '/login/acme' => ['', 'login', 'acme']
const pathSegments = path.split("/");
Expand All @@ -29,6 +25,9 @@ function getOrgSlugFromPath(path: string) {
return pathSegments[2];
}

type Props = {
onSuccess: () => void;
};
export const SSOLoginForm: FC<Props> = ({ onSuccess }) => {
const location = useLocation();
const { data: onboardingState } = useOnboardingState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const ManageRepoSuggestion: FC<Props> = ({ configuration }) => {
await updateTeamSettings.mutateAsync(
{
onboardingSettings: {
...orgSettings?.onboardingSettings,
recommendedRepositories: [...newRepositories],
},
},
Expand All @@ -47,7 +46,7 @@ export const ManageRepoSuggestion: FC<Props> = ({ configuration }) => {
},
);
},
[orgSettings?.onboardingSettings, toast, updateTeamSettings],
[orgSettings?.onboardingSettings?.recommendedRepositories, toast, updateTeamSettings],
);

const isSuggested = orgSettings?.onboardingSettings?.recommendedRepositories?.includes(configuration.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export class JsonRpcOrganizationClient implements PromiseClient<typeof Organizat
...update,
defaultRole: request.defaultRole as OrgMemberRole,
timeoutSettings: {
inactivity: converter.toDurationString(request.timeoutSettings?.inactivity),
inactivity: converter.toDurationStringOpt(request.timeoutSettings?.inactivity),
denyUserTimeouts: request.timeoutSettings?.denyUserTimeouts,
},
roleRestrictions,
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/start/StartWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function parseParameters(search?: string): { notFound?: boolean } {

export interface StartWorkspaceState {
/**
* This is set to the istanceId we started (think we started on).
* This is set to the instanceId we started (think we started on).
* We only receive updates for this particular instance, or none if not set.
*/
startedInstanceId?: string;
Expand Down
23 changes: 15 additions & 8 deletions components/dashboard/src/teams/TeamOnboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
* See License.AGPL.txt in the project root for license information.
*/

import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
import { FormEvent, useCallback, useEffect, useState } from "react";
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
import { useIsOwner } from "../data/organizations/members-query";
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
import { useCurrentOrg } from "../data/organizations/orgs-query";
import { useUpdateOrgSettingsMutation } from "../data/organizations/update-org-settings-mutation";
import {
UpdateOrganizationSettingsArgs,
useUpdateOrgSettingsMutation,
} from "../data/organizations/update-org-settings-mutation";
import { OrgSettingsPage } from "./OrgSettingsPage";
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
import { useDocumentTitle } from "../hooks/use-document-title";
import { useToast } from "../components/toasts/Toasts";
import type { PlainMessage } from "@bufbuild/protobuf";
import { InputField } from "../components/forms/InputField";
import { TextInput } from "../components/forms/TextInputField";
import { LoadingButton } from "@podkit/buttons/LoadingButton";
Expand All @@ -24,11 +25,16 @@ import { useOrgSuggestedRepos } from "../data/organizations/suggested-repositori
import { RepositoryListItem } from "../repositories/list/RepoListItem";
import { LoadingState } from "@podkit/loading/LoadingState";
import { Table, TableHeader, TableRow, TableHead, TableBody } from "@podkit/tables/Table";
import { WelcomeMessageConfigurationField } from "./onboarding/WelcomeMessageConfigurationField";

export type UpdateTeamSettingsOptions = {
throwMutateError?: boolean;
};

export default function TeamOnboardingPage() {
useDocumentTitle("Organization Settings - Onboarding");
const { toast } = useToast();
const org = useCurrentOrg().data;
const { data: org } = useCurrentOrg();
const isOwner = useIsOwner();

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

const handleUpdateTeamSettings = useCallback(
async (newSettings: Partial<PlainMessage<OrganizationSettings>>, options?: { throwMutateError?: boolean }) => {
async (newSettings: UpdateOrganizationSettingsArgs, options?: UpdateTeamSettingsOptions) => {
if (!org?.id) {
throw new Error("no organization selected");
}
Expand Down Expand Up @@ -70,11 +76,10 @@ export default function TeamOnboardingPage() {
await handleUpdateTeamSettings({
onboardingSettings: {
internalLink,
recommendedRepositories: settings?.onboardingSettings?.recommendedRepositories ?? [],
},
});
},
[handleUpdateTeamSettings, internalLink, settings?.onboardingSettings?.recommendedRepositories],
[handleUpdateTeamSettings, internalLink],
);

useEffect(() => {
Expand Down Expand Up @@ -146,7 +151,7 @@ export default function TeamOnboardingPage() {
</TableRow>
</TableHeader>
<TableBody>
{(suggestedRepos ?? []).map((repo) => (
{suggestedRepos?.map((repo) => (
<RepositoryListItem
key={repo.configurationId}
configuration={repo.configuration}
Expand All @@ -157,6 +162,8 @@ export default function TeamOnboardingPage() {
</Table>
)}
</ConfigurationSettingsField>

<WelcomeMessageConfigurationField handleUpdateTeamSettings={handleUpdateTeamSettings} />
</div>
</OrgSettingsPage>
);
Expand Down
11 changes: 4 additions & 7 deletions components/dashboard/src/teams/TeamPolicies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ export default function TeamPoliciesPage() {
throw new Error("no organization settings change permission");
}
try {
await updateTeamSettings.mutateAsync({
...settings,
...newSettings,
});
await updateTeamSettings.mutateAsync(newSettings);
setWorkspaceTimeoutSettingError(undefined);
toast("Organization settings updated");
} catch (error) {
Expand All @@ -69,7 +66,7 @@ export default function TeamPoliciesPage() {
console.error(error);
}
},
[updateTeamSettings, org?.id, isOwner, settings, toast],
[updateTeamSettings, org?.id, isOwner, toast],
);

useEffect(() => {
Expand Down Expand Up @@ -100,7 +97,7 @@ export default function TeamPoliciesPage() {

handleUpdateTeamSettings({
timeoutSettings: {
inactivity: workspaceTimeout ? converter.toDuration(workspaceTimeout) : undefined,
inactivity: converter.toDurationOpt(workspaceTimeout),
denyUserTimeouts: !allowTimeoutChangeByMembers,
},
});
Expand Down Expand Up @@ -185,7 +182,7 @@ export default function TeamPoliciesPage() {
!isOwner ||
!isPaidOrDedicated ||
(workspaceTimeout ===
converter.toDurationString(settings?.timeoutSettings?.inactivity) &&
converter.toDurationStringOpt(settings?.timeoutSettings?.inactivity) &&
allowTimeoutChangeByMembers === !settings?.timeoutSettings?.denyUserTimeouts)
}
>
Expand Down
Loading

0 comments on commit 04f590d

Please sign in to comment.