mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 14:29:34 +00:00
feat(rbac): ui components and hooks for rbac
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { forwardRef } from "react";
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const AccordionItem = forwardRef<HTMLDivElement, AccordionPrimitive.AccordionItemProps>(
|
||||
({ children, className, ...props }, forwardedRef) => (
|
||||
<AccordionPrimitive.Item
|
||||
className={twMerge(
|
||||
"focus-within:shadow-mauve12 mt-px overflow-hidden first:mt-0 first:rounded-t last:rounded-b focus-within:relative focus-within:z-10 focus-within:shadow-[0_0_0_2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
</AccordionPrimitive.Item>
|
||||
)
|
||||
);
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
export const AccordionTrigger = forwardRef<
|
||||
HTMLButtonElement,
|
||||
AccordionPrimitive.AccordionTriggerProps
|
||||
>(({ children, className, ...props }, forwardedRef) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
className={twMerge(
|
||||
"text-violet11 shadow-mauve6 hover:bg-mauve2 group flex h-[45px] flex-1 cursor-default items-center justify-between bg-white px-5 text-[15px] leading-none shadow-[0_1px_0] outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronDown}
|
||||
className="text-violet10 ease-[cubic-bezier(0.87,_0,_0.13,_1)] transition-transform duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden
|
||||
/>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
|
||||
AccordionTrigger.displayName = "AccordionTrigger";
|
||||
|
||||
export const AccordionContent = forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionPrimitive.AccordionContentProps
|
||||
>(({ children, className, ...props }, forwardedRef) => (
|
||||
<AccordionPrimitive.Content
|
||||
className={twMerge(
|
||||
"text-mauve11 bg-mauve2 data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp overflow-hidden text-[15px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<div className="py-[15px] px-5">{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = "AccordionContent";
|
||||
|
||||
// ref: https://www.radix-ui.com/primitives/docs/components/accordion#root
|
||||
export const Accordion = ({
|
||||
children,
|
||||
...props
|
||||
}: AccordionPrimitive.AccordionSingleProps | AccordionPrimitive.AccordionMultipleProps) => (
|
||||
<AccordionPrimitive.Root
|
||||
className="bg-mauve6 w-[300px] rounded-md shadow-[0_2px_10px] shadow-black/5"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AccordionPrimitive.Root>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { Accordion, AccordionContent, AccordionItem,AccordionTrigger } from "./Accordion";
|
||||
@@ -57,7 +57,8 @@ const iconButtonVariants = cva(
|
||||
{
|
||||
colorSchema: "primary",
|
||||
variant: "outline_bg",
|
||||
className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200 hover:text-bunker-100 duration-100"
|
||||
className:
|
||||
"bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200 hover:text-bunker-100 duration-100"
|
||||
},
|
||||
{
|
||||
colorSchema: "danger",
|
||||
|
||||
@@ -79,7 +79,7 @@ export const Tr = ({
|
||||
}: TrProps): JSX.Element => (
|
||||
<tr
|
||||
className={twMerge(
|
||||
"cursor-default border border-solid border-mineshaft-600",
|
||||
"cursor-default border-b last:border-b-0 border-solid border-mineshaft-600",
|
||||
isHoverable && "hover:bg-mineshaft-600",
|
||||
isSelectable && "cursor-pointer",
|
||||
className
|
||||
@@ -124,7 +124,7 @@ export type TdProps = {
|
||||
} & TdHTMLAttributes<HTMLTableCellElement>;
|
||||
|
||||
export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
|
||||
<td className={twMerge("px-5 py-2 text-left", className)} {...props}>
|
||||
<td className={twMerge("px-5 py-3 text-left", className)} {...props}>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
|
||||
@@ -1,142 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import { faArrowUpRightFromSquare, faCheck, faClipboard } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type TabsProps = TabsPrimitive.TabsProps;
|
||||
|
||||
export type TabsProps = Tabs.TabsProps & {};
|
||||
export const Tabs = ({ className, children, ...props }: TabsProps) => (
|
||||
<TabsPrimitive.Root className={twMerge("flex flex-col", className)} {...props}>
|
||||
{children}
|
||||
</TabsPrimitive.Root>
|
||||
);
|
||||
|
||||
function copyToClipboard(id: string, setState: (value: boolean) => void) {
|
||||
// Get the text field
|
||||
const copyText = document.getElementById(id) as HTMLInputElement;
|
||||
export type TabListProps = TabsPrimitive.TabsListProps;
|
||||
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setState(true);
|
||||
setTimeout(() => setState(false), 2000);
|
||||
// Alert the copied text
|
||||
// alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
|
||||
const CodeItem = ({isCopied, setIsCopied, textExplanation, code, id}: {isCopied: boolean, setIsCopied: (value: boolean) => void, textExplanation: string, code: string, id: string}) => {
|
||||
return <>
|
||||
<p className="mb-2 mt-4 text-bunker-300 text-sm leading-normal">
|
||||
{textExplanation}
|
||||
</p>
|
||||
<div className='font-mono text-sm px-3 py-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
|
||||
<input disabled value={code} id={id} className='w-full bg-transparent text-bunker-200'/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(id, setIsCopied)}
|
||||
className='h-full pl-3.5 pr-2 text-bunker-300 hover:text-primary-200 duration-200'
|
||||
>
|
||||
{isCopied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='pr-0.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faClipboard} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export const TabsObject = () => {
|
||||
const [downloadCodeCopied, setDownloadCodeCopied] = useState(false);
|
||||
const [downloadCode2Copied, setDownloadCode2Copied] = useState(false);
|
||||
const [loginCodeCopied, setLoginCodeCopied] = useState(false);
|
||||
const [initCodeCopied, setInitCodeCopied] = useState(false);
|
||||
const [runCodeCopied, setRunCodeCopied] = useState(false);
|
||||
|
||||
return <Tabs.Root
|
||||
className="flex flex-col w-full cursor-default border border-mineshaft-600 rounded-md"
|
||||
defaultValue="tab1"
|
||||
export const TabList = ({ className, children, ...props }: TabListProps) => (
|
||||
<TabsPrimitive.List
|
||||
className={twMerge("flex-shrink-0 flex border-b-2 border-mineshaft-800", className)}
|
||||
{...props}
|
||||
>
|
||||
<Tabs.List className="shrink-0 flex border-b border-mineshaft-600" aria-label="Manage your account">
|
||||
<Tabs.Trigger
|
||||
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
value="tab1"
|
||||
>
|
||||
MacOS
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
value="tab2"
|
||||
>
|
||||
Windows
|
||||
</Tabs.Trigger>
|
||||
{/* <Tabs.Trigger
|
||||
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
value="tab3"
|
||||
>
|
||||
Arch Linux
|
||||
</Tabs.Trigger> */}
|
||||
<a
|
||||
target='_blank'
|
||||
rel="noopener noreferrer"
|
||||
className="bg-bunker-700 hover:text-bunker-100 duration-200 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
href='https://infisical.com/docs/cli/overview'
|
||||
>
|
||||
Other Platforms <FontAwesomeIcon icon={faArrowUpRightFromSquare} className='ml-2'/>
|
||||
</a>
|
||||
</Tabs.List>
|
||||
<Tabs.Content
|
||||
className="grow p-5 pt-0 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
value="tab1"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCode" />
|
||||
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCode" />
|
||||
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCode" />
|
||||
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCode" />
|
||||
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
|
||||
className='text-primary underline underline-offset-2'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href='https://infisical.com/docs/integrations/overview'
|
||||
>
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
className="grow p-5 pt-0 bg-bunker-700 rounded-b-md outline-none"
|
||||
value="tab2"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="scoop bucket add org https://github.com/Infisical/scoop-infisical.git" id="downloadCodeW" />
|
||||
<div className='font-mono text-sm px-3 py-2 mt-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
|
||||
<input disabled value="scoop install infisical" id="downloadCodeW2" className='w-full bg-transparent text-bunker-200'/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard("downloadCodeW2", setDownloadCode2Copied)}
|
||||
className='h-full pl-3.5 pr-2 text-bunker-300 hover:text-primary-200 duration-200'
|
||||
>
|
||||
{downloadCode2Copied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='pr-0.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faClipboard} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCodeW" />
|
||||
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCodeW" />
|
||||
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCodeW" />
|
||||
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
|
||||
className='text-primary underline underline-offset-2'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href='https://infisical.com/docs/integrations/overview'
|
||||
>
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
};
|
||||
{children}
|
||||
</TabsPrimitive.List>
|
||||
);
|
||||
|
||||
export type TabProps = TabsPrimitive.TabsTriggerProps;
|
||||
|
||||
export const Tab = ({ className, children, ...props }: TabProps) => (
|
||||
<TabsPrimitive.Trigger
|
||||
className={twMerge(
|
||||
"px-5 h-11 flex items-center justify-center select-none first:rounded-tl-md last:rounded-tr-md hover:text-primary text-mineshaft-400 transition-all data-[state=active]:text-white data-[state=active]:border-b data-[state=active]:border-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TabsPrimitive.Trigger>
|
||||
);
|
||||
|
||||
export type TabPanelProps = TabsPrimitive.TabsContentProps;
|
||||
|
||||
export const TabPanel = ({ className, children, ...props }: TabPanelProps) => (
|
||||
<TabsPrimitive.Content
|
||||
className={twMerge("outline-none flex-grow p-5 rounded-bl-md rounded-br-md", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TabsPrimitive.Content>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export type {
|
||||
TabsProps
|
||||
} from "./Tabs";
|
||||
export { TabsObject } from "./Tabs";
|
||||
export type { TabListProps,TabPanelProps, TabProps, TabsProps } from "./Tabs";
|
||||
export { Tab, TabList, TabPanel, Tabs } from "./Tabs";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./Accordion";
|
||||
export * from "./Button";
|
||||
export * from "./Card";
|
||||
export * from "./Checkbox";
|
||||
@@ -21,6 +22,7 @@ export * from "./Skeleton";
|
||||
export * from "./Spinner";
|
||||
export * from "./Switch";
|
||||
export * from "./Table";
|
||||
export * from "./Tabs";
|
||||
export * from "./Tag";
|
||||
export * from "./TextArea";
|
||||
export * from "./Tooltip";
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from "./integrationAuth";
|
||||
export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./roles";
|
||||
export * from "./secretFolders";
|
||||
export * from "./secretImports";
|
||||
export * from "./secrets";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateRole, useDeleteRole,useUpdateRole } from "./mutation";
|
||||
export { useGetRoles } from "./queries";
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { roleQueryKeys } from "./queries";
|
||||
import { TCreateRoleDTO, TDeleteRoleDTO, TUpdateRoleDTO } from "./types";
|
||||
|
||||
export const useCreateRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (dto: TCreateRoleDTO) => apiRequest.post("/api/v1/roles", dto),
|
||||
onSuccess: (_, { orgId, workspaceId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...dto }: TUpdateRoleDTO) => apiRequest.patch(`/api/v1/roles/${id}`, dto),
|
||||
onSuccess: (_, { orgId, workspaceId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ orgId, id }: TDeleteRoleDTO) =>
|
||||
apiRequest.delete(`/api/v1/roles/${id}`, {
|
||||
data: { orgId }
|
||||
}),
|
||||
onSuccess: (_, { orgId, workspaceId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGetRolesDTO, TRole } from "./types";
|
||||
|
||||
export const roleQueryKeys = {
|
||||
getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const
|
||||
};
|
||||
|
||||
const getRoles = async ({ orgId, workspaceId }: TGetRolesDTO) => {
|
||||
const { data } = await apiRequest.get<{ data: { roles: TRole[] } }>("/api/v1/roles", {
|
||||
params: {
|
||||
workspaceId,
|
||||
orgId
|
||||
}
|
||||
});
|
||||
|
||||
return data.data.roles;
|
||||
};
|
||||
|
||||
export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) =>
|
||||
useQuery({
|
||||
queryKey: roleQueryKeys.getRoles({ orgId, workspaceId }),
|
||||
queryFn: () => getRoles({ orgId, workspaceId }),
|
||||
enabled: Boolean(orgId)
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
export type TGetRolesDTO = {
|
||||
orgId: string;
|
||||
workspaceId?: string;
|
||||
};
|
||||
|
||||
export type TRole = {
|
||||
_id: string;
|
||||
organization: string;
|
||||
workspace: string;
|
||||
name: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
permissions: TPermission[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TPermission = {
|
||||
condition?: Record<string, any>;
|
||||
action: "read" | "edit" | "create" | "delete";
|
||||
subject: string;
|
||||
};
|
||||
|
||||
export type TCreateRoleDTO = {
|
||||
orgId: string;
|
||||
workspaceId?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
slug: string;
|
||||
permissions: TPermission[];
|
||||
};
|
||||
|
||||
export type TUpdateRoleDTO = {
|
||||
orgId: string;
|
||||
id: string;
|
||||
workspaceId?: string;
|
||||
} & Partial<Omit<TCreateRoleDTO, "orgId" | "workspaceId">>;
|
||||
|
||||
export type TDeleteRoleDTO = {
|
||||
orgId: string;
|
||||
id: string;
|
||||
workspaceId?: string;
|
||||
};
|
||||
@@ -42,9 +42,10 @@ export type OrgUser = {
|
||||
};
|
||||
inviteEmail: string;
|
||||
organization: string;
|
||||
role: "owner" | "admin" | "member";
|
||||
role: "owner" | "admin" | "member" | "custom";
|
||||
status: "invited" | "accepted" | "verified" | "completed";
|
||||
deniedPermissions: any[];
|
||||
customRole: string;
|
||||
};
|
||||
|
||||
export type AddUserToWsDTO = {
|
||||
@@ -76,7 +77,7 @@ export type AddUserToOrgDTO = {
|
||||
export type CreateAPIKeyRes = {
|
||||
apiKey: string;
|
||||
apiKeyData: APIKeyData;
|
||||
}
|
||||
};
|
||||
|
||||
export type RenameUserDTO = {
|
||||
newName: string;
|
||||
@@ -89,7 +90,7 @@ export type APIKeyData = {
|
||||
lastUsed: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type TokenVersion = {
|
||||
_id: string;
|
||||
@@ -99,4 +100,4 @@ export type TokenVersion = {
|
||||
lastUsed: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user