diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx
index 31752aa02..f3ac08e9a 100644
--- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx
+++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportItem.tsx
@@ -1,4 +1,5 @@
import { useEffect } from "react";
+import { subject } from "@casl/ability";
import { useSortable } from "@dnd-kit/sortable";
import {
faFileImport,
@@ -9,12 +10,15 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
-import { useWorkspace } from "@app/context";
-import { useToggle } from "@app/hooks/useToggle";
+import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
+import { useToggle } from "@app/hooks";
type Props = {
onDelete: (environment: string, secretPath: string) => void;
+ environment: string;
+ secretPath: string;
importedEnv: string;
importedSecPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
@@ -39,7 +43,9 @@ export const SecretImportItem = ({
importedSecPath,
onDelete,
importedSecrets = [],
- searchTerm = ""
+ searchTerm = "",
+ secretPath,
+ environment
}: Props) => {
const [isExpanded, setIsExpanded] = useToggle();
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
@@ -49,7 +55,9 @@ export const SecretImportItem = ({
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
useEffect(() => {
- const filteredSecrets = importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
+ const filteredSecrets = importedSecrets.filter((secret) =>
+ secret.key.toUpperCase().includes(searchTerm.toUpperCase())
+ );
if (filteredSecrets.length > 0 && searchTerm) {
setIsExpanded.on();
@@ -58,7 +66,6 @@ export const SecretImportItem = ({
}
}, [searchTerm]);
-
useEffect(() => {
if (isDragging) {
setIsExpanded.off();
@@ -78,7 +85,11 @@ export const SecretImportItem = ({
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
onClick={() => setIsExpanded.toggle()}
>
-
{isExpanded && !isDragging && (
- |
+ |
@@ -146,19 +168,26 @@ export const SecretImportItem = ({
)}
- {importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())).map(({ key, value, overriden }, index) => (
-
- |
- {key}
- |
-
-
- |
-
-
- |
-
- ))}
+ {importedSecrets
+ .filter((secret) =>
+ secret.key.toUpperCase().includes(searchTerm.toUpperCase())
+ )
+ .map(({ key, value, overriden }, index) => (
+
+ |
+ {key}
+ |
+
+
+ |
+
+
+ |
+
+ ))}
diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx
index 9d796d079..db12e84a4 100644
--- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx
+++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx
@@ -59,13 +59,23 @@ export const computeImportedSecretRows = (
type Props = {
secrets?: DecryptedSecret[];
importedSecrets?: TImportedSecrets;
+ environment: string;
+ secretPath: string;
onSecretImportDelete: (env: string, secPath: string) => void;
items: { id: string; environment: string; secretPath: string }[];
searchTerm: string;
};
export const SecretImportSection = memo(
- ({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [], searchTerm = "" }: Props) => {
+ ({
+ secrets = [],
+ environment,
+ secretPath,
+ importedSecrets = [],
+ onSecretImportDelete,
+ items = [],
+ searchTerm = ""
+ }: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
@@ -82,6 +92,8 @@ export const SecretImportSection = memo(
secrets,
environments
)}
+ secretPath={secretPath}
+ environment={environment}
onDelete={onSecretImportDelete}
importedSecPath={impSecPath}
searchTerm={searchTerm}
diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx
index 6c089d695..9788c614b 100644
--- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx
+++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx
@@ -8,6 +8,7 @@ import {
UseFormSetValue,
useWatch
} from "react-hook-form";
+import { subject } from "@casl/ability";
import {
faCheck,
faCodeBranch,
@@ -22,26 +23,34 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { cx } from "cva";
import { twMerge } from "tailwind-merge";
+// TODO:(akhilmhdh): Refactor this
+import AddTagPopoverContent from "@app/components/AddTagPopoverContent/AddTagPopoverContent";
+import { ProjectPermissionCan } from "@app/components/permissions";
import {
+ FormControl,
HoverCard,
HoverCardContent,
HoverCardTrigger,
IconButton,
Input,
Popover,
+ PopoverContent,
PopoverTrigger,
SecretInput,
Tag,
+ TextArea,
Tooltip
} from "@app/components/v2";
+import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { WsTag } from "@app/hooks/api/types";
-import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent";
import { FormData, SecretActionType } from "../../DashboardPage.utils";
type Props = {
index: number;
+ environment: string;
+ secretPath: string;
// backend generated unique id
secUniqId?: string;
// permission and external state's that decided to hide or show
@@ -69,6 +78,8 @@ type Props = {
export const SecretInputRow = memo(
({
index,
+ secretPath,
+ environment,
isSecretValueHidden,
onRowExpand,
isReadOnly,
@@ -79,7 +90,7 @@ export const SecretInputRow = memo(
onSecretDelete,
searchTerm,
control,
- // register,
+ register,
setValue,
isKeyError,
keyError,
@@ -217,7 +228,6 @@ export const SecretInputRow = memo(
{index + 1}
|
-
-
+
{isOverridden ? (
-
-
-
-
-
+
+ {(isAllowed) => (
+
+
+
+ )}
+
{!isAddOnly && (
-
-
-
-
-
-
-
+
+ {(isAllowed) => (
+
+
+
+
+
+ )}
+
)}
-
-
-
-
-
+
+
+
+
-
-
-
- onSelectTag(wsTag)}
- handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)}
- handleTagOnMouseLeave={() => handleTagOnMouseLeave()}
- checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)}
- handleOnCreateTagOpen={() => onCreateTagOpen()}
- />
-
-
-
+ {(isAllowed) => (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
@@ -453,20 +492,27 @@ export const SecretInputRow = memo(
)}
-
- {
- onSecretDelete(index, secKey, secId, idOverride);
- }}
- >
-
-
-
+
+ {(isAllowed) => (
+ {
+ onSecretDelete(index, secKey, secId, idOverride);
+ }}
+ >
+
+
+ )}
+
diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
index bff23753a..c9fa6c585 100644
--- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
+++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
@@ -40,7 +40,7 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
let link = "";
switch (integrationOption.slug) {
case "gcp-secret-manager":
- link = `https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/cloud-platform&response_type=code&access_type=offline&state=${state}&redirect_uri=${window.location.origin}/integrations/gcp-secret-manager/oauth2/callback&client_id=${integrationOption.clientId}`;
+ link = `${window.location.origin}/integrations/gcp-secret-manager/authorize`;
break;
case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
@@ -64,7 +64,7 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
- link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
+ link = `${window.location.origin}/integrations/gitlab/authorize`;
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
@@ -87,6 +87,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
+ case "qovery":
+ link = `${window.location.origin}/integrations/qovery/authorize`;
+ break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
diff --git a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
index 054ce34a1..fdc04af9d 100644
--- a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
+++ b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
@@ -4,7 +4,8 @@ import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, Modal, ModalContent } from "@app/components/v2";
-import { useWorkspace } from "@app/context";
+import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
+import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
useDeleteIntegration,
@@ -31,26 +32,30 @@ type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
};
-export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
- const { t } = useTranslation();
- const { createNotification } = useNotificationContext();
- const router = useRouter();
+export const IntegrationsPage = withProjectPermission(
+ ({ frameworkIntegrations }: Props) => {
+ const { t } = useTranslation();
+ const { createNotification } = useNotificationContext();
+ const router = useRouter();
- const { currentWorkspace } = useWorkspace();
- const workspaceId = currentWorkspace?._id || "";
- const environments = currentWorkspace?.environments || [];
+ const { currentWorkspace } = useWorkspace();
+ const workspaceId = currentWorkspace?._id || "";
+ const environments = currentWorkspace?.environments || [];
- const { data: latestWsKey } = useGetUserWsKey(workspaceId);
+ const { data: latestWsKey } = useGetUserWsKey(workspaceId);
- const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
- "activeBot"
- ] as const);
+ const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
+ "activeBot"
+ ] as const);
- const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
- useGetCloudIntegrations();
+ const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
+ useGetCloudIntegrations();
- const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
- useGetWorkspaceAuthorizations(
+ const {
+ data: integrationAuths,
+ isLoading: isIntegrationAuthLoading,
+ isFetching: isIntegrationAuthFetching
+ } = useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record = {};
@@ -60,162 +65,178 @@ export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
return groupBy;
}, [])
);
- // mutation
- const {
- data: integrations,
- isLoading: isIntegrationLoading,
- isFetching: isIntegrationFetching
- } = useGetWorkspaceIntegrations(workspaceId);
+ // mutation
+ const {
+ data: integrations,
+ isLoading: isIntegrationLoading,
+ isFetching: isIntegrationFetching
+ } = useGetWorkspaceIntegrations(workspaceId);
- const { data: bot } = useGetWorkspaceBot(workspaceId);
+ const { data: bot } = useGetWorkspaceBot(workspaceId);
- // mutation
- const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
- useUpdateBotActiveStatus();
- const { mutateAsync: deleteIntegration } = useDeleteIntegration();
- const {
- mutateAsync: deleteIntegrationAuth,
- isLoading: isDeleteIntegrationAuthSuccess,
- reset: resetDeleteIntegrationAuth
- } = useDeleteIntegrationAuth();
+ // mutation
+ const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
+ useUpdateBotActiveStatus();
+ const { mutateAsync: deleteIntegration } = useDeleteIntegration();
+ const {
+ mutateAsync: deleteIntegrationAuth,
+ isSuccess: isDeleteIntegrationAuthSuccess,
+ reset: resetDeleteIntegrationAuth
+ } = useDeleteIntegrationAuth();
- // summary: this use effect is trigger when all integration auths are removed thus deactivate bot
- // details: so onsuccessfully deleting an integration auth, immediately integration list is refeteched
- // After the refetch is completed check if its empty. Then set bot active and reset the submit hook
- useEffect(() => {
- if (isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !integrations?.length) {
- if (bot?._id)
- updateBotActiveStatusSync({
- isActive: false,
- botId: bot._id,
- workspaceId
- });
- resetDeleteIntegrationAuth();
- }
- }, [isIntegrationFetching, isDeleteIntegrationAuthSuccess, integrations?.length]);
-
- const handleProviderIntegration = async (provider: string) => {
- const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
- if (!selectedCloudIntegration) return;
-
- try {
- if (bot && !bot.isActive) {
- const botKey = generateBotKey(bot.publicKey, latestWsKey!);
- await updateBotActiveStatus({
- workspaceId,
- botKey,
- isActive: true,
- botId: bot._id
- });
+ const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length;
+ const isIntegrationsEmpty = !integrations?.length;
+ // summary: this use effect is trigger when all integration auths are removed thus deactivate bot
+ // details: so on successfully deleting an integration auth, immediately integration list is refeteched
+ // After the refetch is completed check if its empty. Then set bot active and reset the submit hook for isSuccess to go back to false
+ useEffect(() => {
+ if (
+ isDeleteIntegrationAuthSuccess &&
+ !isIntegrationFetching &&
+ !isIntegrationAuthFetching &&
+ isIntegrationsAuthorizedEmpty &&
+ isIntegrationsEmpty
+ ) {
+ if (bot?._id)
+ updateBotActiveStatusSync({
+ isActive: false,
+ botId: bot._id,
+ workspaceId
+ });
+ resetDeleteIntegrationAuth();
}
- const integrationAuthForProvider = integrationAuths?.[provider];
- if (!integrationAuthForProvider) {
- redirectForProviderAuth(selectedCloudIntegration);
+ }, [
+ isIntegrationFetching,
+ isDeleteIntegrationAuthSuccess,
+ isIntegrationAuthFetching,
+ isIntegrationsAuthorizedEmpty,
+ isIntegrationsEmpty
+ ]);
+
+ const handleProviderIntegration = async (provider: string) => {
+ const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
+ if (!selectedCloudIntegration) return;
+
+ try {
+ if (bot && !bot.isActive) {
+ const botKey = generateBotKey(bot.publicKey, latestWsKey!);
+ await updateBotActiveStatus({
+ workspaceId,
+ botKey,
+ isActive: true,
+ botId: bot._id
+ });
+ }
+ const integrationAuthForProvider = integrationAuths?.[provider];
+ if (!integrationAuthForProvider) {
+ redirectForProviderAuth(selectedCloudIntegration);
+ return;
+ }
+
+ const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
+ router.push(url);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ // function to strat integration for a provider
+ // confirmation to user passing the bot key for provider to get secret access
+ const handleProviderIntegrationStart = (provider: string) => {
+ if (!bot?.isActive) {
+ handlePopUpOpen("activeBot", { provider });
return;
}
+ handleProviderIntegration(provider);
+ };
- const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
- router.push(url);
- } catch (error) {
- console.error(error);
- }
- };
+ const handleUserAcceptBotCondition = () => {
+ const { provider } = popUp.activeBot?.data as { provider: string };
+ handleProviderIntegration(provider);
+ handlePopUpClose("activeBot");
+ };
- // function to strat integration for a provider
- // confirmation to user passing the bot key for provider to get secret access
- const handleProviderIntegrationStart = (provider: string) => {
- if (!bot?.isActive) {
- handlePopUpOpen("activeBot", { provider });
- return;
- }
- handleProviderIntegration(provider);
- };
+ const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
+ try {
+ await deleteIntegration({ id: integrationId, workspaceId });
+ if (cb) cb();
+ createNotification({
+ type: "success",
+ text: "Deleted integration"
+ });
+ } catch (err) {
+ console.log(err);
+ createNotification({
+ type: "error",
+ text: "Failed to delete integration"
+ });
+ }
+ };
- const handleUserAcceptBotCondition = () => {
- const { provider } = popUp.activeBot?.data as { provider: string };
- handleProviderIntegration(provider);
- handlePopUpClose("activeBot");
- };
+ const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
+ const integrationAuthForProvider = integrationAuths?.[provider];
+ if (!integrationAuthForProvider) return;
+ try {
+ await deleteIntegrationAuth({
+ id: integrationAuthForProvider._id,
+ workspaceId
+ });
+ if (cb) cb();
+ createNotification({
+ type: "success",
+ text: "Revoked provider authentication"
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ type: "error",
+ text: "Failed to revoke provider authentication"
+ });
+ }
+ };
- const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
- try {
- await deleteIntegration({ id: integrationId, workspaceId });
- if (cb) cb();
- createNotification({
- type: "success",
- text: "Deleted integration"
- });
- } catch (err) {
- console.log(err);
- createNotification({
- type: "error",
- text: "Failed to delete integration"
- });
- }
- };
-
- const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
- const integrationAuthForProvider = integrationAuths?.[provider];
- if (!integrationAuthForProvider) return;
- try {
- await deleteIntegrationAuth({
- id: integrationAuthForProvider._id,
- workspaceId
- });
- if (cb) cb();
- createNotification({
- type: "success",
- text: "Revoked provider authentication"
- });
- } catch (err) {
- console.error(err);
- createNotification({
- type: "error",
- text: "Failed to revoke provider authentication"
- });
- }
- };
-
- return (
-
- handleIntegrationDelete(id, cb)}
- />
-
- handlePopUpToggle("activeBot", isOpen)}
- >
-
-
-
-
- }
+ return (
+
+ handleIntegrationDelete(id, cb)}
+ />
+
+ handlePopUpToggle("activeBot", isOpen)}
>
- {t("integrations.why-infisical-needs-access")}
-
-
-
-
- );
-};
+
+
+
+
+ }
+ >
+ {t("integrations.why-infisical-needs-access")}
+
+
+
+
+ );
+ },
+ { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Integrations }
+);
diff --git a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx
index 7f458d5e5..b0abc2603 100644
--- a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx
+++ b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx
@@ -2,7 +2,9 @@ import { useTranslation } from "react-i18next";
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { DeleteActionModal,Skeleton, Tooltip } from "@app/components/v2";
+import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
+import { DeleteActionModal, Skeleton, Tooltip } from "@app/components/v2";
+import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { usePopUp } from "@app/hooks";
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
@@ -28,11 +30,13 @@ export const CloudIntegrationSection = ({
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
+ const permission = useProjectPermission();
+ const { createNotification } = useNotificationContext();
const isEmpty = !isLoading && !cloudIntegrations?.length;
const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
-
+
return (
@@ -57,6 +61,18 @@ export const CloudIntegrationSection = ({
} flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
onClick={() => {
if (!cloudIntegration.isAvailable) return;
+ if (
+ permission.cannot(
+ ProjectPermissionActions.Create,
+ ProjectPermissionSub.Integrations
+ )
+ ) {
+ createNotification({
+ type: "error",
+ text: "You do not have permission to create an integration"
+ });
+ return;
+ }
onIntegrationStart(cloudIntegration.slug);
}}
key={cloudIntegration.slug}
diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx
index 72533796d..04f7e5bd9 100644
--- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx
+++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx
@@ -2,6 +2,7 @@ import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
+import { ProjectPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
EmptyState,
@@ -13,6 +14,7 @@ import {
Skeleton,
Tooltip
} from "@app/components/v2";
+import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks";
import { TIntegration } from "@app/hooks/api/types";
@@ -94,8 +96,30 @@ export const IntegrationsSection = ({
{integrationSlugNameMapping[integration.integration]}
+ {(integration.integration === "qovery") && (
+
+
+
+
+ {integration?.owner || "-"}
+
+
+
+
+
+ {integration?.targetService || "-"}
+
+
+
+
+
+ {integration?.targetEnvironment || "-"}
+
+
+
+ )}
-
+
{integration.integration === "hashicorp-vault"
? `${integration.app} - path: ${integration.path}`
@@ -125,18 +149,26 @@ export const IntegrationsSection = ({
)}
-
-
- handlePopUpOpen("deleteConfirmation", integration)}
- ariaLabel="delete"
- colorSchema="danger"
- variant="star"
- >
-
-
-
-
+
+ {(isAllowed: boolean) => (
+
+
+ handlePopUpOpen("deleteConfirmation", integration)}
+ ariaLabel="delete"
+ isDisabled={!isAllowed}
+ colorSchema="danger"
+ variant="star"
+ >
+
+
+
+
+ )}
+
))}
diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx
index 0d6b8e7f2..f05b44f4b 100644
--- a/frontend/src/views/Org/MembersPage/MembersPage.tsx
+++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx
@@ -1,238 +1,67 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
-import { useState } from "react";
import { useTranslation } from "react-i18next";
-import { useRouter } from "next/router";
+import { motion } from "framer-motion";
-import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
-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 { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
+import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
+import { withPermission } from "@app/hoc";
+import { useGetRoles } from "@app/hooks/api";
+import { TRole } from "@app/hooks/api/roles/types";
-import {
- OrgIncidentContactsTable,
- OrgMembersTable,
- OrgNameChangeSection,
- OrgServiceAccountsTable
-} from "./components";
+import { OrgMembersTable } from "./components/OrgMembersTable";
+import { OrgRoleTabSection } from "./components/OrgRoleTabSection";
-export const MembersPage = () => {
- const host = window.location.origin;
- const router = useRouter();
- const { action } = router.query;
+enum TabSections {
+ Member = "members",
+ Roles = "roles"
+}
- const { t } = useTranslation();
- const { currentOrg } = useOrganization();
- const { currentWorkspace } = useWorkspace();
- const { user } = useUser();
- const { subscription } = useSubscription();
- const { createNotification } = useNotificationContext();
+export const MembersPage = withPermission(
+ () => {
+ const { t } = useTranslation();
+ const { currentOrg } = useOrganization();
- const orgId = currentOrg?._id || "";
+ const orgId = currentOrg?._id || "";
- const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
- const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
- useGetUserWorkspaceMemberships(orgId);
- const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || "");
- const { data: incidentContact, isLoading: IsIncidentContactLoading } =
- useGetOrgIncidentContact(orgId);
+ const { data: roles, isLoading: isRolesLoading } = useGetRoles({
+ orgId
+ });
- const renameOrg = useRenameOrg();
- const removeUserOrgMembership = useDeleteOrgMembership();
- const addUserToOrg = useAddUserToOrg();
- const updateOrgUserRole = useUpdateOrgUserRole();
- const uploadWsKey = useUploadWsKey();
- const addIncidentContact = useAddIncidentContact();
- const removeIncidentContact = useDeleteIncidentContact();
-
- const [completeInviteLink, setcompleteInviteLink] = useState("");
-
- const isMoreUsersNotAllowed = subscription?.memberLimit ? (subscription.membersUsed >= subscription.memberLimit) : false;
-
- 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 user from org",
- type: "success"
- });
- } catch (error) {
- console.error(error);
- createNotification({
- text: "Failed to remove user from the organization",
- type: "error"
- });
- }
- };
- const onAddUserToOrg = async (email: string) => {
- if (!currentOrg?._id) return;
-
- try {
- const { data } = await addUserToOrg.mutateAsync({
- organizationId: currentOrg?._id,
- inviteeEmail: email
- });
- setcompleteInviteLink(data?.completeInviteLink);
-
- // only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
- if (!data.completeInviteLink) {
- createNotification({
- text: "Successfully invited user to the organization.",
- 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"
- });
- }
- };
-
- return (
-
-
-
- {t("section.members.org-members")}
-
-
+ return (
+
+
+
+ {t("section.members.org-members")}
+
+
+
+ Members
+ Roles
+
+
+
+ []}
+ isRolesLoading={isRolesLoading}
+ />
+
+
+
+ []}
+ isRolesLoading={isRolesLoading}
+ />
+
+
+
-
- );
-};
+ );
+ },
+ { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Member }
+);
diff --git a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
deleted file mode 100644
index cfafae765..000000000
--- a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import { useState } from "react";
-import { Controller, useForm } from "react-hook-form";
-import {
- faContactBook,
- 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,
- EmailServiceSetupModal,
- EmptyState,
- FormControl,
- IconButton,
- Input,
- Modal,
- ModalContent,
- Table,
- TableContainer,
- TableSkeleton,
- TBody,
- Td,
- Th,
- THead,
- Tr
-} from "@app/components/v2";
-import { usePopUp } from "@app/hooks";
-import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
-import { IncidentContact } from "@app/hooks/api/types";
-
-type Props = {
- isLoading?: boolean;
- 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,
- isLoading
-}: Props) => {
- const [searchContact, setSearchContact] = useState("");
- const { data: serverDetails } = useFetchServerStatus();
- const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
- "addContact",
- "removeContact",
- "setUpEmail"
- ] 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");
- };
-
- const filteredContacts = contacts.filter(({ email }) =>
- email.toLocaleLowerCase().includes(searchContact)
- );
-
- return (
-
-
-
- setSearchContact(e.target.value)}
- leftIcon={}
- placeholder="Search incident contact by email..."
- />
-
-
- }
- onClick={() => {
- if (serverDetails?.emailConfigured) {
- handlePopUpOpen("addContact");
- } else {
- handlePopUpOpen("setUpEmail");
- }
- }}
- >
- Add Contact
-
-
-
-
-
-
-
-
- | Email |
- |
-
-
-
- {isLoading && }
- {filteredContacts?.map(({ email }) => (
-
- | {email} |
-
- handlePopUpOpen("removeContact", { email })}
- >
-
-
- |
-
- ))}
-
-
- {filteredContacts?.length === 0 && !isLoading && (
-
- )}
-
-
- {
- handlePopUpToggle("addContact", isOpen);
- reset();
- }}
- >
-
-
-
-
- handlePopUpToggle("removeContact", isOpen)}
- onDeleteApproved={onRemoveIncidentContact}
- />
- handlePopUpToggle("setUpEmail", isOpen)}
- />
-
- );
-};
diff --git a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/index.tsx
deleted file mode 100644
index b1df7cd68..000000000
--- a/frontend/src/views/Org/MembersPage/components/OrgIncidentContactsTable/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx
index 2955f5b51..c08a1cf1e 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTable/OrgMembersTable.tsx
@@ -1,4 +1,4 @@
-import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import {
@@ -14,6 +14,11 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
+import { OrgPermissionCan } from "@app/components/permissions";
+import {
+ decryptAssymmetric,
+ encryptAssymmetric
+} from "@app/components/utilities/cryptography/crypto";
import {
Button,
DeleteActionModal,
@@ -37,26 +42,31 @@ import {
Tr,
UpgradePlanModal
} from "@app/components/v2";
-import { useOrganization, useWorkspace } from "@app/context";
+import {
+ OrgPermissionActions,
+ OrgPermissionSubjects,
+ useOrganization,
+ useSubscription,
+ useUser,
+ useWorkspace
+} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
-import { useGetSSOConfig } from "@app/hooks/api";
+import {
+ useAddUserToOrg,
+ useDeleteOrgMembership,
+ useGetOrgUsers,
+ useGetSSOConfig,
+ useGetUserWorkspaceMemberships,
+ useGetUserWsKey,
+ useUpdateOrgUserRole,
+ useUploadWsKey
+} from "@app/hooks/api";
+import { TRole } from "@app/hooks/api/roles/types";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
-import { OrgUser, Workspace } from "@app/hooks/api/types";
type Props = {
- members?: OrgUser[];
- workspaceMemberships?: Record;
- orgName: string;
- isLoading?: boolean;
- 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;
- completeInviteLink: string | undefined;
- setCompleteInviteLink: Dispatch>;
+ roles?: TRole[];
+ isRolesLoading?: boolean;
};
const addMemberFormSchema = yup.object({
@@ -65,27 +75,21 @@ const addMemberFormSchema = yup.object({
type TAddMemberForm = yup.InferType;
-export const OrgMembersTable = ({
- members = [],
- workspaceMemberships = {},
- orgName,
- isMoreUserNotAllowed,
- onRemoveMember,
- onInviteMember,
- onGrantAccess,
- onRoleChange,
- userId,
- isLoading,
- completeInviteLink,
- setCompleteInviteLink
-}: Props) => {
+export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
const router = useRouter();
const { createNotification } = useNotificationContext();
+
const { currentOrg } = useOrganization();
- const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
+ const { workspaces, currentWorkspace } = useWorkspace();
+ const { user } = useUser();
+ const userId = user?._id || "";
+ const orgId = currentOrg?._id || "";
+ const workspaceId = currentWorkspace?._id || "";
+
+ const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { data: serverDetails } = useFetchServerStatus();
- const { workspaces } = useWorkspace();
+
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addMember",
@@ -93,6 +97,23 @@ export const OrgMembersTable = ({
"upgradePlan",
"setUpEmail"
] as const);
+ const { subscription } = useSubscription();
+
+ const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
+ const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
+ useGetUserWorkspaceMemberships(orgId);
+ const { data: wsKey } = useGetUserWsKey(workspaceId);
+
+ const removeUserOrgMembership = useDeleteOrgMembership();
+ const addUserToOrg = useAddUserToOrg();
+ const updateOrgUserRole = useUpdateOrgUserRole();
+ const uploadWsKey = useUploadWsKey();
+
+ const [completeInviteLink, setCompleteInviteLink] = useState("");
+
+ const isMoreUsersNotAllowed = subscription?.memberLimit
+ ? subscription.membersUsed >= subscription.memberLimit
+ : false;
useEffect(() => {
if (router.query.action === "invite") {
@@ -108,32 +129,100 @@ export const OrgMembersTable = ({
} = useForm({ resolver: yupResolver(addMemberFormSchema) });
const onAddMember = async ({ email }: TAddMemberForm) => {
- await onInviteMember(email);
+ if (!currentOrg?._id) return;
+
+ try {
+ const { data } = await addUserToOrg.mutateAsync({
+ organizationId: currentOrg?._id,
+ inviteeEmail: email
+ });
+ setCompleteInviteLink(data?.completeInviteLink);
+ // only show this notification when email is configured.
+ // A [completeInviteLink] will not be sent if smtp is configured
+ if (!data.completeInviteLink) {
+ createNotification({
+ text: "Successfully invited user to the organization.",
+ type: "success"
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: "Failed to invite user to org",
+ type: "error"
+ });
+ }
if (serverDetails?.emailConfigured) {
handlePopUpClose("addMember");
}
-
reset();
};
+ const onAddUserToOrg = async (email: string) => {
+ if (!currentOrg?._id) return;
+
+ try {
+ const { data } = await addUserToOrg.mutateAsync({
+ organizationId: currentOrg?._id,
+ inviteeEmail: email
+ });
+ setCompleteInviteLink(data?.completeInviteLink);
+
+ // only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
+ if (!data.completeInviteLink) {
+ createNotification({
+ text: "Successfully invited user to the organization.",
+ type: "success"
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: "Failed to invite user to org",
+ type: "error"
+ });
+ }
+ };
+
const onRemoveOrgMemberApproved = async () => {
- const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id;
- await onRemoveMember(orgMembershipId);
+ const membershipId = (popUp?.removeMember?.data as { id: string })?.id;
+ if (!currentOrg?._id) return;
+
+ try {
+ await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
+ createNotification({
+ text: "Successfully removed user from org",
+ type: "success"
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: "Failed to remove user from the organization",
+ type: "error"
+ });
+ }
handlePopUpClose("removeMember");
};
const isIamOwner = useMemo(
- () => members.find(({ user }) => userId === user?._id)?.role === "owner",
+ () => members?.find(({ user: u }) => userId === u?._id)?.role === "owner",
[userId, members]
);
+ const findRoleFromId = useCallback(
+ (roleId: string) => {
+ return roles.find(({ _id: id }) => id === roleId);
+ },
+ [roles]
+ );
+
const filterdUser = useMemo(
() =>
- members.filter(
- ({ user, inviteEmail }) =>
- user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
- user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
- user?.email?.toLowerCase().includes(searchMemberFilter) ||
+ members?.filter(
+ ({ user: u, inviteEmail }) =>
+ u?.firstName?.toLowerCase().includes(searchMemberFilter) ||
+ u?.lastName?.toLowerCase().includes(searchMemberFilter) ||
+ u?.email?.toLowerCase().includes(searchMemberFilter) ||
inviteEmail?.includes(searchMemberFilter)
),
[members, searchMemberFilter]
@@ -147,11 +236,65 @@ export const OrgMembersTable = ({
return () => clearTimeout(timer);
}, [isInviteLinkCopied]);
+ const onRoleChange = 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 onGrantAccess = async (grantedUserId: 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: grantedUserId,
+ nonce,
+ encryptedKey: ciphertext,
+ workspaceId: currentWorkspace?._id || ""
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: "Failed to grant access to user",
+ type: "error"
+ });
+ }
+ };
+
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(completeInviteLink as string);
setInviteLinkCopied.on();
};
+ const isLoading = isMembersLoading || IsWsMembershipLoading || isRolesLoading;
+
return (
@@ -163,27 +306,32 @@ export const OrgMembersTable = ({
placeholder="Search members..."
/>
- }
- onClick={() => {
- if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
- createNotification({
- text: "You cannot invite users when SAML SSO is configured for your organization",
- type: "error"
- });
+
+ {(isAllowed) => (
+ }
+ onClick={() => {
+ if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
+ createNotification({
+ text: "You cannot invite users when SAML SSO is configured for your organization",
+ type: "error"
+ });
- return;
- }
+ return;
+ }
- if (isMoreUserNotAllowed) {
- handlePopUpOpen("upgradePlan");
- } else {
- handlePopUpOpen("addMember");
- }
- }}
- >
- Add Member
-
+ if (isMoreUsersNotAllowed) {
+ handlePopUpOpen("upgradePlan");
+ } else {
+ handlePopUpOpen("addMember");
+ }
+ }}
+ >
+ Add Member
+
+ )}
+
@@ -200,106 +348,134 @@ export const OrgMembersTable = ({
{isLoading && }
{!isLoading &&
- 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];
+ filterdUser?.map(
+ ({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => {
+ const name = u ? `${u.firstName} ${u.lastName}` : "-";
+ const email = u?.email || inviteEmail;
+ const userWs = workspaceMemberships?.[u?._id];
- return (
-
- | {name} |
- {email} |
-
- {status === "accepted" && (
- |
+
+ {userId !== u?._id && (
+
+ {(isAllowed) => (
+
+ handlePopUpOpen("removeMember", { id: orgMembershipId })
+ }
+ >
+
+
+ )}
+
+ )}
+ |
+
+ );
+ }
+ )}
{!isLoading && filterdUser?.length === 0 && (
@@ -315,7 +491,7 @@ export const OrgMembersTable = ({
}}
>
{!completeInviteLink && (
diff --git a/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx
deleted file mode 100644
index 525b52471..000000000
--- a/frontend/src/views/Org/MembersPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-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 (
- |