Skip to content

Commit ba2f367

Browse files
Repo suggestions improvements (#20582)
* 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
1 parent 58c9a18 commit ba2f367

File tree

9 files changed

+239
-127
lines changed

9 files changed

+239
-127
lines changed

components/dashboard/src/components/RepositoryFinder.tsx

+18-10
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,15 @@ import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provide
1919
import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg";
2020
import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
2121
import { SuggestedRepository } from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
22-
import { PREDEFINED_REPOS } from "../data/git-providers/predefined-repos";
22+
import { PREDEFINED_REPOS, PredefinedRepo } from "../data/git-providers/predefined-repos";
2323
import { useConfiguration, useListConfigurations } from "../data/configurations/configuration-queries";
2424
import { useUserLoader } from "../hooks/use-user-loader";
2525
import { conjunctScmProviders, getDeduplicatedScmProviders } from "../utils";
2626
import { cn } from "@podkit/lib/cn";
2727
import { useOrgSuggestedRepos } from "../data/organizations/suggested-repositories-query";
2828
import { toRemoteURL } from "../projects/render-utils";
2929

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

4342
type PredefinedRepositoryOptionProps = {
44-
repo: PredefinedRepoOption;
43+
repo: PredefinedRepo;
4544
};
4645
const PredefinedRepositoryOption: FC<PredefinedRepositoryOptionProps> = ({ repo }) => {
4746
const prettyUrl = toRemoteURL(repo.url);
@@ -50,7 +49,12 @@ const PredefinedRepositoryOption: FC<PredefinedRepositoryOptionProps> = ({ repo
5049
return (
5150
<div className="flex flex-col overflow-hidden" aria-label={`Demo: ${repo.url}`}>
5251
<div className="flex items-center">
53-
<img className={cn("w-5 mr-2 text-pk-content-secondary")} src={icon} alt="" />
52+
{repo.configurationId ? (
53+
<RepositoryIcon className={cn("w-5 mr-2 text-kumquat-ripe")} />
54+
) : (
55+
<img className={cn("w-5 mr-2 text-pk-content-secondary")} src={icon} alt="" />
56+
)}
57+
5458
<span className="text-sm font-semibold">{repo.repoName}</span>
5559
<MiddleDot className="px-0.5 text-pk-content-secondary" />
5660
<span
@@ -280,6 +284,7 @@ export default function RepositoryFinder({
280284
url: repo.url,
281285
repoName: repo.repoName,
282286
description: "",
287+
configurationId: repo.configurationId,
283288
}));
284289
}
285290

@@ -317,11 +322,14 @@ export default function RepositoryFinder({
317322
repo.url.toLowerCase().includes(searchString.toLowerCase()) ||
318323
repo.repoName.toLowerCase().includes(searchString.toLowerCase())
319324
) {
320-
result.push({
321-
id: repo.url,
322-
element: <PredefinedRepositoryOption repo={repo} />,
323-
isSelectable: true,
324-
});
325+
const alreadyPresent = result.find((r) => r.id === repo.configurationId);
326+
if (!alreadyPresent) {
327+
result.push({
328+
id: repo.url,
329+
element: <PredefinedRepositoryOption repo={repo} />,
330+
isSelectable: true,
331+
});
332+
}
325333
}
326334
});
327335
}

components/dashboard/src/data/git-providers/predefined-repos.ts

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

7-
export const PREDEFINED_REPOS = [
7+
export type PredefinedRepo = {
8+
url: string;
9+
repoName: string;
10+
description: string;
11+
/**
12+
* The configuration ID of the repository.
13+
* This is only set for org-recommended repos.
14+
*/
15+
configurationId?: string;
16+
};
17+
18+
export const PREDEFINED_REPOS: PredefinedRepo[] = [
819
{
920
url: "https://github.com/gitpod-demos/voting-app",
1021
repoName: "demo-docker",

components/dashboard/src/insights/download/DownloadInsights.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export const DownloadInsightsToast = ({ organizationId, from, to, organizationNa
5353
return (
5454
<div>
5555
<span>Preparing usage export</span>
56-
Exporting page {progress}
56+
<br />
57+
<span className="text-sm">Exporting page {progress}</span>
5758
</div>
5859
);
5960
}

components/dashboard/src/repositories/detail/ConfigurationDetailGeneral.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FC } from "react";
88
import { ConfigurationNameForm } from "./general/ConfigurationName";
99
import { RemoveConfiguration } from "./general/RemoveConfiguration";
1010
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
11+
import { ManageRepoSuggestion } from "./general/ManageRepoSuggestion";
1112

1213
type Props = {
1314
configuration: Configuration;
@@ -16,6 +17,7 @@ export const ConfigurationDetailGeneral: FC<Props> = ({ configuration }) => {
1617
return (
1718
<>
1819
<ConfigurationNameForm configuration={configuration} />
20+
<ManageRepoSuggestion configuration={configuration} />
1921
<RemoveConfiguration configuration={configuration} />
2022
</>
2123
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 { SwitchInputField } from "@podkit/switch/Switch";
8+
import { Heading3, Subheading } from "@podkit/typography/Headings";
9+
import { FC, useCallback } from "react";
10+
import { InputField } from "../../../components/forms/InputField";
11+
import PillLabel from "../../../components/PillLabel";
12+
import { useToast } from "../../../components/toasts/Toasts";
13+
import { useOrgSettingsQuery } from "../../../data/organizations/org-settings-query";
14+
import { useUpdateOrgSettingsMutation } from "../../../data/organizations/update-org-settings-mutation";
15+
import { useId } from "../../../hooks/useId";
16+
import { ConfigurationSettingsField } from "../ConfigurationSettingsField";
17+
import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
18+
import { SquareArrowOutUpRight } from "lucide-react";
19+
20+
type Props = {
21+
configuration: Configuration;
22+
};
23+
export const ManageRepoSuggestion: FC<Props> = ({ configuration }) => {
24+
const { data: orgSettings } = useOrgSettingsQuery();
25+
const { toast } = useToast();
26+
const updateTeamSettings = useUpdateOrgSettingsMutation();
27+
const updateRecommendedRepository = useCallback(
28+
async (configurationId: string, suggested: boolean) => {
29+
const newRepositories = new Set(orgSettings?.onboardingSettings?.recommendedRepositories ?? []);
30+
if (suggested) {
31+
newRepositories.add(configurationId);
32+
} else {
33+
newRepositories.delete(configurationId);
34+
}
35+
36+
await updateTeamSettings.mutateAsync(
37+
{
38+
onboardingSettings: {
39+
...orgSettings?.onboardingSettings,
40+
recommendedRepositories: [...newRepositories],
41+
},
42+
},
43+
{
44+
onError: (error) => {
45+
toast(`Failed to update recommended repositories: ${error.message}`);
46+
},
47+
},
48+
);
49+
},
50+
[orgSettings?.onboardingSettings, toast, updateTeamSettings],
51+
);
52+
53+
const isSuggested = orgSettings?.onboardingSettings?.recommendedRepositories?.includes(configuration.id);
54+
55+
const inputId = useId({ prefix: "suggested-repository" });
56+
57+
return (
58+
<ConfigurationSettingsField>
59+
<Heading3 className="flex flex-row items-center gap-2">
60+
Mark this repository as{" "}
61+
<PillLabel className="capitalize bg-kumquat-light shrink-0 text-sm hidden xl:block" type="warn">
62+
Suggested
63+
</PillLabel>
64+
</Heading3>
65+
<Subheading className="max-w-lg flex flex-col gap-2">
66+
The Suggested section highlights recommended repositories on the dashboard for new members, making it
67+
easier to find and start working on key projects in Gitpod.
68+
<a
69+
className="gp-link flex flex-row items-center gap-1"
70+
href="https://www.gitpod.io/docs/configure/orgs/onboarding#suggested-repositories"
71+
target="_blank"
72+
rel="noreferrer"
73+
>
74+
Learn about suggestions
75+
<SquareArrowOutUpRight size={12} />
76+
</a>
77+
</Subheading>
78+
<InputField id={inputId}>
79+
<SwitchInputField
80+
id={inputId}
81+
checked={isSuggested}
82+
disabled={updateTeamSettings.isLoading}
83+
onCheckedChange={(checked) => {
84+
updateRecommendedRepository(configuration.id, checked);
85+
}}
86+
label={isSuggested ? "Listed in “Suggested”" : "Not listed in “Suggested”"}
87+
/>
88+
</InputField>
89+
</ConfigurationSettingsField>
90+
);
91+
};

components/dashboard/src/repositories/list/RepoListItem.tsx

+3-47
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,15 @@ import { TextMuted } from "@podkit/typography/TextMuted";
1010
import { Text } from "@podkit/typography/Text";
1111
import { LinkButton } from "@podkit/buttons/LinkButton";
1212
import type { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
13-
import { AlertTriangleIcon, CheckCircle2Icon, SquareArrowOutUpRight, Ellipsis } from "lucide-react";
13+
import { AlertTriangleIcon, CheckCircle2Icon } from "lucide-react";
1414
import { TableCell, TableRow } from "@podkit/tables/Table";
15-
import { Button } from "@podkit/buttons/Button";
16-
import {
17-
DropdownLinkMenuItem,
18-
DropdownMenu,
19-
DropdownMenuContent,
20-
DropdownMenuItem,
21-
DropdownMenuTrigger,
22-
} from "@podkit/dropdown/DropDown";
2315
import PillLabel from "../../components/PillLabel";
2416

2517
type Props = {
2618
configuration: Configuration;
2719
isSuggested: boolean;
28-
handleModifySuggestedRepository?: (configurationId: string, suggested: boolean) => void;
2920
};
30-
export const RepositoryListItem: FC<Props> = ({ configuration, isSuggested, handleModifySuggestedRepository }) => {
21+
export const RepositoryListItem: FC<Props> = ({ configuration, isSuggested }) => {
3122
const url = usePrettyRepoURL(configuration.cloneUrl);
3223
const prebuildsEnabled = !!configuration.prebuildSettings?.enabled;
3324
const created =
@@ -73,45 +64,10 @@ export const RepositoryListItem: FC<Props> = ({ configuration, isSuggested, hand
7364
</div>
7465
</TableCell>
7566

76-
<TableCell className="flex items-center gap-4">
67+
<TableCell>
7768
<LinkButton href={`/repositories/${configuration.id}`} variant="secondary">
7869
View
7970
</LinkButton>
80-
{handleModifySuggestedRepository && (
81-
<DropdownMenu>
82-
<DropdownMenuTrigger asChild>
83-
<Button variant="ghost">
84-
<Ellipsis size={20} />
85-
</Button>
86-
</DropdownMenuTrigger>
87-
<DropdownMenuContent className="w-52">
88-
{isSuggested ? (
89-
<DropdownMenuItem
90-
onClick={() => handleModifySuggestedRepository(configuration.id, false)}
91-
>
92-
Remove from suggested repos
93-
</DropdownMenuItem>
94-
) : (
95-
<>
96-
<DropdownMenuItem
97-
onClick={() => handleModifySuggestedRepository(configuration.id, true)}
98-
>
99-
Add to suggested repos
100-
</DropdownMenuItem>
101-
<DropdownLinkMenuItem
102-
href="https://www.gitpod.io/docs/configure/orgs/onboarding#suggested-repositories"
103-
className="gap-1 text-xs"
104-
target="_blank"
105-
rel="noreferrer"
106-
>
107-
Learn about suggestions
108-
<SquareArrowOutUpRight size={12} />
109-
</DropdownLinkMenuItem>
110-
</>
111-
)}
112-
</DropdownMenuContent>
113-
</DropdownMenu>
114-
)}
11571
</TableCell>
11672
</TableRow>
11773
);

components/dashboard/src/repositories/list/RepositoryTable.tsx

-27
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ import { SortableTableHead, TableSortOrder } from "@podkit/tables/SortableTable"
1717
import { LoadingState } from "@podkit/loading/LoadingState";
1818
import { Button } from "@podkit/buttons/Button";
1919
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@podkit/select/Select";
20-
import { useUpdateOrgSettingsMutation } from "../../data/organizations/update-org-settings-mutation";
2120
import { useOrgSettingsQuery } from "../../data/organizations/org-settings-query";
22-
import { useToast } from "../../components/toasts/Toasts";
2321

2422
type Props = {
2523
configurations: Configuration[];
@@ -54,31 +52,7 @@ export const RepositoryTable: FC<Props> = ({
5452
onLoadNextPage,
5553
onSort,
5654
}) => {
57-
const updateTeamSettings = useUpdateOrgSettingsMutation();
5855
const { data: settings } = useOrgSettingsQuery();
59-
const { toast } = useToast();
60-
61-
const updateRecommendedRepository = async (configurationId: string, suggested: boolean) => {
62-
const newRepositories = new Set(settings?.onboardingSettings?.recommendedRepositories ?? []);
63-
if (suggested) {
64-
newRepositories.add(configurationId);
65-
} else {
66-
newRepositories.delete(configurationId);
67-
}
68-
69-
await updateTeamSettings.mutateAsync(
70-
{
71-
onboardingSettings: {
72-
recommendedRepositories: [...newRepositories],
73-
},
74-
},
75-
{
76-
onError: (error) => {
77-
toast(`Failed to update recommended repositories: ${error.message}`);
78-
},
79-
},
80-
);
81-
};
8256

8357
return (
8458
<>
@@ -156,7 +130,6 @@ export const RepositoryTable: FC<Props> = ({
156130
configuration.id,
157131
) ?? false
158132
}
159-
handleModifySuggestedRepository={updateRecommendedRepository}
160133
/>
161134
);
162135
})}

components/dashboard/src/teams/TeamOnboarding.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@ export default function TeamOnboardingPage() {
115115
<ConfigurationSettingsField>
116116
<Heading3>Suggested repositories</Heading3>
117117
<Subheading>
118-
A list of repositories suggested to new organization members. To manage recommended
119-
repositories, visit the{" "}
118+
A list of repositories suggested to new organization members. You can toggle a repository's
119+
visibility in the onboarding process by visiting the{" "}
120120
<Link to="/repositories" className="gp-link">
121121
Repository settings
122122
</Link>{" "}
123-
page and add / remove repositories from the list using the context menu on the corresponding
124-
repository's row.
123+
page and toggling the "Mark this repository as Suggested" setting under the details of the
124+
repository.
125125
</Subheading>
126126
{(suggestedRepos ?? []).length > 0 && (
127127
<Table className="mt-4">

0 commit comments

Comments
 (0)