mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 08:29:55 +00:00
Merge pull request #3850 from Infisical/policy-edit-revisions
improvement(project-policies): Revamp edit role page and access tree
This commit is contained in:
@@ -2,10 +2,9 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||
import {
|
||||
faAnglesUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faDownLeftAndUpRightToCenter,
|
||||
faUpRightAndDownLeftFromCenter,
|
||||
faWindowRestore
|
||||
faWindowRestore,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
@@ -23,8 +22,8 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { Button, IconButton, Select, SelectItem, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionSet, ProjectPermissionSub } from "@app/context/ProjectPermissionContext";
|
||||
|
||||
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
|
||||
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
||||
@@ -36,15 +35,17 @@ import { ViewMode } from "./types";
|
||||
|
||||
export type AccessTreeProps = {
|
||||
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
subject: ProjectPermissionSub;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const EdgeTypes = { base: BasePermissionEdge };
|
||||
|
||||
const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode };
|
||||
|
||||
const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
const AccessTreeContent = ({ permissions, subject, onClose }: AccessTreeProps) => {
|
||||
const [selectedPath, setSelectedPath] = useState<string>("/");
|
||||
const accessTreeData = useAccessTree(permissions, selectedPath);
|
||||
const accessTreeData = useAccessTree(permissions, selectedPath, subject);
|
||||
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
|
||||
const [initialRender, setInitialRender] = useState(true);
|
||||
|
||||
@@ -78,32 +79,32 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
setInitialRender(true);
|
||||
}, [selectedPath, environment]);
|
||||
}, [selectedPath, environment, subject, viewMode]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (initialRender) {
|
||||
timer = setTimeout(() => {
|
||||
goToRootNode();
|
||||
fitView({ duration: 500 });
|
||||
setInitialRender(false);
|
||||
}, 500);
|
||||
}, 50);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [nodes, edges, getViewport(), initialRender, goToRootNode]);
|
||||
}, [nodes, edges, getViewport(), initialRender, fitView]);
|
||||
|
||||
const handleToggleModalView = () =>
|
||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
||||
|
||||
const handleToggleUndockedView = () =>
|
||||
setViewMode((prev) => (prev === ViewMode.Undocked ? ViewMode.Docked : ViewMode.Undocked));
|
||||
const handleToggleView = () =>
|
||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Undocked : ViewMode.Modal));
|
||||
|
||||
const undockButtonLabel = `${viewMode === ViewMode.Undocked ? "Dock" : "Undock"} View`;
|
||||
const windowButtonLabel = `${viewMode === ViewMode.Modal ? "Dock" : "Expand"} View`;
|
||||
const expandButtonLabel = viewMode === ViewMode.Modal ? "Anchor View" : "Expand View";
|
||||
const hideButtonLabel = "Hide Access Tree";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-full",
|
||||
"mt-4 w-full",
|
||||
viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10",
|
||||
viewMode === ViewMode.Undocked &&
|
||||
"fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]"
|
||||
@@ -130,7 +131,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
type="submit"
|
||||
className="h-10 rounded-r-none bg-mineshaft-700"
|
||||
leftIcon={<FontAwesomeIcon icon={faWindowRestore} />}
|
||||
onClick={handleToggleUndockedView}
|
||||
onClick={handleToggleView}
|
||||
>
|
||||
Undock
|
||||
</Button>
|
||||
@@ -176,48 +177,62 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
<Spinner />
|
||||
</Panel>
|
||||
)}
|
||||
{viewMode !== ViewMode.Undocked && (
|
||||
<Panel position="top-left" className="flex gap-2">
|
||||
<Select
|
||||
value={environment}
|
||||
onValueChange={accessTreeData.setEnvironment}
|
||||
className="w-60"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
aria-label="Environment"
|
||||
>
|
||||
{Object.values(accessTreeData.environments).map((env) => (
|
||||
<SelectItem
|
||||
key={env.slug}
|
||||
value={env.slug}
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 truncate font-medium">{env.name}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<AccessTreeSecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
onChange={setSelectedPath}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
{viewMode !== ViewMode.Docked && (
|
||||
<Panel position="top-right" className="flex gap-1.5">
|
||||
{viewMode !== ViewMode.Undocked && (
|
||||
<AccessTreeSecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
onChange={setSelectedPath}
|
||||
/>
|
||||
)}
|
||||
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
|
||||
<Panel position="top-right" className="flex gap-2">
|
||||
<Tooltip position="bottom" align="center" content={expandButtonLabel}>
|
||||
<IconButton
|
||||
className="ml-1 w-10 rounded"
|
||||
className="rounded p-2"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={handleToggleUndockedView}
|
||||
ariaLabel={undockButtonLabel}
|
||||
onClick={handleToggleView}
|
||||
ariaLabel={expandButtonLabel}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
viewMode === ViewMode.Undocked
|
||||
? faArrowUpRightFromSquare
|
||||
? faUpRightAndDownLeftFromCenter
|
||||
: faWindowRestore
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip align="end" position="bottom" content={windowButtonLabel}>
|
||||
<Tooltip align="end" position="bottom" content={hideButtonLabel}>
|
||||
<IconButton
|
||||
className="w-10 rounded"
|
||||
className="rounded p-2"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={handleToggleModalView}
|
||||
ariaLabel={windowButtonLabel}
|
||||
onClick={onClose}
|
||||
ariaLabel={hideButtonLabel}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
viewMode === ViewMode.Modal
|
||||
? faDownLeftAndUpRightToCenter
|
||||
: faUpRightAndDownLeftFromCenter
|
||||
}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Panel>
|
||||
@@ -253,6 +268,9 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
};
|
||||
|
||||
export const AccessTree = (props: AccessTreeProps) => {
|
||||
const { subject } = props;
|
||||
if (!subject) return null;
|
||||
|
||||
return (
|
||||
<AccessTreeErrorBoundary {...props}>
|
||||
<AccessTreeProvider>
|
||||
|
||||
@@ -29,7 +29,7 @@ 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 [viewMode, setViewMode] = useState(ViewMode.Modal);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -33,7 +33,8 @@ type LevelFolderMap = Record<
|
||||
|
||||
export const useAccessTree = (
|
||||
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>,
|
||||
searchPath: string
|
||||
searchPath: string,
|
||||
subject: ProjectPermissionSub
|
||||
) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
|
||||
@@ -41,7 +42,6 @@ export const useAccessTree = (
|
||||
const metadata = useWatch({ control, name: "metadata" });
|
||||
const [nodes, setNodes] = useNodesState<Node>([]);
|
||||
const [edges, setEdges] = useEdgesState<Edge>([]);
|
||||
const [subject, setSubject] = useState(ProjectPermissionSub.Secrets);
|
||||
const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? "");
|
||||
const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders(
|
||||
currentWorkspace.id
|
||||
@@ -147,9 +147,7 @@ export const useAccessTree = (
|
||||
const roleNode = createRoleNode({
|
||||
subject,
|
||||
environment: slug,
|
||||
environments: environmentsFolders,
|
||||
onSubjectChange: setSubject,
|
||||
onEnvironmentChange: setEnvironment
|
||||
environments: environmentsFolders
|
||||
});
|
||||
|
||||
const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
|
||||
@@ -280,7 +278,6 @@ export const useAccessTree = (
|
||||
subject,
|
||||
environment,
|
||||
setEnvironment,
|
||||
setSubject,
|
||||
isLoading: isPending,
|
||||
environments: currentWorkspace.environments,
|
||||
secretName,
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ export const AccessTreeSecretPathInput = ({
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip position="bottom" content="Search paths">
|
||||
<Tooltip position="bottom" content="Search Paths">
|
||||
<div
|
||||
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
|
||||
onClick={toggleSearch}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
|
||||
import { Select, SelectItem } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
@@ -29,7 +28,7 @@ const formatLabel = (text: string) => {
|
||||
};
|
||||
|
||||
export const RoleNode = ({
|
||||
data: { subject, environment, onSubjectChange, onEnvironmentChange, environments }
|
||||
data: { subject }
|
||||
}: NodeProps & {
|
||||
data: ReturnType<typeof createRoleNode>["data"] & {
|
||||
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
|
||||
@@ -44,61 +43,10 @@ export const RoleNode = ({
|
||||
className="pointer-events-none !cursor-pointer opacity-0"
|
||||
position={Position.Top}
|
||||
/>
|
||||
<div className="flex w-full flex-col items-center justify-center rounded-md border-2 border-mineshaft-500 bg-gradient-to-b from-mineshaft-700 to-mineshaft-800 px-5 py-4 font-inter shadow-2xl">
|
||||
<div className="flex w-full min-w-[240px] flex-col gap-4">
|
||||
<div className="flex w-full flex-col gap-1.5">
|
||||
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Subject</div>
|
||||
<Select
|
||||
value={subject}
|
||||
onValueChange={(value) => onSubjectChange(value as ProjectPermissionSub)}
|
||||
className="w-full rounded-md border border-mineshaft-600 bg-mineshaft-900/90 text-sm shadow-inner backdrop-blur-sm transition-all hover:border-amber-600/50 focus:border-amber-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
aria-label="Subject"
|
||||
>
|
||||
{[
|
||||
ProjectPermissionSub.Secrets,
|
||||
ProjectPermissionSub.SecretFolders,
|
||||
ProjectPermissionSub.DynamicSecrets,
|
||||
ProjectPermissionSub.SecretImports
|
||||
].map((sub) => {
|
||||
return (
|
||||
<SelectItem
|
||||
className="relative flex items-center gap-2 py-2 pl-8 pr-8 text-sm capitalize hover:bg-mineshaft-700"
|
||||
value={sub}
|
||||
key={sub}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{getSubjectIcon(sub)}
|
||||
<span className="font-medium">{formatLabel(sub)}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-1.5">
|
||||
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Environment</div>
|
||||
<Select
|
||||
value={environment}
|
||||
onValueChange={onEnvironmentChange}
|
||||
className="w-full rounded-md border border-mineshaft-600 bg-mineshaft-900/90 text-sm shadow-inner backdrop-blur-sm transition-all hover:border-amber-600/50 focus:border-amber-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
aria-label="Environment"
|
||||
>
|
||||
{Object.values(environments).map((env) => (
|
||||
<SelectItem
|
||||
key={env.slug}
|
||||
value={env.slug}
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">{env.name}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex h-14 w-full flex-col items-center justify-center rounded-md border border-mineshaft bg-mineshaft-800 px-2 py-3 font-inter shadow-lg transition-opacity duration-500">
|
||||
<div className="flex items-center space-x-2 text-mineshaft-100">
|
||||
{getSubjectIcon(subject)}
|
||||
<span className="text-sm">{formatLabel(subject)} Access</span>
|
||||
</div>
|
||||
</div>
|
||||
<Handle
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
@@ -8,24 +6,18 @@ import { PermissionNode } from "../types";
|
||||
export const createRoleNode = ({
|
||||
subject,
|
||||
environment,
|
||||
environments,
|
||||
onSubjectChange,
|
||||
onEnvironmentChange
|
||||
environments
|
||||
}: {
|
||||
subject: string;
|
||||
subject: ProjectPermissionSub;
|
||||
environment: string;
|
||||
environments: TProjectEnvironmentsFolders;
|
||||
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
|
||||
onEnvironmentChange: (value: string) => void;
|
||||
}) => ({
|
||||
id: `role-${subject}-${environment}`,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
subject,
|
||||
environment,
|
||||
environments,
|
||||
onSubjectChange,
|
||||
onEnvironmentChange
|
||||
environments
|
||||
},
|
||||
type: PermissionNode.Role,
|
||||
height: 48,
|
||||
|
||||
@@ -39,16 +39,6 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
||||
const positionedNodes = nodes.map((node) => {
|
||||
const { x, y } = dagre.node(node.id);
|
||||
|
||||
if (node.type === "role") {
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: x - (node.width ? node.width / 2 : 0),
|
||||
y: y - 150
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
|
||||
+13
-11
@@ -173,17 +173,19 @@ export const ProjectTemplateEditRoleForm = ({
|
||||
<div className="p-4">
|
||||
<div className="mb-2 text-lg">Policies</div>
|
||||
<PermissionEmptyState />
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
<div>
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
|
||||
@@ -40,9 +40,9 @@ export const Checkbox = ({
|
||||
<div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
|
||||
<CheckboxPrimitive.Root
|
||||
className={twMerge(
|
||||
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",
|
||||
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400/50 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",
|
||||
isDisabled && "bg-bunker-400 hover:bg-bunker-400",
|
||||
isChecked && "bg-primary hover:bg-primary",
|
||||
isChecked && "border-primary/30 bg-primary/10",
|
||||
Boolean(children) && "mr-3",
|
||||
className
|
||||
)}
|
||||
@@ -53,7 +53,10 @@ export const Checkbox = ({
|
||||
id={id}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={twMerge(`${checkIndicatorBg || "text-bunker-800"}`, indicatorClassName)}
|
||||
className={twMerge(
|
||||
`${checkIndicatorBg || "mt-[0.1rem] text-mineshaft-200"}`,
|
||||
indicatorClassName
|
||||
)}
|
||||
>
|
||||
{isIndeterminate ? (
|
||||
<FontAwesomeIcon icon={faMinus} size="sm" />
|
||||
|
||||
Reference in New Issue
Block a user