mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 07:27:58 +00:00
merge main
This commit is contained in:
+14
-2
@@ -27,6 +27,11 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useListWorkspaceGroups } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
@@ -62,7 +67,14 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
||||
orderDirection,
|
||||
orderBy,
|
||||
toggleOrderDirection
|
||||
} = usePagination(GroupsOrderBy.Name, { initPerPage: 20 });
|
||||
} = usePagination(GroupsOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("projectGroupsTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("projectGroupsTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const { data: groupMemberships = [], isPending } = useListWorkspaceGroups(
|
||||
currentWorkspace?.id || ""
|
||||
@@ -183,7 +195,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !filteredGroupMemberships?.length && (
|
||||
|
||||
+14
-2
@@ -42,6 +42,11 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
||||
@@ -72,7 +77,14 @@ export const IdentityTab = withProjectPermission(
|
||||
perPage,
|
||||
page,
|
||||
setPerPage
|
||||
} = usePagination(ProjectIdentityOrderBy.Name);
|
||||
} = usePagination(ProjectIdentityOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("projectIdentityTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("projectIdentityTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const workspaceId = currentWorkspace?.id ?? "";
|
||||
|
||||
@@ -403,7 +415,7 @@ export const IdentityTab = withProjectPermission(
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && data && data?.identityMemberships.length === 0 && (
|
||||
|
||||
+282
-206
@@ -1,11 +1,15 @@
|
||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faEye, faTrashCan } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRotateLeft,
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faClock,
|
||||
faLockOpen,
|
||||
faPencil,
|
||||
faPlus,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -205,7 +209,7 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
if (!data.secretPath) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Please select a secret path",
|
||||
text: "Please select a secret path...",
|
||||
title: "Error"
|
||||
});
|
||||
return;
|
||||
@@ -264,18 +268,18 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 w-full">
|
||||
<div className="w-full">
|
||||
<form onSubmit={privilegeForm.handleSubmit(handleSubmit)}>
|
||||
<div className={twMerge("flex items-start gap-4", !privilege && "flex-wrap")}>
|
||||
<div className={twMerge("flex items-start", !privilege && "flex-col")}>
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="environmentSlug"
|
||||
render={({ field: { onChange, ...field } }) => (
|
||||
<FormControl label="Environment">
|
||||
<FormControl label="Environment" className="w-full">
|
||||
<Select
|
||||
{...field}
|
||||
isDisabled={isMemberEditDisabled}
|
||||
className="bg-mineshaft-600 hover:bg-mineshaft-500"
|
||||
className="w-full bg-mineshaft-900 hover:bg-mineshaft-800"
|
||||
onValueChange={(e) => onChange(e)}
|
||||
>
|
||||
{currentWorkspace?.environments?.map(({ slug, id, name }) => (
|
||||
@@ -297,12 +301,13 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
isDisabled={!!selectablePaths.length}
|
||||
content="The selected environment doesn't have any policies."
|
||||
>
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<FormControl label="Secret Path">
|
||||
<Select
|
||||
{...field}
|
||||
isDisabled={isMemberEditDisabled || !selectablePaths.length}
|
||||
className="w-48"
|
||||
className="w-full hover:bg-mineshaft-800"
|
||||
placeholder="Select a secret path"
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
>
|
||||
{selectablePaths.map((path) => (
|
||||
@@ -328,222 +333,287 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-grow justify-between">
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="read"
|
||||
render={({ field }) => (
|
||||
<div className="flex flex-col items-center">
|
||||
<FormLabel label="View" className="mb-4" />
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-read"
|
||||
className="h-5 w-5"
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="create"
|
||||
render={({ field }) => (
|
||||
<div className="flex flex-col items-center">
|
||||
<FormLabel label="Create" className="mb-4" />
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-create"
|
||||
className="h-5 w-5"
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="edit"
|
||||
render={({ field }) => (
|
||||
<div className="flex flex-col items-center">
|
||||
<FormLabel label="Modify" className="mb-4" />
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-modify"
|
||||
className="h-5 w-5"
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="delete"
|
||||
render={({ field }) => (
|
||||
<div className="flex flex-col items-center">
|
||||
<FormLabel label="Delete" className="mb-4" />
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-delete"
|
||||
className="h-5 w-5"
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center space-x-2">
|
||||
<Popover>
|
||||
<PopoverTrigger disabled={isMemberEditDisabled}>
|
||||
<div>
|
||||
<Tooltip content={getAccessLabel(true)}>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
|
||||
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
|
||||
isDisabled={isMemberEditDisabled}
|
||||
className={twMerge(
|
||||
"border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
|
||||
isTemporary && "text-primary",
|
||||
isExpired && "text-red-600"
|
||||
)}
|
||||
<FormControl label="Permissions" className="w-full">
|
||||
<div className="flex w-full flex-col justify-between">
|
||||
<div className="flex w-full flex-row gap-2">
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="read"
|
||||
render={({ field }) => (
|
||||
<label
|
||||
className={`group my-1 flex w-full cursor-pointer flex-row items-center justify-start gap-2 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-3 duration-100 hover:border-primary/30 hover:bg-primary/10 ${field.value ? "border-primary/50 bg-primary/10 hover:border-primary/50" : ""}`}
|
||||
htmlFor="secret-read"
|
||||
>
|
||||
{getAccessLabel(false)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
arrowClassName="fill-gray-600"
|
||||
side="right"
|
||||
sideOffset={12}
|
||||
hideCloseBtn
|
||||
className="border border-gray-600 pt-4"
|
||||
>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
||||
Configure timed access
|
||||
</div>
|
||||
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
defaultValue="1h"
|
||||
name="temporaryAccess.temporaryRange"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Validity" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-read"
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "bg-primary hover:bg-primary/80" : ""}`}
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
<div className="ml-1 flex flex-col text-mineshaft-300">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<FontAwesomeIcon
|
||||
icon={faEye}
|
||||
className={`text-sm ${field.value ? "text-primary-200" : ""}`}
|
||||
/>
|
||||
<FormLabel
|
||||
label="View"
|
||||
className={`ml-0.5 mt-0 text-mineshaft-300 ${field.value ? "text-primary-200" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-mineshaft-400">Read secret values</p>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="create"
|
||||
render={({ field }) => (
|
||||
<label
|
||||
className={`group my-1 flex w-full cursor-pointer flex-row items-center justify-start gap-2 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-3 duration-100 hover:border-primary/30 hover:bg-primary/10 ${field.value ? "border-primary/50 bg-primary/10 hover:border-primary/50" : ""}`}
|
||||
htmlFor="secret-change"
|
||||
>
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-change"
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "bg-primary hover:bg-primary/80" : ""}`}
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
<div className="ml-1 flex flex-col text-mineshaft-300">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<FontAwesomeIcon
|
||||
icon={faPlus}
|
||||
className={`text-sm ${field.value ? "text-primary-200" : ""}`}
|
||||
/>
|
||||
<FormLabel
|
||||
label="Create"
|
||||
className={`ml-0.5 mt-0 text-mineshaft-300 ${field.value ? "text-primary-200" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-mineshaft-400">Create new secrets</p>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-row gap-2">
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="edit"
|
||||
render={({ field }) => (
|
||||
<label
|
||||
className={`group my-1 flex w-full cursor-pointer flex-row items-center justify-start gap-2 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-3 duration-100 hover:border-primary/30 hover:bg-primary/10 ${field.value ? "border-primary/50 bg-primary/10 hover:border-primary/50" : ""}`}
|
||||
htmlFor="secret-modify"
|
||||
>
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-modify"
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "bg-primary hover:bg-primary/80" : ""}`}
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
<div className="ml-1 flex flex-col text-mineshaft-300">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<FontAwesomeIcon
|
||||
icon={faPencil}
|
||||
className={`text-sm ${field.value ? "text-primary-200" : ""}`}
|
||||
/>
|
||||
<FormLabel
|
||||
label="Modify"
|
||||
className={`ml-0.5 mt-0 text-mineshaft-300 ${field.value ? "text-primary-200" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-mineshaft-400">Update existing secrets</p>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
name="delete"
|
||||
render={({ field }) => (
|
||||
<label
|
||||
className={`group my-1 flex w-full cursor-pointer flex-row items-center justify-start gap-2 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-3 duration-100 hover:border-primary/30 hover:bg-primary/10 ${field.value ? "border-primary/50 bg-primary/10 hover:border-primary/50" : ""}`}
|
||||
htmlFor="secret-delete"
|
||||
>
|
||||
<Checkbox
|
||||
isDisabled={isMemberEditDisabled}
|
||||
id="secret-delete"
|
||||
className={`mx-2 h-5 w-5 ${field.value ? "bg-primary hover:bg-primary/80" : ""}`}
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
||||
/>
|
||||
<div className="ml-1 flex flex-col text-mineshaft-300">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<FontAwesomeIcon
|
||||
icon={faTrashCan}
|
||||
className={`text-sm ${field.value ? "text-primary-200" : ""}`}
|
||||
/>
|
||||
<FormLabel
|
||||
label="Delete"
|
||||
className={`ml-0.5 mt-0 text-mineshaft-300 ${field.value ? "text-primary-200" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-mineshaft-400">Delete existing secrets</p>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormControl label="Time Period" className="w-full">
|
||||
<div className="mt-1 flex w-full items-center space-x-2">
|
||||
<Popover>
|
||||
<PopoverTrigger disabled={isMemberEditDisabled}>
|
||||
<div className="w-full">
|
||||
<Tooltip content={getAccessLabel(true)}>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
|
||||
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-4" />}
|
||||
isDisabled={isMemberEditDisabled}
|
||||
className={twMerge(
|
||||
"w-full border-mineshaft-600 bg-mineshaft-900 py-2.5 text-sm capitalize text-mineshaft-300 hover:border-mineshaft-600 hover:bg-mineshaft-800",
|
||||
isExpired && "text-red-600"
|
||||
)}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
const temporaryRange = privilegeForm.getValues(
|
||||
"temporaryAccess.temporaryRange"
|
||||
);
|
||||
if (!temporaryRange) {
|
||||
privilegeForm.setError(
|
||||
"temporaryAccess.temporaryRange",
|
||||
{ type: "required", message: "Required" },
|
||||
{ shouldFocus: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
privilegeForm.clearErrors("temporaryAccess.temporaryRange");
|
||||
privilegeForm.setValue(
|
||||
"temporaryAccess",
|
||||
{
|
||||
isTemporary: true,
|
||||
temporaryAccessStartTime: new Date().toISOString(),
|
||||
temporaryRange,
|
||||
temporaryAccessEndTime: new Date(
|
||||
new Date().getTime() + ms(temporaryRange)
|
||||
).toISOString()
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
>
|
||||
{temporaryAccessField.isTemporary && !policies ? "Restart" : "Grant"}
|
||||
</Button>
|
||||
|
||||
{temporaryAccessField.isTemporary && (
|
||||
{getAccessLabel(false)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
arrowClassName="fill-mineshaft-600"
|
||||
side="right"
|
||||
sideOffset={12}
|
||||
hideCloseBtn
|
||||
className="border border-mineshaft-600 bg-mineshaft-800 pt-4"
|
||||
>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="text-sm text-mineshaft-300">Configure timed access</div>
|
||||
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
||||
<Controller
|
||||
control={privilegeForm.control}
|
||||
defaultValue="1h"
|
||||
name="temporaryAccess.temporaryRange"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Validity" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
colorSchema="danger"
|
||||
onClick={() => {
|
||||
privilegeForm.setValue("temporaryAccess", {
|
||||
isTemporary: false
|
||||
});
|
||||
const temporaryRange = privilegeForm.getValues(
|
||||
"temporaryAccess.temporaryRange"
|
||||
);
|
||||
if (!temporaryRange) {
|
||||
privilegeForm.setError(
|
||||
"temporaryAccess.temporaryRange",
|
||||
{ type: "required", message: "Required" },
|
||||
{ shouldFocus: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
privilegeForm.clearErrors("temporaryAccess.temporaryRange");
|
||||
privilegeForm.setValue(
|
||||
"temporaryAccess",
|
||||
{
|
||||
isTemporary: true,
|
||||
temporaryAccessStartTime: new Date().toISOString(),
|
||||
temporaryRange,
|
||||
temporaryAccessEndTime: new Date(
|
||||
new Date().getTime() + ms(temporaryRange)
|
||||
).toISOString()
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{temporaryAccessField.isTemporary && !policies ? "Restart" : "Grant"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{temporaryAccessField.isTemporary && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
colorSchema="danger"
|
||||
onClick={() => {
|
||||
privilegeForm.setValue("temporaryAccess", {
|
||||
isTemporary: false
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{privilegeForm.formState.isDirty && privilege ? (
|
||||
<>
|
||||
<Tooltip content="Cancel" className="mr-4">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
className="border border-mineshaft-500 bg-mineshaft-600 py-2.5 hover:border-red/70 hover:bg-red/20"
|
||||
ariaLabel="delete-privilege"
|
||||
isDisabled={privilegeForm.formState.isSubmitting}
|
||||
onClick={() => privilegeForm.reset()}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{privilegeForm.formState.isDirty && privilege ? (
|
||||
<>
|
||||
<Tooltip content="Cancel" className="mr-4">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
className="border border-mineshaft-500 bg-mineshaft-600 py-2.5 hover:border-red/70 hover:bg-red/20"
|
||||
ariaLabel="delete-privilege"
|
||||
isDisabled={privilegeForm.formState.isSubmitting}
|
||||
onClick={() => privilegeForm.reset()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateLeft} className="py-0.5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={isMemberEditDisabled ? "Access restricted" : "Save"}
|
||||
className="mr-4"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateLeft} className="py-0.5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
isDisabled={isMemberEditDisabled}
|
||||
className="border-none py-3"
|
||||
ariaLabel="save-privilege"
|
||||
type="submit"
|
||||
>
|
||||
{privilegeForm.formState.isSubmitting ? (
|
||||
<Spinner size="xs" className="m-0 h-3 w-3 text-slate-500" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="px-0.5" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : // eslint-disable-next-line no-nested-ternary
|
||||
privilege ? (
|
||||
<Tooltip
|
||||
content={isMemberEditDisabled ? "Access restricted" : "Save"}
|
||||
content={isMemberEditDisabled ? "Access restricted" : "Delete"}
|
||||
className="mr-4"
|
||||
>
|
||||
<IconButton
|
||||
isDisabled={isMemberEditDisabled}
|
||||
className="border-none py-3"
|
||||
ariaLabel="save-privilege"
|
||||
type="submit"
|
||||
variant="outline_bg"
|
||||
className="border border-mineshaft-500 bg-mineshaft-600 py-3 hover:border-red/70 hover:bg-red/20"
|
||||
ariaLabel="delete-privilege"
|
||||
onClick={() => handlePopUpOpen("deletePrivilege")}
|
||||
>
|
||||
{privilegeForm.formState.isSubmitting ? (
|
||||
<Spinner size="xs" className="m-0 h-3 w-3 text-slate-500" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="px-0.5" />
|
||||
)}
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : // eslint-disable-next-line no-nested-ternary
|
||||
privilege ? (
|
||||
<Tooltip
|
||||
content={isMemberEditDisabled ? "Access restricted" : "Delete"}
|
||||
className="mr-4"
|
||||
>
|
||||
<IconButton
|
||||
isDisabled={isMemberEditDisabled}
|
||||
variant="outline_bg"
|
||||
className="border border-mineshaft-500 bg-mineshaft-600 py-3 hover:border-red/70 hover:bg-red/20"
|
||||
ariaLabel="delete-privilege"
|
||||
onClick={() => handlePopUpOpen("deletePrivilege")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="mb-4 flex w-full">
|
||||
<Controller
|
||||
@@ -552,7 +622,13 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
render={({ field }) => (
|
||||
<div className="w-full">
|
||||
<FormLabel label="Note" className="mb-2" />
|
||||
<Input {...field} isDisabled={isMemberEditDisabled} maxLength={255} />
|
||||
<Input
|
||||
{...field}
|
||||
isDisabled={isMemberEditDisabled}
|
||||
maxLength={255}
|
||||
placeholder="Add the reason for this access request..."
|
||||
className="text-mineshaft-300"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
+7
-2
@@ -51,6 +51,11 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useGetProjectRoles, useGetWorkspaceUsers } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
@@ -101,12 +106,12 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
setOrderDirection,
|
||||
toggleOrderDirection
|
||||
} = usePagination<MembersOrderBy>(MembersOrderBy.Name, {
|
||||
initPerPage: parseInt(localStorage.getItem("PROJECT_MEMBERS_TABLE_PER_PAGE") || "20", 10)
|
||||
initPerPage: getUserTablePreference("projectMembersTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
localStorage.setItem("PROJECT_MEMBERS_TABLE_PER_PAGE", newPerPage.toString());
|
||||
setUserTablePreference("projectMembersTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const { data: members = [], isPending: isMembersLoading } = useGetWorkspaceUsers(
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const PkiTemplatePermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
|
||||
const permissionSubject = ProjectPermissionSub.CertificateTemplates;
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${permissionSubject}.${position}.conditions`
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||
</p>
|
||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{items.fields.map((el, index) => {
|
||||
const condition =
|
||||
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
|
||||
lhs: string;
|
||||
rhs: string;
|
||||
operator: string;
|
||||
}) || {};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<div className="w-1/4">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="name">Name</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "name",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+81
-13
@@ -18,6 +18,7 @@ import {
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretScanningConfigActions,
|
||||
@@ -55,6 +56,13 @@ const SecretPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretActions.Create]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const ApprovalPolicyActionSchema = z.object({
|
||||
[ProjectPermissionActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionActions.Create]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const CmekPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
edit: z.boolean().optional(),
|
||||
@@ -164,6 +172,15 @@ const PkiSubscriberPolicyActionSchema = z.object({
|
||||
[ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const PkiTemplatePolicyActionSchema = z.object({
|
||||
[ProjectPermissionPkiTemplateActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiTemplateActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiTemplateActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiTemplateActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiTemplateActions.IssueCert]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiTemplateActions.ListCerts]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretRollbackPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
@@ -271,7 +288,12 @@ export const projectRoleFormSchema = z.object({
|
||||
.default([]),
|
||||
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateTemplates]: PkiTemplatePolicyActionSchema.extend({
|
||||
inverted: z.boolean().optional(),
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
|
||||
[]
|
||||
),
|
||||
@@ -284,7 +306,7 @@ export const projectRoleFormSchema = z.object({
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.SshHostGroups]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretApproval]: ApprovalPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
|
||||
@@ -317,6 +339,7 @@ type TConditionalFields =
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.DynamicSecrets
|
||||
| ProjectPermissionSub.PkiSubscribers
|
||||
| ProjectPermissionSub.CertificateTemplates
|
||||
| ProjectPermissionSub.SshHosts
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| ProjectPermissionSub.Identity;
|
||||
@@ -331,7 +354,8 @@ export const isConditionalSubjects = (
|
||||
subject === ProjectPermissionSub.Identity ||
|
||||
subject === ProjectPermissionSub.SshHosts ||
|
||||
subject === ProjectPermissionSub.SecretRotation ||
|
||||
subject === ProjectPermissionSub.PkiSubscribers;
|
||||
subject === ProjectPermissionSub.PkiSubscribers ||
|
||||
subject === ProjectPermissionSub.CertificateTemplates;
|
||||
|
||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
||||
@@ -430,8 +454,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.CertificateAuthorities,
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.CertificateTemplates,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.Tags,
|
||||
ProjectPermissionSub.SecretRotation,
|
||||
ProjectPermissionSub.Kms,
|
||||
@@ -593,6 +615,22 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretApproval) {
|
||||
const canCreate = action.includes(ProjectPermissionActions.Create);
|
||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// Map actions to the keys defined in ApprovalPolicyActionSchema
|
||||
if (canCreate) formVal[subject]![0][ProjectPermissionActions.Create] = true;
|
||||
if (canDelete) formVal[subject]![0][ProjectPermissionActions.Delete] = true;
|
||||
if (canEdit) formVal[subject]![0][ProjectPermissionActions.Edit] = true;
|
||||
if (canRead) formVal[subject]![0][ProjectPermissionActions.Read] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretRollback) {
|
||||
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||
const canCreate = action.includes(ProjectPermissionActions.Create);
|
||||
@@ -846,6 +884,34 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.CertificateTemplates) {
|
||||
if (!formVal[subject]) formVal[subject] = [];
|
||||
|
||||
formVal[subject]!.push({
|
||||
[ProjectPermissionPkiTemplateActions.Edit]: action.includes(
|
||||
ProjectPermissionPkiTemplateActions.Edit
|
||||
),
|
||||
[ProjectPermissionPkiTemplateActions.Delete]: action.includes(
|
||||
ProjectPermissionPkiTemplateActions.Delete
|
||||
),
|
||||
[ProjectPermissionPkiTemplateActions.Create]: action.includes(
|
||||
ProjectPermissionPkiTemplateActions.Create
|
||||
),
|
||||
[ProjectPermissionPkiTemplateActions.Read]: action.includes(
|
||||
ProjectPermissionPkiTemplateActions.Read
|
||||
),
|
||||
[ProjectPermissionPkiTemplateActions.IssueCert]: action.includes(
|
||||
ProjectPermissionPkiTemplateActions.IssueCert
|
||||
),
|
||||
[ProjectPermissionPkiTemplateActions.ListCerts]: action.includes(
|
||||
ProjectPermissionPkiTemplateActions.ListCerts
|
||||
),
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1184,10 +1250,12 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
[ProjectPermissionSub.CertificateTemplates]: {
|
||||
title: "Certificate Templates",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
{ label: "Read", value: ProjectPermissionPkiTemplateActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionPkiTemplateActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionPkiTemplateActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionPkiTemplateActions.Delete },
|
||||
{ label: "Issue Certificates", value: ProjectPermissionPkiTemplateActions.IssueCert },
|
||||
{ label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SshCertificateAuthorities]: {
|
||||
@@ -1268,10 +1336,10 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
[ProjectPermissionSub.SecretApproval]: {
|
||||
title: "Secret Approval Policies",
|
||||
actions: [
|
||||
{ label: "Read", value: "read" },
|
||||
{ label: "Create", value: "create" },
|
||||
{ label: "Modify", value: "edit" },
|
||||
{ label: "Remove", value: "delete" }
|
||||
{ label: "Read", value: ProjectPermissionActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionActions.Delete }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SecretRotation]: {
|
||||
|
||||
+7
-2
@@ -23,6 +23,7 @@ import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
||||
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
|
||||
import { PkiTemplatePermissionConditions } from "./PkiTemplatePermissionConditions";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
isConditionalSubjects,
|
||||
@@ -63,6 +64,10 @@ export const renderConditionalComponents = (
|
||||
return <PkiSubscriberPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.CertificateTemplates) {
|
||||
return <PkiTemplatePermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
||||
}
|
||||
|
||||
@@ -153,8 +158,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
"mr-4 h-10 border",
|
||||
isDirty && "bg-primary text-black hover:bg-primary hover:opacity-80"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
|
||||
Reference in New Issue
Block a user