mirror of
https://github.com/Infisical/infisical.git
synced 2025-06-29 04:31:59 +00:00
Compare commits
12 Commits
secret-syn
...
update-aws
Author | SHA1 | Date | |
---|---|---|---|
2e256e4282 | |||
dcd21883d1 | |||
205442bff5 | |||
e8d19eb823 | |||
5d30215ea7 | |||
29fedfdde5 | |||
b5317d1d75 | |||
86c145301e | |||
5b4790ee78 | |||
8683693103 | |||
737fffcceb | |||
ffac24ce75 |
@ -148,3 +148,11 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="What's the relationship between 'path' and 'key schema'?">
|
||||
The path is required and will be prepended to the key schema. For example, if you have a path of `/demo/path/` and a key schema of `INFISICAL_{{secretKey}}`, then the result will be `/demo/path/INFISICAL_{{secretKey}}`.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
@ -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,
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -30,7 +30,29 @@ export const AwsParameterStoreSyncFields = () => {
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Path">
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Path"
|
||||
tooltipText={
|
||||
<>
|
||||
The path is required and will be prepended to the key schema. For example, if you
|
||||
have a path of{" "}
|
||||
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
|
||||
/demo/path/
|
||||
</code>{" "}
|
||||
and a key schema of{" "}
|
||||
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
|
||||
INFISICAL_{"{{secretKey}}"}
|
||||
</code>
|
||||
, then the result will be{" "}
|
||||
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
|
||||
/demo/path/INFISICAL_{"{{secretKey}}"}
|
||||
</code>
|
||||
</>
|
||||
}
|
||||
tooltipClassName="max-w-lg"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="Path..." />
|
||||
</FormControl>
|
||||
)}
|
||||
|
@ -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" />
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { TooltipProps as RootProps } from "@radix-ui/react-tooltip";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & {
|
||||
@ -14,6 +15,7 @@ export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "
|
||||
isDisabled?: boolean;
|
||||
center?: boolean;
|
||||
size?: "sm" | "md";
|
||||
rootProps?: RootProps;
|
||||
};
|
||||
|
||||
export const Tooltip = ({
|
||||
@ -28,12 +30,14 @@ export const Tooltip = ({
|
||||
isDisabled,
|
||||
position = "top",
|
||||
size = "md",
|
||||
rootProps,
|
||||
...props
|
||||
}: TooltipProps) =>
|
||||
// just render children if tooltip content is empty
|
||||
content ? (
|
||||
<TooltipPrimitive.Root
|
||||
delayDuration={50}
|
||||
{...rootProps}
|
||||
open={isOpen}
|
||||
defaultOpen={defaultOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
|
@ -161,6 +161,18 @@ export type IdentityManagementSubjectFields = {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type ConditionalProjectPermissionSubject =
|
||||
| ProjectPermissionSub.SecretSyncs
|
||||
| ProjectPermissionSub.Secrets
|
||||
| ProjectPermissionSub.DynamicSecrets
|
||||
| ProjectPermissionSub.Identity
|
||||
| ProjectPermissionSub.SshHosts
|
||||
| ProjectPermissionSub.PkiSubscribers
|
||||
| ProjectPermissionSub.CertificateTemplates
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.SecretRotation;
|
||||
|
||||
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
|
||||
[PermissionConditionOperators.$EQ]: "equal to",
|
||||
[PermissionConditionOperators.$IN]: "in",
|
||||
|
@ -352,19 +352,21 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
<div className="p-4">
|
||||
<div className="mb-2 text-lg">Policies</div>
|
||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
|
||||
(permissionSubject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={permissionSubject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
|
||||
key={`project-permission-${permissionSubject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(permissionSubject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
)
|
||||
)}
|
||||
<div>
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
|
||||
(permissionSubject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={permissionSubject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
|
||||
key={`project-permission-${permissionSubject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(permissionSubject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
|
@ -348,17 +348,19 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
<div className="p-4">
|
||||
<div className="mb-2 text-lg">Policies</div>
|
||||
{(isCreate || !isPending) && <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>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faCopy, faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@ -12,19 +14,17 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
PageHeader,
|
||||
Tooltip
|
||||
PageHeader
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
|
||||
import { RolePermissionsSection } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection";
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { RoleDetailsSection } from "./components/RoleDetailsSection";
|
||||
import { RoleModal } from "./components/RoleModal";
|
||||
import { RolePermissionsSection } from "./components/RolePermissionsSection";
|
||||
|
||||
const Page = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -88,17 +88,29 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.name}>
|
||||
<PageHeader
|
||||
title={
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
<span>{data.name}</span>
|
||||
<p className="text-sm font-[400] normal-case leading-3 text-mineshaft-400">
|
||||
{data.slug} {data.description && `- ${data.description}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{isCustomRole && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<Button variant="outline_bg">More</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
rightIcon={<FontAwesomeIcon icon={faEllipsisV} className="ml-2" />}
|
||||
>
|
||||
Options
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="p-1">
|
||||
<DropdownMenuContent align="end" sideOffset={2} className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
@ -113,6 +125,7 @@ const Page = () => {
|
||||
roleSlug
|
||||
})
|
||||
}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Edit Role
|
||||
@ -128,6 +141,7 @@ const Page = () => {
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("duplicateRole");
|
||||
}}
|
||||
@ -143,13 +157,9 @@ const Page = () => {
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={() => handlePopUpOpen("deleteRole")}
|
||||
disabled={!isAllowed}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Delete Role
|
||||
</DropdownMenuItem>
|
||||
@ -159,12 +169,7 @@ const Page = () => {
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
|
||||
</div>
|
||||
<RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
|
||||
</div>
|
||||
)}
|
||||
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
|
@ -24,7 +24,7 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Button
|
||||
className="h-10 rounded-r-none"
|
||||
variant="outline_bg"
|
||||
@ -73,6 +73,6 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
|
||||
isOpen={popUp.applyTemplate.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,206 @@
|
||||
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 {
|
||||
ConditionalProjectPermissionSubject,
|
||||
PermissionConditionOperators
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
getConditionOperatorHelperInfo,
|
||||
renderOperatorSelectItems
|
||||
} from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
export const ConditionsFields = ({
|
||||
isDisabled,
|
||||
subject,
|
||||
position,
|
||||
selectOptions
|
||||
}: {
|
||||
isDisabled: boolean | undefined;
|
||||
subject: ConditionalProjectPermissionSubject;
|
||||
position: number;
|
||||
selectOptions: [{ value: string; label: string }, ...{ value: string; label: string }[]];
|
||||
}) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${subject}.${position}.conditions`
|
||||
});
|
||||
|
||||
const conditionErrorMessage =
|
||||
errors?.permissions?.[subject]?.[position]?.conditions?.message ||
|
||||
errors?.permissions?.[subject]?.[position]?.conditions?.root?.message;
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="mt-2.5 flex items-center text-gray-300">
|
||||
<span>Conditions</span>
|
||||
<Tooltip
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
Conditions determine when a policy will be applied (always if no conditions are
|
||||
present).
|
||||
</p>
|
||||
<p className="mt-3">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon size="xs" className="ml-1 text-mineshaft-400" icon={faInfoCircle} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="mt-2"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: selectOptions[0].value,
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{Boolean(items.fields.length) &&
|
||||
items.fields.map((el, index) => {
|
||||
const condition =
|
||||
(watch(`permissions.${subject}.${position}.conditions.${index}`) as {
|
||||
lhs: string;
|
||||
rhs: string;
|
||||
operator: string;
|
||||
}) || {};
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex items-start gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<div className="w-1/4">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${subject}.${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) => {
|
||||
setValue(
|
||||
`permissions.${subject}.${position}.conditions.${index}.operator`,
|
||||
PermissionConditionOperators.$IN as never
|
||||
);
|
||||
field.onChange(e);
|
||||
}}
|
||||
position="popper"
|
||||
className="w-full"
|
||||
>
|
||||
{selectOptions.map(({ value, label }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-44 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${subject}.${position}.conditions.${index}.operator`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Select
|
||||
position="popper"
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{renderOperatorSelectItems(condition.lhs)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-bunker-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${subject}.${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>
|
||||
<IconButton
|
||||
ariaLabel="remove"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{conditionErrorMessage && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{conditionErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,26 +1,6 @@
|
||||
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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
getConditionOperatorHelperInfo,
|
||||
renderOperatorSelectItems
|
||||
} from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
@ -28,162 +8,17 @@ type Props = {
|
||||
};
|
||||
|
||||
export const DynamicSecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions`
|
||||
});
|
||||
|
||||
const conditionErrorMessage =
|
||||
errors?.permissions?.[ProjectPermissionSub.DynamicSecrets]?.[position]?.conditions?.message ||
|
||||
errors?.permissions?.[ProjectPermissionSub.DynamicSecrets]?.[position]?.conditions?.root
|
||||
?.message;
|
||||
|
||||
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.${ProjectPermissionSub.DynamicSecrets}.${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.${ProjectPermissionSub.DynamicSecrets}.${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) => {
|
||||
setValue(
|
||||
`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.operator`,
|
||||
PermissionConditionOperators.$IN as never
|
||||
);
|
||||
field.onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="environment">Environment Slug</SelectItem>
|
||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||
<SelectItem value="metadataKey">Metadata Key</SelectItem>
|
||||
<SelectItem value="metadataValue">Metadata Value</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${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"
|
||||
>
|
||||
{renderOperatorSelectItems(condition.lhs)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${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="plus"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{conditionErrorMessage && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{conditionErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "environment",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={ProjectPermissionSub.DynamicSecrets}
|
||||
position={position}
|
||||
selectOptions={[
|
||||
{ value: "environment", label: "Environment Slug" },
|
||||
{ value: "secretPath", label: "Secret Path" },
|
||||
{ value: "metadataKey", label: "Metadata Key" },
|
||||
{ value: "metadataValue", label: "Metadata Value" }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,180 +1,26 @@
|
||||
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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
getConditionOperatorHelperInfo,
|
||||
renderOperatorSelectItems
|
||||
} from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
isDisabled?: boolean;
|
||||
type:
|
||||
| ProjectPermissionSub.DynamicSecrets
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.SecretRotation;
|
||||
};
|
||||
|
||||
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${type}.${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.${type}.${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.${type}.${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="environment">Environment Slug</SelectItem>
|
||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${type}.${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"
|
||||
>
|
||||
{renderOperatorSelectItems(condition.lhs)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${type}.${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="plus"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{errors?.permissions?.[type]?.[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?.[type]?.[position]?.conditions?.message}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>{}</div>
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "environment",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={type}
|
||||
position={position}
|
||||
selectOptions={[
|
||||
{ value: "environment", label: "Environment Slug" },
|
||||
{ value: "secretPath", label: "Secret Path" }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { Control, Controller, useFieldArray, useFormContext, useWatch } from "re
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronRight,
|
||||
faDiagramProject,
|
||||
faGripVertical,
|
||||
faInfoCircle,
|
||||
faPlus,
|
||||
@ -27,6 +28,7 @@ type Props<T extends ProjectPermissionSub> = {
|
||||
actions: TProjectPermissionObject[T]["actions"];
|
||||
children?: JSX.Element;
|
||||
isDisabled?: boolean;
|
||||
onShowAccessTree?: (subject: ProjectPermissionSub) => void;
|
||||
};
|
||||
|
||||
type ActionProps = {
|
||||
@ -71,7 +73,8 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
actions,
|
||||
children,
|
||||
title,
|
||||
isDisabled
|
||||
isDisabled,
|
||||
onShowAccessTree
|
||||
}: Props<T>) => {
|
||||
const { control, watch } = useFormContext<TFormSchema>();
|
||||
const { fields, remove, insert, move } = useFieldArray({
|
||||
@ -89,7 +92,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||
|
||||
if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return <div />;
|
||||
if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return null;
|
||||
|
||||
const handleDragStart = (_: React.DragEvent, index: number) => {
|
||||
setDraggedItem(index);
|
||||
@ -121,9 +124,9 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md">
|
||||
<div className="overflow-clip border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md">
|
||||
<div
|
||||
className="flex cursor-pointer items-center space-x-8 px-5 py-4 text-sm text-gray-300"
|
||||
className="flex h-14 cursor-pointer items-center px-5 py-4 text-sm text-gray-300"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setIsOpen.toggle()}
|
||||
@ -133,20 +136,50 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={isOpen ? faChevronDown : faChevronRight} />
|
||||
</div>
|
||||
<div className="flex-grow">{title}</div>
|
||||
<FontAwesomeIcon className="mr-8" icon={isOpen ? faChevronDown : faChevronRight} />
|
||||
|
||||
<div className="flex-grow text-base">{title}</div>
|
||||
{fields.length > 1 && (
|
||||
<div>
|
||||
<Tag size="xs" className="px-2">
|
||||
{fields.length} rules
|
||||
<Tag size="xs" className="mr-2 px-2">
|
||||
{fields.length} Rules
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
{isOpen && onShowAccessTree && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faDiagramProject} />}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="ml-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowAccessTree(subject);
|
||||
}}
|
||||
>
|
||||
Visualize Access
|
||||
</Button>
|
||||
)}
|
||||
{!isDisabled && isOpen && isConditionalSubjects(subject) && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="outline_bg"
|
||||
className="ml-2"
|
||||
size="xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
insert(fields.length, [
|
||||
{ read: false, edit: false, create: false, delete: false } as any
|
||||
]);
|
||||
}}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
Add Rule
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6">
|
||||
<div key={`select-${subject}-type`} className="flex flex-col space-y-3 bg-bunker-700 p-3">
|
||||
{fields.map((el, rootIndex) => {
|
||||
let isFullReadAccessEnabled = false;
|
||||
|
||||
@ -154,78 +187,103 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any);
|
||||
}
|
||||
|
||||
const isInverted = watch(`permissions.${subject}.${rootIndex}.inverted` as any);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className={twMerge(
|
||||
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md",
|
||||
dragOverItem === rootIndex ? "border-2 border-blue-400" : "",
|
||||
"relative rounded-md border-l-[6px] bg-mineshaft-800 px-5 py-4 transition-colors duration-300",
|
||||
isInverted ? "border-l-red-600/50" : "border-l-green-600/50",
|
||||
dragOverItem === rootIndex ? "border-2 border-primary/50" : "",
|
||||
draggedItem === rootIndex ? "opacity-50" : ""
|
||||
)}
|
||||
onDragOver={(e) => handleDragOver(e, rootIndex)}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{!isDisabled && (
|
||||
<Tooltip position="left" content="Drag to reorder permission">
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, rootIndex)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="absolute right-3 top-2 cursor-move rounded-md bg-mineshaft-700 p-2 text-gray-400 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGripVertical} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div className="mb-4 flex items-center">
|
||||
{isConditionalSubjects(subject) && (
|
||||
{isConditionalSubjects(subject) && (
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex w-full items-center text-gray-300">
|
||||
<div className="w-1/4">Permission</div>
|
||||
<div className="mr-4 w-1/4">
|
||||
<Controller
|
||||
defaultValue={false as any}
|
||||
name={`permissions.${subject}.${rootIndex}.inverted`}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={String(field.value)}
|
||||
onValueChange={(val) => field.onChange(val === "true")}
|
||||
containerClassName="w-full"
|
||||
className="w-full"
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<SelectItem value="false">Allow</SelectItem>
|
||||
<SelectItem value="true">Forbid</SelectItem>
|
||||
</Select>
|
||||
)}
|
||||
<div className="mr-3">Permission</div>
|
||||
<Controller
|
||||
defaultValue={false as any}
|
||||
name={`permissions.${subject}.${rootIndex}.inverted`}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={String(field.value)}
|
||||
onValueChange={(val) => field.onChange(val === "true")}
|
||||
containerClassName="w-40"
|
||||
className="w-full"
|
||||
isDisabled={isDisabled}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value="false">Allow</SelectItem>
|
||||
<SelectItem value="true">Forbid</SelectItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
Whether to allow or forbid the selected actions when the following
|
||||
conditions (if any) are met.
|
||||
</p>
|
||||
<p className="mt-2">Forbid rules must come after allow rules.</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
size="sm"
|
||||
className="ml-2 text-bunker-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
Whether to allow or forbid the selected actions when the following
|
||||
conditions (if any) are met.
|
||||
</p>
|
||||
<p className="mt-2">Forbid rules must come after allow rules.</p>
|
||||
</>
|
||||
}
|
||||
</Tooltip>
|
||||
{!isDisabled && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="ml-auto mr-3"
|
||||
onClick={() => remove(rootIndex)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
size="sm"
|
||||
className="text-gray-400"
|
||||
/>
|
||||
Remove Rule
|
||||
</Button>
|
||||
)}
|
||||
{!isDisabled && (
|
||||
<Tooltip position="left" content="Drag to reorder permission">
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, rootIndex)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="cursor-move text-bunker-300 hover:text-bunker-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGripVertical} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-4 text-gray-300">
|
||||
<div className="w-1/4">Actions</div>
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col text-gray-300">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="mb-2">Actions</div>
|
||||
{!isDisabled && !isConditionalSubjects(subject) && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="ml-auto"
|
||||
onClick={() => remove(rootIndex)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
Remove Rule
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
|
||||
{actions.map(({ label, value }, index) => {
|
||||
if (typeof value !== "string") return undefined;
|
||||
|
||||
@ -255,41 +313,6 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
cloneElement(children, {
|
||||
position: rootIndex
|
||||
})}
|
||||
<div
|
||||
className={twMerge(
|
||||
"mt-4 flex justify-start space-x-4",
|
||||
isConditionalSubjects(subject) && "justify-end"
|
||||
)}
|
||||
>
|
||||
{!isDisabled && isConditionalSubjects(subject) && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-2"
|
||||
onClick={() => {
|
||||
insert(rootIndex + 1, [
|
||||
{ read: false, edit: false, create: false, delete: false } as any
|
||||
]);
|
||||
}}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
Add policy
|
||||
</Button>
|
||||
)}
|
||||
{!isDisabled && (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="mt-2 hover:border-red"
|
||||
onClick={() => remove(rootIndex)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
Remove policy
|
||||
</Button>
|
||||
)}{" "}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -1,23 +1,6 @@
|
||||
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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
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";
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
@ -25,150 +8,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
const permissionSubject = ProjectPermissionSub.Identity;
|
||||
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="identityId">Identity ID</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}>Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</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="plus"
|
||||
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>{}</div>
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "identityId",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={ProjectPermissionSub.Identity}
|
||||
position={position}
|
||||
selectOptions={[{ value: "identityId", label: "Identity ID" }]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -17,17 +17,36 @@ export const getConditionOperatorHelperInfo = (type: PermissionConditionOperator
|
||||
}
|
||||
};
|
||||
|
||||
// scott: we may need to pass the subject in the future to further refine returned items
|
||||
export const renderOperatorSelectItems = (type: string) => {
|
||||
if (type === "secretTags") {
|
||||
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
|
||||
switch (type) {
|
||||
case "secretTags":
|
||||
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
|
||||
case "identityId":
|
||||
return (
|
||||
<>
|
||||
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||
</>
|
||||
);
|
||||
case "hostname":
|
||||
case "name":
|
||||
return (
|
||||
<>
|
||||
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ export const PermissionEmptyState = () => {
|
||||
([key, value]) => key && value?.length > 0
|
||||
);
|
||||
|
||||
if (isNotEmptyPermissions) return <div />;
|
||||
if (isNotEmptyPermissions) return null;
|
||||
|
||||
return <EmptyState title="No policies applied" className="py-8" />;
|
||||
};
|
||||
|
@ -1,23 +1,6 @@
|
||||
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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
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";
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
@ -25,149 +8,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
|
||||
const permissionSubject = ProjectPermissionSub.PkiSubscribers;
|
||||
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="plus"
|
||||
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>
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={ProjectPermissionSub.PkiSubscribers}
|
||||
position={position}
|
||||
selectOptions={[{ value: "name", label: "Name" }]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,23 +1,6 @@
|
||||
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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
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";
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
@ -25,149 +8,12 @@ type Props = {
|
||||
};
|
||||
|
||||
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>
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={ProjectPermissionSub.CertificateTemplates}
|
||||
position={position}
|
||||
selectOptions={[{ value: "name", label: "Name" }]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,98 +0,0 @@
|
||||
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
roleSlug: string;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: object) => void;
|
||||
};
|
||||
|
||||
export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
|
||||
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data } = useGetProjectRoleBySlug(currentWorkspace?.id ?? "", roleSlug as string);
|
||||
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
||||
(data?.slug ?? "") as ProjectMembershipRole
|
||||
);
|
||||
|
||||
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">Project Role Details</h3>
|
||||
{isCustomRole && (
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Role}>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Tooltip content="Edit Role">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("role", {
|
||||
roleSlug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Role ID</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm text-mineshaft-300">{data.id}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextId}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(data.id);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.name}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.slug}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.description?.length ? data.description : "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
|
||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
@ -88,6 +88,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
roleSlug as string
|
||||
);
|
||||
|
||||
const [showAccessTree, setShowAccessTree] = useState<ProjectPermissionSub | null>(null);
|
||||
|
||||
const form = useForm<TFormSchema>({
|
||||
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||
resolver: zodResolver(projectRoleFormSchema)
|
||||
@ -133,71 +135,90 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
[JSON.stringify(permissions)]
|
||||
);
|
||||
|
||||
const isSecretManagerProject = currentWorkspace.type === ProjectType.SecretManager;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{currentWorkspace.type === ProjectType.SecretManager && (
|
||||
<AccessTree permissions={formattedPermissions} />
|
||||
)}
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
className="flex h-full w-full flex-1 flex-col rounded-lg border border-mineshaft-600 bg-mineshaft-900 py-4"
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isCustomRole && (
|
||||
<>
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"mr-4 h-10 border",
|
||||
isDirty && "bg-primary text-black hover:bg-primary hover:opacity-80"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<AddPoliciesButton isDisabled={isDisabled} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mx-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
||||
<p className="text-sm leading-3 text-mineshaft-400">
|
||||
Configure granular access policies
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
{!isPending && <PermissionEmptyState />}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
|
||||
.filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject))
|
||||
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
|
||||
.map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
{isCustomRole && (
|
||||
<div className="flex items-center gap-2">
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
className={twMerge("h-10 border")}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
Save
|
||||
</Button>
|
||||
<div className="ml-2 border-l border-mineshaft-500 pl-4">
|
||||
<AddPoliciesButton isDisabled={isDisabled} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col overflow-hidden pl-4 pr-1">
|
||||
<div className="thin-scrollbar flex-1 overflow-y-scroll py-4">
|
||||
{!isPending && <PermissionEmptyState />}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
|
||||
.filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject))
|
||||
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
|
||||
.map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
onShowAccessTree={
|
||||
isSecretManagerProject &&
|
||||
[
|
||||
ProjectPermissionSub.Secrets,
|
||||
ProjectPermissionSub.SecretFolders,
|
||||
ProjectPermissionSub.DynamicSecrets,
|
||||
ProjectPermissionSub.SecretImports
|
||||
].includes(subject)
|
||||
? setShowAccessTree
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
{isSecretManagerProject && showAccessTree && (
|
||||
<AccessTree
|
||||
permissions={formattedPermissions}
|
||||
subject={showAccessTree}
|
||||
onClose={() => setShowAccessTree(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,23 +1,6 @@
|
||||
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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
getConditionOperatorHelperInfo,
|
||||
renderOperatorSelectItems
|
||||
} from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
@ -25,159 +8,17 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.secrets.${position}.conditions`
|
||||
});
|
||||
|
||||
const conditionErrorMessage =
|
||||
errors?.permissions?.secrets?.[position]?.conditions?.message ||
|
||||
errors?.permissions?.secrets?.[position]?.conditions?.root?.message;
|
||||
|
||||
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.secrets.${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.secrets.${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) => {
|
||||
setValue(
|
||||
`permissions.secrets.${position}.conditions.${index}.operator`,
|
||||
PermissionConditionOperators.$IN as never
|
||||
);
|
||||
field.onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="environment">Environment Slug</SelectItem>
|
||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||
<SelectItem value="secretName">Secret Name</SelectItem>
|
||||
<SelectItem value="secretTags">Secret Tags</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.secrets.${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"
|
||||
>
|
||||
{renderOperatorSelectItems(condition.lhs)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.secrets.${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="plus"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{conditionErrorMessage && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{conditionErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "environment",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={ProjectPermissionSub.Secrets}
|
||||
position={position}
|
||||
selectOptions={[
|
||||
{ value: "environment", label: "Environment Slug" },
|
||||
{ value: "secretPath", label: "Secret Path" },
|
||||
{ value: "secretName", label: "Secret Name" },
|
||||
{ value: "secretTags", label: "Secret Tags" }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,26 +1,6 @@
|
||||
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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
getConditionOperatorHelperInfo,
|
||||
renderOperatorSelectItems
|
||||
} from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
@ -28,159 +8,15 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions`
|
||||
});
|
||||
|
||||
const conditionErrorMessage =
|
||||
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.message ||
|
||||
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.root?.message;
|
||||
|
||||
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.${ProjectPermissionSub.SecretSyncs}.${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.${ProjectPermissionSub.SecretSyncs}.${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) => {
|
||||
setValue(
|
||||
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`,
|
||||
PermissionConditionOperators.$IN as never
|
||||
);
|
||||
field.onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="environment">Environment Slug</SelectItem>
|
||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${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"
|
||||
>
|
||||
{renderOperatorSelectItems(condition.lhs)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${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="remove"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{conditionErrorMessage && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{conditionErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "environment",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={ProjectPermissionSub.SecretSyncs}
|
||||
position={position}
|
||||
selectOptions={[
|
||||
{ value: "environment", label: "Environment Slug" },
|
||||
{ value: "secretPath", label: "Secret Path" }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,23 +1,6 @@
|
||||
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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
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";
|
||||
import { ConditionsFields } from "./ConditionsFields";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
@ -25,149 +8,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
|
||||
const permissionSubject = ProjectPermissionSub.SshHosts;
|
||||
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="hostname">Hostname</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} onChange={(e) => field.onChange(e.target.value.trim())} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
ariaLabel="plus"
|
||||
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: "hostname",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConditionsFields
|
||||
isDisabled={isDisabled}
|
||||
subject={ProjectPermissionSub.SshHosts}
|
||||
position={position}
|
||||
selectOptions={[{ value: "hostname", label: "Hostname" }]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -6,6 +6,9 @@ import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRightToBracket,
|
||||
faArrowUp,
|
||||
faFileImport,
|
||||
faFingerprint,
|
||||
@ -69,7 +72,7 @@ import {
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useCreateFolder,
|
||||
useCreateSecretV3,
|
||||
@ -164,6 +167,18 @@ export const OverviewPage = () => {
|
||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
|
||||
const secretPath = (routerSearch?.secretPath as string) || "/";
|
||||
const { subscription } = useSubscription();
|
||||
const [collapseEnvironments, setCollapseEnvironments] = useToggle(
|
||||
Boolean(localStorage.getItem("overview-collapse-environments"))
|
||||
);
|
||||
|
||||
const handleToggleNarrowHeader = () => {
|
||||
setCollapseEnvironments.toggle();
|
||||
if (collapseEnvironments) {
|
||||
localStorage.removeItem("overview-collapse-environments");
|
||||
} else {
|
||||
localStorage.setItem("overview-collapse-environments", "true");
|
||||
}
|
||||
};
|
||||
|
||||
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
|
||||
const [filterHistory, setFilterHistory] = useState<
|
||||
@ -1173,47 +1188,81 @@ export const OverviewPage = () => {
|
||||
className="thin-scrollbar rounded-b-none"
|
||||
>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="sticky top-0 z-20 border-0">
|
||||
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
|
||||
<div className="flex items-center border-b border-r border-mineshaft-600 pb-3 pl-3 pr-5 pt-3.5">
|
||||
<Tooltip
|
||||
className="max-w-[20rem] whitespace-nowrap capitalize"
|
||||
content={
|
||||
totalCount > 0
|
||||
? `${
|
||||
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
|
||||
} all folders and secrets on page`
|
||||
: ""
|
||||
}
|
||||
<THead className={collapseEnvironments ? "h-24" : ""}>
|
||||
<Tr
|
||||
className={twMerge("sticky top-0 z-20 border-0", collapseEnvironments && "h-24")}
|
||||
>
|
||||
<Th
|
||||
className={twMerge(
|
||||
"sticky left-0 z-20 min-w-[20rem] border-b-0 p-0",
|
||||
collapseEnvironments && "h-24"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-full border-b border-mineshaft-600 pb-3 pl-3 pr-5",
|
||||
!collapseEnvironments && "border-r pt-3.5"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge("flex items-center", collapseEnvironments && "mt-auto")}
|
||||
>
|
||||
<div className="ml-2 mr-4">
|
||||
<Checkbox
|
||||
isDisabled={totalCount === 0}
|
||||
id="checkbox-select-all-rows"
|
||||
isChecked={allRowsSelectedOnPage.isChecked}
|
||||
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
|
||||
onCheckedChange={toggleSelectAllRows}
|
||||
<Tooltip
|
||||
className="max-w-[20rem] whitespace-nowrap capitalize"
|
||||
content={
|
||||
totalCount > 0
|
||||
? `${
|
||||
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
|
||||
} all folders and secrets on page`
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div className="ml-2 mr-4">
|
||||
<Checkbox
|
||||
isDisabled={totalCount === 0}
|
||||
id="checkbox-select-all-rows"
|
||||
isChecked={allRowsSelectedOnPage.isChecked}
|
||||
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
|
||||
onCheckedChange={toggleSelectAllRows}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className="ml-2"
|
||||
ariaLabel="sort"
|
||||
onClick={() =>
|
||||
setOrderDirection((prev) =>
|
||||
prev === OrderByDirection.ASC
|
||||
? OrderByDirection.DESC
|
||||
: OrderByDirection.ASC
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === "asc" ? faArrowDown : faArrowUp}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className="ml-2"
|
||||
ariaLabel="sort"
|
||||
onClick={() =>
|
||||
setOrderDirection((prev) =>
|
||||
prev === OrderByDirection.ASC
|
||||
? OrderByDirection.DESC
|
||||
: OrderByDirection.ASC
|
||||
)
|
||||
</IconButton>
|
||||
</div>
|
||||
<Tooltip
|
||||
content={
|
||||
collapseEnvironments ? "Expand Environments" : "Collapse Environments"
|
||||
}
|
||||
className="capitalize"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === "asc" ? faArrowDown : faArrowUp}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="Toggle Environment View"
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
className="ml-auto mt-auto h-min p-1"
|
||||
onClick={handleToggleNarrowHeader}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={collapseEnvironments ? faArrowLeft : faArrowRight}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Th>
|
||||
{visibleEnvs?.map(({ name, slug }, index) => {
|
||||
@ -1223,28 +1272,76 @@ export const OverviewPage = () => {
|
||||
|
||||
return (
|
||||
<Th
|
||||
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center"
|
||||
className={twMerge(
|
||||
"min-table-row border-b-0 p-0 text-xs",
|
||||
collapseEnvironments && index === visibleEnvs.length - 1 && "mr-8",
|
||||
collapseEnvironments ? "h-24 w-[1rem]" : "min-w-[11rem] text-center"
|
||||
)}
|
||||
key={`secret-overview-${name}-${index + 1}`}
|
||||
>
|
||||
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pb-[0.83rem] pt-3.5">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-medium duration-100 hover:text-mineshaft-100"
|
||||
onClick={() => handleExploreEnvClick(slug)}
|
||||
<Tooltip
|
||||
content={
|
||||
collapseEnvironments ? (
|
||||
<p className="whitespace-break-spaces">{name}</p>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
side="bottom"
|
||||
sideOffset={-1}
|
||||
align="end"
|
||||
className="max-w-xl text-xs normal-case"
|
||||
rootProps={{
|
||||
disableHoverableContent: true
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"border-b border-mineshaft-600",
|
||||
collapseEnvironments
|
||||
? "relative h-24 w-[2.9rem]"
|
||||
: "flex items-center justify-center px-5 pb-[0.82rem] pt-3.5",
|
||||
collapseEnvironments &&
|
||||
index === visibleEnvs.length - 1 &&
|
||||
"overflow-clip"
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
{missingKeyCount > 0 && (
|
||||
<Tooltip
|
||||
className="max-w-none lowercase"
|
||||
content={`${missingKeyCount} secrets missing\n compared to other environments`}
|
||||
<div
|
||||
className={twMerge(
|
||||
"border-mineshaft-600",
|
||||
collapseEnvironments
|
||||
? "ml-[0.85rem] h-24 -skew-x-[16rad] transform border-l text-xs"
|
||||
: "flex items-center justify-center"
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"duration-100 hover:text-mineshaft-100",
|
||||
collapseEnvironments &&
|
||||
(index === visibleEnvs.length - 1
|
||||
? "bottom-[1.75rem] w-14"
|
||||
: "bottom-10 w-20"),
|
||||
collapseEnvironments
|
||||
? "absolute -rotate-[72.25deg] text-left !text-[12px] font-normal"
|
||||
: "flex items-center text-center text-sm font-medium"
|
||||
)}
|
||||
onClick={() => handleExploreEnvClick(slug)}
|
||||
>
|
||||
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
|
||||
<span className="text-bunker-100">{missingKeyCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="truncate font-medium">{name}</p>
|
||||
</button>
|
||||
{!collapseEnvironments && missingKeyCount > 0 && (
|
||||
<Tooltip
|
||||
className="max-w-none lowercase"
|
||||
content={`${missingKeyCount} secrets missing\n compared to other environments`}
|
||||
>
|
||||
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
|
||||
<span className="text-bunker-100">{missingKeyCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Th>
|
||||
);
|
||||
})}
|
||||
@ -1409,16 +1506,40 @@ export const OverviewPage = () => {
|
||||
/>
|
||||
</Td>
|
||||
{visibleEnvs?.map(({ name, slug }) => (
|
||||
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
|
||||
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
isFullWidth
|
||||
onClick={() => handleExploreEnvClick(slug)}
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
<Td
|
||||
key={`explore-${name}-btn`}
|
||||
className="border-0 border-r border-mineshaft-600 p-0"
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full items-center justify-center border-t border-mineshaft-600 py-2"
|
||||
)}
|
||||
>
|
||||
{collapseEnvironments ? (
|
||||
<Tooltip className="normal-case" content="Explore Environment">
|
||||
<IconButton
|
||||
ariaLabel="Explore Environment"
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
className="mx-auto h-[1.76rem] rounded"
|
||||
onClick={() => handleExploreEnvClick(slug)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRightToBracket} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
leftIcon={
|
||||
<FontAwesomeIcon className="mr-1" icon={faArrowRightToBracket} />
|
||||
}
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="mx-2 w-full"
|
||||
onClick={() => handleExploreEnvClick(slug)}
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
))}
|
||||
|
@ -36,7 +36,7 @@ export const SecretOverviewDynamicSecretRow = ({
|
||||
isPresent ? "text-green-600" : "text-red-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
<div className="mx-auto flex w-[0.03rem] justify-center">
|
||||
<FontAwesomeIcon
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
icon={isPresent ? faCheck : faXmark}
|
||||
|
@ -70,7 +70,7 @@ export const SecretOverviewFolderRow = ({
|
||||
isPresent ? "text-green-600" : "text-red-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
<div className="mx-auto flex w-[0.03rem] justify-center">
|
||||
<FontAwesomeIcon
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
icon={isPresent ? faCheck : faXmark}
|
||||
|
@ -94,7 +94,7 @@ export const SecretOverviewSecretRotationRow = ({
|
||||
isPresent ? "text-green-600" : "text-red-600"
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
<div className="mx-auto flex w-[0.03rem] justify-center">
|
||||
<FontAwesomeIcon
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
icon={isPresent ? faCheck : faXmark}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faCheck,
|
||||
faCircle,
|
||||
faCodeBranch,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
@ -145,14 +145,14 @@ export const SecretOverviewTableRow = ({
|
||||
<Td
|
||||
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||
className={twMerge(
|
||||
"px-0 py-0 group-hover:bg-mineshaft-700",
|
||||
"border-r border-mineshaft-600 px-0 py-3 group-hover:bg-mineshaft-700",
|
||||
isFormExpanded && "border-t-2 border-mineshaft-500",
|
||||
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
|
||||
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
|
||||
!isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : ""
|
||||
)}
|
||||
>
|
||||
<div className="h-full w-full border-r border-mineshaft-600 px-5 py-[0.85rem]">
|
||||
<div className="mx-auto flex w-[0.03rem] justify-center">
|
||||
<div className="flex justify-center">
|
||||
{!isSecretEmpty && (
|
||||
<Tooltip
|
||||
|
Reference in New Issue
Block a user