diff --git a/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx b/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx index 450b58aef..0f1fd61e5 100644 --- a/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx +++ b/frontend/src/components/v2/FilterableSelect/FilterableSelect.tsx @@ -32,7 +32,13 @@ export const FilterableSelect = ({ }) }} tabSelectsValue={tabSelectsValue} - components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }} + components={{ + DropdownIndicator, + ClearIndicator, + MultiValueRemove, + Option, + ...props.components + }} classNames={{ container: () => "w-full font-inter", control: ({ isFocused }) => diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 2d12b3eda..8c4f8e1c8 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -10,7 +10,6 @@ import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faStar } from "@fortawesome/free-regular-svg-icons"; import { faAngleDown, faArrowLeft, @@ -22,15 +21,11 @@ import { faInfo, faMobile, faPlus, - faQuestion, - faStar as faSolidStar + faQuestion } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -import { twMerge } from "tailwind-merge"; -import { createNotification } from "@app/components/notifications"; -import { OrgPermissionCan } from "@app/components/permissions"; import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { @@ -39,20 +34,9 @@ import { DropdownMenuContent, DropdownMenuItem, Menu, - MenuItem, - Select, - SelectItem, - UpgradePlanModal + MenuItem } from "@app/components/v2"; -import { NewProjectModal } from "@app/components/v2/projects/NewProjectModal"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization, - useSubscription, - useUser, - useWorkspace -} from "@app/context"; +import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { useGetAccessRequestsCount, @@ -62,11 +46,9 @@ import { useSelectOrganization } from "@app/hooks/api"; import { MfaMethod } from "@app/hooks/api/auth/types"; -import { Workspace } from "@app/hooks/api/types"; -import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; -import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; import { AuthMethod } from "@app/hooks/api/users/types"; import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner"; +import { ProjectSelect } from "@app/layouts/AppLayout/components/ProjectSelect"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; import { Mfa } from "@app/views/Login/Mfa"; import { CreateOrgModal } from "@app/views/Org/components"; @@ -108,23 +90,10 @@ export const AppLayout = ({ children }: LayoutProps) => { const { workspaces, currentWorkspace } = useWorkspace(); const { orgs, currentOrg } = useOrganization(); - const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); - const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); - const workspacesWithFaveProp = useMemo( - () => - workspaces - .map((w): Workspace & { isFavorite: boolean } => ({ - ...w, - isFavorite: Boolean(projectFavorites?.includes(w.id)) - })) - .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)), - [workspaces, projectFavorites] - ); - const { user } = useUser(); const { subscription } = useSubscription(); const workspaceId = currentWorkspace?.id || ""; @@ -137,17 +106,9 @@ export const AppLayout = ({ children }: LayoutProps) => { return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); }, [secretApprovalReqCount, accessApprovalRequestCount]); - const isAddingProjectsAllowed = subscription?.workspaceLimit - ? subscription.workspacesUsed < subscription.workspaceLimit - : true; - const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION; - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ - "addNewWs", - "upgradePlan", - "createOrg" - ] as const); + const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); const { t } = useTranslation(); @@ -230,38 +191,6 @@ export const AppLayout = ({ children }: LayoutProps) => { putUserInOrg(); }, [router.query.id]); - const addProjectToFavorites = async (projectId: string) => { - try { - if (currentOrg?.id) { - await updateUserProjectFavorites({ - orgId: currentOrg?.id, - projectFavorites: [...(projectFavorites || []), projectId] - }); - } - } catch (err) { - createNotification({ - text: "Failed to add project to favorites.", - type: "error" - }); - } - }; - - const removeProjectFromFavorites = async (projectId: string) => { - try { - if (currentOrg?.id) { - await updateUserProjectFavorites({ - orgId: currentOrg?.id, - projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] - }); - } - } catch (err) { - createNotification({ - text: "Failed to remove project from favorites.", - type: "error" - }); - } - }; - if (shouldShowMfa) { return (
@@ -448,97 +377,7 @@ export const AppLayout = ({ children }: LayoutProps) => { )} {!router.asPath.includes("org") && (!router.asPath.includes("personal") && currentWorkspace ? ( -
-

- Project -

- -
+ ) : (
@@ -816,15 +655,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
- handlePopUpToggle("addNewWs", isOpen)} - /> - handlePopUpToggle("upgradePlan", isOpen)} - text="You have exceeded the number of projects allowed on the free plan." - /> handlePopUpToggle("createOrg", false)} diff --git a/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx new file mode 100644 index 000000000..9300a3e93 --- /dev/null +++ b/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx @@ -0,0 +1,214 @@ +import { useMemo } from "react"; +import { components, MenuProps, OptionProps } from "react-select"; +import { faStar } from "@fortawesome/free-regular-svg-icons"; +import { faEye, faPlus, faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FilterableSelect, UpgradePlanModal } from "@app/components/v2"; +import { NewProjectModal } from "@app/components/v2/projects"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useSubscription, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; +import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; +import { Workspace } from "@app/hooks/api/workspace/types"; + +type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean }; + +const ProjectsMenu = ({ children, ...props }: MenuProps) => { + return ( + + {children} +
+
+ + {(isAllowed) => ( + + )} + +
+
+ ); +}; + +const ProjectOption = ({ + isSelected, + children, + data, + ...props +}: OptionProps) => { + const { currentOrg } = useOrganization(); + const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); + const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); + + const removeProjectFromFavorites = async (projectId: string) => { + try { + await updateUserProjectFavorites({ + orgId: currentOrg!.id, + projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] + }); + } catch (err) { + createNotification({ + text: "Failed to remove project from favorites.", + type: "error" + }); + } + }; + + const addProjectToFavorites = async (projectId: string) => { + try { + await updateUserProjectFavorites({ + orgId: currentOrg!.id, + projectFavorites: [...(projectFavorites || []), projectId] + }); + } catch (err) { + createNotification({ + text: "Failed to add project to favorites.", + type: "error" + }); + } + }; + return ( + +
+ {isSelected && ( + + )} +

{children}

+ {data.isFavorite ? ( + { + e.stopPropagation(); + await removeProjectFromFavorites(data.id); + }} + /> + ) : ( + { + e.stopPropagation(); + await addProjectToFavorites(data.id); + }} + /> + )} +
+
+ ); +}; + +export const ProjectSelect = () => { + const { workspaces, currentWorkspace } = useWorkspace(); + const { currentOrg } = useOrganization(); + const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); + + const { subscription } = useSubscription(); + + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "addNewWs", + "upgradePlan" + ] as const); + + const { options, value } = useMemo(() => { + const projectOptions = workspaces + .map((w): Workspace & { isFavorite: boolean } => ({ + ...w, + isFavorite: Boolean(projectFavorites?.includes(w.id)) + })) + .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)); + + const currentOption = projectOptions.find((option) => option.id === currentWorkspace?.id); + + if (!currentOption) { + return { + options: projectOptions, + value: null + }; + } + + return { + options: [ + currentOption, + ...projectOptions.filter((option) => option.id !== currentOption.id) + ], + value: currentOption + }; + }, [workspaces, projectFavorites, currentWorkspace]); + + return ( +
+

Project

+ + option.data.name.toLowerCase().includes(inputValue.toLowerCase()) + } + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + onChange={(newValue) => { + // hacky use of null as indication to create project + if (!newValue) { + if (isAddingProjectsAllowed) { + handlePopUpOpen("addNewWs"); + } else { + handlePopUpOpen("upgradePlan"); + } + return; + } + + const project = newValue as TWorkspaceWithFaveProp; + localStorage.setItem("projectData.id", project.id); + // this is not using react query because react query in overview is throwing error when envs are not exact same count + // to reproduce change this back to router.push and switch between two projects with different env count + // look into this on dashboard revamp + window.location.assign(`/project/${project.id}/secrets/overview`); + }} + options={options} + components={{ + Option: ProjectOption, + Menu: ProjectsMenu + }} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You have exceeded the number of projects allowed on the free plan." + /> + + handlePopUpToggle("addNewWs", isOpen)} + /> +
+ ); +}; diff --git a/frontend/src/layouts/AppLayout/components/ProjectSelect/index.ts b/frontend/src/layouts/AppLayout/components/ProjectSelect/index.ts new file mode 100644 index 000000000..d0be7c203 --- /dev/null +++ b/frontend/src/layouts/AppLayout/components/ProjectSelect/index.ts @@ -0,0 +1 @@ +export * from "./ProjectSelect";