mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
219 lines
7.3 KiB
TypeScript
219 lines
7.3 KiB
TypeScript
import { useMemo } from "react";
|
|
import { components, MenuProps, OptionProps } from "react-select";
|
|
import { faStar } from "@fortawesome/free-regular-svg-icons";
|
|
import { faChevronRight, faPlus, faStar as faSolidStar } from "@fortawesome/free-solid-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { twMerge } from "tailwind-merge";
|
|
|
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
|
import { createNotification } from "@app/components/notifications";
|
|
import { OrgPermissionCan } from "@app/components/permissions";
|
|
import { NewProjectModal } from "@app/components/projects";
|
|
import { Button, FilterableSelect } from "@app/components/v2";
|
|
import {
|
|
OrgPermissionActions,
|
|
OrgPermissionSubjects,
|
|
useOrganization,
|
|
useSubscription,
|
|
useWorkspace
|
|
} from "@app/context";
|
|
import { getProjectTitle } from "@app/helpers/project";
|
|
import { usePopUp } from "@app/hooks";
|
|
import { useGetUserWorkspaces } from "@app/hooks/api";
|
|
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
|
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
|
import { ProjectType, 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 {
|
|
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 {
|
|
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-primary" icon={faChevronRight} size="xs" />
|
|
)}
|
|
<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 { currentWorkspace } = useWorkspace();
|
|
const { currentOrg } = useOrganization();
|
|
const { data: workspaces = [] } = useGetUserWorkspaces({ type: currentWorkspace.type });
|
|
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="mb-4 mt-5 w-full p-3">
|
|
<p className="mb-1 ml-1.5 text-xs font-semibold uppercase text-gray-400">
|
|
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "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;
|
|
// 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.type}/${project.id}/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)}
|
|
projectType={currentWorkspace?.type || ProjectType.SecretManager}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|