Merge pull request #2200 from Infisical/secret-sharing-fix

Minor UI Improvements
This commit is contained in:
BlackMagiq
2024-07-30 11:17:53 -07:00
committed by GitHub
24 changed files with 85 additions and 1134 deletions

View File

@@ -25,7 +25,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Details</h3>
<h3 className="text-lg font-semibold text-mineshaft-100">Identity Details</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (

View File

@@ -48,7 +48,7 @@ export const IdentityProjectRow = ({
return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`identity-project-membership-${id}`}
onClick={() => {
if (isAccessible) {

View File

@@ -86,7 +86,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
data?.map(({ identity: { id, name }, role, customRole }) => {
return (
<Tr
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`identity-${id}`}
onClick={() => router.push(`/org/${orgId}/identities/${id}`)}
>

View File

@@ -184,7 +184,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
return (
<Tr
key={`org-membership-${orgMembershipId}`}
className="h-10 w-full cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => router.push(`/org/${orgId}/memberships/${orgMembershipId}`)}
>
<Td className={isActive ? "" : "text-mineshaft-400"}>{name}</Td>

View File

@@ -93,7 +93,7 @@ export const OrgRoleTable = () => {
return (
<Tr
key={`role-list-${id}`}
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => router.push(`/org/${orgId}/roles/${id}`)}
>
<Td>{name}</Td>

View File

@@ -26,7 +26,7 @@ export const RoleDetailsSection = ({ roleId, handlePopUpOpen }: Props) => {
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Details</h3>
<h3 className="text-lg font-semibold text-mineshaft-100">Org Role Details</h3>
{isCustomRole && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
{(isAllowed) => {

View File

@@ -150,7 +150,7 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>

View File

@@ -16,23 +16,23 @@ import { RolePermissionRow } from "./RolePermissionRow";
const SIMPLE_PERMISSION_OPTIONS = [
{
title: "User management",
title: "User Management",
formName: "member"
},
{
title: "Group management",
title: "Group Management",
formName: "groups"
},
{
title: "Machine identity management",
title: "Machine Identity Management",
formName: "identity"
},
{
title: "Billing & usage",
title: "Usage & Billing",
formName: "billing"
},
{
title: "Role management",
title: "Role Management",
formName: "role"
},
{
@@ -40,7 +40,7 @@ const SIMPLE_PERMISSION_OPTIONS = [
formName: "incident-contact"
},
{
title: "Organization profile",
title: "Organization Profile",
formName: "settings"
},
{

View File

@@ -3,7 +3,8 @@ import {
faCheckCircle,
faCircleXmark,
faCopy,
faPencil} from "@fortawesome/free-solid-svg-icons";
faPencil
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
@@ -82,7 +83,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
return membership ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Details</h3>
<h3 className="text-lg font-semibold text-mineshaft-100">User Details</h3>
{userId !== membership.user.id && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {

View File

@@ -44,7 +44,7 @@ export const UserProjectRow = ({
return (
<Tr
className="group h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`user-project-membership-${id}`}
onClick={() => {
if (isAccessible) {

View File

@@ -92,7 +92,7 @@ export const ProjectRoleList = () => {
return (
<Tr
key={`role-list-${id}`}
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => router.push(`/project/${projectId}/roles/${slug}`)}
>
<Td>{name}</Td>

View File

@@ -1,258 +0,0 @@
import { useMemo } from "react";
import { Control, Controller, UseFormGetValues, UseFormSetValue, useWatch } from "react-hook-form";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
import {
Checkbox,
FormControl,
Input,
Select,
SelectItem,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
formName: "secrets";
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
getValue: UseFormGetValues<TFormSchema>;
control: Control<TFormSchema>;
title: string;
subtitle: string;
icon: IconProp;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
export const MultiEnvProjectPermission = ({
isNonEditable,
setValue,
getValue,
control,
formName,
title,
subtitle,
icon
}: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
const customRule = useWatch({
control,
name: `permissions.${formName}.custom`
});
const isCustom = Boolean(customRule);
const allRule = useWatch({ control, name: `permissions.${formName}.all` });
const selectedPermissionCategory = useMemo(() => {
const { read, delete: del, edit, create } = allRule || {};
if (read && del && edit && create) return Permission.FullAccess;
if (read) return Permission.ReadOnly;
return Permission.NoAccess;
}, [allRule]);
const handlePermissionChange = (val: Permission) => {
if(!val) return
switch (val) {
case Permission.NoAccess: {
const permissions = getValue("permissions");
if (permissions) delete permissions[formName];
setValue("permissions", permissions, { shouldDirty: true });
break;
}
case Permission.FullAccess:
setValue(
`permissions.${formName}`,
{ all: { read: true, edit: true, create: true, delete: true } },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
`permissions.${formName}`,
{ all: { read: true, edit: false, create: false, delete: false } },
{ shouldDirty: true }
);
break;
default:
setValue(
`permissions.${formName}`,
{ custom: { read: false, edit: false, create: false, delete: false } },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
(selectedPermissionCategory !== Permission.NoAccess || isCustom) &&
"border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={icon} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">{title}</div>
<div className="text-xs font-light">{subtitle}</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={isCustom ? Permission.Custom : selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "auto" : 0 }}
className="overflow-hidden"
>
<TableContainer className="mt-6 border-mineshaft-500">
<Table>
<THead>
<Tr>
<Th />
<Th className="min-w-[8rem]">
<div className="flex items-center gap-2">
Secret Path
<span className="text-xs normal-case">
<GlobPatternExamples />
</span>
</div>
</Th>
<Th className="text-center">View</Th>
<Th className="text-center">Create</Th>
<Th className="text-center">Modify</Th>
<Th className="text-center">Delete</Th>
</Tr>
</THead>
<TBody>
{isCustom &&
environments.map(({ name, slug }) => (
<Tr key={`custom-role-project-secret-${slug}`}>
<Td>{name}</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.secretPath`}
control={control}
render={({ field }) => (
/* eslint-disable-next-line no-template-curly-in-string */
<FormControl helperText="Supports glob path pattern string">
<Input
{...field}
className="w-full overflow-ellipsis"
placeholder="Glob patterns are supported"
/>
</FormControl>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.read`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.${formName}.${slug}.read`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.create`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
onBlur={field.onBlur}
id={`permissions.${formName}.${slug}.modify`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
name={`permissions.${formName}.${slug}.edit`}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
onBlur={field.onBlur}
id={`permissions.${formName}.${slug}.modify`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
<Td>
<Controller
defaultValue={false}
name={`permissions.${formName}.${slug}.delete`}
control={control}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.${formName}.${slug}.delete`}
isDisabled={isNonEditable}
/>
</div>
)}
/>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</motion.div>
</div>
);
};

View File

@@ -1,319 +0,0 @@
import { useForm } from "react-hook-form";
import { faElementor } from "@fortawesome/free-brands-svg-icons";
import {
faAnchorLock,
faArrowLeft,
faBook,
faCertificate,
faCog,
faKey,
faLock,
faNetworkWired,
faPuzzlePiece,
faServer,
faShield,
faTags,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Spinner } from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import {
useCreateProjectRole,
useGetProjectRoleBySlug,
useUpdateProjectRole
} from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
import {
formRolePermission2API,
formSchema,
rolePermission2Form,
TFormSchema
} from "./ProjectRoleModifySection.utils";
import { SecretRollbackPermission } from "./SecretRollbackPermission";
import { SingleProjectPermission } from "./SingleProjectPermission";
import { WsProjectPermission } from "./WsProjectPermission";
const SINGLE_PERMISSION_LIST = [
{
title: "Integrations",
subtitle: "Integration management control",
icon: faPuzzlePiece,
formName: "integrations"
},
{
title: "Secret Protect policy",
subtitle: "Manage policies for secret protection for unauthorized secret changes",
icon: faShield,
formName: ProjectPermissionSub.SecretApproval
},
{
title: "Roles",
subtitle: "Role management control",
icon: faUsers,
formName: "role"
},
{
title: "User management",
subtitle: "Add, view and remove users from the project",
icon: faUser,
formName: "member"
},
{
title: "Group management",
subtitle: "Add, view and remove user groups from the project",
icon: faUsers,
formName: "groups"
},
{
title: "Machine identity management",
subtitle: "Add, view, update and remove (machine) identities from the project",
icon: faServer,
formName: "identity"
},
{
title: "Webhooks",
subtitle: "Webhook management control",
icon: faAnchorLock,
formName: "webhooks"
},
{
title: "Service Tokens",
subtitle: "Token management control",
icon: faKey,
formName: "service-tokens"
},
{
title: "Settings",
subtitle: "Settings control",
icon: faCog,
formName: "settings"
},
{
title: "Environments",
subtitle: "Environment management control",
icon: faElementor,
formName: "environments"
},
{
title: "Tags",
subtitle: "Tag management control",
icon: faTags,
formName: "tags"
},
{
title: "Audit Logs",
subtitle: "Audit log management control",
icon: faBook,
formName: "audit-logs"
},
{
title: "IP Allowlist",
subtitle: "IP allowlist management control",
icon: faNetworkWired,
formName: "ip-allowlist"
},
{
title: "Certificate Authorities",
subtitle: "CA management control",
icon: faCertificate,
formName: "certificate-authorities"
},
{
title: "Certificates",
subtitle: "Certificate management control",
icon: faCertificate,
formName: "certificates"
}
] as const;
type Props = {
roleSlug?: string;
onGoBack: VoidFunction;
};
export const ProjectRoleModifySection = ({ roleSlug, onGoBack }: Props) => {
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(roleSlug || "");
const isNewRole = !roleSlug;
const { currentWorkspace } = useWorkspace();
const projectSlug = currentWorkspace?.slug || "";
const { data: roleDetails, isLoading: isRoleDetailsLoading } = useGetProjectRoleBySlug(
currentWorkspace?.slug || "",
roleSlug as string
);
const {
handleSubmit,
register,
formState: { isSubmitting, isDirty, errors },
setValue,
getValues,
control
} = useForm<TFormSchema>({
values: roleDetails
? { ...roleDetails, permissions: rolePermission2Form(roleDetails.permissions) }
: ({} as TProjectRole),
resolver: zodResolver(formSchema)
});
const { mutateAsync: createRole } = useCreateProjectRole();
const { mutateAsync: updateRole } = useUpdateProjectRole();
const handleRoleUpdate = async (el: TFormSchema) => {
if (!roleDetails?.id) return;
try {
await updateRole({
id: roleDetails?.id as string,
projectSlug,
...el,
permissions: formRolePermission2API(el.permissions)
});
createNotification({ type: "success", text: "Successfully updated role" });
onGoBack();
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update role" });
}
};
const handleFormSubmit = async (el: TFormSchema) => {
if (!isNewRole) {
await handleRoleUpdate(el);
return;
}
try {
await createRole({
projectSlug,
...el,
permissions: formRolePermission2API(el.permissions)
});
createNotification({ type: "success", text: "Created new role" });
onGoBack();
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to create role" });
}
};
if (!isNewRole && isRoleDetailsLoading) {
return (
<div className="flex w-full items-center justify-center p-8">
<Spinner />
</div>
);
}
return (
<div>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="mb-2 flex items-center justify-between">
<h1 className="text-xl font-semibold text-mineshaft-100">
{isNewRole ? "New" : "Edit"} Role
</h1>
<Button
onClick={onGoBack}
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
>
Go back
</Button>
</div>
<p className="mb-8 text-gray-400">
Project-level roles allow you to define permissions for resources within projects at a
granular level
</p>
<div className="flex flex-col space-y-6">
<FormControl
label="Name"
isRequired
className="mb-0"
isError={Boolean(errors?.name)}
errorText={errors?.name?.message}
>
<Input {...register("name")} placeholder="Billing Team" isReadOnly={isNonEditable} />
</FormControl>
<FormControl
label="Slug"
isRequired
isError={Boolean(errors?.slug)}
errorText={errors?.slug?.message}
>
<Input {...register("slug")} placeholder="biller" isReadOnly={isNonEditable} />
</FormControl>
<FormControl
label="Description"
helperText="A short description about this role"
isError={Boolean(errors?.description)}
errorText={errors?.description?.message}
>
<Input {...register("description")} isReadOnly={isNonEditable} />
</FormControl>
<div className="flex items-center justify-between border-t border-t-mineshaft-800 pt-6">
<div>
<h2 className="text-xl font-medium">Add Permission</h2>
</div>
</div>
<div>
<MultiEnvProjectPermission
getValue={getValues}
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
icon={faLock}
title="Secrets"
subtitle="Create, modify and remove secrets, folders and secret imports"
formName="secrets"
/>
</div>
<div key="permission-ws">
<WsProjectPermission
control={control}
setValue={setValue}
isNonEditable={isNonEditable}
/>
</div>
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
<div key={`permission-${title}`}>
<SingleProjectPermission
isNonEditable={isNonEditable}
control={control}
setValue={setValue}
icon={icon}
title={title}
subtitle={subtitle}
formName={formName}
/>
</div>
))}
<div key="permission-secret-rollback">
<SecretRollbackPermission
control={control}
setValue={setValue}
isNonEditable={isNonEditable}
/>
</div>
</div>
<div className="mt-12 flex items-center space-x-4">
<Button
type="submit"
isDisabled={isSubmitting || isNonEditable || !isDirty}
isLoading={isSubmitting}
>
{isNewRole ? "Create Role" : "Save Role"}
</Button>
<Button onClick={onGoBack} variant="outline_bg">
Cancel
</Button>
</div>
</form>
</div>
);
};

View File

@@ -1,147 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "create", label: "Perform Rollback" },
{ action: "read", label: "View" }
] as const;
export const SecretRollbackPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.secret-rollback"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if(!val) return;
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.secret-rollback",
{ read: false, create: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.secret-rollback",
{ read: true, create: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.secret-rollback",
{ read: true, create: false },
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.secret-rollback",
{ read: false, create: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">Secret Rollback</div>
<div className="text-xs font-light">Secret rollback control actions</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.secret-rollback.${action}`}
key={`permissions.secret-rollback.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.secret-rollback.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};

View File

@@ -1,194 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
formName:
| "role"
| "member"
| "groups"
| "integrations"
| "webhooks"
| "service-tokens"
| "settings"
| "environments"
| "tags"
| "audit-logs"
| "ip-allowlist"
| "identity"
| "certificate-authorities"
| "certificates"
| ProjectPermissionSub.SecretApproval;
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
title: string;
subtitle: string;
icon: IconProp;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "View" },
{ action: "create", label: "Create" },
{ action: "edit", label: "Modify" },
{ action: "delete", label: "Remove" }
] as const;
const MEMBERS_PERMISSIONS = [
{ action: "read", label: "View all members" },
{ action: "create", label: "Invite members" },
{ action: "edit", label: "Edit members" },
{ action: "delete", label: "Remove members" }
] as const;
const getPermissionList = (option: Props["formName"]) => {
switch (option) {
case "member":
return MEMBERS_PERMISSIONS;
default:
return PERMISSIONS;
}
};
export const SingleProjectPermission = ({
isNonEditable,
setValue,
control,
formName,
subtitle,
title,
icon
}: Props) => {
const rule = useWatch({
control,
name: `permissions.${formName}`
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if(!val) return;
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
`permissions.${formName}`,
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
`permissions.${formName}`,
{ read: true, edit: true, create: true, delete: true },
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
`permissions.${formName}`,
{ read: true, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
default:
setValue(
`permissions.${formName}`,
{ read: false, edit: false, create: false, delete: false },
{ shouldDirty: true }
);
break;
}
};
return (
<div
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={icon} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">{title}</div>
<div className="text-xs font-light">{subtitle}</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
>
{isCustom &&
getPermissionList(formName).map(({ action, label }) => (
<Controller
name={`permissions.${formName}.${action}`}
key={`permissions.${formName}.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.${formName}.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};

View File

@@ -1,126 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "edit", label: "Update project details" },
{ action: "delete", label: "Delete projects" }
] as const;
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.workspace"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if(!val) return;
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
break;
case Permission.FullAccess:
setValue("permissions.workspace", { edit: true, delete: true }, { shouldDirty: true });
break;
default:
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
break;
}
};
return (
<div
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">Project</div>
<div className="text-xs font-light">Project control actions</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.workspace.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};

View File

@@ -1 +0,0 @@
export { ProjectRoleModifySection } from "./ProjectRoleModifySection";

View File

@@ -26,7 +26,7 @@ export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Details</h3>
<h3 className="text-lg font-semibold text-mineshaft-100">Project Role Details</h3>
{isCustomRole && (
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Role}>
{(isAllowed) => {

View File

@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
const GENERAL_PERMISSIONS = [
{ action: "read", label: "View" },
@@ -153,7 +153,7 @@ export const RolePermissionRow = ({ isEditable, title, formName, control, setVal
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>

View File

@@ -19,7 +19,7 @@ import {
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { TFormSchema } from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
type Props = {
title: string;

View File

@@ -10,7 +10,7 @@ import {
formSchema,
rolePermission2Form,
TFormSchema
} from "@app/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils";
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
import { RolePermissionRow } from "./RolePermissionRow";
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
@@ -33,15 +33,15 @@ const SINGLE_PERMISSION_LIST = [
formName: "role"
},
{
title: "User management",
title: "User Management",
formName: "member"
},
{
title: "Group management",
title: "Group Management",
formName: "groups"
},
{
title: "Machine identity management",
title: "Machine Identity Management",
formName: "identity"
},
{

View File

@@ -7,9 +7,9 @@ import { ShareSecretForm } from "./components";
export const ShareSecretPublicPage = () => {
return (
<div className="flex h-screen flex-col justify-between bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<div />
<div className="mx-auto w-full max-w-xl px-4">
<div className="mx-auto w-full max-w-xl p-4">
<div className="mb-8 text-center">
<div className="mb-4 flex justify-center pt-8">
<Link href="https://infisical.com">
@@ -43,36 +43,34 @@ export const ShareSecretPublicPage = () => {
<div className="m-auto my-8 flex w-full">
<div className="w-full border-t border-mineshaft-600" />
</div>
<div className="m-auto flex max-w-2xl flex-col items-center justify-center">
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
Open source{" "}
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
secret management
</span>{" "}
for developers
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
Open source{" "}
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
secret management
</span>{" "}
for developers
</p>
<div className="flex flex-col items-start sm:flex-row sm:items-center">
<p className="md:text-md text-md mr-4">
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical
</a>{" "}
is the all-in-one secret management platform to securely manage secrets, configs, and
certificates across your team and infrastructure.
</p>
<div className="flex items-center">
<p className="md:text-md text-md mr-4">
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical
</a>{" "}
is the all-in-one secret management platform to securely manage secrets, configs,
and certificates across your team and infrastructure.
</p>
<div className="cursor-pointer">
<Link href="https://infisical.com">
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
<FontAwesomeIcon icon={faArrowRight} />
</div>
</Link>
</div>
<div className="mt-4 cursor-pointer sm:mt-0">
<Link href="https://infisical.com">
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
<FontAwesomeIcon icon={faArrowRight} />
</div>
</Link>
</div>
</div>
</div>

View File

@@ -22,9 +22,9 @@ export const ViewSecretPublicPage = () => {
});
return (
<div className="flex h-screen flex-col justify-between bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<div />
<div className="mx-auto w-full max-w-xl px-4 ">
<div className="mx-auto w-full max-w-xl p-4 ">
<div className="mb-8 text-center">
<div className="mb-4 flex justify-center pt-8">
<Link href="https://infisical.com">
@@ -57,47 +57,44 @@ export const ViewSecretPublicPage = () => {
<div className="m-auto my-8 flex w-full">
<div className="w-full border-t border-mineshaft-600" />
</div>
<div className="m-auto flex max-w-2xl flex-col items-center justify-center">
<div className="m-auto mb-12 flex w-full max-w-2xl flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
Open source{" "}
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
secret management
</span>{" "}
for developers
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
Open source{" "}
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
secret management
</span>{" "}
for developers
</p>
<div className="flex flex-col items-start sm:flex-row sm:items-center">
<p className="md:text-md text-md mr-4">
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical
</a>{" "}
is the all-in-one secret management platform to securely manage secrets, configs, and
certificates across your team and infrastructure.
</p>
<div className="flex items-center">
<p className="md:text-md text-md mr-4">
<a
href="https://github.com/infisical/infisical"
target="_blank"
rel="noopener noreferrer"
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Infisical
</a>{" "}
is the all-in-one secret management platform to securely manage secrets, configs,
and certificates across your team and infrastructure.
</p>
<div className="cursor-pointer">
<Link href="https://infisical.com">
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
<FontAwesomeIcon icon={faArrowRight} />
</div>
</Link>
</div>
<div className="mt-4 cursor-pointer sm:mt-0">
<Link href="https://infisical.com">
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 py-2 px-3 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
<FontAwesomeIcon icon={faArrowRight} />
</div>
</Link>
</div>
</div>
</div>
</div>
<div className="w-full bg-mineshaft-600 p-2">
<p className="text-center text-sm text-mineshaft-300">
© 2024{" "}
Made with ❤️ by{" "}
<a className="text-primary" href="https://infisical.com">
Infisical
</a>
. All rights reserved.
<br />
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
</p>