diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 856a1b0157bb06..f3268ce60c0cdc 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -19,7 +19,7 @@ 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"; @@ -27,8 +27,7 @@ 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 @@ -41,7 +40,7 @@ const resolveIcon = (contextUrl?: string): string => { }; type PredefinedRepositoryOptionProps = { - repo: PredefinedRepoOption; + repo: PredefinedRepo; }; const PredefinedRepositoryOption: FC = ({ repo }) => { const prettyUrl = toRemoteURL(repo.url); @@ -50,7 +49,12 @@ const PredefinedRepositoryOption: FC = ({ repo return (
- + {repo.configurationId ? ( + + ) : ( + + )} + {repo.repoName} , - isSelectable: true, - }); + const alreadyPresent = result.find((r) => r.id === repo.configurationId); + if (!alreadyPresent) { + result.push({ + id: repo.url, + element: , + isSelectable: true, + }); + } } }); } diff --git a/components/dashboard/src/data/git-providers/predefined-repos.ts b/components/dashboard/src/data/git-providers/predefined-repos.ts index 1b431d8e5b2a91..2970fe8f57f475 100644 --- a/components/dashboard/src/data/git-providers/predefined-repos.ts +++ b/components/dashboard/src/data/git-providers/predefined-repos.ts @@ -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", diff --git a/components/dashboard/src/insights/download/DownloadInsights.tsx b/components/dashboard/src/insights/download/DownloadInsights.tsx index 9a7d3fc4572db6..fcd17806ef4bfe 100644 --- a/components/dashboard/src/insights/download/DownloadInsights.tsx +++ b/components/dashboard/src/insights/download/DownloadInsights.tsx @@ -53,7 +53,8 @@ export const DownloadInsightsToast = ({ organizationId, from, to, organizationNa return (
Preparing usage export - Exporting page {progress} +
+ Exporting page {progress}
); } diff --git a/components/dashboard/src/repositories/detail/ConfigurationDetailGeneral.tsx b/components/dashboard/src/repositories/detail/ConfigurationDetailGeneral.tsx index 2fe64f76023078..8725385d6820d3 100644 --- a/components/dashboard/src/repositories/detail/ConfigurationDetailGeneral.tsx +++ b/components/dashboard/src/repositories/detail/ConfigurationDetailGeneral.tsx @@ -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; @@ -16,6 +17,7 @@ export const ConfigurationDetailGeneral: FC = ({ configuration }) => { return ( <> + ); diff --git a/components/dashboard/src/repositories/detail/general/ManageRepoSuggestion.tsx b/components/dashboard/src/repositories/detail/general/ManageRepoSuggestion.tsx new file mode 100644 index 00000000000000..e7f871d925e097 --- /dev/null +++ b/components/dashboard/src/repositories/detail/general/ManageRepoSuggestion.tsx @@ -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 = ({ 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 ( + + + Mark this repository as{" "} + + Suggested + + + + 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. + + Learn about suggestions + + + + + { + updateRecommendedRepository(configuration.id, checked); + }} + label={isSuggested ? "Listed in “Suggested”" : "Not listed in “Suggested”"} + /> + + + ); +}; diff --git a/components/dashboard/src/repositories/list/RepoListItem.tsx b/components/dashboard/src/repositories/list/RepoListItem.tsx index a213354f246359..852bdcd69158a0 100644 --- a/components/dashboard/src/repositories/list/RepoListItem.tsx +++ b/components/dashboard/src/repositories/list/RepoListItem.tsx @@ -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 = ({ configuration, isSuggested, handleModifySuggestedRepository }) => { +export const RepositoryListItem: FC = ({ configuration, isSuggested }) => { const url = usePrettyRepoURL(configuration.cloneUrl); const prebuildsEnabled = !!configuration.prebuildSettings?.enabled; const created = @@ -73,45 +64,10 @@ export const RepositoryListItem: FC = ({ configuration, isSuggested, hand
- + View - {handleModifySuggestedRepository && ( - - - - - - {isSuggested ? ( - handleModifySuggestedRepository(configuration.id, false)} - > - Remove from suggested repos - - ) : ( - <> - handleModifySuggestedRepository(configuration.id, true)} - > - Add to suggested repos - - - Learn about suggestions - - - - )} - - - )} ); diff --git a/components/dashboard/src/repositories/list/RepositoryTable.tsx b/components/dashboard/src/repositories/list/RepositoryTable.tsx index 18dad407a9f25f..0e78802f6b9950 100644 --- a/components/dashboard/src/repositories/list/RepositoryTable.tsx +++ b/components/dashboard/src/repositories/list/RepositoryTable.tsx @@ -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[]; @@ -54,31 +52,7 @@ export const RepositoryTable: FC = ({ 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 ( <> @@ -156,7 +130,6 @@ export const RepositoryTable: FC = ({ configuration.id, ) ?? false } - handleModifySuggestedRepository={updateRecommendedRepository} /> ); })} diff --git a/components/dashboard/src/teams/TeamOnboarding.tsx b/components/dashboard/src/teams/TeamOnboarding.tsx index c799f507a04472..48dc86a61169a2 100644 --- a/components/dashboard/src/teams/TeamOnboarding.tsx +++ b/components/dashboard/src/teams/TeamOnboarding.tsx @@ -115,13 +115,13 @@ export default function TeamOnboardingPage() { Suggested repositories - 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{" "} Repository settings {" "} - 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. {(suggestedRepos ?? []).length > 0 && ( diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index 5198be9af12114..35378bea3eae30 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -37,7 +37,9 @@ import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutati import { useUserLoader } from "../hooks/use-user-loader"; import Tooltip from "../components/Tooltip"; import { useFeatureFlag } from "../data/featureflag-query"; -import { useOrgSuggestedRepos } from "../data/organizations/suggested-repositories-query"; +import { SuggestedOrgRepository, useOrgSuggestedRepos } from "../data/organizations/suggested-repositories-query"; +import { useSuggestedRepositories } from "../data/git-providers/suggested-repositories-query"; +import PillLabel from "../components/PillLabel"; export const GETTING_STARTED_DISMISSAL_KEY = "workspace-list-getting-started"; @@ -130,8 +132,26 @@ const WorkspacesPage: FunctionComponent = () => { } }, [user?.profile?.coachmarksDismissals]); + const { data: userSuggestedRepos } = useSuggestedRepositories({ excludeConfigurations: false }); const { data: orgSuggestedRepos } = useOrgSuggestedRepos(); + const suggestedRepos = useMemo(() => { + const userSuggestions = + userSuggestedRepos + ?.filter((repo) => { + const autostartMatch = user?.workspaceAutostartOptions.find((option) => { + return option.cloneUrl.includes(repo.url); + }); + return autostartMatch; + }) + .slice(0, 3) ?? []; + const orgSuggestions = (orgSuggestedRepos ?? []).filter((repo) => { + return !userSuggestions.find((userSuggestion) => userSuggestion.configurationId === repo.configurationId); // don't show duplicates from user's autostart options + }); + + return [...userSuggestions, ...orgSuggestions].slice(0, 3); + }, [userSuggestedRepos, user, orgSuggestedRepos]); + const toggleGettingStarted = useCallback( (show: boolean) => { setShowGettingStarted(show); @@ -192,53 +212,103 @@ const WorkspacesPage: FunctionComponent = () => { {showGettingStarted && ( -
- setVideoModalVisible(true)}> - - - {orgSettings?.onboardingSettings?.internalLink ? ( - - + <> +
+ setVideoModalVisible(true)}> + - ) : ( - - + + {orgSettings?.onboardingSettings?.internalLink ? ( + + +
+ Learn more about Gitpod at {org?.name} + + Read through the internal Gitpod landing page of your organization. + +
+
+ ) : ( + + +
+ Open a sample repository + + Explore{" "} + {orgSuggestedRepos?.length + ? "repositories recommended by your organization" + : "a sample repository"} + to quickly experience Gitpod. + +
+
+ )} + + +
- Open a sample repository + Visit the docs - Explore{" "} - {orgSuggestedRepos?.length - ? "repositories recommended by your organization" - : "a sample repository"} - to quickly experience Gitpod. + We have extensive documentation to help if you get stuck.
- )} +
- - -
- Visit the docs - - We have extensive documentation to help if you get stuck. - -
-
-
+ {suggestedRepos.length > 0 && ( + <> + + Suggested + + +
+ {suggestedRepos.map((repo) => { + const isOrgSuggested = + (repo as SuggestedOrgRepository).orgSuggested ?? false; + + return ( + +
+ + + {repo.configurationName || repo.repoName} + + {isOrgSuggested && ( + + Recommended + + )} + + + {repo.url} + +
+
+ ); + })} +
+ + )} + )}