Skip to content

Commit

Permalink
Repo suggestions improvements (#20582)
Browse files Browse the repository at this point in the history
* center view button

Tool: gitpod/catfood.gitpod.cloud

* Small drive-by for insights export toast

Tool: gitpod/catfood.gitpod.cloud

* Colored suggestions

Tool: gitpod/catfood.gitpod.cloud

* Remove suggested repository management from repository list

Tool: gitpod/catfood.gitpod.cloud

* Add recommended list to getting started

Tool: gitpod/catfood.gitpod.cloud

* fix copy

Tool: gitpod/catfood.gitpod.cloud
  • Loading branch information
filiptronicek authored Feb 7, 2025
1 parent 58c9a18 commit ba2f367
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 127 deletions.
28 changes: 18 additions & 10 deletions components/dashboard/src/components/RepositoryFinder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@ import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provide
import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg";
import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import { SuggestedRepository } from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
import { PREDEFINED_REPOS } from "../data/git-providers/predefined-repos";
import { PREDEFINED_REPOS, PredefinedRepo } from "../data/git-providers/predefined-repos";
import { useConfiguration, useListConfigurations } from "../data/configurations/configuration-queries";
import { useUserLoader } from "../hooks/use-user-loader";
import { conjunctScmProviders, getDeduplicatedScmProviders } from "../utils";
import { cn } from "@podkit/lib/cn";
import { useOrgSuggestedRepos } from "../data/organizations/suggested-repositories-query";
import { toRemoteURL } from "../projects/render-utils";

type PredefinedRepoOption = typeof PREDEFINED_REPOS[number];
const isPredefined = (repo: SuggestedRepository | PredefinedRepoOption): boolean => {
const isPredefined = (repo: SuggestedRepository | PredefinedRepo): boolean => {
return (
PREDEFINED_REPOS.some((predefined) => predefined.url === repo.url) &&
!(repo as SuggestedRepository).configurationId
Expand All @@ -41,7 +40,7 @@ const resolveIcon = (contextUrl?: string): string => {
};

type PredefinedRepositoryOptionProps = {
repo: PredefinedRepoOption;
repo: PredefinedRepo;
};
const PredefinedRepositoryOption: FC<PredefinedRepositoryOptionProps> = ({ repo }) => {
const prettyUrl = toRemoteURL(repo.url);
Expand All @@ -50,7 +49,12 @@ const PredefinedRepositoryOption: FC<PredefinedRepositoryOptionProps> = ({ repo
return (
<div className="flex flex-col overflow-hidden" aria-label={`Demo: ${repo.url}`}>
<div className="flex items-center">
<img className={cn("w-5 mr-2 text-pk-content-secondary")} src={icon} alt="" />
{repo.configurationId ? (
<RepositoryIcon className={cn("w-5 mr-2 text-kumquat-ripe")} />
) : (
<img className={cn("w-5 mr-2 text-pk-content-secondary")} src={icon} alt="" />
)}

<span className="text-sm font-semibold">{repo.repoName}</span>
<MiddleDot className="px-0.5 text-pk-content-secondary" />
<span
Expand Down Expand Up @@ -280,6 +284,7 @@ export default function RepositoryFinder({
url: repo.url,
repoName: repo.repoName,
description: "",
configurationId: repo.configurationId,
}));
}

Expand Down Expand Up @@ -317,11 +322,14 @@ export default function RepositoryFinder({
repo.url.toLowerCase().includes(searchString.toLowerCase()) ||
repo.repoName.toLowerCase().includes(searchString.toLowerCase())
) {
result.push({
id: repo.url,
element: <PredefinedRepositoryOption repo={repo} />,
isSelectable: true,
});
const alreadyPresent = result.find((r) => r.id === repo.configurationId);
if (!alreadyPresent) {
result.push({
id: repo.url,
element: <PredefinedRepositoryOption repo={repo} />,
isSelectable: true,
});
}
}
});
}
Expand Down
13 changes: 12 additions & 1 deletion components/dashboard/src/data/git-providers/predefined-repos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@
* See License.AGPL.txt in the project root for license information.
*/

export const PREDEFINED_REPOS = [
export type PredefinedRepo = {
url: string;
repoName: string;
description: string;
/**
* The configuration ID of the repository.
* This is only set for org-recommended repos.
*/
configurationId?: string;
};

export const PREDEFINED_REPOS: PredefinedRepo[] = [
{
url: "https://github.com/gitpod-demos/voting-app",
repoName: "demo-docker",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export const DownloadInsightsToast = ({ organizationId, from, to, organizationNa
return (
<div>
<span>Preparing usage export</span>
Exporting page {progress}
<br />
<span className="text-sm">Exporting page {progress}</span>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FC } from "react";
import { ConfigurationNameForm } from "./general/ConfigurationName";
import { RemoveConfiguration } from "./general/RemoveConfiguration";
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { ManageRepoSuggestion } from "./general/ManageRepoSuggestion";

type Props = {
configuration: Configuration;
Expand All @@ -16,6 +17,7 @@ export const ConfigurationDetailGeneral: FC<Props> = ({ configuration }) => {
return (
<>
<ConfigurationNameForm configuration={configuration} />
<ManageRepoSuggestion configuration={configuration} />
<RemoveConfiguration configuration={configuration} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* 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 { SwitchInputField } from "@podkit/switch/Switch";
import { Heading3, Subheading } from "@podkit/typography/Headings";
import { FC, useCallback } from "react";
import { InputField } from "../../../components/forms/InputField";
import PillLabel from "../../../components/PillLabel";
import { useToast } from "../../../components/toasts/Toasts";
import { useOrgSettingsQuery } from "../../../data/organizations/org-settings-query";
import { useUpdateOrgSettingsMutation } from "../../../data/organizations/update-org-settings-mutation";
import { useId } from "../../../hooks/useId";
import { ConfigurationSettingsField } from "../ConfigurationSettingsField";
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { SquareArrowOutUpRight } from "lucide-react";

type Props = {
configuration: Configuration;
};
export const ManageRepoSuggestion: FC<Props> = ({ configuration }) => {
const { data: orgSettings } = useOrgSettingsQuery();
const { toast } = useToast();
const updateTeamSettings = useUpdateOrgSettingsMutation();
const updateRecommendedRepository = useCallback(
async (configurationId: string, suggested: boolean) => {
const newRepositories = new Set(orgSettings?.onboardingSettings?.recommendedRepositories ?? []);
if (suggested) {
newRepositories.add(configurationId);
} else {
newRepositories.delete(configurationId);
}

await updateTeamSettings.mutateAsync(
{
onboardingSettings: {
...orgSettings?.onboardingSettings,
recommendedRepositories: [...newRepositories],
},
},
{
onError: (error) => {
toast(`Failed to update recommended repositories: ${error.message}`);
},
},
);
},
[orgSettings?.onboardingSettings, toast, updateTeamSettings],
);

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

const inputId = useId({ prefix: "suggested-repository" });

return (
<ConfigurationSettingsField>
<Heading3 className="flex flex-row items-center gap-2">
Mark this repository as{" "}
<PillLabel className="capitalize bg-kumquat-light shrink-0 text-sm hidden xl:block" type="warn">
Suggested
</PillLabel>
</Heading3>
<Subheading className="max-w-lg flex flex-col gap-2">
The Suggested section highlights recommended repositories on the dashboard for new members, making it
easier to find and start working on key projects in Gitpod.
<a
className="gp-link flex flex-row items-center gap-1"
href="https://www.gitpod.io/docs/configure/orgs/onboarding#suggested-repositories"
target="_blank"
rel="noreferrer"
>
Learn about suggestions
<SquareArrowOutUpRight size={12} />
</a>
</Subheading>
<InputField id={inputId}>
<SwitchInputField
id={inputId}
checked={isSuggested}
disabled={updateTeamSettings.isLoading}
onCheckedChange={(checked) => {
updateRecommendedRepository(configuration.id, checked);
}}
label={isSuggested ? "Listed in “Suggested”" : "Not listed in “Suggested”"}
/>
</InputField>
</ConfigurationSettingsField>
);
};
50 changes: 3 additions & 47 deletions components/dashboard/src/repositories/list/RepoListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,15 @@ import { TextMuted } from "@podkit/typography/TextMuted";
import { Text } from "@podkit/typography/Text";
import { LinkButton } from "@podkit/buttons/LinkButton";
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { AlertTriangleIcon, CheckCircle2Icon, SquareArrowOutUpRight, Ellipsis } from "lucide-react";
import { AlertTriangleIcon, CheckCircle2Icon } from "lucide-react";
import { TableCell, TableRow } from "@podkit/tables/Table";
import { Button } from "@podkit/buttons/Button";
import {
DropdownLinkMenuItem,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@podkit/dropdown/DropDown";
import PillLabel from "../../components/PillLabel";

type Props = {
configuration: Configuration;
isSuggested: boolean;
handleModifySuggestedRepository?: (configurationId: string, suggested: boolean) => void;
};
export const RepositoryListItem: FC<Props> = ({ configuration, isSuggested, handleModifySuggestedRepository }) => {
export const RepositoryListItem: FC<Props> = ({ configuration, isSuggested }) => {
const url = usePrettyRepoURL(configuration.cloneUrl);
const prebuildsEnabled = !!configuration.prebuildSettings?.enabled;
const created =
Expand Down Expand Up @@ -73,45 +64,10 @@ export const RepositoryListItem: FC<Props> = ({ configuration, isSuggested, hand
</div>
</TableCell>

<TableCell className="flex items-center gap-4">
<TableCell>
<LinkButton href={`/repositories/${configuration.id}`} variant="secondary">
View
</LinkButton>
{handleModifySuggestedRepository && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">
<Ellipsis size={20} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52">
{isSuggested ? (
<DropdownMenuItem
onClick={() => handleModifySuggestedRepository(configuration.id, false)}
>
Remove from suggested repos
</DropdownMenuItem>
) : (
<>
<DropdownMenuItem
onClick={() => handleModifySuggestedRepository(configuration.id, true)}
>
Add to suggested repos
</DropdownMenuItem>
<DropdownLinkMenuItem
href="https://www.gitpod.io/docs/configure/orgs/onboarding#suggested-repositories"
className="gap-1 text-xs"
target="_blank"
rel="noreferrer"
>
Learn about suggestions
<SquareArrowOutUpRight size={12} />
</DropdownLinkMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</TableCell>
</TableRow>
);
Expand Down
27 changes: 0 additions & 27 deletions components/dashboard/src/repositories/list/RepositoryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ import { SortableTableHead, TableSortOrder } from "@podkit/tables/SortableTable"
import { LoadingState } from "@podkit/loading/LoadingState";
import { Button } from "@podkit/buttons/Button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@podkit/select/Select";
import { useUpdateOrgSettingsMutation } from "../../data/organizations/update-org-settings-mutation";
import { useOrgSettingsQuery } from "../../data/organizations/org-settings-query";
import { useToast } from "../../components/toasts/Toasts";

type Props = {
configurations: Configuration[];
Expand Down Expand Up @@ -54,31 +52,7 @@ export const RepositoryTable: FC<Props> = ({
onLoadNextPage,
onSort,
}) => {
const updateTeamSettings = useUpdateOrgSettingsMutation();
const { data: settings } = useOrgSettingsQuery();
const { toast } = useToast();

const updateRecommendedRepository = async (configurationId: string, suggested: boolean) => {
const newRepositories = new Set(settings?.onboardingSettings?.recommendedRepositories ?? []);
if (suggested) {
newRepositories.add(configurationId);
} else {
newRepositories.delete(configurationId);
}

await updateTeamSettings.mutateAsync(
{
onboardingSettings: {
recommendedRepositories: [...newRepositories],
},
},
{
onError: (error) => {
toast(`Failed to update recommended repositories: ${error.message}`);
},
},
);
};

return (
<>
Expand Down Expand Up @@ -156,7 +130,6 @@ export const RepositoryTable: FC<Props> = ({
configuration.id,
) ?? false
}
handleModifySuggestedRepository={updateRecommendedRepository}
/>
);
})}
Expand Down
8 changes: 4 additions & 4 deletions components/dashboard/src/teams/TeamOnboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,13 @@ export default function TeamOnboardingPage() {
<ConfigurationSettingsField>
<Heading3>Suggested repositories</Heading3>
<Subheading>
A list of repositories suggested to new organization members. To manage recommended
repositories, visit the{" "}
A list of repositories suggested to new organization members. You can toggle a repository's
visibility in the onboarding process by visiting the{" "}
<Link to="/repositories" className="gp-link">
Repository settings
</Link>{" "}
page and add / remove repositories from the list using the context menu on the corresponding
repository's row.
page and toggling the "Mark this repository as Suggested" setting under the details of the
repository.
</Subheading>
{(suggestedRepos ?? []).length > 0 && (
<Table className="mt-4">
Expand Down
Loading

0 comments on commit ba2f367

Please sign in to comment.