mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 18:27:19 +00:00
Merge pull request #2810 from Infisical/project-sidebar-dropdown-filter
Improvement: Sidebar Project Selection Filter Support
This commit is contained in:
@@ -32,7 +32,13 @@ export const FilterableSelect = <T,>({
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
tabSelectsValue={tabSelectsValue}
|
tabSelectsValue={tabSelectsValue}
|
||||||
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
components={{
|
||||||
|
DropdownIndicator,
|
||||||
|
ClearIndicator,
|
||||||
|
MultiValueRemove,
|
||||||
|
Option,
|
||||||
|
...props.components
|
||||||
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
container: () => "w-full font-inter",
|
container: () => "w-full font-inter",
|
||||||
control: ({ isFocused }) =>
|
control: ({ isFocused }) =>
|
||||||
@@ -53,13 +59,13 @@ export const FilterableSelect = <T,>({
|
|||||||
indicatorSeparator: () => "bg-bunker-400",
|
indicatorSeparator: () => "bg-bunker-400",
|
||||||
dropdownIndicator: () => "text-bunker-200 p-1",
|
dropdownIndicator: () => "text-bunker-200 p-1",
|
||||||
menu: () =>
|
menu: () =>
|
||||||
"mt-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
"mt-2 p-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||||
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
||||||
option: ({ isFocused, isSelected }) =>
|
option: ({ isFocused, isSelected }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
||||||
isSelected && "text-mineshaft-200",
|
isSelected && "text-mineshaft-200",
|
||||||
"hover:cursor-pointer text-xs px-3 py-2"
|
"hover:cursor-pointer mb-1 rounded text-xs px-3 py-2"
|
||||||
),
|
),
|
||||||
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import {
|
import {
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
@@ -22,15 +21,11 @@ import {
|
|||||||
faInfo,
|
faInfo,
|
||||||
faMobile,
|
faMobile,
|
||||||
faPlus,
|
faPlus,
|
||||||
faQuestion,
|
faQuestion
|
||||||
faStar as faSolidStar
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
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 { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import {
|
import {
|
||||||
@@ -39,20 +34,9 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
UpgradePlanModal
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { NewProjectModal } from "@app/components/v2/projects/NewProjectModal";
|
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
useOrganization,
|
|
||||||
useSubscription,
|
|
||||||
useUser,
|
|
||||||
useWorkspace
|
|
||||||
} from "@app/context";
|
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetAccessRequestsCount,
|
useGetAccessRequestsCount,
|
||||||
@@ -62,11 +46,9 @@ import {
|
|||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
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 { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner";
|
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 { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||||
import { Mfa } from "@app/views/Login/Mfa";
|
import { Mfa } from "@app/views/Login/Mfa";
|
||||||
import { CreateOrgModal } from "@app/views/Org/components";
|
import { CreateOrgModal } from "@app/views/Org/components";
|
||||||
@@ -108,23 +90,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
const { workspaces, currentWorkspace } = useWorkspace();
|
const { workspaces, currentWorkspace } = useWorkspace();
|
||||||
const { orgs, currentOrg } = useOrganization();
|
const { orgs, currentOrg } = useOrganization();
|
||||||
|
|
||||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
|
||||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
|
||||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
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 { user } = useUser();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
@@ -137,17 +106,9 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||||
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
|
||||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
|
||||||
: true;
|
|
||||||
|
|
||||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||||
"addNewWs",
|
|
||||||
"upgradePlan",
|
|
||||||
"createOrg"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -230,38 +191,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
putUserInOrg();
|
putUserInOrg();
|
||||||
}, [router.query.id]);
|
}, [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) {
|
if (shouldShowMfa) {
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
||||||
@@ -448,97 +377,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
)}
|
)}
|
||||||
{!router.asPath.includes("org") &&
|
{!router.asPath.includes("org") &&
|
||||||
(!router.asPath.includes("personal") && currentWorkspace ? (
|
(!router.asPath.includes("personal") && currentWorkspace ? (
|
||||||
<div className="mt-5 mb-4 w-full p-3">
|
<ProjectSelect />
|
||||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
|
||||||
Project
|
|
||||||
</p>
|
|
||||||
<Select
|
|
||||||
defaultValue={currentWorkspace?.id}
|
|
||||||
value={currentWorkspace?.id}
|
|
||||||
className="w-full bg-mineshaft-600 py-2.5 font-medium [&>*:first-child]:truncate"
|
|
||||||
onValueChange={(value) => {
|
|
||||||
localStorage.setItem("projectData.id", value);
|
|
||||||
// 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/${value}/secrets/overview`);
|
|
||||||
}}
|
|
||||||
position="popper"
|
|
||||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
|
||||||
>
|
|
||||||
<div className="no-scrollbar::-webkit-scrollbar h-full no-scrollbar">
|
|
||||||
{workspacesWithFaveProp
|
|
||||||
.filter((ws) => ws.orgId === currentOrg?.id)
|
|
||||||
.map(({ id, name, isFavorite }) => (
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"mb-1 grid grid-cols-7 rounded-md hover:bg-mineshaft-500",
|
|
||||||
id === currentWorkspace?.id && "bg-mineshaft-500"
|
|
||||||
)}
|
|
||||||
key={id}
|
|
||||||
>
|
|
||||||
<div className="col-span-6">
|
|
||||||
<SelectItem
|
|
||||||
key={`ws-layout-list-${id}`}
|
|
||||||
value={id}
|
|
||||||
className="transition-none data-[highlighted]:bg-mineshaft-500"
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1 flex items-center">
|
|
||||||
{isFavorite ? (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSolidStar}
|
|
||||||
className="text-sm text-mineshaft-300 hover:text-mineshaft-400"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removeProjectFromFavorites(id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faStar}
|
|
||||||
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
addProjectToFavorites(id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
|
||||||
<div className="w-full">
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Create}
|
|
||||||
a={OrgPermissionSubjects.Workspace}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="sm"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onClick={() => {
|
|
||||||
if (isAddingProjectsAllowed) {
|
|
||||||
handlePopUpOpen("addNewWs");
|
|
||||||
} else {
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
>
|
|
||||||
Add Project
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</div>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Link href={`/org/${currentOrg?.id}/overview`}>
|
<Link href={`/org/${currentOrg?.id}/overview`}>
|
||||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||||
@@ -816,15 +655,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<NewProjectModal
|
|
||||||
isOpen={popUp.addNewWs.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
|
||||||
/>
|
|
||||||
<UpgradePlanModal
|
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
|
||||||
text="You have exceeded the number of projects allowed on the free plan."
|
|
||||||
/>
|
|
||||||
<CreateOrgModal
|
<CreateOrgModal
|
||||||
isOpen={popUp?.createOrg?.isOpen}
|
isOpen={popUp?.createOrg?.isOpen}
|
||||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
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<TWorkspaceWithFaveProp>) => {
|
||||||
|
return (
|
||||||
|
<components.Menu {...props}>
|
||||||
|
{children}
|
||||||
|
<hr className="mb-2 h-px border-0 bg-mineshaft-500" />
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Workspace}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
className="w-full bg-mineshaft-700 pt-2 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() => props.clearValue()}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Project
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</components.Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectOption = ({
|
||||||
|
isSelected,
|
||||||
|
children,
|
||||||
|
data,
|
||||||
|
...props
|
||||||
|
}: OptionProps<TWorkspaceWithFaveProp>) => {
|
||||||
|
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 (
|
||||||
|
<components.Option
|
||||||
|
isSelected={isSelected}
|
||||||
|
data={data}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(props.className, isSelected && "bg-mineshaft-500")}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
{isSelected && (
|
||||||
|
<FontAwesomeIcon className="mr-2 text-mineshaft-300" icon={faEye} size="sm" />
|
||||||
|
)}
|
||||||
|
<p className="truncate">{children}</p>
|
||||||
|
{data.isFavorite ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSolidStar}
|
||||||
|
className="ml-auto text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await removeProjectFromFavorites(data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faStar}
|
||||||
|
className="ml-auto text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await addProjectToFavorites(data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</components.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="mt-5 mb-4 w-full p-3">
|
||||||
|
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">Project</p>
|
||||||
|
<FilterableSelect
|
||||||
|
className="text-sm"
|
||||||
|
value={value}
|
||||||
|
filterOption={(option, inputValue) =>
|
||||||
|
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);
|
||||||
|
// todo(akhi): 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
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You have exceeded the number of projects allowed on the free plan."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewProjectModal
|
||||||
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ProjectSelect";
|
||||||
Reference in New Issue
Block a user