diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx
index 27346195c1..438517aa5f 100644
--- a/frontend/src/components/basic/table/ProjectUsersTable.tsx
+++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx
@@ -30,7 +30,7 @@ type Props = {
type EnvironmentProps = {
name: string;
slug: string;
-}
+};
/**
* This is the component that shows the users of a certin project
@@ -91,26 +91,38 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
});
};
- const handlePermissionUpdate = (index: number, val: string, membershipId: string, slug: string ) => {
- let denials: { ability: string; environmentSlug: string; }[];
- if (val === "Read Only") {
- denials = [{
- ability: "write",
- environmentSlug: slug
- }];
- } else if (val === "No Access") {
- denials = [{
- ability: "write",
- environmentSlug: slug
- }, {
- ability: "read",
- environmentSlug: slug
- }];
- } else if (val === "Add Only") {
- denials = [{
- ability: "read",
- environmentSlug: slug
- }];
+ const handlePermissionUpdate = (
+ index: number,
+ val: string,
+ membershipId: string,
+ slug: string
+ ) => {
+ let denials: { ability: string; environmentSlug: string }[];
+ if (val === 'Read Only') {
+ denials = [
+ {
+ ability: 'write',
+ environmentSlug: slug
+ }
+ ];
+ } else if (val === 'No Access') {
+ denials = [
+ {
+ ability: 'write',
+ environmentSlug: slug
+ },
+ {
+ ability: 'read',
+ environmentSlug: slug
+ }
+ ];
+ } else if (val === 'Add Only') {
+ denials = [
+ {
+ ability: 'read',
+ environmentSlug: slug
+ }
+ ];
} else {
denials = [];
}
@@ -118,8 +130,12 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
setIsUpgradeModalOpen(true);
} else {
- const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
- updateUserProjectPermission({ membershipId, denials: allDenials});
+ const allDenials = userData[index].deniedPermissions
+ .filter(
+ (perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
+ )
+ .concat(denials);
+ updateUserProjectPermission({ membershipId, denials: allDenials });
changeData([
...userData.slice(0, index),
...[
@@ -156,7 +172,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
orgId
});
if (subscriptions) {
- setCurrentPlan(subscriptions.data[0].plan.product)
+ setCurrentPlan(subscriptions.data[0].plan.product);
}
})();
}, [userData, myUser]);
@@ -186,25 +202,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
const closeUpgradeModal = () => {
setIsUpgradeModalOpen(false);
- }
+ };
return (
-
-
+
+
-
-
+
+
- NAME |
- EMAIL |
- ROLE |
- {workspaceEnvs.map(env => (
-
- {env.slug.toUpperCase()}
+ | NAME |
+ EMAIL |
+ ROLE |
+ {workspaceEnvs.map((env) => (
+
+
+ {env.slug.toUpperCase()}
+
+
{/* PERMISSION */}
|
))}
@@ -227,28 +246,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)
.map((row, index) => (
-
+ |
{row.firstName} {row.lastName}
|
-
+ |
{row.email}
|
-
-
- |
- {workspaceEnvs.map((env) =>
- handlePermissionUpdate(index, val, row.membershipId, env.slug)}
- value={
- // eslint-disable-next-line no-nested-ternary
- (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
- ? "No Access"
- // eslint-disable-next-line no-nested-ternary
- : (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Read Only"
- : !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Add Only" : "Read & Write")
- }
- icon={
- // eslint-disable-next-line no-nested-ternary
- (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
- ? faEyeSlash
- // eslint-disable-next-line no-nested-ternary
- : (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faEye
- : !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faPlus : faPenToSquare)
- }
- disabled={myRole !== 'admin'}
- // onOpenChange={(open) => setIsOpen(open)}
+ {workspaceEnvs.map((env) => (
+
- No Access
- Read Only
- Add Only
- Read & Write
-
- | )}
-
+
+ handlePermissionUpdate(index, val, row.membershipId, env.slug)
+ }
+ value={
+ // eslint-disable-next-line no-nested-ternary
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? 'No Access'
+ : // eslint-disable-next-line no-nested-ternary
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ !row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? 'Read Only'
+ : !row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? 'Add Only'
+ : 'Read & Write'
+ }
+ icon={
+ // eslint-disable-next-line no-nested-ternary
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? faEyeSlash
+ : // eslint-disable-next-line no-nested-ternary
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ !row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? faEye
+ : !row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('write') &&
+ row.deniedPermissions
+ .filter((perm: any) => perm.environmentSlug === env.slug)
+ .map((perm: { ability: string }) => perm.ability)
+ .includes('read')
+ ? faPlus
+ : faPenToSquare
+ }
+ isDisabled={myRole !== 'admin'}
+ // onOpenChange={(open) => setIsOpen(open)}
+ >
+
+ No Access
+
+
+ Read Only
+
+
+ Add Only
+
+
+ Read & Write
+
+
+ |
+ ))}
+
{myUser !== row.email &&
// row.role !== "admin" &&
myRole !== 'member' ? (
-
+
) : (
-
+
)}
|
|
diff --git a/frontend/src/components/v2/Card/Card.tsx b/frontend/src/components/v2/Card/Card.tsx
index 4bb3c36748..0950186143 100644
--- a/frontend/src/components/v2/Card/Card.tsx
+++ b/frontend/src/components/v2/Card/Card.tsx
@@ -3,14 +3,19 @@ import { twMerge } from 'tailwind-merge';
export type CardTitleProps = {
children: ReactNode;
- subTitle?: string;
+ subTitle?: ReactNode;
className?: string;
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
-
+
{children}
- {subTitle &&
{subTitle}
}
+ {subTitle &&
{subTitle}
}
);
diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
index 1d072cc89c..31dcc611ab 100644
--- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
+++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
@@ -5,7 +5,7 @@ import { useToggle } from '@app/hooks';
import { Button } from '../Button';
import { FormControl } from '../FormControl';
import { Input } from '../Input';
-import { Modal, ModalContent } from '../Modal';
+import { Modal, ModalClose, ModalContent } from '../Modal';
type Props = {
isOpen?: boolean;
@@ -64,9 +64,11 @@ export const DeleteActionModal = ({
>
Delete
-
+
+
+ {' '}
}
onClose={onClose}
diff --git a/frontend/src/components/v2/Input/Input.tsx b/frontend/src/components/v2/Input/Input.tsx
index 28524c72d4..ed9ead6468 100644
--- a/frontend/src/components/v2/Input/Input.tsx
+++ b/frontend/src/components/v2/Input/Input.tsx
@@ -84,19 +84,19 @@ export const Input = forwardRef(
): JSX.Element => {
return (
- {leftIcon && {leftIcon}}
+ {leftIcon && {leftIcon}}
- {rightIcon && {rightIcon}}
+ {rightIcon && {rightIcon}}
);
}
diff --git a/frontend/src/components/v2/Modal/Modal.tsx b/frontend/src/components/v2/Modal/Modal.tsx
index c338817725..fc9a90642e 100644
--- a/frontend/src/components/v2/Modal/Modal.tsx
+++ b/frontend/src/components/v2/Modal/Modal.tsx
@@ -9,7 +9,7 @@ import { IconButton } from '../IconButton';
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
title?: ReactNode;
- subTitle?: string;
+ subTitle?: ReactNode;
footerContent?: ReactNode;
onClose?: () => void;
};
diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx
index f8d559abe8..ffeaf3c3f8 100644
--- a/frontend/src/components/v2/Select/Select.tsx
+++ b/frontend/src/components/v2/Select/Select.tsx
@@ -14,18 +14,28 @@ type Props = {
dropdownContainerClassName?: string;
isLoading?: boolean;
position?: 'item-aligned' | 'popper';
+ isDisabled?: boolean;
icon?: IconProp;
};
-export type SelectProps = SelectPrimitive.SelectProps & Props;
+export type SelectProps = Omit & Props;
export const Select = forwardRef(
(
- { children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
+ {
+ children,
+ placeholder,
+ className,
+ isLoading,
+ isDisabled,
+ dropdownContainerClassName,
+ position,
+ ...props
+ },
ref
): JSX.Element => {
return (
-
+
(
className
)}
>
-
+
{props.icon ? : placeholder}
- {!props.disabled && (
+ {!isDisabled && (
@@ -46,7 +56,7 @@ export const Select = forwardRef(
(
;
+
+const tagVariants = cva('inline-flex whitespace-nowrap text-sm rounded-md mx-1 text-bunker-200 ', {
+ variants: {
+ colorSchema: {
+ gray: 'bg-mineshaft-500',
+ red: 'bg-red/80 text-bunker-100'
+ },
+ size: {
+ sm: 'px-2 py-1'
+ }
+ }
+});
+
+export const Tag = ({ children, className, colorSchema = 'gray', size = 'sm' }: Props) => (
+ {children}
+);
diff --git a/frontend/src/components/v2/Tag/index.tsx b/frontend/src/components/v2/Tag/index.tsx
new file mode 100644
index 0000000000..ba2338b7be
--- /dev/null
+++ b/frontend/src/components/v2/Tag/index.tsx
@@ -0,0 +1 @@
+export { Tag } from './Tag';
diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx
index 0f89a3a2b7..d69b0bfb44 100644
--- a/frontend/src/components/v2/index.tsx
+++ b/frontend/src/components/v2/index.tsx
@@ -12,5 +12,6 @@ export * from './Select';
export * from './Spinner';
export * from './Switch';
export * from './Table';
+export * from './Tag';
export * from './TextArea';
export * from './UpgradePlanModal';
diff --git a/frontend/src/hooks/api/incidentContacts/index.tsx b/frontend/src/hooks/api/incidentContacts/index.tsx
new file mode 100644
index 0000000000..6c53db28eb
--- /dev/null
+++ b/frontend/src/hooks/api/incidentContacts/index.tsx
@@ -0,0 +1,5 @@
+export {
+ useAddIncidentContact,
+ useDeleteIncidentContact,
+ useGetOrgIncidentContact
+} from './queries';
diff --git a/frontend/src/hooks/api/incidentContacts/queries.tsx b/frontend/src/hooks/api/incidentContacts/queries.tsx
new file mode 100644
index 0000000000..4f028f16db
--- /dev/null
+++ b/frontend/src/hooks/api/incidentContacts/queries.tsx
@@ -0,0 +1,57 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import { apiRequest } from '@app/config/request';
+
+import { AddIncidentContactDTO, DeleteIncidentContactDTO, IncidentContact } from './types';
+
+const incidentContactKeys = {
+ getAllContact: (orgId: string) => ['org-incident-contacts', { orgId }] as const
+};
+
+const fetchOrgIncidentContacts = async (orgId: string) => {
+ const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>(
+ `/api/v1/organization/${orgId}/incidentContactOrg`
+ );
+
+ return data.incidentContactsOrg;
+};
+
+export const useGetOrgIncidentContact = (orgId: string) =>
+ useQuery({
+ queryKey: incidentContactKeys.getAllContact(orgId),
+ queryFn: () => fetchOrgIncidentContacts(orgId),
+ enabled: Boolean(orgId)
+ });
+
+// mutation
+export const useAddIncidentContact = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, AddIncidentContactDTO>({
+ mutationFn: async ({ orgId, email }) => {
+ const { data } = await apiRequest.post(`/api/v1/organization/${orgId}/incidentContactOrg`, {
+ email
+ });
+ return data;
+ },
+ onSuccess: (_, { orgId }) => {
+ queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
+ }
+ });
+};
+
+export const useDeleteIncidentContact = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, DeleteIncidentContactDTO>({
+ mutationFn: async ({ orgId, email }) => {
+ const { data } = await apiRequest.delete(`/api/v1/organization/${orgId}/incidentContactOrg`, {
+ data: { email }
+ });
+ return data;
+ },
+ onSuccess: (_, { orgId }) => {
+ queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/incidentContacts/types.ts b/frontend/src/hooks/api/incidentContacts/types.ts
new file mode 100644
index 0000000000..a1ef8ab957
--- /dev/null
+++ b/frontend/src/hooks/api/incidentContacts/types.ts
@@ -0,0 +1,18 @@
+export type IncidentContact = {
+ _id: string;
+ email: string;
+ organization: string;
+ __v: number;
+ createdAt: Date;
+ updatedAt: Date;
+};
+
+export type DeleteIncidentContactDTO = {
+ orgId: string;
+ email: string;
+};
+
+export type AddIncidentContactDTO = {
+ orgId: string;
+ email: string;
+};
diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx
index ad9acf0c3e..c2035bbbab 100644
--- a/frontend/src/hooks/api/index.tsx
+++ b/frontend/src/hooks/api/index.tsx
@@ -1,4 +1,5 @@
export * from './auth';
+export * from './incidentContacts';
export * from './keys';
export * from './organization';
export * from './serviceTokens';
diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts
index 4d9dd04f8b..a11a7dd741 100644
--- a/frontend/src/hooks/api/organization/index.ts
+++ b/frontend/src/hooks/api/organization/index.ts
@@ -1 +1 @@
-export { useGetOrganization } from './queries';
+export { useGetOrganization, useRenameOrg } from './queries';
diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx
index b2f9c7ce6e..0770fc4136 100644
--- a/frontend/src/hooks/api/organization/queries.tsx
+++ b/frontend/src/hooks/api/organization/queries.tsx
@@ -1,8 +1,8 @@
-import { useQuery } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
-import { Organization } from './types';
+import { Organization, RenameOrgDTO } from './types';
const organizationKeys = {
getUserOrganization: ['organization'] as const
@@ -16,3 +16,16 @@ const fetchUserOrganization = async () => {
export const useGetOrganization = () =>
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });
+
+// mutation
+export const useRenameOrg = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, RenameOrgDTO>({
+ mutationFn: ({ newOrgName, orgId }) =>
+ apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }),
+ onSuccess: () => {
+ queryClient.invalidateQueries(organizationKeys.getUserOrganization);
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts
index 74720320ba..92a5c5f1dd 100644
--- a/frontend/src/hooks/api/organization/types.ts
+++ b/frontend/src/hooks/api/organization/types.ts
@@ -4,3 +4,8 @@ export type Organization = {
createAt: string;
updatedAt: string;
};
+
+export type RenameOrgDTO = {
+ orgId: string;
+ newOrgName: string;
+};
diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts
index 699e9a81dc..a8821e5a51 100644
--- a/frontend/src/hooks/api/types.ts
+++ b/frontend/src/hooks/api/types.ts
@@ -1,14 +1,17 @@
export type { GetAuthTokenAPI } from './auth/types';
+export type { IncidentContact } from './incidentContacts/types';
export type { UserWsKeyPair } from './keys/types';
export type { Organization } from './organization/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
-export type { User } from './users/types';
+export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
export type {
CreateEnvironmentDTO,
+ CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
+ ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
Workspace,
WorkspaceEnv,
diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx
index 69c0fc98a1..0980d4108b 100644
--- a/frontend/src/hooks/api/users/index.tsx
+++ b/frontend/src/hooks/api/users/index.tsx
@@ -1,7 +1,10 @@
export {
fetchOrgUsers,
+ useAddUserToOrg,
useAddUserToWs,
+ useDeleteOrgMembership,
useGetOrgUsers,
useGetUser,
- useLogoutUser
+ useLogoutUser,
+ useUpdateOrgUserRole
} from './queries';
diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx
index 1ddcf3809e..c6b35960d3 100644
--- a/frontend/src/hooks/api/users/queries.tsx
+++ b/frontend/src/hooks/api/users/queries.tsx
@@ -1,4 +1,4 @@
-import { useMutation, useQuery } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
decryptAssymmetric,
@@ -8,7 +8,15 @@ import { apiRequest } from '@app/config/request';
import { setAuthToken } from '@app/reactQuery';
import { useUploadWsKey } from '../keys/queries';
-import { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './types';
+import {
+ AddUserToOrgDTO,
+ AddUserToWsDTO,
+ AddUserToWsRes,
+ DeletOrgMembershipDTO,
+ OrgUser,
+ UpdateOrgUserRoleDTO,
+ User
+} from './types';
const userKeys = {
getUser: ['user'] as const,
@@ -32,7 +40,11 @@ export const fetchOrgUsers = async (orgId: string) => {
};
export const useGetOrgUsers = (orgId: string) =>
- useQuery(userKeys.getOrgUsers(orgId), () => fetchOrgUsers(orgId));
+ useQuery({
+ queryKey: userKeys.getOrgUsers(orgId),
+ queryFn: () => fetchOrgUsers(orgId),
+ enabled: Boolean(orgId)
+ });
// mutation
export const useAddUserToWs = () => {
@@ -69,6 +81,47 @@ export const useAddUserToWs = () => {
});
};
+export const useAddUserToOrg = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, AddUserToOrgDTO>({
+ mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto),
+ onSuccess: (_, { organizationId }) => {
+ queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
+ }
+ });
+};
+
+export const useDeleteOrgMembership = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, DeletOrgMembershipDTO>({
+ mutationFn: ({ membershipId, orgId }) =>
+ apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`),
+ onSuccess: (_, { orgId }) => {
+ queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
+ }
+ });
+};
+
+export const useUpdateOrgUserRole = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
+ mutationFn: ({ organizationId, membershipId, role }) =>
+ apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
+ role
+ }),
+ onSuccess: (_, { organizationId }) => {
+ queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
+ },
+ // to remove old states
+ onError: (_, { organizationId }) => {
+ queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
+ }
+ });
+};
+
export const useLogoutUser = () =>
useMutation({
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts
index 1b17cd023e..b3fb5cf6bf 100644
--- a/frontend/src/hooks/api/users/types.ts
+++ b/frontend/src/hooks/api/users/types.ts
@@ -32,7 +32,7 @@ export type OrgUser = {
inviteEmail: string;
organization: string;
role: 'owner' | 'admin' | 'member';
- status: 'invited' | 'accepted';
+ status: 'invited' | 'accepted' | 'verified' | 'completed';
deniedPermissions: any[];
};
@@ -45,3 +45,19 @@ export type AddUserToWsRes = {
invitee: OrgUser['user'];
latestKey: UserWsKeyPair;
};
+
+export type UpdateOrgUserRoleDTO = {
+ organizationId: string;
+ membershipId: string;
+ role: string;
+};
+
+export type DeletOrgMembershipDTO = {
+ membershipId: string;
+ orgId: string;
+};
+
+export type AddUserToOrgDTO = {
+ inviteeEmail: string;
+ organizationId: string;
+};
diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
index 52a01029eb..cab8617bd9 100644
--- a/frontend/src/hooks/api/workspace/index.tsx
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -3,9 +3,9 @@ export {
useCreateWsEnvironment,
useDeleteWorkspace,
useDeleteWsEnvironment,
+ useGetUserWorkspaceMemberships,
useGetUserWorkspaces,
useGetWorkspaceById,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment} from './queries';
-
\ No newline at end of file
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
index 0c4ba970b6..2bca2e9a52 100644
--- a/frontend/src/hooks/api/workspace/queries.tsx
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -13,16 +13,18 @@ import {
Workspace
} from './types';
-
const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
+ getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
getAllUserWorkspace: ['workspaces'] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
- const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
- return data.workspace;
-}
+ const { data } = await apiRequest.get<{ workspace: Workspace }>(
+ `/api/v1/workspace/${workspaceId}`
+ );
+ return data.workspace;
+};
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
@@ -40,6 +42,21 @@ export const useGetWorkspaceById = (workspaceId: string) => {
export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
+const fetchUserWorkspaceMemberships = async (orgId: string) => {
+ const { data } = await apiRequest.get>(
+ `/api/v1/organization/${orgId}/workspace-memberships`
+ );
+ return data;
+};
+
+// to get all userids in an org with the workspace they are part of
+export const useGetUserWorkspaceMemberships = (orgId: string) =>
+ useQuery({
+ queryKey: workspaceKeys.getWorkspaceMemberships(orgId),
+ queryFn: () => fetchUserWorkspaceMemberships(orgId),
+ enabled: Boolean(orgId)
+ });
+
// mutation
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
@@ -70,7 +87,9 @@ export const useToggleAutoCapitalization = () => {
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
mutationFn: ({ workspaceID, state }) =>
- apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, { autoCapitalization: state }),
+ apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, {
+ autoCapitalization: state
+ }),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
diff --git a/frontend/src/pages/settings/org/[id].tsx b/frontend/src/pages/settings/org/[id].tsx
index 562c45c9ba..59c8c373ca 100644
--- a/frontend/src/pages/settings/org/[id].tsx
+++ b/frontend/src/pages/settings/org/[id].tsx
@@ -1,374 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
-import { useEffect, useState } from 'react';
import Head from 'next/head';
-import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
-import { faCheck, faMagnifyingGlass, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { plans } from 'public/data/frequentConstants';
-import Button from '@app/components/basic/buttons/Button';
-import AddIncidentContactDialog from '@app/components/basic/dialog/AddIncidentContactDialog';
-import AddUserDialog from '@app/components/basic/dialog/AddUserDialog';
-import UpgradePlanModal from '@app/components/basic/dialog/UpgradePlan';
-import InputField from '@app/components/basic/InputField';
-import UserTable from '@app/components/basic/table/UserTable';
-import NavHeader from '@app/components/navigation/NavHeader';
-import guidGenerator from '@app/components/utilities/randomId';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
-
-import addUserToOrg from '../../api/organization/addUserToOrg';
-import deleteIncidentContact from '../../api/organization/deleteIncidentContact';
-import getIncidentContacts from '../../api/organization/getIncidentContacts';
-import getOrganization from '../../api/organization/GetOrg';
-import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
-import getOrganizationUsers from '../../api/organization/GetOrgUsers';
-import renameOrg from '../../api/organization/renameOrg';
-import getUser from '../../api/user/getUser';
-import deleteWorkspace from '../../api/workspace/deleteWorkspace';
-import getWorkspaces from '../../api/workspace/getWorkspaces';
+import { OrgSettingsPage } from '@app/views/Settings/OrgSettingsPage';
export default function SettingsOrg() {
- const [buttonReady, setButtonReady] = useState(false);
- const router = useRouter();
- const host = window.location.origin;
- const [orgName, setOrgName] = useState('');
- const [emailUser, setEmailUser] = useState('');
- const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
- const [searchUsers, setSearchUsers] = useState('');
- const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] = useState(false);
- const [isAddUserOpen, setIsAddUserOpen] = useState(router.asPath.split('?')[1] === 'invite');
- const [incidentContacts, setIncidentContacts] = useState([]);
- const [searchIncidentContact, setSearchIncidentContact] = useState('');
- const [userList, setUserList] = useState([]);
- const [personalEmail, setPersonalEmail] = useState('');
- const [email, setEmail] = useState('');
- const [currentPlan, setCurrentPlan] = useState('');
-
- const workspaceId = router.query.id as string;
-
const { t } = useTranslation();
- useEffect(() => {
- (async () => {
- const orgId = localStorage.getItem('orgData.id') as string;
- const org = await getOrganization({
- orgId
- });
-
- setOrgName(org.name);
- const incidentContactsData = await getIncidentContacts(
- localStorage.getItem('orgData.id') as string
- );
-
- setIncidentContacts(incidentContactsData?.map((contact) => contact.email));
-
- const user = await getUser();
- setPersonalEmail(user.email);
-
- const orgUsers = await getOrganizationUsers({
- orgId
- });
-
- setUserList(
- orgUsers.map((orgUser) => ({
- key: guidGenerator(),
- firstName: orgUser.user?.firstName,
- lastName: orgUser.user?.lastName,
- email: orgUser.user?.email == null ? orgUser.inviteEmail : orgUser.user?.email,
- role: orgUser?.role,
- status: orgUser?.status,
- userId: orgUser.user?._id,
- membershipId: orgUser._id,
- publicKey: orgUser.user?.publicKey
- }))
- );
-
- const subscriptions = await getOrganizationSubscriptions({
- orgId
- });
- if (subscriptions) {
- setCurrentPlan(subscriptions.data[0].plan.product);
- }
- })();
- }, []);
-
- const modifyOrgName = (newName: string) => {
- setButtonReady(true);
- setOrgName(newName);
- };
-
- const submitChanges = (newOrgName: string) => {
- renameOrg(localStorage.getItem('orgData.id') as string, newOrgName);
- setButtonReady(false);
- };
-
- const closeAddUserModal = () => {
- setIsAddUserOpen(false);
- };
-
- const closeAddIncidentContactModal = () => {
- setIsAddIncidentContactOpen(false);
- };
-
- const openAddUserModal = () => {
- setIsAddUserOpen(true);
- };
-
- const openAddIncidentContactModal = () => {
- setIsAddIncidentContactOpen(true);
- };
-
- const submitAddUserModal = async (newUserEmail: string) => {
- await addUserToOrg(newUserEmail, localStorage.getItem('orgData.id') as string);
- setEmail('');
- setIsAddUserOpen(false);
- router.reload();
- };
-
- const deleteIncidentContactFully = (incidentContact: string) => {
- setIncidentContacts(incidentContacts.filter((contact) => contact !== incidentContact));
- deleteIncidentContact(localStorage.getItem('orgData.id') as string, incidentContact);
- };
-
- /**
- * This function deleted a workspace.
- * It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
- * It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
- * It then deletes the workspace and forwards the user to another aviable workspace.
- */
- const executeDeletingWorkspace = async () => {
- const userWorkspaces = await getWorkspaces();
-
- if (userWorkspaces.length > 1) {
- if (
- userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
- workspaceToBeDeletedName
- ) {
- await deleteWorkspace(workspaceId);
- const ws = await getWorkspaces();
- router.push(`/dashboard/${ws[0]._id}`);
- }
- }
- };
-
return (
-
+ <>
{t('common:head-title', { title: t('settings-org:title') })}
-
-
-
-
-
-
-
{t('settings-org:title')}
-
- {t('settings-org:description')}
-
-
-
-
-
-
-
-
-
- {t('common:display-name')}
-
-
-
-
-
-
-
-
-
-
-
-
- {t('section-members:org-members')}
-
-
- {t('section-members:org-members-description')}
-
-
-
= 5 && currentPlan === plans.starter && host === 'https://app.infisical.com'}
- onClose={closeAddUserModal}
- text="You can add more members if you switch to Infisical's Team plan."
- />
- {/* */}
-
-
-
- setSearchUsers(e.target.value)}
- placeholder={t('section-members:search-members') as string}
- />
-
-
-
-
-
- {userList && (
-
-
-
- )}
-
-
-
-
-
-
- {t('section-incident:incident-contacts')}
-
-
- {t('section-incident:incident-contacts-description')}
-
-
-
-
-
-
-
-
- setSearchIncidentContact(e.target.value)}
- placeholder={t('common:search') as string}
- />
-
- {incidentContacts?.filter((incidentEmail) =>
- incidentEmail.includes(searchIncidentContact)
- ).length > 0 ? (
- incidentContacts
- .filter((incidentEmail) => incidentEmail.includes(searchIncidentContact))
- .map((contact) => (
-
-
{contact}
-
-
-
- ))
- ) : (
-
-
{t('section-incident:no-incident-contacts')}
-
- )}
-
-
- {/*
-
- Danger Zone
-
-
- As soon as you delete an organization, you will
- not be able to undo it. This will immediately
- remove all organization members and cancel your
- subscription. If you still want to do that,
- please enter the name of the organization below.
-
-
-
-
-
-
- Note: You can only delete a project in case you
- have more than one.
-
-
*/}
-
-
-
-
+
+ >
);
}
diff --git a/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
new file mode 100644
index 0000000000..26c06a9d21
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
@@ -0,0 +1,308 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { useTranslation } from 'next-i18next';
+import { plans } from 'public/data/frequentConstants';
+
+import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
+import NavHeader from '@app/components/navigation/NavHeader';
+import {
+ decryptAssymmetric,
+ encryptAssymmetric
+} from '@app/components/utilities/cryptography/crypto';
+import { useOrganization, useSubscription, useUser, useWorkspace } from '@app/context';
+import {
+ useAddIncidentContact,
+ useAddUserToOrg,
+ useDeleteIncidentContact,
+ useDeleteOrgMembership,
+ useGetOrgIncidentContact,
+ useGetOrgUsers,
+ useGetUserWorkspaceMemberships,
+ useGetUserWsKey,
+ useRenameOrg,
+ useUpdateOrgUserRole,
+ useUploadWsKey
+} from '@app/hooks/api';
+
+import { OrgIncidentContactsTable, OrgMembersTable, OrgNameChangeSection } from './components';
+
+export const OrgSettingsPage = () => {
+ const host = window.location.origin;
+
+ const { t } = useTranslation();
+ const { currentOrg } = useOrganization();
+ const { currentWorkspace } = useWorkspace();
+ const { user } = useUser();
+ const { subscriptionPlan } = useSubscription();
+ const { createNotification } = useNotificationContext();
+
+ const orgId = currentOrg?._id || '';
+ const { data: orgUsers } = useGetOrgUsers(orgId);
+ const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId);
+ const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
+ const { data: incidentContact } = useGetOrgIncidentContact(orgId);
+
+ const renameOrg = useRenameOrg();
+ const removeUserOrgMembership = useDeleteOrgMembership();
+ const addUserToOrg = useAddUserToOrg();
+ const updateOrgUserRole = useUpdateOrgUserRole();
+ const uploadWsKey = useUploadWsKey();
+ const addIncidentContact = useAddIncidentContact();
+ const removeIncidentContact = useDeleteIncidentContact();
+
+ const isMoreUsersNotAllowed =
+ (orgUsers || []).length >= 5 &&
+ subscriptionPlan === plans.starter &&
+ host === 'https://app.infisical.com';
+
+ const onRenameOrg = async (name: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await renameOrg.mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
+ createNotification({
+ text: 'Successfully renamed organization',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to rename organization',
+ type: 'error'
+ });
+ }
+ };
+
+ const onRemoveUserOrgMembership = async (membershipId: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
+ createNotification({
+ text: 'Successfully removed used from org',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to remove user from org',
+ type: 'error'
+ });
+ }
+ };
+ const onAddUserToOrg = async (email: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
+ createNotification({
+ text: 'Successfully invited user to org',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to invite user to org',
+ type: 'error'
+ });
+ }
+ };
+
+ const onUpdateOrgUserRole = async (membershipId: string, role: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role });
+ createNotification({
+ text: 'Successfully updated user role',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to update user role',
+ type: 'error'
+ });
+ }
+ };
+
+ const onGrantUserAccess = async (userId: string, publicKey: string) => {
+ try {
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
+ if (!PRIVATE_KEY || !wsKey) return;
+
+ // assymmetrically decrypt symmetric key with local private key
+ const key = decryptAssymmetric({
+ ciphertext: wsKey.encryptedKey,
+ nonce: wsKey.nonce,
+ publicKey: wsKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ const { ciphertext, nonce } = encryptAssymmetric({
+ plaintext: key,
+ publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ await uploadWsKey.mutateAsync({
+ userId,
+ nonce,
+ encryptedKey: ciphertext,
+ workspaceId: currentWorkspace?._id || ''
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: 'Failed to grant access to user',
+ type: 'error'
+ });
+ }
+ };
+
+ const onAddIncidentContact = async (email: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await addIncidentContact.mutateAsync({ orgId, email });
+ createNotification({
+ text: 'Successfully added incident contact',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to add incident contact',
+ type: 'error'
+ });
+ }
+ };
+
+ const onRemoveIncidentContact = async (email: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ await removeIncidentContact.mutateAsync({ orgId, email });
+ createNotification({
+ text: 'Successfully removed incident contact',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to remove incident contact',
+ type: 'error'
+ });
+ }
+ };
+
+ /**
+ * This function deleted a workspace.
+ * It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
+ * It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
+ * It then deletes the workspace and forwards the user to another aviable workspace.
+ */
+ // const executeDeletingWorkspace = async () => {
+ // const userWorkspaces = await getWorkspaces();
+ //
+ // if (userWorkspaces.length > 1) {
+ // if (
+ // userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
+ // workspaceToBeDeletedName
+ // ) {
+ // await deleteWorkspace(workspaceId);
+ // const ws = await getWorkspaces();
+ // router.push(`/dashboard/${ws[0]._id}`);
+ // }
+ // }
+ // };
+ //
+ return (
+
+
+
+
+
{t('settings-org:title')}
+
+ {t('settings-org:description')}
+
+
+
+
+
+
+
+ {t('section-members:org-members')}
+
+
+ {t('section-members:org-members-description')}
+
+
+
+
+
+
+
+ {t('section-incident:incident-contacts')}
+
+
+ {t('section-incident:incident-contacts-description')}
+
+
+
+
+
+
+
+ {/*
+
+ Danger Zone
+
+
+ As soon as you delete an organization, you will
+ not be able to undo it. This will immediately
+ remove all organization members and cancel your
+ subscription. If you still want to do that,
+ please enter the name of the organization below.
+
+
+
+
+
+
+ Note: You can only delete a project in case you
+ have more than one.
+
+
*/}
+
+
+ );
+};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
new file mode 100644
index 0000000000..296e5031d3
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
@@ -0,0 +1,171 @@
+import { useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import {
+ Button,
+ DeleteActionModal,
+ FormControl,
+ IconButton,
+ Input,
+ Modal,
+ ModalContent,
+ Table,
+ TableContainer,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr
+} from '@app/components/v2';
+import { usePopUp } from '@app/hooks';
+import { IncidentContact } from '@app/hooks/api/types';
+
+type Props = {
+ contacts?: IncidentContact[];
+ onRemoveContact: (email: string) => Promise;
+ onAddContact: (email: string) => Promise;
+};
+
+const addContactFormSchema = yup.object({
+ email: yup.string().email().required().label('Email').trim()
+});
+
+type TAddContactForm = yup.InferType;
+
+export const OrgIncidentContactsTable = ({
+ contacts = [],
+ onAddContact,
+ onRemoveContact
+}: Props) => {
+ const [searchContact, setSearchContact] = useState('');
+ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
+ 'addContact',
+ 'removeContact'
+ ] as const);
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({ resolver: yupResolver(addContactFormSchema) });
+
+ const onAddIncidentContact = ({ email }: TAddContactForm) => {
+ onAddContact(email);
+ handlePopUpClose('addContact');
+ reset();
+ };
+
+ const onRemoveIncidentContact = async () => {
+ const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
+ await onRemoveContact(incidentContactEmail);
+ handlePopUpClose('removeContact');
+ };
+
+ return (
+
+
+
+ setSearchContact(e.target.value)}
+ leftIcon={}
+ placeholder="Search incident contact by email..."
+ />
+
+
+ }
+ onClick={() => handlePopUpOpen('addContact')}
+ >
+ Add Contact
+
+
+
+
+
+
+
+
+ Email |
+ |
+
+
+
+ {contacts
+ ?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
+ ?.map(({ email }) => (
+
+ {email} |
+
+ handlePopUpOpen('removeContact', { email })}
+ >
+
+
+ |
+
+ ))}
+
+
+
+
+
{
+ handlePopUpToggle('addContact', isOpen);
+ reset();
+ }}
+ >
+
+
+
+
+
handlePopUpToggle('removeContact', isOpen)}
+ onDeleteApproved={onRemoveIncidentContact}
+ />
+
+ );
+};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/index.tsx
new file mode 100644
index 0000000000..8fbc246111
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/index.tsx
@@ -0,0 +1 @@
+export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx
new file mode 100644
index 0000000000..e23bf42ba3
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx
@@ -0,0 +1,274 @@
+import { useMemo, useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import {
+ Button,
+ DeleteActionModal,
+ FormControl,
+ IconButton,
+ Input,
+ Modal,
+ ModalContent,
+ Select,
+ SelectItem,
+ Table,
+ TableContainer,
+ Tag,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr,
+ UpgradePlanModal
+} from '@app/components/v2';
+import { usePopUp } from '@app/hooks';
+import { OrgUser, Workspace } from '@app/hooks/api/types';
+
+type Props = {
+ members?: OrgUser[];
+ workspaceMemberships?: Record;
+ orgName: string;
+ isMoreUserNotAllowed: boolean;
+ onRemoveMember: (userId: string) => Promise;
+ onInviteMember: (email: string) => Promise;
+ onRoleChange: (membershipId: string, role: string) => Promise;
+ onGrantAccess: (userId: string, publicKey: string) => Promise;
+ // the current user id to block remove org button
+ userId: string;
+};
+
+const addMemberFormSchema = yup.object({
+ email: yup.string().email().required().label('Email').trim()
+});
+
+type TAddMemberForm = yup.InferType;
+
+export const OrgMembersTable = ({
+ members = [],
+ workspaceMemberships = {},
+ orgName,
+ isMoreUserNotAllowed,
+ onRemoveMember,
+ onInviteMember,
+ onGrantAccess,
+ onRoleChange,
+ userId
+}: Props) => {
+ const [searchMemberFilter, setSearchMemberFilter] = useState('');
+ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
+ 'addMember',
+ 'removeMember',
+ 'upgradePlan'
+ ] as const);
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({ resolver: yupResolver(addMemberFormSchema) });
+
+ const onAddMember = ({ email }: TAddMemberForm) => {
+ onInviteMember(email);
+ handlePopUpClose('addMember');
+ reset();
+ };
+
+ const onRemoveOrgMemberApproved = async () => {
+ const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id;
+ await onRemoveMember(orgMembershipId);
+ handlePopUpClose('removeMember');
+ };
+
+ const isIamOwner = useMemo(
+ () => members.find(({ user }) => userId === user?._id)?.role === 'owner',
+ [userId, members]
+ );
+
+ const filterdUser = useMemo(
+ () =>
+ members.filter(
+ ({ user, inviteEmail }) =>
+ user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
+ user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
+ user?.email?.toLowerCase().includes(searchMemberFilter) ||
+ inviteEmail?.includes(searchMemberFilter)
+ ),
+ [members, searchMemberFilter]
+ );
+
+ return (
+
+
+
+ setSearchMemberFilter(e.target.value)}
+ leftIcon={}
+ placeholder="Search members..."
+ />
+
+
+ }
+ onClick={() => {
+ if (isMoreUserNotAllowed) {
+ handlePopUpOpen('upgradePlan');
+ } else {
+ handlePopUpOpen('addMember');
+ }
+ }}
+ >
+ Add Member
+
+
+
+
+
+
+
+
+ Name |
+ Email |
+ Role |
+ Projects |
+ |
+
+
+
+ {filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
+ const name = user ? `${user.firstName} ${user.lastName}` : '-';
+ const email = user?.email || inviteEmail;
+ const userWs = workspaceMemberships?.[user?._id];
+
+ return (
+
+ {name} |
+ {email} |
+
+ {status === 'accepted' && (
+
+ onRoleChange(orgMembershipId, selectedRole)
+ }
+ >
+ {(isIamOwner || role === 'owner') && (
+ owner
+ )}
+ admin
+ member
+
+ )}
+ {(status === 'invited' || status === 'verified') && (
+
+ )}
+ {status === 'completed' && (
+
+ )}
+ |
+
+ {userWs ? (
+ userWs?.map(({ name: wsName, _id }) => (
+
+ {wsName}
+
+ ))
+ ) : (
+ This user isn't part of any projects yet
+ )}
+ |
+
+ handlePopUpOpen('removeMember', { id: orgMembershipId })}
+ >
+
+
+ |
+
+ );
+ })}
+
+
+
+
+
{
+ handlePopUpToggle('addMember', isOpen);
+ reset();
+ }}
+ >
+
+ An invite is specific to an email address and expires after 1 day.
+
+ For security reasons, you will need to separately add members to projects.
+ >
+ }
+ >
+
+
+
+
handlePopUpToggle('removeMember', isOpen)}
+ onDeleteApproved={onRemoveOrgMemberApproved}
+ />
+ handlePopUpToggle('upgradePlan', isOpen)}
+ text="You can add custom environments if you switch to Infisical's Team plan."
+ />
+
+ );
+};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/index.tsx
new file mode 100644
index 0000000000..729d065929
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/index.tsx
@@ -0,0 +1 @@
+export { OrgMembersTable } from './OrgMembersTable';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx
new file mode 100644
index 0000000000..9b18285c0a
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx
@@ -0,0 +1,68 @@
+import { useEffect } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { faCheck } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import { Button, FormControl, Input } from '@app/components/v2';
+
+type Props = {
+ orgName?: string;
+ onOrgNameChange: (name: string) => Promise;
+};
+
+const formSchema = yup.object({
+ name: yup.string().required().label('Project Name')
+});
+
+type FormData = yup.InferType;
+
+export const OrgNameChangeSection = ({ onOrgNameChange, orgName }: Props): JSX.Element => {
+ const {
+ handleSubmit,
+ control,
+ reset,
+ formState: { isDirty, isSubmitting }
+ } = useForm({ resolver: yupResolver(formSchema) });
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ reset({ name: orgName });
+ }, [orgName]);
+
+ const onFormSubmit = async ({ name }: FormData) => {
+ await onOrgNameChange(name);
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/index.tsx
new file mode 100644
index 0000000000..b668d37323
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgNameChangeSection/index.tsx
@@ -0,0 +1 @@
+export { OrgNameChangeSection } from './OrgNameChangeSection';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx
new file mode 100644
index 0000000000..15a09b0d23
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx
@@ -0,0 +1,3 @@
+export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
+export { OrgMembersTable } from './OrgMembersTable';
+export { OrgNameChangeSection } from './OrgNameChangeSection';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/index.tsx
new file mode 100644
index 0000000000..3ea2e3b0f5
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/index.tsx
@@ -0,0 +1 @@
+export { OrgSettingsPage } from './OrgSettingsPage';