Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organization onboarding welcome message #20577

Merged
merged 6 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading