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

[dashboard] Deprecate the creation of projects under individual accounts #13414

Merged
merged 1 commit into from
Oct 5, 2022
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
279 changes: 146 additions & 133 deletions components/dashboard/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ import gitpodIcon from "./icons/gitpod.svg";
import { getGitpodService, gitpodHostUrl } from "./service/service";
import { UserContext } from "./user-context";
import { TeamsContext, getCurrentTeam, getSelectedTeamSlug } from "./teams/teams-context";
import getSettingsMenu from "./settings/settings-menu";
import { getAdminMenu } from "./admin/admin-menu";
import ContextMenu from "./components/ContextMenu";
import ContextMenu, { ContextMenuEntry } from "./components/ContextMenu";
import Separator from "./components/Separator";
import PillMenuItem from "./components/PillMenuItem";
import TabMenuItem from "./components/TabMenuItem";
Expand All @@ -36,7 +35,7 @@ interface Entry {
}

export default function Menu() {
const { user, userBillingMode, refreshUserBillingMode } = useContext(UserContext);
const { user } = useContext(UserContext);
const { showUsageView } = useContext(FeatureFlagContext);
const { teams } = useContext(TeamsContext);
const location = useLocation();
Expand All @@ -46,6 +45,11 @@ export default function Menu() {
const { project, setProject } = useContext(ProjectContext);
const [isFeedbackFormVisible, setFeedbackFormVisible] = useState<boolean>(false);

const [hasIndividualProjects, setHasIndividualProjects] = useState(false);
getGitpodService()
.server.getUserProjects()
.then((projects) => setHasIndividualProjects(projects.length > 0));

const match = useRouteMatch<{ segment1?: string; segment2?: string; segment3?: string }>(
"/(t/)?:segment1/:segment2?/:segment3?",
);
Expand Down Expand Up @@ -90,14 +94,21 @@ export default function Menu() {
return all.some((n) => n === path || n + "/" === path);
}

const userFullName = user?.fullName || user?.name || "...";

// Hide most of the top menu when in a full-page form.
const isMinimalUI = inResource(location.pathname, ["new", "teams/new", "open"]);
const isWorkspacesUI = inResource(location.pathname, ["workspaces"]);
const isAccountUI = inResource(location.pathname, [
"account",
"notifications",
"variables",
"keys",
"integrations",
"preferences",
]);
const isAdminUI = inResource(window.location.pathname, ["admin"]);

const [teamMembers, setTeamMembers] = useState<Record<string, TeamMemberInfo[]>>({});

useEffect(() => {
if (!teams) {
return;
Expand Down Expand Up @@ -154,13 +165,8 @@ export default function Menu() {
}
}, [team]);

useEffect(() => {
// Refresh billing mode
refreshUserBillingMode();
}, [teams]);

const teamOrUserSlug = !!team ? "/t/" + team.slug : "/projects";
const leftMenu: Entry[] = (() => {
const secondLevelMenu: Entry[] = (() => {
// Project menu
if (projectSlug) {
return [
Expand All @@ -180,55 +186,45 @@ export default function Menu() {
];
}
// Team menu
if (team) {
const currentUserInTeam = (teamMembers[team.id] || []).find((m) => m.userId === user?.id);

const teamSettingsList = [
{
title: "Projects",
link: `/t/${team.slug}/projects`,
alternatives: [] as string[],
},
{
title: "Members",
link: `/t/${team.slug}/members`,
},
];
if (showUsageView || (teamBillingMode && teamBillingMode.mode === "usage-based")) {
teamSettingsList.push({
title: "Usage",
link: `/t/${team.slug}/usage`,
});
}
if (currentUserInTeam?.role === "owner") {
teamSettingsList.push({
title: "Settings",
link: `/t/${team.slug}/settings`,
alternatives: getTeamSettingsMenu({ team, billingMode: teamBillingMode }).flatMap((e) => e.link),
});
}

return teamSettingsList;
if (!team) {
return [];
}
// User menu
const userMenu = [];
userMenu.push({
title: "Projects",
link: "/projects",
});
if (showUsageView) {
userMenu.push({
const currentUserInTeam = (teamMembers[team.id] || []).find((m) => m.userId === user?.id);

const teamSettingsList = [
{
title: "Projects",
link: `/t/${team.slug}/projects`,
alternatives: [] as string[],
},
{
title: "Members",
link: `/t/${team.slug}/members`,
},
];
if (showUsageView || (teamBillingMode && teamBillingMode.mode === "usage-based")) {
teamSettingsList.push({
title: "Usage",
link: "/usage",
link: `/t/${team.slug}/usage`,
});
}
userMenu.push({
title: "Settings",
link: "/settings",
alternatives: getSettingsMenu({ userBillingMode }).flatMap((e) => e.link),
});
return userMenu;
if (currentUserInTeam?.role === "owner") {
teamSettingsList.push({
title: "Settings",
link: `/t/${team.slug}/settings`,
alternatives: getTeamSettingsMenu({ team, billingMode: teamBillingMode }).flatMap((e) => e.link),
});
}

return teamSettingsList;
})();
const leftMenu: Entry[] = [
{
title: "Workspaces",
svenefftinge marked this conversation as resolved.
Show resolved Hide resolved
svenefftinge marked this conversation as resolved.
Show resolved Hide resolved
link: "/workspaces",
alternatives: ["/"],
},
];
const rightMenu: Entry[] = [
...(user?.rolesOrPermissions?.includes("admin")
? [
Expand All @@ -239,11 +235,6 @@ export default function Menu() {
},
]
: []),
{
title: "Workspaces",
link: "/workspaces",
alternatives: ["/"],
},
];

const handleFeedbackFormClick = () => {
Expand All @@ -253,86 +244,93 @@ export default function Menu() {
const onFeedbackFormClose = () => {
setFeedbackFormVisible(false);
};
const isTeamLevelActive = !projectSlug && !isWorkspacesUI && !isAdminUI && teamOrUserSlug;
const isTeamLevelActive = !projectSlug && !isWorkspacesUI && !isAccountUI && !isAdminUI && teamOrUserSlug;
const renderTeamMenu = () => {
if (!teams || teams.length === 0) {
return (
<div className="p-1 text-base text-gray-500 dark:text-gray-400 border-gray-800">
<PillMenuItem
additionalClasses="border-2 border-gray-200 dark:border-gray-700 border-dashed"
svenefftinge marked this conversation as resolved.
Show resolved Hide resolved
name="New Team →"
link="/teams/new"
/>
</div>
);
}
const userFullName = user?.fullName || user?.name || "...";
const entries: (ContextMenuEntry & { slug: string })[] = [
...(hasIndividualProjects
? [
{
title: userFullName,
customContent: (
<div className="w-full text-gray-500 flex flex-col">
<span className="text-gray-800 dark:text-gray-100 text-base font-semibold">
{userFullName}
</span>
<span className="">Personal Account</span>
</div>
),
active: getSelectedTeamSlug() === "",
separator: true,
slug: "",
link: "/projects",
svenefftinge marked this conversation as resolved.
Show resolved Hide resolved
},
]
: []),
...(teams || [])
.map((t) => ({
slug: t.slug,
title: t.name,
customContent: (
<div className="w-full text-gray-400 flex flex-col">
<span className="text-gray-800 dark:text-gray-300 text-base font-semibold">{t.name}</span>
<span className="">
{!!teamMembers[t.id]
? `${teamMembers[t.id].length} member${teamMembers[t.id].length === 1 ? "" : "s"}`
: "..."}
</span>
</div>
),
active: getSelectedTeamSlug() === t.slug,
separator: true,
link: `/t/${t.slug}`,
}))
.sort((a, b) => (a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1)),
{
slug: "new",
title: "Create a new team",
customContent: (
<div className="w-full text-gray-400 flex items-center">
<span className="flex-1 font-semibold">New Team</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" className="w-3.5">
<path
fill="currentColor"
fillRule="evenodd"
d="M7 0a1 1 0 011 1v5h5a1 1 0 110 2H8v5a1 1 0 11-2 0V8H1a1 1 0 010-2h5V1a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
</div>
),
link: "/teams/new",
},
];
const classes =
"flex h-full text-base py-0 " +
(isTeamLevelActive
? "text-gray-50 bg-gray-800 dark:bg-gray-50 dark:text-gray-900 border-gray-700 dark:border-gray-200"
: "text-gray-500 bg-gray-50 dark:bg-gray-800 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 dark:border-gray-700");
const selectedEntry = entries.find((e) => e.slug === getSelectedTeamSlug()) || entries[0];
return (
<div className="flex p-1 pl-3">
<Link to={getSelectedTeamSlug() ? `/t/${getSelectedTeamSlug()}/projects` : `/projects`}>
<span
className={`${classes} rounded-tl-2xl rounded-bl-2xl border-r pl-3 pr-2 py-1 bg-gray-50 font-semibold`}
>
{teams?.find((t) => t.slug === getSelectedTeamSlug())?.name || userFullName}
<div className="flex p-1">
svenefftinge marked this conversation as resolved.
Show resolved Hide resolved
<Link to={selectedEntry.link!}>
<span className={`${classes} rounded-tl-2xl rounded-bl-2xl border-r pl-3 pr-2 py-1 font-semibold`}>
{selectedEntry.title!}
</span>
</Link>
<div className={`${classes} rounded-tr-2xl rounded-br-2xl px-1`}>
<ContextMenu
customClasses="w-64 left-0"
menuEntries={[
{
title: userFullName,
customContent: (
<div className="w-full text-gray-500 flex flex-col">
<span className="text-gray-800 dark:text-gray-100 text-base font-semibold">
{userFullName}
</span>
<span className="">Personal Account</span>
</div>
),
active: getSelectedTeamSlug() === "",
separator: true,
link: "/projects",
},
...(teams || [])
.map((t) => ({
title: t.name,
customContent: (
<div className="w-full text-gray-400 flex flex-col">
<span className="text-gray-800 dark:text-gray-300 text-base font-semibold">
{t.name}
</span>
<span className="">
{!!teamMembers[t.id]
? `${teamMembers[t.id].length} member${
teamMembers[t.id].length === 1 ? "" : "s"
}`
: "..."}
</span>
</div>
),
active: getSelectedTeamSlug() === t.slug,
separator: true,
link: `/t/${t.slug}`,
}))
.sort((a, b) => (a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1)),
{
title: "Create a new team",
customContent: (
<div className="w-full text-gray-400 flex items-center">
<span className="flex-1 font-semibold">New Team</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 14"
className="w-3.5"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M7 0a1 1 0 011 1v5h5a1 1 0 110 2H8v5a1 1 0 11-2 0V8H1a1 1 0 010-2h5V1a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
</div>
),
link: "/teams/new",
},
]}
>
<ContextMenu customClasses="w-64 left-0" menuEntries={entries}>
<div className="flex h-full pl-0 pr-1 py-1.5 text-gray-50">
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
Expand Down Expand Up @@ -391,7 +389,22 @@ export default function Menu() {
<Link to="/">
<img src={gitpodIcon} className="h-6" alt="Gitpod's logo" />
</Link>
{!isMinimalUI && <div className="ml-2 text-base">{renderTeamMenu()}</div>}
{!isMinimalUI && (
<>
<div className="pl-2 text-base text-gray-500 dark:text-gray-400 flex">
{leftMenu.map((entry) => (
<div className="p-1" key={entry.title}>
<PillMenuItem
name={entry.title}
selected={isSelected(entry, location)}
link={entry.link}
/>
</div>
))}
</div>
{renderTeamMenu()}
</>
)}
</div>
<div className="flex flex-1 items-center w-auto" id="menu">
<nav className="flex-1">
Expand Down Expand Up @@ -454,9 +467,9 @@ export default function Menu() {
</div>
{isFeedbackFormVisible && <FeedbackFormModal onClose={onFeedbackFormClose} />}
</div>
{!isMinimalUI && !prebuildId && !isWorkspacesUI && !isAdminUI && (
{!isMinimalUI && !prebuildId && !isWorkspacesUI && !isAccountUI && !isAdminUI && (
<nav className="flex">
{leftMenu.map((entry: Entry) => (
{secondLevelMenu.map((entry: Entry) => (
<TabMenuItem
key={entry.title}
name={entry.title}
Expand Down
6 changes: 5 additions & 1 deletion components/dashboard/src/components/PillMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ export default function PillMenuItem(p: {
name: string;
selected?: boolean;
link?: string;
additionalClasses?: string;
onClick?: (event: React.MouseEvent) => void;
}) {
const classes =
let classes =
"flex block font-medium dark:text-gray-400 px-3 py-1 rounded-2xl transition ease-in-out font-semibold " +
(p.selected
? "text-gray-50 bg-gray-800 dark:text-gray-900 dark:bg-gray-50"
: "hover:bg-gray-100 dark:hover:bg-gray-700");
if (p.additionalClasses) {
classes = classes + " " + p.additionalClasses;
}
return !p.link || p.link.startsWith("https://") ? (
<a className={classes} href={p.link} onClick={p.onClick} data-analytics='{"button_type":"pill_menu"}'>
{p.name}
Expand Down
Loading