mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +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>
|
</Tab>
|
||||||
</Tabs>
|
</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 { MongoAbility, MongoQuery } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
faAnglesUp,
|
faAnglesUp,
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faDownLeftAndUpRightToCenter,
|
|
||||||
faUpRightAndDownLeftFromCenter,
|
faUpRightAndDownLeftFromCenter,
|
||||||
faWindowRestore
|
faWindowRestore,
|
||||||
|
faXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
@@ -23,8 +22,8 @@ import {
|
|||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
import { Button, IconButton, Select, SelectItem, Spinner, Tooltip } from "@app/components/v2";
|
||||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
import { ProjectPermissionSet, ProjectPermissionSub } from "@app/context/ProjectPermissionContext";
|
||||||
|
|
||||||
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
|
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
|
||||||
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
||||||
@@ -36,15 +35,17 @@ import { ViewMode } from "./types";
|
|||||||
|
|
||||||
export type AccessTreeProps = {
|
export type AccessTreeProps = {
|
||||||
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||||
|
subject: ProjectPermissionSub;
|
||||||
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EdgeTypes = { base: BasePermissionEdge };
|
const EdgeTypes = { base: BasePermissionEdge };
|
||||||
|
|
||||||
const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode };
|
const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode };
|
||||||
|
|
||||||
const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
const AccessTreeContent = ({ permissions, subject, onClose }: AccessTreeProps) => {
|
||||||
const [selectedPath, setSelectedPath] = useState<string>("/");
|
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 { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
|
||||||
const [initialRender, setInitialRender] = useState(true);
|
const [initialRender, setInitialRender] = useState(true);
|
||||||
|
|
||||||
@@ -78,32 +79,32 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInitialRender(true);
|
setInitialRender(true);
|
||||||
}, [selectedPath, environment]);
|
}, [selectedPath, environment, subject, viewMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
if (initialRender) {
|
if (initialRender) {
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
goToRootNode();
|
fitView({ duration: 500 });
|
||||||
setInitialRender(false);
|
setInitialRender(false);
|
||||||
}, 500);
|
}, 50);
|
||||||
}
|
}
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [nodes, edges, getViewport(), initialRender, goToRootNode]);
|
}, [nodes, edges, getViewport(), initialRender, fitView]);
|
||||||
|
|
||||||
const handleToggleModalView = () =>
|
const handleToggleModalView = () =>
|
||||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
||||||
|
|
||||||
const handleToggleUndockedView = () =>
|
const handleToggleView = () =>
|
||||||
setViewMode((prev) => (prev === ViewMode.Undocked ? ViewMode.Docked : ViewMode.Undocked));
|
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Undocked : ViewMode.Modal));
|
||||||
|
|
||||||
const undockButtonLabel = `${viewMode === ViewMode.Undocked ? "Dock" : "Undock"} View`;
|
const expandButtonLabel = viewMode === ViewMode.Modal ? "Anchor View" : "Expand View";
|
||||||
const windowButtonLabel = `${viewMode === ViewMode.Modal ? "Dock" : "Expand"} View`;
|
const hideButtonLabel = "Hide Access Tree";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-full",
|
"mt-4 w-full",
|
||||||
viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10",
|
viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10",
|
||||||
viewMode === ViewMode.Undocked &&
|
viewMode === ViewMode.Undocked &&
|
||||||
"fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]"
|
"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"
|
type="submit"
|
||||||
className="h-10 rounded-r-none bg-mineshaft-700"
|
className="h-10 rounded-r-none bg-mineshaft-700"
|
||||||
leftIcon={<FontAwesomeIcon icon={faWindowRestore} />}
|
leftIcon={<FontAwesomeIcon icon={faWindowRestore} />}
|
||||||
onClick={handleToggleUndockedView}
|
onClick={handleToggleView}
|
||||||
>
|
>
|
||||||
Undock
|
Undock
|
||||||
</Button>
|
</Button>
|
||||||
@@ -176,48 +177,62 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
</Panel>
|
</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 && (
|
{viewMode !== ViewMode.Docked && (
|
||||||
<Panel position="top-right" className="flex gap-1.5">
|
<Panel position="top-right" className="flex gap-2">
|
||||||
{viewMode !== ViewMode.Undocked && (
|
<Tooltip position="bottom" align="center" content={expandButtonLabel}>
|
||||||
<AccessTreeSecretPathInput
|
|
||||||
placeholder="Provide a path, default is /"
|
|
||||||
environment={environment}
|
|
||||||
value={selectedPath}
|
|
||||||
onChange={setSelectedPath}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
className="ml-1 w-10 rounded"
|
className="rounded p-2"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={handleToggleUndockedView}
|
onClick={handleToggleView}
|
||||||
ariaLabel={undockButtonLabel}
|
ariaLabel={expandButtonLabel}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={
|
icon={
|
||||||
viewMode === ViewMode.Undocked
|
viewMode === ViewMode.Undocked
|
||||||
? faArrowUpRightFromSquare
|
? faUpRightAndDownLeftFromCenter
|
||||||
: faWindowRestore
|
: faWindowRestore
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip align="end" position="bottom" content={windowButtonLabel}>
|
<Tooltip align="end" position="bottom" content={hideButtonLabel}>
|
||||||
<IconButton
|
<IconButton
|
||||||
className="w-10 rounded"
|
className="rounded p-2"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={handleToggleModalView}
|
onClick={onClose}
|
||||||
ariaLabel={windowButtonLabel}
|
ariaLabel={hideButtonLabel}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
icon={
|
|
||||||
viewMode === ViewMode.Modal
|
|
||||||
? faDownLeftAndUpRightToCenter
|
|
||||||
: faUpRightAndDownLeftFromCenter
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -253,6 +268,9 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AccessTree = (props: AccessTreeProps) => {
|
export const AccessTree = (props: AccessTreeProps) => {
|
||||||
|
const { subject } = props;
|
||||||
|
if (!subject) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessTreeErrorBoundary {...props}>
|
<AccessTreeErrorBoundary {...props}>
|
||||||
<AccessTreeProvider>
|
<AccessTreeProvider>
|
||||||
|
@@ -29,7 +29,7 @@ export type AccessTreeForm = { metadata: { key: string; value: string }[] };
|
|||||||
export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => {
|
export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => {
|
||||||
const [secretName, setSecretName] = useState("");
|
const [secretName, setSecretName] = useState("");
|
||||||
const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } });
|
const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } });
|
||||||
const [viewMode, setViewMode] = useState(ViewMode.Docked);
|
const [viewMode, setViewMode] = useState(ViewMode.Modal);
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@@ -33,7 +33,8 @@ type LevelFolderMap = Record<
|
|||||||
|
|
||||||
export const useAccessTree = (
|
export const useAccessTree = (
|
||||||
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>,
|
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>,
|
||||||
searchPath: string
|
searchPath: string,
|
||||||
|
subject: ProjectPermissionSub
|
||||||
) => {
|
) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
|
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
|
||||||
@@ -41,7 +42,6 @@ export const useAccessTree = (
|
|||||||
const metadata = useWatch({ control, name: "metadata" });
|
const metadata = useWatch({ control, name: "metadata" });
|
||||||
const [nodes, setNodes] = useNodesState<Node>([]);
|
const [nodes, setNodes] = useNodesState<Node>([]);
|
||||||
const [edges, setEdges] = useEdgesState<Edge>([]);
|
const [edges, setEdges] = useEdgesState<Edge>([]);
|
||||||
const [subject, setSubject] = useState(ProjectPermissionSub.Secrets);
|
|
||||||
const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? "");
|
const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? "");
|
||||||
const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders(
|
const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders(
|
||||||
currentWorkspace.id
|
currentWorkspace.id
|
||||||
@@ -147,9 +147,7 @@ export const useAccessTree = (
|
|||||||
const roleNode = createRoleNode({
|
const roleNode = createRoleNode({
|
||||||
subject,
|
subject,
|
||||||
environment: slug,
|
environment: slug,
|
||||||
environments: environmentsFolders,
|
environments: environmentsFolders
|
||||||
onSubjectChange: setSubject,
|
|
||||||
onEnvironmentChange: setEnvironment
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
|
const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
|
||||||
@@ -280,7 +278,6 @@ export const useAccessTree = (
|
|||||||
subject,
|
subject,
|
||||||
environment,
|
environment,
|
||||||
setEnvironment,
|
setEnvironment,
|
||||||
setSubject,
|
|
||||||
isLoading: isPending,
|
isLoading: isPending,
|
||||||
environments: currentWorkspace.environments,
|
environments: currentWorkspace.environments,
|
||||||
secretName,
|
secretName,
|
||||||
|
@@ -81,7 +81,7 @@ export const AccessTreeSecretPathInput = ({
|
|||||||
<FontAwesomeIcon icon={faSearch} />
|
<FontAwesomeIcon icon={faSearch} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip position="bottom" content="Search paths">
|
<Tooltip position="bottom" content="Search Paths">
|
||||||
<div
|
<div
|
||||||
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
|
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
|
||||||
onClick={toggleSearch}
|
onClick={toggleSearch}
|
||||||
|
@@ -3,7 +3,6 @@ import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||||
|
|
||||||
import { Select, SelectItem } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
|
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ const formatLabel = (text: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RoleNode = ({
|
export const RoleNode = ({
|
||||||
data: { subject, environment, onSubjectChange, onEnvironmentChange, environments }
|
data: { subject }
|
||||||
}: NodeProps & {
|
}: NodeProps & {
|
||||||
data: ReturnType<typeof createRoleNode>["data"] & {
|
data: ReturnType<typeof createRoleNode>["data"] & {
|
||||||
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
|
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
|
||||||
@@ -44,61 +43,10 @@ export const RoleNode = ({
|
|||||||
className="pointer-events-none !cursor-pointer opacity-0"
|
className="pointer-events-none !cursor-pointer opacity-0"
|
||||||
position={Position.Top}
|
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 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 w-full min-w-[240px] flex-col gap-4">
|
<div className="flex items-center space-x-2 text-mineshaft-100">
|
||||||
<div className="flex w-full flex-col gap-1.5">
|
{getSubjectIcon(subject)}
|
||||||
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Subject</div>
|
<span className="text-sm">{formatLabel(subject)} Access</span>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Handle
|
<Handle
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
import { Dispatch, SetStateAction } from "react";
|
|
||||||
|
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
|
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
|
||||||
|
|
||||||
@@ -8,24 +6,18 @@ import { PermissionNode } from "../types";
|
|||||||
export const createRoleNode = ({
|
export const createRoleNode = ({
|
||||||
subject,
|
subject,
|
||||||
environment,
|
environment,
|
||||||
environments,
|
environments
|
||||||
onSubjectChange,
|
|
||||||
onEnvironmentChange
|
|
||||||
}: {
|
}: {
|
||||||
subject: string;
|
subject: ProjectPermissionSub;
|
||||||
environment: string;
|
environment: string;
|
||||||
environments: TProjectEnvironmentsFolders;
|
environments: TProjectEnvironmentsFolders;
|
||||||
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
|
|
||||||
onEnvironmentChange: (value: string) => void;
|
|
||||||
}) => ({
|
}) => ({
|
||||||
id: `role-${subject}-${environment}`,
|
id: `role-${subject}-${environment}`,
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
data: {
|
data: {
|
||||||
subject,
|
subject,
|
||||||
environment,
|
environment,
|
||||||
environments,
|
environments
|
||||||
onSubjectChange,
|
|
||||||
onEnvironmentChange
|
|
||||||
},
|
},
|
||||||
type: PermissionNode.Role,
|
type: PermissionNode.Role,
|
||||||
height: 48,
|
height: 48,
|
||||||
|
@@ -39,16 +39,6 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
|||||||
const positionedNodes = nodes.map((node) => {
|
const positionedNodes = nodes.map((node) => {
|
||||||
const { x, y } = dagre.node(node.id);
|
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 {
|
return {
|
||||||
...node,
|
...node,
|
||||||
position: {
|
position: {
|
||||||
|
@@ -173,17 +173,19 @@ export const ProjectTemplateEditRoleForm = ({
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mb-2 text-lg">Policies</div>
|
<div className="mb-2 text-lg">Policies</div>
|
||||||
<PermissionEmptyState />
|
<PermissionEmptyState />
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
<div>
|
||||||
<GeneralPermissionPolicies
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||||
subject={subject}
|
<GeneralPermissionPolicies
|
||||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
subject={subject}
|
||||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||||
key={`project-permission-${subject}`}
|
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||||
isDisabled={isDisabled}
|
key={`project-permission-${subject}`}
|
||||||
>
|
isDisabled={isDisabled}
|
||||||
{renderConditionalComponents(subject, isDisabled)}
|
>
|
||||||
</GeneralPermissionPolicies>
|
{renderConditionalComponents(subject, isDisabled)}
|
||||||
))}
|
</GeneralPermissionPolicies>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -30,7 +30,29 @@ export const AwsParameterStoreSyncFields = () => {
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
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..." />
|
<Input value={value} onChange={onChange} placeholder="Path..." />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
@@ -40,9 +40,9 @@ export const Checkbox = ({
|
|||||||
<div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
|
<div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
className={twMerge(
|
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",
|
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",
|
Boolean(children) && "mr-3",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -53,7 +53,10 @@ export const Checkbox = ({
|
|||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
className={twMerge(`${checkIndicatorBg || "text-bunker-800"}`, indicatorClassName)}
|
className={twMerge(
|
||||||
|
`${checkIndicatorBg || "mt-[0.1rem] text-mineshaft-200"}`,
|
||||||
|
indicatorClassName
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isIndeterminate ? (
|
{isIndeterminate ? (
|
||||||
<FontAwesomeIcon icon={faMinus} size="sm" />
|
<FontAwesomeIcon icon={faMinus} size="sm" />
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
import { TooltipProps as RootProps } from "@radix-ui/react-tooltip";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & {
|
export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & {
|
||||||
@@ -14,6 +15,7 @@ export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "
|
|||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
size?: "sm" | "md";
|
size?: "sm" | "md";
|
||||||
|
rootProps?: RootProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tooltip = ({
|
export const Tooltip = ({
|
||||||
@@ -28,12 +30,14 @@ export const Tooltip = ({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
position = "top",
|
position = "top",
|
||||||
size = "md",
|
size = "md",
|
||||||
|
rootProps,
|
||||||
...props
|
...props
|
||||||
}: TooltipProps) =>
|
}: TooltipProps) =>
|
||||||
// just render children if tooltip content is empty
|
// just render children if tooltip content is empty
|
||||||
content ? (
|
content ? (
|
||||||
<TooltipPrimitive.Root
|
<TooltipPrimitive.Root
|
||||||
delayDuration={50}
|
delayDuration={50}
|
||||||
|
{...rootProps}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
defaultOpen={defaultOpen}
|
defaultOpen={defaultOpen}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
|
@@ -161,6 +161,18 @@ export type IdentityManagementSubjectFields = {
|
|||||||
identityId: string;
|
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 } = {
|
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
|
||||||
[PermissionConditionOperators.$EQ]: "equal to",
|
[PermissionConditionOperators.$EQ]: "equal to",
|
||||||
[PermissionConditionOperators.$IN]: "in",
|
[PermissionConditionOperators.$IN]: "in",
|
||||||
|
@@ -352,19 +352,21 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mb-2 text-lg">Policies</div>
|
<div className="mb-2 text-lg">Policies</div>
|
||||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
{(isCreate || !isPending) && <PermissionEmptyState />}
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
|
<div>
|
||||||
(permissionSubject) => (
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
|
||||||
<GeneralPermissionPolicies
|
(permissionSubject) => (
|
||||||
subject={permissionSubject}
|
<GeneralPermissionPolicies
|
||||||
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
|
subject={permissionSubject}
|
||||||
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
|
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
|
||||||
key={`project-permission-${permissionSubject}`}
|
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
|
||||||
isDisabled={isDisabled}
|
key={`project-permission-${permissionSubject}`}
|
||||||
>
|
isDisabled={isDisabled}
|
||||||
{renderConditionalComponents(permissionSubject, isDisabled)}
|
>
|
||||||
</GeneralPermissionPolicies>
|
{renderConditionalComponents(permissionSubject, isDisabled)}
|
||||||
)
|
</GeneralPermissionPolicies>
|
||||||
)}
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -348,17 +348,19 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mb-2 text-lg">Policies</div>
|
<div className="mb-2 text-lg">Policies</div>
|
||||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
{(isCreate || !isPending) && <PermissionEmptyState />}
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
<div>
|
||||||
<GeneralPermissionPolicies
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||||
subject={subject}
|
<GeneralPermissionPolicies
|
||||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
subject={subject}
|
||||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||||
key={`project-permission-${subject}`}
|
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||||
isDisabled={isDisabled}
|
key={`project-permission-${subject}`}
|
||||||
>
|
isDisabled={isDisabled}
|
||||||
{renderConditionalComponents(subject, isDisabled)}
|
>
|
||||||
</GeneralPermissionPolicies>
|
{renderConditionalComponents(subject, isDisabled)}
|
||||||
))}
|
</GeneralPermissionPolicies>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -12,19 +14,17 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
PageHeader,
|
PageHeader
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
|
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
|
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 { ProjectAccessControlTabs } from "@app/types/project";
|
||||||
|
|
||||||
import { RoleDetailsSection } from "./components/RoleDetailsSection";
|
|
||||||
import { RoleModal } from "./components/RoleModal";
|
import { RoleModal } from "./components/RoleModal";
|
||||||
import { RolePermissionsSection } from "./components/RolePermissionsSection";
|
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -88,17 +88,29 @@ const Page = () => {
|
|||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
{data && (
|
{data && (
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
<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 && (
|
{isCustomRole && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
<Button
|
||||||
<Tooltip content="More options">
|
colorSchema="secondary"
|
||||||
<Button variant="outline_bg">More</Button>
|
rightIcon={<FontAwesomeIcon icon={faEllipsisV} className="ml-2" />}
|
||||||
</Tooltip>
|
>
|
||||||
</div>
|
Options
|
||||||
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="p-1">
|
<DropdownMenuContent align="end" sideOffset={2} className="p-1">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Role}
|
a={ProjectPermissionSub.Role}
|
||||||
@@ -113,6 +125,7 @@ const Page = () => {
|
|||||||
roleSlug
|
roleSlug
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
disabled={!isAllowed}
|
disabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Edit Role
|
Edit Role
|
||||||
@@ -128,6 +141,7 @@ const Page = () => {
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
)}
|
)}
|
||||||
|
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("duplicateRole");
|
handlePopUpOpen("duplicateRole");
|
||||||
}}
|
}}
|
||||||
@@ -143,13 +157,9 @@ const Page = () => {
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={() => handlePopUpOpen("deleteRole")}
|
onClick={() => handlePopUpOpen("deleteRole")}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Delete Role
|
Delete Role
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -159,12 +169,7 @@ const Page = () => {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div className="flex">
|
<RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
|
||||||
<div className="mr-4 w-96">
|
|
||||||
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />
|
|
||||||
</div>
|
|
||||||
<RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
@@ -24,7 +24,7 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
|
|||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className="h-10 rounded-r-none"
|
className="h-10 rounded-r-none"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
@@ -73,6 +73,6 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
|
|||||||
isOpen={popUp.applyTemplate.isOpen}
|
isOpen={popUp.applyTemplate.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("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 { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
import { ConditionsFields } from "./ConditionsFields";
|
||||||
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";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
@@ -28,162 +8,17 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DynamicSecretPermissionConditions = ({ position = 0, isDisabled }: 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 (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<ConditionsFields
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
isDisabled={isDisabled}
|
||||||
<p className="text-sm text-mineshaft-400">
|
subject={ProjectPermissionSub.DynamicSecrets}
|
||||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
position={position}
|
||||||
</p>
|
selectOptions={[
|
||||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
{ value: "environment", label: "Environment Slug" },
|
||||||
All conditions must evaluate to true for the policy to take effect.
|
{ value: "secretPath", label: "Secret Path" },
|
||||||
</p>
|
{ value: "metadataKey", label: "Metadata Key" },
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
{ value: "metadataValue", label: "Metadata Value" }
|
||||||
{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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,180 +1,26 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
import { ConditionsFields } from "./ConditionsFields";
|
||||||
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";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
type:
|
type:
|
||||||
| ProjectPermissionSub.DynamicSecrets
|
|
||||||
| ProjectPermissionSub.SecretFolders
|
| ProjectPermissionSub.SecretFolders
|
||||||
| ProjectPermissionSub.SecretImports
|
| ProjectPermissionSub.SecretImports
|
||||||
| ProjectPermissionSub.SecretRotation;
|
| ProjectPermissionSub.SecretRotation;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
|
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 (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<ConditionsFields
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
isDisabled={isDisabled}
|
||||||
<p className="text-sm text-mineshaft-400">
|
subject={type}
|
||||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
position={position}
|
||||||
</p>
|
selectOptions={[
|
||||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
{ value: "environment", label: "Environment Slug" },
|
||||||
All conditions must evaluate to true for the policy to take effect.
|
{ value: "secretPath", label: "Secret Path" }
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -3,6 +3,7 @@ import { Control, Controller, useFieldArray, useFormContext, useWatch } from "re
|
|||||||
import {
|
import {
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
|
faDiagramProject,
|
||||||
faGripVertical,
|
faGripVertical,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faPlus,
|
faPlus,
|
||||||
@@ -27,6 +28,7 @@ type Props<T extends ProjectPermissionSub> = {
|
|||||||
actions: TProjectPermissionObject[T]["actions"];
|
actions: TProjectPermissionObject[T]["actions"];
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
onShowAccessTree?: (subject: ProjectPermissionSub) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActionProps = {
|
type ActionProps = {
|
||||||
@@ -71,7 +73,8 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
actions,
|
actions,
|
||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
isDisabled
|
isDisabled,
|
||||||
|
onShowAccessTree
|
||||||
}: Props<T>) => {
|
}: Props<T>) => {
|
||||||
const { control, watch } = useFormContext<TFormSchema>();
|
const { control, watch } = useFormContext<TFormSchema>();
|
||||||
const { fields, remove, insert, move } = useFieldArray({
|
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 [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||||
const [dragOverItem, setDragOverItem] = 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) => {
|
const handleDragStart = (_: React.DragEvent, index: number) => {
|
||||||
setDraggedItem(index);
|
setDraggedItem(index);
|
||||||
@@ -121,9 +124,9 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<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"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setIsOpen.toggle()}
|
onClick={() => setIsOpen.toggle()}
|
||||||
@@ -133,20 +136,50 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<FontAwesomeIcon className="mr-8" icon={isOpen ? faChevronDown : faChevronRight} />
|
||||||
<FontAwesomeIcon icon={isOpen ? faChevronDown : faChevronRight} />
|
|
||||||
</div>
|
<div className="flex-grow text-base">{title}</div>
|
||||||
<div className="flex-grow">{title}</div>
|
|
||||||
{fields.length > 1 && (
|
{fields.length > 1 && (
|
||||||
<div>
|
<div>
|
||||||
<Tag size="xs" className="px-2">
|
<Tag size="xs" className="mr-2 px-2">
|
||||||
{fields.length} rules
|
{fields.length} Rules
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{isOpen && (
|
{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) => {
|
{fields.map((el, rootIndex) => {
|
||||||
let isFullReadAccessEnabled = false;
|
let isFullReadAccessEnabled = false;
|
||||||
|
|
||||||
@@ -154,78 +187,103 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any);
|
isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInverted = watch(`permissions.${subject}.${rootIndex}.inverted` as any);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={el.id}
|
key={el.id}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md",
|
"relative rounded-md border-l-[6px] bg-mineshaft-800 px-5 py-4 transition-colors duration-300",
|
||||||
dragOverItem === rootIndex ? "border-2 border-blue-400" : "",
|
isInverted ? "border-l-red-600/50" : "border-l-green-600/50",
|
||||||
|
dragOverItem === rootIndex ? "border-2 border-primary/50" : "",
|
||||||
draggedItem === rootIndex ? "opacity-50" : ""
|
draggedItem === rootIndex ? "opacity-50" : ""
|
||||||
)}
|
)}
|
||||||
onDragOver={(e) => handleDragOver(e, rootIndex)}
|
onDragOver={(e) => handleDragOver(e, rootIndex)}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
{!isDisabled && (
|
{isConditionalSubjects(subject) && (
|
||||||
<Tooltip position="left" content="Drag to reorder permission">
|
<div className="mb-4 flex items-center gap-3">
|
||||||
<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) && (
|
|
||||||
<div className="flex w-full items-center text-gray-300">
|
<div className="flex w-full items-center text-gray-300">
|
||||||
<div className="w-1/4">Permission</div>
|
<div className="mr-3">Permission</div>
|
||||||
<div className="mr-4 w-1/4">
|
<Controller
|
||||||
<Controller
|
defaultValue={false as any}
|
||||||
defaultValue={false as any}
|
name={`permissions.${subject}.${rootIndex}.inverted`}
|
||||||
name={`permissions.${subject}.${rootIndex}.inverted`}
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<Select
|
||||||
<Select
|
value={String(field.value)}
|
||||||
value={String(field.value)}
|
onValueChange={(val) => field.onChange(val === "true")}
|
||||||
onValueChange={(val) => field.onChange(val === "true")}
|
containerClassName="w-40"
|
||||||
containerClassName="w-full"
|
className="w-full"
|
||||||
className="w-full"
|
isDisabled={isDisabled}
|
||||||
isDisabled={isDisabled}
|
position="popper"
|
||||||
>
|
>
|
||||||
<SelectItem value="false">Allow</SelectItem>
|
<SelectItem value="false">Allow</SelectItem>
|
||||||
<SelectItem value="true">Forbid</SelectItem>
|
<SelectItem value="true">Forbid</SelectItem>
|
||||||
</Select>
|
</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>
|
</Tooltip>
|
||||||
<div>
|
{!isDisabled && (
|
||||||
<Tooltip
|
<Button
|
||||||
asChild
|
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
content={
|
variant="outline_bg"
|
||||||
<>
|
size="xs"
|
||||||
<p>
|
className="ml-auto mr-3"
|
||||||
Whether to allow or forbid the selected actions when the following
|
onClick={() => remove(rootIndex)}
|
||||||
conditions (if any) are met.
|
isDisabled={isDisabled}
|
||||||
</p>
|
|
||||||
<p className="mt-2">Forbid rules must come after allow rules.</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
Remove Rule
|
||||||
icon={faInfoCircle}
|
</Button>
|
||||||
size="sm"
|
)}
|
||||||
className="text-gray-400"
|
{!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>
|
</Tooltip>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="flex gap-4 text-gray-300">
|
<div className="flex flex-col text-gray-300">
|
||||||
<div className="w-1/4">Actions</div>
|
<div className="flex w-full justify-between">
|
||||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
<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) => {
|
{actions.map(({ label, value }, index) => {
|
||||||
if (typeof value !== "string") return undefined;
|
if (typeof value !== "string") return undefined;
|
||||||
|
|
||||||
@@ -255,41 +313,6 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
cloneElement(children, {
|
cloneElement(children, {
|
||||||
position: rootIndex
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -1,23 +1,6 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
import { ConditionsFields } from "./ConditionsFields";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
PermissionConditionOperators,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
|
||||||
|
|
||||||
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
|
|
||||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
@@ -25,150 +8,12 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: 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 (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<ConditionsFields
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
isDisabled={isDisabled}
|
||||||
<p className="text-sm text-mineshaft-400">
|
subject={ProjectPermissionSub.Identity}
|
||||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
position={position}
|
||||||
</p>
|
selectOptions={[{ value: "identityId", label: "Identity ID" }]}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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) => {
|
export const renderOperatorSelectItems = (type: string) => {
|
||||||
if (type === "secretTags") {
|
switch (type) {
|
||||||
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
|
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
|
([key, value]) => key && value?.length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNotEmptyPermissions) return <div />;
|
if (isNotEmptyPermissions) return null;
|
||||||
|
|
||||||
return <EmptyState title="No policies applied" className="py-8" />;
|
return <EmptyState title="No policies applied" className="py-8" />;
|
||||||
};
|
};
|
||||||
|
@@ -1,23 +1,6 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
import { ConditionsFields } from "./ConditionsFields";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
PermissionConditionOperators,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
|
||||||
|
|
||||||
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
|
|
||||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
@@ -25,149 +8,12 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: 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 (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<ConditionsFields
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
isDisabled={isDisabled}
|
||||||
<p className="text-sm text-mineshaft-400">
|
subject={ProjectPermissionSub.PkiSubscribers}
|
||||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
position={position}
|
||||||
</p>
|
selectOptions={[{ value: "name", label: "Name" }]}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,23 +1,6 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
import { ConditionsFields } from "./ConditionsFields";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
PermissionConditionOperators,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
|
||||||
|
|
||||||
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
|
|
||||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
@@ -25,149 +8,12 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PkiTemplatePermissionConditions = ({ position = 0, isDisabled }: 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 (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<ConditionsFields
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
isDisabled={isDisabled}
|
||||||
<p className="text-sm text-mineshaft-400">
|
subject={ProjectPermissionSub.CertificateTemplates}
|
||||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
position={position}
|
||||||
</p>
|
selectOptions={[{ value: "name", label: "Name" }]}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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 { FormProvider, useForm } from "react-hook-form";
|
||||||
import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
|
import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
|
||||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -88,6 +88,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
roleSlug as string
|
roleSlug as string
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [showAccessTree, setShowAccessTree] = useState<ProjectPermissionSub | null>(null);
|
||||||
|
|
||||||
const form = useForm<TFormSchema>({
|
const form = useForm<TFormSchema>({
|
||||||
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||||
resolver: zodResolver(projectRoleFormSchema)
|
resolver: zodResolver(projectRoleFormSchema)
|
||||||
@@ -133,71 +135,90 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
[JSON.stringify(permissions)]
|
[JSON.stringify(permissions)]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSecretManagerProject = currentWorkspace.type === ProjectType.SecretManager;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{currentWorkspace.type === ProjectType.SecretManager && (
|
|
||||||
<AccessTree permissions={formattedPermissions} />
|
|
||||||
)}
|
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
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}>
|
<FormProvider {...form}>
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="mx-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
<div>
|
||||||
<div className="flex items-center space-x-4">
|
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
||||||
{isCustomRole && (
|
<p className="text-sm leading-3 text-mineshaft-400">
|
||||||
<>
|
Configure granular access policies
|
||||||
{isDirty && (
|
</p>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
{isCustomRole && (
|
||||||
<div className="py-4">
|
<div className="flex items-center gap-2">
|
||||||
{!isPending && <PermissionEmptyState />}
|
{isDirty && (
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
|
<Button
|
||||||
.filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject))
|
className="mr-4 text-mineshaft-300"
|
||||||
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
|
variant="link"
|
||||||
.map((subject) => (
|
isDisabled={isSubmitting}
|
||||||
<GeneralPermissionPolicies
|
isLoading={isSubmitting}
|
||||||
subject={subject}
|
onClick={() => reset()}
|
||||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
>
|
||||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
Discard
|
||||||
key={`project-permission-${subject}`}
|
</Button>
|
||||||
isDisabled={isDisabled}
|
)}
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
className={twMerge("h-10 border")}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||||
>
|
>
|
||||||
{renderConditionalComponents(subject, isDisabled)}
|
Save
|
||||||
</GeneralPermissionPolicies>
|
</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>
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</form>
|
</form>
|
||||||
|
{isSecretManagerProject && showAccessTree && (
|
||||||
|
<AccessTree
|
||||||
|
permissions={formattedPermissions}
|
||||||
|
subject={showAccessTree}
|
||||||
|
onClose={() => setShowAccessTree(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,23 +1,6 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
import { ConditionsFields } from "./ConditionsFields";
|
||||||
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";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
@@ -25,159 +8,17 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SecretPermissionConditions = ({ position = 0, isDisabled }: 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 (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<ConditionsFields
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
isDisabled={isDisabled}
|
||||||
<p className="text-sm text-mineshaft-400">
|
subject={ProjectPermissionSub.Secrets}
|
||||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
position={position}
|
||||||
</p>
|
selectOptions={[
|
||||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
{ value: "environment", label: "Environment Slug" },
|
||||||
All conditions must evaluate to true for the policy to take effect.
|
{ value: "secretPath", label: "Secret Path" },
|
||||||
</p>
|
{ value: "secretName", label: "Secret Name" },
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
{ value: "secretTags", label: "Secret Tags" }
|
||||||
{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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,26 +1,6 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
import { ConditionsFields } from "./ConditionsFields";
|
||||||
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";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
@@ -28,159 +8,15 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: 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 (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<ConditionsFields
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
isDisabled={isDisabled}
|
||||||
<p className="text-sm text-mineshaft-400">
|
subject={ProjectPermissionSub.SecretSyncs}
|
||||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
position={position}
|
||||||
</p>
|
selectOptions={[
|
||||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
{ value: "environment", label: "Environment Slug" },
|
||||||
All conditions must evaluate to true for the policy to take effect.
|
{ value: "secretPath", label: "Secret Path" }
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,23 +1,6 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
import { ConditionsFields } from "./ConditionsFields";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
PermissionConditionOperators,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
|
||||||
|
|
||||||
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
|
|
||||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
@@ -25,149 +8,12 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SshHostPermissionConditions = ({ position = 0, isDisabled }: 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 (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<ConditionsFields
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
isDisabled={isDisabled}
|
||||||
<p className="text-sm text-mineshaft-400">
|
subject={ProjectPermissionSub.SshHosts}
|
||||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
position={position}
|
||||||
</p>
|
selectOptions={[{ value: "hostname", label: "Hostname" }]}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -6,6 +6,9 @@ import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
|||||||
import {
|
import {
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
|
faArrowLeft,
|
||||||
|
faArrowRight,
|
||||||
|
faArrowRightToBracket,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faFileImport,
|
faFileImport,
|
||||||
faFingerprint,
|
faFingerprint,
|
||||||
@@ -69,7 +72,7 @@ import {
|
|||||||
PreferenceKey,
|
PreferenceKey,
|
||||||
setUserTablePreference
|
setUserTablePreference
|
||||||
} from "@app/helpers/userTablePreferences";
|
} from "@app/helpers/userTablePreferences";
|
||||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
import { useDebounce, usePagination, usePopUp, useResetPageHelper, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
useCreateSecretV3,
|
useCreateSecretV3,
|
||||||
@@ -164,6 +167,18 @@ export const OverviewPage = () => {
|
|||||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
|
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
|
||||||
const secretPath = (routerSearch?.secretPath as string) || "/";
|
const secretPath = (routerSearch?.secretPath as string) || "/";
|
||||||
const { subscription } = useSubscription();
|
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 [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
|
||||||
const [filterHistory, setFilterHistory] = useState<
|
const [filterHistory, setFilterHistory] = useState<
|
||||||
@@ -1173,47 +1188,81 @@ export const OverviewPage = () => {
|
|||||||
className="thin-scrollbar rounded-b-none"
|
className="thin-scrollbar rounded-b-none"
|
||||||
>
|
>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead className={collapseEnvironments ? "h-24" : ""}>
|
||||||
<Tr className="sticky top-0 z-20 border-0">
|
<Tr
|
||||||
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
|
className={twMerge("sticky top-0 z-20 border-0", collapseEnvironments && "h-24")}
|
||||||
<div className="flex items-center border-b border-r border-mineshaft-600 pb-3 pl-3 pr-5 pt-3.5">
|
>
|
||||||
<Tooltip
|
<Th
|
||||||
className="max-w-[20rem] whitespace-nowrap capitalize"
|
className={twMerge(
|
||||||
content={
|
"sticky left-0 z-20 min-w-[20rem] border-b-0 p-0",
|
||||||
totalCount > 0
|
collapseEnvironments && "h-24"
|
||||||
? `${
|
)}
|
||||||
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
|
>
|
||||||
} all folders and secrets on page`
|
<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">
|
<Tooltip
|
||||||
<Checkbox
|
className="max-w-[20rem] whitespace-nowrap capitalize"
|
||||||
isDisabled={totalCount === 0}
|
content={
|
||||||
id="checkbox-select-all-rows"
|
totalCount > 0
|
||||||
isChecked={allRowsSelectedOnPage.isChecked}
|
? `${
|
||||||
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
|
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
|
||||||
onCheckedChange={toggleSelectAllRows}
|
} 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>
|
</IconButton>
|
||||||
</Tooltip>
|
</div>
|
||||||
Name
|
<Tooltip
|
||||||
<IconButton
|
content={
|
||||||
variant="plain"
|
collapseEnvironments ? "Expand Environments" : "Collapse Environments"
|
||||||
className="ml-2"
|
|
||||||
ariaLabel="sort"
|
|
||||||
onClick={() =>
|
|
||||||
setOrderDirection((prev) =>
|
|
||||||
prev === OrderByDirection.ASC
|
|
||||||
? OrderByDirection.DESC
|
|
||||||
: OrderByDirection.ASC
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
className="capitalize"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<IconButton
|
||||||
icon={orderDirection === "asc" ? faArrowDown : faArrowUp}
|
ariaLabel="Toggle Environment View"
|
||||||
/>
|
variant="plain"
|
||||||
</IconButton>
|
colorSchema="secondary"
|
||||||
|
className="ml-auto mt-auto h-min p-1"
|
||||||
|
onClick={handleToggleNarrowHeader}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={collapseEnvironments ? faArrowLeft : faArrowRight}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
{visibleEnvs?.map(({ name, slug }, index) => {
|
{visibleEnvs?.map(({ name, slug }, index) => {
|
||||||
@@ -1223,28 +1272,76 @@ export const OverviewPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Th
|
<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}`}
|
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">
|
<Tooltip
|
||||||
<button
|
content={
|
||||||
type="button"
|
collapseEnvironments ? (
|
||||||
className="text-sm font-medium duration-100 hover:text-mineshaft-100"
|
<p className="whitespace-break-spaces">{name}</p>
|
||||||
onClick={() => handleExploreEnvClick(slug)}
|
) : (
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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}
|
<div
|
||||||
</button>
|
className={twMerge(
|
||||||
{missingKeyCount > 0 && (
|
"border-mineshaft-600",
|
||||||
<Tooltip
|
collapseEnvironments
|
||||||
className="max-w-none lowercase"
|
? "ml-[0.85rem] h-24 -skew-x-[16rad] transform border-l text-xs"
|
||||||
content={`${missingKeyCount} secrets missing\n compared to other environments`}
|
: "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">
|
<p className="truncate font-medium">{name}</p>
|
||||||
<span className="text-bunker-100">{missingKeyCount}</span>
|
</button>
|
||||||
</div>
|
{!collapseEnvironments && missingKeyCount > 0 && (
|
||||||
</Tooltip>
|
<Tooltip
|
||||||
)}
|
className="max-w-none lowercase"
|
||||||
</div>
|
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>
|
</Th>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -1409,16 +1506,40 @@ export const OverviewPage = () => {
|
|||||||
/>
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
{visibleEnvs?.map(({ name, slug }) => (
|
{visibleEnvs?.map(({ name, slug }) => (
|
||||||
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
|
<Td
|
||||||
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
|
key={`explore-${name}-btn`}
|
||||||
<Button
|
className="border-0 border-r border-mineshaft-600 p-0"
|
||||||
size="xs"
|
>
|
||||||
variant="outline_bg"
|
<div
|
||||||
isFullWidth
|
className={twMerge(
|
||||||
onClick={() => handleExploreEnvClick(slug)}
|
"flex w-full items-center justify-center border-t border-mineshaft-600 py-2"
|
||||||
>
|
)}
|
||||||
Explore
|
>
|
||||||
</Button>
|
{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>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
))}
|
))}
|
||||||
|
@@ -36,7 +36,7 @@ export const SecretOverviewDynamicSecretRow = ({
|
|||||||
isPresent ? "text-green-600" : "text-red-600"
|
isPresent ? "text-green-600" : "text-red-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-center">
|
<div className="mx-auto flex w-[0.03rem] justify-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
icon={isPresent ? faCheck : faXmark}
|
icon={isPresent ? faCheck : faXmark}
|
||||||
|
@@ -70,7 +70,7 @@ export const SecretOverviewFolderRow = ({
|
|||||||
isPresent ? "text-green-600" : "text-red-600"
|
isPresent ? "text-green-600" : "text-red-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-center">
|
<div className="mx-auto flex w-[0.03rem] justify-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
icon={isPresent ? faCheck : faXmark}
|
icon={isPresent ? faCheck : faXmark}
|
||||||
|
@@ -94,7 +94,7 @@ export const SecretOverviewSecretRotationRow = ({
|
|||||||
isPresent ? "text-green-600" : "text-red-600"
|
isPresent ? "text-green-600" : "text-red-600"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-center">
|
<div className="mx-auto flex w-[0.03rem] justify-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
// eslint-disable-next-line no-nested-ternary
|
// eslint-disable-next-line no-nested-ternary
|
||||||
icon={isPresent ? faCheck : faXmark}
|
icon={isPresent ? faCheck : faXmark}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import {
|
import {
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faCheck,
|
faCheck,
|
||||||
|
faCircle,
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
@@ -145,14 +145,14 @@ export const SecretOverviewTableRow = ({
|
|||||||
<Td
|
<Td
|
||||||
key={`sec-overview-${slug}-${i + 1}-value`}
|
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||||
className={twMerge(
|
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",
|
isFormExpanded && "border-t-2 border-mineshaft-500",
|
||||||
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
|
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
|
||||||
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
|
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
|
||||||
!isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : ""
|
!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">
|
<div className="flex justify-center">
|
||||||
{!isSecretEmpty && (
|
{!isSecretEmpty && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
Reference in New Issue
Block a user