mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-02 08:27:38 +00:00
Compare commits
11 Commits
daniel/sec
...
daniel/upd
Author | SHA1 | Date | |
---|---|---|---|
|
4c8063c532 | ||
|
f4ae40cb86 | ||
|
14449b8b41 | ||
|
bcdcaa33a4 | ||
|
e8a8542757 | ||
|
73902c3ad6 | ||
|
da792d144d | ||
|
f7b09f5fc2 | ||
|
0c5155f8e6 | ||
|
4afe2f2377 | ||
|
1e07c2fe23 |
@@ -665,9 +665,9 @@
|
||||
"sdks/languages/node",
|
||||
"sdks/languages/python",
|
||||
"sdks/languages/java",
|
||||
"sdks/languages/csharp",
|
||||
"sdks/languages/go",
|
||||
"sdks/languages/ruby",
|
||||
"sdks/languages/csharp"
|
||||
"sdks/languages/ruby"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@@ -1,9 +1,10 @@
|
||||
---
|
||||
title: "Infisical .NET SDK"
|
||||
sidebarTitle: ".NET"
|
||||
url: "https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk"
|
||||
icon: "bars"
|
||||
---
|
||||
|
||||
{/*
|
||||
If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [Nuget Package](https://www.nuget.org/packages/Infisical.Sdk)
|
||||
@@ -590,4 +591,4 @@ var decryptedPlaintext = infisical.DecryptSymmetric(decryptOptions);
|
||||
|
||||
#### Returns (string)
|
||||
`Plaintext` (string): The decrypted plaintext.
|
||||
|
||||
*/}
|
||||
|
@@ -7,6 +7,7 @@ import React, {
|
||||
useMemo,
|
||||
useState
|
||||
} from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
|
||||
import { ViewMode } from "../types";
|
||||
|
||||
@@ -23,8 +24,11 @@ interface AccessTreeProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export type AccessTreeForm = { metadata: { key: string; value: string }[] };
|
||||
|
||||
export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => {
|
||||
const [secretName, setSecretName] = useState("");
|
||||
const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } });
|
||||
const [viewMode, setViewMode] = useState(ViewMode.Docked);
|
||||
|
||||
const value = useMemo(
|
||||
@@ -37,7 +41,11 @@ export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children
|
||||
[secretName, setSecretName, viewMode, setViewMode]
|
||||
);
|
||||
|
||||
return <AccessTreeContext.Provider value={value}>{children}</AccessTreeContext.Provider>;
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<AccessTreeContext.Provider value={value}>{children}</AccessTreeContext.Provider>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAccessTreeContext = (): AccessTreeContextProps => {
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Panel } from "@xyflow/react";
|
||||
|
||||
import { Button, FormLabel, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { MetadataForm } from "@app/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/MetadataForm";
|
||||
|
||||
import { ViewMode } from "../types";
|
||||
|
||||
@@ -32,6 +34,7 @@ export const PermissionSimulation = ({
|
||||
setSecretName
|
||||
}: TProps) => {
|
||||
const [expand, setExpand] = useState(false);
|
||||
const { control } = useFormContext();
|
||||
|
||||
const handlePermissionSimulation = () => {
|
||||
setExpand(true);
|
||||
@@ -139,6 +142,11 @@ export const PermissionSimulation = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{subject === ProjectPermissionSub.DynamicSecrets && (
|
||||
<div>
|
||||
<MetadataForm control={control} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||
import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react";
|
||||
|
||||
@@ -7,7 +8,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries";
|
||||
import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
import { useAccessTreeContext } from "../components";
|
||||
import { AccessTreeForm, useAccessTreeContext } from "../components";
|
||||
import { PermissionAccess } from "../types";
|
||||
import {
|
||||
createBaseEdge,
|
||||
@@ -36,6 +37,8 @@ export const useAccessTree = (
|
||||
) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
|
||||
const { control } = useFormContext<AccessTreeForm>();
|
||||
const metadata = useWatch({ control, name: "metadata" });
|
||||
const [nodes, setNodes] = useNodesState<Node>([]);
|
||||
const [edges, setEdges] = useEdgesState<Edge>([]);
|
||||
const [subject, setSubject] = useState(ProjectPermissionSub.Secrets);
|
||||
@@ -168,7 +171,8 @@ export const useAccessTree = (
|
||||
environment,
|
||||
subject,
|
||||
secretName,
|
||||
actionRuleMap
|
||||
actionRuleMap,
|
||||
metadata
|
||||
})
|
||||
);
|
||||
|
||||
@@ -266,7 +270,8 @@ export const useAccessTree = (
|
||||
subject,
|
||||
secretName,
|
||||
setNodes,
|
||||
setEdges
|
||||
setEdges,
|
||||
metadata
|
||||
]);
|
||||
|
||||
return {
|
||||
|
@@ -17,6 +17,27 @@ type Props = {
|
||||
access: PermissionAccess;
|
||||
} & Pick<ReturnType<typeof createFolderNode>["data"], "actionRuleMap" | "subject">;
|
||||
|
||||
type ConditionDisplayProps = {
|
||||
_key: string;
|
||||
operator: string;
|
||||
value: string | string[];
|
||||
};
|
||||
|
||||
const ConditionDisplay = ({ _key: key, value, operator }: ConditionDisplayProps) => {
|
||||
return (
|
||||
<li>
|
||||
<span className="font-medium capitalize text-mineshaft-100">{camelCaseToSpaces(key)}</span>{" "}
|
||||
<span className="text-mineshaft-200">
|
||||
{formatedConditionsOperatorNames[operator as PermissionConditionOperators]}
|
||||
</span>{" "}
|
||||
<span className="rounded bg-mineshaft-600 p-0.5 font-mono">
|
||||
{typeof value === "string" ? value : value.join(", ")}
|
||||
</span>
|
||||
.
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const FolderNodeTooltipContent = ({ action, access, actionRuleMap, subject }: Props) => {
|
||||
let component: ReactElement;
|
||||
|
||||
@@ -56,43 +77,58 @@ export const FolderNodeTooltipContent = ({ action, access, actionRuleMap, subjec
|
||||
{actionRuleMap.map((ruleMap, index) => {
|
||||
const rule = ruleMap[action];
|
||||
|
||||
if (
|
||||
!rule ||
|
||||
!rule.conditions ||
|
||||
(!rule.conditions.secretName && !rule.conditions.secretTags)
|
||||
)
|
||||
return null;
|
||||
if (!rule || !rule.conditions) return null;
|
||||
|
||||
return (
|
||||
<li key={`${action}_${index + 1}`}>
|
||||
<span className={`italic ${rule.inverted ? "text-red" : "text-green"} `}>
|
||||
{rule.inverted ? "Forbids" : "Allows"}
|
||||
</span>
|
||||
<span> when:</span>
|
||||
{Object.entries(rule.conditions).map(([key, condition]) => (
|
||||
<ul key={`${action}_${index + 1}_${key}`} className="list-[square] pl-4">
|
||||
{Object.entries(condition as object).map(([operator, value]) => (
|
||||
<li key={`${action}_${index + 1}_${key}_${operator}`}>
|
||||
<span className="font-medium capitalize text-mineshaft-100">
|
||||
{camelCaseToSpaces(key)}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">
|
||||
{
|
||||
formatedConditionsOperatorNames[
|
||||
operator as PermissionConditionOperators
|
||||
]
|
||||
if (
|
||||
rule.conditions.secretName ||
|
||||
rule.conditions.secretTags ||
|
||||
rule.conditions.metadata
|
||||
) {
|
||||
return (
|
||||
<li key={`${action}_${index + 1}`}>
|
||||
<span className="italic">{rule.inverted ? "Forbids" : "Allows"}</span>
|
||||
<span> when:</span>
|
||||
{Object.entries(rule.conditions).map(([key, condition]) => {
|
||||
if (key.match(/secretPath|environment/)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul key={`${action}_${index + 1}_${key}`} className="list-[square] pl-4">
|
||||
{Object.entries(condition as object).map(([operator, value]) => {
|
||||
if (operator === "$elemMatch") {
|
||||
return Object.entries(value as object).map(
|
||||
([nestedKey, nestedCondition]) =>
|
||||
Object.entries(nestedCondition as object).map(
|
||||
([nestedOperator, nestedValue]) => (
|
||||
<ConditionDisplay
|
||||
_key={`${key} ${nestedKey}`}
|
||||
operator={nestedOperator}
|
||||
value={nestedValue}
|
||||
key={`${action}_${index + 1}_${key}_${operator}_${nestedKey}_${nestedOperator}`}
|
||||
/>
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
</span>{" "}
|
||||
<span className={rule.inverted ? "text-red" : "text-green"}>
|
||||
{typeof value === "string" ? value : value.join(", ")}
|
||||
</span>
|
||||
.
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConditionDisplay
|
||||
_key={key}
|
||||
operator={operator}
|
||||
value={value}
|
||||
key={`${action}_${index + 1}_${key}_${operator}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
})}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { faFileImport, faFolder, faKey, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
|
||||
@@ -12,15 +12,15 @@ import { createRoleNode } from "../utils";
|
||||
const getSubjectIcon = (subject: ProjectPermissionSub) => {
|
||||
switch (subject) {
|
||||
case ProjectPermissionSub.Secrets:
|
||||
return <FontAwesomeIcon icon={faLock} className="h-4 w-4 text-yellow-700" />;
|
||||
return <FontAwesomeIcon icon={faKey} className="h-4 w-4 text-bunker-300" />;
|
||||
case ProjectPermissionSub.SecretFolders:
|
||||
return <FontAwesomeIcon icon={faFolder} className="h-4 w-4 text-yellow-700" />;
|
||||
case ProjectPermissionSub.DynamicSecrets:
|
||||
return <FontAwesomeIcon icon={faKey} className="h-4 w-4 text-yellow-700" />;
|
||||
return <FontAwesomeIcon icon={faFingerprint} className="h-4 w-4 text-yellow-700" />;
|
||||
case ProjectPermissionSub.SecretImports:
|
||||
return <FontAwesomeIcon icon={faFileImport} className="h-4 w-4 text-yellow-700" />;
|
||||
return <FontAwesomeIcon icon={faFileImport} className="h-4 w-4 text-green-700" />;
|
||||
default:
|
||||
return <FontAwesomeIcon icon={faLock} className="h-4 w-4 text-yellow-700" />;
|
||||
return <FontAwesomeIcon icon={faKey} className="h-4 w-4 text-bunker-300" />;
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -33,6 +33,12 @@ const ACTION_MAP: Record<string, string[] | undefined> = {
|
||||
]
|
||||
};
|
||||
|
||||
const SUBJECT_HEIGHT_MAP: Record<string, number> = {
|
||||
[ProjectPermissionSub.DynamicSecrets]: 130,
|
||||
[ProjectPermissionSub.Secrets]: 85,
|
||||
default: 64
|
||||
};
|
||||
|
||||
const evaluateCondition = (
|
||||
value: string,
|
||||
operator: PermissionConditionOperators,
|
||||
@@ -52,13 +58,113 @@ const evaluateCondition = (
|
||||
}
|
||||
};
|
||||
|
||||
const doesConditionMatch = (
|
||||
conditions: Record<string, any> | undefined,
|
||||
value: string
|
||||
): boolean => {
|
||||
if (!conditions) return true;
|
||||
|
||||
return Object.entries(conditions).every(([operator, comparisonValue]) =>
|
||||
evaluateCondition(value, operator as PermissionConditionOperators, comparisonValue)
|
||||
);
|
||||
};
|
||||
|
||||
const doBaseConditionsApply = (
|
||||
ruleConditions: any,
|
||||
environment: string,
|
||||
folderPath: string
|
||||
): boolean => {
|
||||
return (
|
||||
doesConditionMatch(ruleConditions?.environment, environment) &&
|
||||
doesConditionMatch(ruleConditions?.secretPath, folderPath)
|
||||
);
|
||||
};
|
||||
|
||||
const shouldShowConditionalAccess = (
|
||||
actionRuleMap: TActionRuleMap,
|
||||
action: string,
|
||||
environment: string,
|
||||
folderPath: string,
|
||||
conditionalFields: string[]
|
||||
): boolean => {
|
||||
return actionRuleMap.some((rule) => {
|
||||
const ruleConditions = rule[action]?.conditions;
|
||||
if (!ruleConditions) return false;
|
||||
|
||||
// Check if any of the conditional fields are present
|
||||
const hasConditionalField = conditionalFields.some((field) => ruleConditions[field]);
|
||||
if (!hasConditionalField) return false;
|
||||
|
||||
// Check if base conditions (environment and secretPath) apply
|
||||
return doBaseConditionsApply(ruleConditions, environment, folderPath);
|
||||
});
|
||||
};
|
||||
|
||||
const determineAccessLevel = (
|
||||
hasPermission: boolean,
|
||||
subject: ProjectPermissionSub,
|
||||
action: string,
|
||||
actionRuleMap: TActionRuleMap,
|
||||
environment: string,
|
||||
folderPath: string,
|
||||
secretName: string,
|
||||
metadata: Array<{ key: string; value: string }>
|
||||
): PermissionAccess => {
|
||||
if (!hasPermission) {
|
||||
return PermissionAccess.None;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.Secrets) {
|
||||
if (
|
||||
!secretName &&
|
||||
shouldShowConditionalAccess(actionRuleMap, action, environment, folderPath, [
|
||||
"secretName",
|
||||
"secretTags"
|
||||
])
|
||||
) {
|
||||
return PermissionAccess.Partial;
|
||||
}
|
||||
} else if (subject === ProjectPermissionSub.DynamicSecrets) {
|
||||
if (
|
||||
!metadata.length &&
|
||||
shouldShowConditionalAccess(actionRuleMap, action, environment, folderPath, ["metadata"])
|
||||
) {
|
||||
return PermissionAccess.Partial;
|
||||
}
|
||||
}
|
||||
|
||||
return PermissionAccess.Full;
|
||||
};
|
||||
|
||||
const checkPermission = (
|
||||
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>,
|
||||
subject: ProjectPermissionSub,
|
||||
action: string,
|
||||
subjectFields: any
|
||||
): boolean => {
|
||||
if (
|
||||
subject === ProjectPermissionSub.Secrets &&
|
||||
(action === ProjectPermissionSecretActions.ReadValue ||
|
||||
action === ProjectPermissionSecretActions.DescribeSecret)
|
||||
) {
|
||||
return hasSecretReadValueOrDescribePermission(permissions, action, subjectFields);
|
||||
}
|
||||
|
||||
return permissions.can(
|
||||
// @ts-expect-error we are not specifying which so can't resolve if valid
|
||||
action,
|
||||
abilitySubject(subject, subjectFields)
|
||||
);
|
||||
};
|
||||
|
||||
export const createFolderNode = ({
|
||||
folder,
|
||||
permissions,
|
||||
environment,
|
||||
subject,
|
||||
secretName,
|
||||
actionRuleMap
|
||||
actionRuleMap,
|
||||
metadata
|
||||
}: {
|
||||
folder: TSecretFolderWithPath;
|
||||
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
@@ -66,6 +172,7 @@ export const createFolderNode = ({
|
||||
subject: ProjectPermissionSub;
|
||||
secretName: string;
|
||||
actionRuleMap: TActionRuleMap;
|
||||
metadata: Array<{ key: string; value: string }>;
|
||||
}) => {
|
||||
const actions = Object.fromEntries(
|
||||
Object.values(ACTION_MAP[subject] ?? Object.values(ProjectPermissionActions)).map((action) => {
|
||||
@@ -73,74 +180,26 @@ export const createFolderNode = ({
|
||||
|
||||
// wrapped in try because while editing certain conditions, if their values are empty it throws an error
|
||||
try {
|
||||
let hasPermission: boolean;
|
||||
|
||||
const subjectFields = {
|
||||
secretPath: folder.path,
|
||||
environment,
|
||||
secretName: secretName || "*",
|
||||
secretTags: ["*"]
|
||||
secretTags: ["*"],
|
||||
metadata: metadata.length ? metadata : ["*"]
|
||||
};
|
||||
|
||||
if (
|
||||
subject === ProjectPermissionSub.Secrets &&
|
||||
(action === ProjectPermissionSecretActions.ReadValue ||
|
||||
action === ProjectPermissionSecretActions.DescribeSecret)
|
||||
) {
|
||||
hasPermission = hasSecretReadValueOrDescribePermission(
|
||||
permissions,
|
||||
action,
|
||||
subjectFields
|
||||
);
|
||||
} else {
|
||||
hasPermission = permissions.can(
|
||||
// @ts-expect-error we are not specifying which so can't resolve if valid
|
||||
action,
|
||||
abilitySubject(subject, subjectFields)
|
||||
);
|
||||
}
|
||||
const hasPermission = checkPermission(permissions, subject, action, subjectFields);
|
||||
|
||||
if (hasPermission) {
|
||||
// we want to show yellow/conditional access if user hasn't specified secret name to fully resolve access
|
||||
if (
|
||||
!secretName &&
|
||||
actionRuleMap.some((el) => {
|
||||
// we only show conditional if secretName/secretTags are present - environment and path can be directly determined
|
||||
if (!el[action]?.conditions?.secretName && !el[action]?.conditions?.secretTags)
|
||||
return false;
|
||||
|
||||
// make sure condition applies to env
|
||||
if (el[action]?.conditions?.environment) {
|
||||
if (
|
||||
!Object.entries(el[action]?.conditions?.environment).every(([operator, value]) =>
|
||||
evaluateCondition(environment, operator as PermissionConditionOperators, value)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// and applies to path
|
||||
if (el[action]?.conditions?.secretPath) {
|
||||
if (
|
||||
!Object.entries(el[action]?.conditions?.secretPath).every(([operator, value]) =>
|
||||
evaluateCondition(folder.path, operator as PermissionConditionOperators, value)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
) {
|
||||
access = PermissionAccess.Partial;
|
||||
} else {
|
||||
access = PermissionAccess.Full;
|
||||
}
|
||||
} else {
|
||||
access = PermissionAccess.None;
|
||||
}
|
||||
access = determineAccessLevel(
|
||||
hasPermission,
|
||||
subject,
|
||||
action,
|
||||
actionRuleMap,
|
||||
environment,
|
||||
folder.path,
|
||||
secretName,
|
||||
metadata
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
access = PermissionAccess.None;
|
||||
@@ -150,18 +209,7 @@ export const createFolderNode = ({
|
||||
})
|
||||
);
|
||||
|
||||
let height: number;
|
||||
|
||||
switch (subject) {
|
||||
case ProjectPermissionSub.DynamicSecrets:
|
||||
height = 130;
|
||||
break;
|
||||
case ProjectPermissionSub.Secrets:
|
||||
height = 85;
|
||||
break;
|
||||
default:
|
||||
height = 64;
|
||||
}
|
||||
const height = SUBJECT_HEIGHT_MAP[subject] ?? SUBJECT_HEIGHT_MAP.default;
|
||||
|
||||
return {
|
||||
type: PermissionNode.Folder,
|
||||
|
@@ -137,7 +137,7 @@ export const SecretPathInput = ({
|
||||
maxHeight: "var(--radix-select-content-available-height)"
|
||||
}}
|
||||
>
|
||||
<div className="max-h-[25vh] w-full flex-col items-center justify-center overflow-y-scroll rounded-md text-white">
|
||||
<div className="thin-scrollbar max-h-[25vh] w-full flex-col items-center justify-center overflow-y-scroll rounded-md text-white">
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<div
|
||||
tabIndex={0}
|
||||
|
@@ -163,7 +163,7 @@ export type IdentityManagementSubjectFields = {
|
||||
|
||||
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
|
||||
[PermissionConditionOperators.$EQ]: "equal to",
|
||||
[PermissionConditionOperators.$IN]: "contains",
|
||||
[PermissionConditionOperators.$IN]: "in",
|
||||
[PermissionConditionOperators.$ALL]: "contains all",
|
||||
[PermissionConditionOperators.$NEQ]: "not equal to",
|
||||
[PermissionConditionOperators.$GLOB]: "matches glob pattern",
|
||||
|
@@ -72,7 +72,7 @@ export const GroupsSection = () => {
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddGroupModal()}
|
||||
|
@@ -2,10 +2,11 @@ import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash,
|
||||
faUsers
|
||||
faUsers,
|
||||
faUsersSlash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -13,6 +14,10 @@ import { format } from "date-fns";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
@@ -184,33 +189,42 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content="Remove">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteGroup", {
|
||||
id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faUsersSlash} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteGroup", {
|
||||
id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Group From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
@@ -3,12 +3,13 @@ import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCircleXmark,
|
||||
faClock,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faServer,
|
||||
faXmark
|
||||
faServer
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -21,6 +22,10 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
@@ -163,20 +168,21 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<div className="flex w-full justify-end pr-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
>
|
||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||
Documentation{" "}
|
||||
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] ml-1 text-xs"
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
@@ -185,7 +191,7 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("identity")}
|
||||
@@ -231,7 +237,7 @@ export const IdentityTab = withProjectPermission(
|
||||
</Th>
|
||||
<Th className="w-1/3">Role</Th>
|
||||
<Th>Added on</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
@@ -372,37 +378,46 @@ export const IdentityTab = withProjectPermission(
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td className="flex justify-end space-x-2 opacity-0 duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton ariaLabel="more-icon" variant="plain">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
<Td className="flex justify-end space-x-2">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCircleXmark} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Identity From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
@@ -56,12 +56,12 @@ export const MembersSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addMember")}
|
||||
|
@@ -9,8 +9,8 @@ import {
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash,
|
||||
faUsers
|
||||
faUsers,
|
||||
faUserXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -416,32 +416,40 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== u?.id && (
|
||||
<div className="flex items-center space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("removeMember", { username: u.username });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton ariaLabel="more-icon" variant="plain">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("removeMember", { username: u.username });
|
||||
}}
|
||||
>
|
||||
Remove User From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
@@ -10,10 +10,10 @@ export const ProjectRoleListTab = withProjectPermission(
|
||||
return (
|
||||
<motion.div
|
||||
key="role-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: -30 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ProjectRoleList />
|
||||
</motion.div>
|
||||
|
@@ -1,4 +1,16 @@
|
||||
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faEye,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -12,6 +24,10 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -19,15 +35,27 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
|
||||
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
|
||||
|
||||
enum RolesOrderBy {
|
||||
Name = "name",
|
||||
Slug = "slug"
|
||||
}
|
||||
|
||||
export const ProjectRoleList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
@@ -57,6 +85,75 @@ export const ProjectRoleList = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy,
|
||||
search,
|
||||
setSearch,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
setPage,
|
||||
offset
|
||||
} = usePagination<RolesOrderBy>(RolesOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("projectRolesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("projectRolesTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredRoles = useMemo(
|
||||
() =>
|
||||
roles
|
||||
?.filter((role) => {
|
||||
const { slug, name } = role;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
name.toLowerCase().includes(searchValue) || slug.toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [roleOne, roleTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case RolesOrderBy.Slug:
|
||||
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
|
||||
case RolesOrderBy.Name:
|
||||
default:
|
||||
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
|
||||
}
|
||||
}) ?? [],
|
||||
[roles, orderDirection, search, orderBy]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredRoles.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: RolesOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: RolesOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: RolesOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
@@ -64,7 +161,7 @@ export const ProjectRoleList = () => {
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("role")}
|
||||
@@ -75,18 +172,50 @@ export const ProjectRoleList = () => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search project roles..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Slug
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Slug)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Slug)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Slug)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th aria-label="actions" className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||
{roles?.map((role) => {
|
||||
{isRolesLoading && <TableSkeleton columns={3} innerKey="project-roles" />}
|
||||
{filteredRoles?.slice(offset, perPage * page).map((role) => {
|
||||
const { id, name, slug } = role;
|
||||
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
|
||||
slug as ProjectMembershipRole
|
||||
@@ -109,88 +238,118 @@ export const ProjectRoleList = () => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
roleSlug: slug
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("duplicateRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Duplicate Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isNonMutatable && (
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isNonMutatable ? faEye : faEdit} />}
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", role);
|
||||
navigate({
|
||||
to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
roleSlug: slug
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Role
|
||||
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("duplicateRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Duplicate Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isNonMutatable && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50",
|
||||
"transition-colors duration-100"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredRoles?.length) && (
|
||||
<Pagination
|
||||
count={filteredRoles!.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!filteredRoles?.length && !isRolesLoading && (
|
||||
<EmptyState
|
||||
title={
|
||||
roles?.length
|
||||
? "No project roles match search..."
|
||||
: "This project does not have any roles"
|
||||
}
|
||||
icon={roles?.length ? faSearch : undefined}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
|
Reference in New Issue
Block a user