mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 02:51:51 +00:00
Merge remote-tracking branch 'origin/main' into feat/add-render-app-connection-and-secret-sync
This commit is contained in:
+31
-4
@@ -15,7 +15,8 @@ import {
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent
|
||||
ModalContent,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { findOrgMembershipRole } from "@app/helpers/roles";
|
||||
@@ -27,6 +28,7 @@ const schema = z
|
||||
.object({
|
||||
name: z.string().min(1, "Required"),
|
||||
role: z.object({ slug: z.string(), name: z.string() }),
|
||||
hasDeleteProtection: z.boolean(),
|
||||
metadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
@@ -64,7 +66,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: ""
|
||||
name: "",
|
||||
hasDeleteProtection: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,6 +81,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
identityId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
hasDeleteProtection: boolean;
|
||||
metadata?: { key: string; value: string }[];
|
||||
customRole: {
|
||||
name: string;
|
||||
@@ -91,22 +95,25 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
reset({
|
||||
name: identity.name,
|
||||
role: identity.customRole ?? findOrgMembershipRole(roles, identity.role),
|
||||
hasDeleteProtection: identity.hasDeleteProtection,
|
||||
metadata: identity.metadata
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole)
|
||||
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole),
|
||||
hasDeleteProtection: false
|
||||
});
|
||||
}
|
||||
}, [popUp?.identity?.data, roles]);
|
||||
|
||||
const onFormSubmit = async ({ name, role, metadata }: FormData) => {
|
||||
const onFormSubmit = async ({ name, role, metadata, hasDeleteProtection }: FormData) => {
|
||||
try {
|
||||
const identity = popUp?.identity?.data as {
|
||||
identityId: string;
|
||||
name: string;
|
||||
role: string;
|
||||
hasDeleteProtection: boolean;
|
||||
};
|
||||
|
||||
if (identity) {
|
||||
@@ -116,6 +123,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
identityId: identity.identityId,
|
||||
name,
|
||||
role: role.slug || undefined,
|
||||
hasDeleteProtection,
|
||||
organizationId: orgId,
|
||||
metadata
|
||||
});
|
||||
@@ -127,6 +135,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { id: createdId } = await createMutateAsync({
|
||||
name,
|
||||
role: role.slug || undefined,
|
||||
hasDeleteProtection,
|
||||
organizationId: orgId,
|
||||
metadata
|
||||
});
|
||||
@@ -215,6 +224,24 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="hasDeleteProtection"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)}>
|
||||
<Switch
|
||||
className="ml-0 mr-2 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
containerClassName="flex-row-reverse w-fit"
|
||||
id="delete-protection-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>Delete Protection {value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<FormLabel label="Metadata" />
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -329,7 +329,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuContent align="start" className="mt-3 p-1">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
|
||||
+5
@@ -18,6 +18,7 @@ import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
|
||||
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
import { FlyioConnectionForm } from "./FlyioConnectionForm";
|
||||
import { GcpConnectionForm } from "./GcpConnectionForm";
|
||||
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||
import { GitHubRadarConnectionForm } from "./GitHubRadarConnectionForm";
|
||||
@@ -122,6 +123,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
return <OnePassConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Render:
|
||||
return <RenderConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Flyio:
|
||||
return <FlyioConnectionForm onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${app}`);
|
||||
}
|
||||
@@ -208,6 +211,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <OnePassConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Render:
|
||||
return <RenderConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Flyio:
|
||||
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
|
||||
}
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { FlyioConnectionMethod, TFlyioConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TFlyioConnection;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Flyio)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(FlyioConnectionMethod.AccessToken),
|
||||
credentials: z.object({
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.startsWith("FlyV1", "Token must start with 'FlyV1'")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const FlyioConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Flyio,
|
||||
method: FlyioConnectionMethod.AccessToken
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Flyio].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(FlyioConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.accessToken"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Access Token"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to Fly.io"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+15
-2
@@ -47,13 +47,19 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
|
||||
rightIcon={
|
||||
<FontAwesomeIcon
|
||||
className="ml-1 transition-transform duration-200 group-data-[state=open]:rotate-180"
|
||||
icon={faChevronDown}
|
||||
/>
|
||||
}
|
||||
colorSchema="secondary"
|
||||
className="group select-none"
|
||||
>
|
||||
Options
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[120px]" align="end">
|
||||
<DropdownMenuContent className="mt-3 min-w-[120px]" align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
@@ -68,6 +74,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
handlePopUpOpen("identity", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
hasDeleteProtection: data.identity.hasDeleteProtection,
|
||||
role: data.role,
|
||||
customRole: data.customRole
|
||||
});
|
||||
@@ -131,6 +138,12 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Delete Protection</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.identity.hasDeleteProtection ? "On" : "Off"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.role}</p>
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props
|
||||
Options
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[120px]" align="end">
|
||||
<DropdownMenuContent className="mt-3 min-w-[120px]" align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ export const GroupsSection = () => {
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddGroupModal()}
|
||||
|
||||
+43
-29
@@ -2,10 +2,11 @@ import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash,
|
||||
faUsers
|
||||
faUsers,
|
||||
faUsersSlash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -13,6 +14,10 @@ import { format } from "date-fns";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
@@ -184,33 +189,42 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content="Remove">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteGroup", {
|
||||
id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faUsersSlash} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteGroup", {
|
||||
id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Group From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
+57
-42
@@ -3,12 +3,13 @@ import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faCircleXmark,
|
||||
faClock,
|
||||
faEllipsisV,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faServer,
|
||||
faXmark
|
||||
faServer
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -21,6 +22,10 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
@@ -163,20 +168,21 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<div className="flex w-full justify-end pr-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
>
|
||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||
Documentation{" "}
|
||||
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.06rem] ml-1 text-xs"
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
@@ -185,7 +191,7 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("identity")}
|
||||
@@ -231,7 +237,7 @@ export const IdentityTab = withProjectPermission(
|
||||
</Th>
|
||||
<Th className="w-1/3">Role</Th>
|
||||
<Th>Added on</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
@@ -372,37 +378,46 @@ export const IdentityTab = withProjectPermission(
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td className="flex justify-end space-x-2 opacity-0 duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton ariaLabel="more-icon" variant="plain">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
<Td className="flex justify-end space-x-2">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCircleXmark} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Identity From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
+2
-2
@@ -56,12 +56,12 @@ export const MembersSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addMember")}
|
||||
|
||||
+32
-24
@@ -9,8 +9,8 @@ import {
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash,
|
||||
faUsers
|
||||
faUsers,
|
||||
faUserXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -416,32 +416,40 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== u?.id && (
|
||||
<div className="flex items-center space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("removeMember", { username: u.username });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton ariaLabel="more-icon" variant="plain">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faUserXmark} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("removeMember", { username: u.username });
|
||||
}}
|
||||
>
|
||||
Remove User From Project
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
+3
-3
@@ -10,10 +10,10 @@ export const ProjectRoleListTab = withProjectPermission(
|
||||
return (
|
||||
<motion.div
|
||||
key="role-list"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: -30 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ProjectRoleList />
|
||||
</motion.div>
|
||||
|
||||
+227
-68
@@ -1,4 +1,16 @@
|
||||
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faEye,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -12,6 +24,10 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -19,15 +35,27 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
|
||||
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
|
||||
|
||||
enum RolesOrderBy {
|
||||
Name = "name",
|
||||
Slug = "slug"
|
||||
}
|
||||
|
||||
export const ProjectRoleList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
@@ -57,6 +85,75 @@ export const ProjectRoleList = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy,
|
||||
search,
|
||||
setSearch,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
setPage,
|
||||
offset
|
||||
} = usePagination<RolesOrderBy>(RolesOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("projectRolesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("projectRolesTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredRoles = useMemo(
|
||||
() =>
|
||||
roles
|
||||
?.filter((role) => {
|
||||
const { slug, name } = role;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
return (
|
||||
name.toLowerCase().includes(searchValue) || slug.toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [roleOne, roleTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case RolesOrderBy.Slug:
|
||||
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
|
||||
case RolesOrderBy.Name:
|
||||
default:
|
||||
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
|
||||
}
|
||||
}) ?? [],
|
||||
[roles, orderDirection, search, orderBy]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredRoles.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: RolesOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: RolesOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: RolesOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
@@ -64,7 +161,7 @@ export const ProjectRoleList = () => {
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("role")}
|
||||
@@ -75,18 +172,50 @@ export const ProjectRoleList = () => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search project roles..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Slug
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Slug)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Slug)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Slug)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th aria-label="actions" className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||
{roles?.map((role) => {
|
||||
{isRolesLoading && <TableSkeleton columns={3} innerKey="project-roles" />}
|
||||
{filteredRoles?.slice(offset, perPage * page).map((role) => {
|
||||
const { id, name, slug } = role;
|
||||
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
|
||||
slug as ProjectMembershipRole
|
||||
@@ -109,88 +238,118 @@ export const ProjectRoleList = () => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
roleSlug: slug
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("duplicateRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Duplicate Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isNonMutatable && (
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isNonMutatable ? faEye : faEdit} />}
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", role);
|
||||
navigate({
|
||||
to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
roleSlug: slug
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Role
|
||||
{`${isNonMutatable ? "View" : "Edit"} Role`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faCopy} />}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("duplicateRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Duplicate Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isNonMutatable && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50",
|
||||
"transition-colors duration-100"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", role);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Role
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredRoles?.length) && (
|
||||
<Pagination
|
||||
count={filteredRoles!.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!filteredRoles?.length && !isRolesLoading && (
|
||||
<EmptyState
|
||||
title={
|
||||
roles?.length
|
||||
? "No project roles match search..."
|
||||
: "This project does not have any roles"
|
||||
}
|
||||
icon={roles?.length ? faSearch : undefined}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
|
||||
+188
-192
@@ -67,13 +67,12 @@ type Props = {
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createAPIToken"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const ServiceTokenForm = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
@@ -152,13 +151,197 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
return !hasServiceToken ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={t("section.token.add-dialog.name")}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Type your token name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{tokenScopes.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.environment`}
|
||||
defaultValue={currentWorkspace?.environments?.[0]?.slug}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
label={index === 0 ? "Environment" : undefined}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{currentWorkspace?.environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.secretPath`}
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Secrets Path" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<IconButton
|
||||
className="p-3"
|
||||
ariaLabel="remove"
|
||||
colorSchema="danger"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} size="sm" />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
append({
|
||||
environment: currentWorkspace?.environments?.[0]?.slug || "",
|
||||
secretPath: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Scope
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Expiration" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{apiTokenExpiry.map(({ label, value }) => (
|
||||
<SelectItem value={String(value)} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="permissions"
|
||||
defaultValue={{
|
||||
read: true,
|
||||
readValue: false,
|
||||
write: false
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
const options = [
|
||||
{
|
||||
label: "Read (default)",
|
||||
value: "read"
|
||||
},
|
||||
{
|
||||
label: "Write (optional)",
|
||||
value: "write"
|
||||
}
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<FormControl label="Permissions" errorText={error?.message} isError={Boolean(error)}>
|
||||
<>
|
||||
{options.map(({ label, value: optionValue }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
id={String(value[optionValue])}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
isDisabled={optionValue === "read"}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
...value,
|
||||
[optionValue]: state
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="mb-3 mr-2 mt-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{newToken}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{t("common.click-to-copy")}
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.createAPIToken?.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
handlePopUpToggle("createAPIToken", open);
|
||||
reset();
|
||||
setToken("");
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
@@ -169,194 +352,7 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
subTitle={t("section.token.add-dialog.description") as string}
|
||||
>
|
||||
{!hasServiceToken ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={t("section.token.add-dialog.name")}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Type your token name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{tokenScopes.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.environment`}
|
||||
defaultValue={currentWorkspace?.environments?.[0]?.slug}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0"
|
||||
label={index === 0 ? "Environment" : undefined}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{currentWorkspace?.environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`scopes.${index}.secretPath`}
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Secrets Path" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<IconButton
|
||||
className="p-3"
|
||||
ariaLabel="remove"
|
||||
colorSchema="danger"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} size="sm" />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
append({
|
||||
environment: currentWorkspace?.environments?.[0]?.slug || "",
|
||||
secretPath: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Scope
|
||||
</Button>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Expiration" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{apiTokenExpiry.map(({ label, value }) => (
|
||||
<SelectItem value={String(value)} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="permissions"
|
||||
defaultValue={{
|
||||
read: true,
|
||||
readValue: false,
|
||||
write: false
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
const options = [
|
||||
{
|
||||
label: "Read (default)",
|
||||
value: "read"
|
||||
},
|
||||
{
|
||||
label: "Write (optional)",
|
||||
value: "write"
|
||||
}
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
label="Permissions"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<>
|
||||
{options.map(({ label, value: optionValue }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
id={String(value[optionValue])}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
isDisabled={optionValue === "read"}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
...value,
|
||||
[optionValue]: state
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="mb-3 mr-2 mt-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{newToken}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{t("common.click-to-copy")}
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
<ServiceTokenForm />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
+25
-5
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -48,8 +48,29 @@ export const ServiceTokenSection = withProjectPermission(
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-2 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Service Tokens</p>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Service Tokens</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/token"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
{t("section.token.service-tokens-description")}
|
||||
</p>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
@@ -63,12 +84,11 @@ export const ServiceTokenSection = withProjectPermission(
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create token
|
||||
Create Token
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
|
||||
<ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddServiceTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
|
||||
+243
-65
@@ -1,10 +1,28 @@
|
||||
import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEllipsisV,
|
||||
faFolder,
|
||||
faKey,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -12,10 +30,18 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useGetUserWsServiceTokens } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -31,78 +57,230 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
enum TokensOrderBy {
|
||||
Name = "name",
|
||||
Expiration = "expiration"
|
||||
}
|
||||
|
||||
export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isPending } = useGetUserWsServiceTokens({
|
||||
workspaceID: currentWorkspace?.id || ""
|
||||
});
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<TokensOrderBy>(TokensOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("projectServiceTokens", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("projectServiceTokens", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const filteredTokens = useMemo(
|
||||
() =>
|
||||
data
|
||||
?.filter((token) => {
|
||||
const { name, scopes } = token;
|
||||
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
|
||||
if (name.toLowerCase().includes(searchValue)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return scopes.some(
|
||||
({ environment, secretPath }) =>
|
||||
environment.toLowerCase().includes(searchValue) ||
|
||||
secretPath.toLowerCase().includes(searchValue)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [tokenOne, tokenTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case TokensOrderBy.Expiration:
|
||||
if (!tokenOne.expiresAt && !tokenTwo.expiresAt) return 0;
|
||||
if (!tokenOne.expiresAt) return 1;
|
||||
if (!tokenTwo.expiresAt) return -1;
|
||||
|
||||
return (
|
||||
new Date(tokenOne.expiresAt).getTime() - new Date(tokenTwo.expiresAt).getTime()
|
||||
);
|
||||
|
||||
case TokensOrderBy.Name:
|
||||
default:
|
||||
return tokenOne.name.toLowerCase().localeCompare(tokenTwo.name.toLowerCase());
|
||||
}
|
||||
}) ?? [],
|
||||
[data, orderDirection, search, orderBy]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredTokens.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: TokensOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: TokensOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: TokensOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Token Name</Th>
|
||||
<Th>Environment - Secret Path</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th aria-label="button" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="project-service-tokens" />}
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((row) => (
|
||||
<Tr key={row.id}>
|
||||
<Td>{row.name}</Td>
|
||||
<Td>
|
||||
<div className="mb-2 flex flex-col flex-wrap space-y-1">
|
||||
{row?.scopes.map(({ secretPath, environment }) => (
|
||||
<div
|
||||
key={`${row.id}-${environment}-${secretPath}`}
|
||||
className="inline-flex items-center space-x-1 rounded-md border border-mineshaft-600 p-1 px-2"
|
||||
>
|
||||
<div className="mr-2 border-r border-mineshaft-600 pr-2">{environment}</div>
|
||||
<FontAwesomeIcon icon={faFolder} size="sm" />
|
||||
<span className="pl-2">{secretPath}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
||||
<Td>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||
name: row.name,
|
||||
id: row.id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{!isPending && data && data?.length === 0 && (
|
||||
<div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search service tokens by name, environment or secret path..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4 mt-2"
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||
<EmptyState title="No service tokens found" icon={faKey} />
|
||||
</Td>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(TokensOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(TokensOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(TokensOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Environment / Secret Path</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Valid Until
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(TokensOrderBy.Expiration)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(TokensOrderBy.Expiration)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(TokensOrderBy.Expiration)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" aria-label="button" />
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="project-service-tokens" />}
|
||||
{!isPending &&
|
||||
filteredTokens.slice(offset, perPage * page).map((row) => (
|
||||
<Tr key={row.id}>
|
||||
<Td>{row.name}</Td>
|
||||
<Td>
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
{row?.scopes.map(({ secretPath, environment }) => (
|
||||
<div
|
||||
key={`${row.id}-${environment}-${secretPath}`}
|
||||
className="inline-flex items-center space-x-1 rounded-md border border-mineshaft-500 p-1 px-2"
|
||||
>
|
||||
<div className="mr-1 border-r border-mineshaft-500 pr-2">
|
||||
{environment}
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faFolder} size="sm" className="text-yellow" />
|
||||
<span className="pl-1">{secretPath}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{row.expiresAt ? (
|
||||
format(row.expiresAt, "MM/dd/yyyy h:mm:ss aa")
|
||||
) : (
|
||||
<span className="text-mineshaft-400">N/A</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||
name: row.name,
|
||||
id: row.id
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete Token
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredTokens.length) && (
|
||||
<Pagination
|
||||
count={filteredTokens.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !filteredTokens?.length && (
|
||||
<EmptyState
|
||||
title={data?.length ? "No service tokens match search..." : "No service tokens found"}
|
||||
icon={data?.length ? faSearch : faKey}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+43
-29
@@ -291,6 +291,13 @@ export const projectRoleFormSchema = z.object({
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.extend({
|
||||
inverted: z.boolean().optional(),
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
|
||||
[ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]),
|
||||
@@ -342,7 +349,6 @@ export const projectRoleFormSchema = z.object({
|
||||
.default([]),
|
||||
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretScanningDataSources]:
|
||||
SecretScanningDataSourcePolicyActionSchema.array().default([]),
|
||||
@@ -366,7 +372,8 @@ type TConditionalFields =
|
||||
| ProjectPermissionSub.CertificateTemplates
|
||||
| ProjectPermissionSub.SshHosts
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| ProjectPermissionSub.Identity;
|
||||
| ProjectPermissionSub.Identity
|
||||
| ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
export const isConditionalSubjects = (
|
||||
subject: ProjectPermissionSub
|
||||
@@ -379,7 +386,8 @@ export const isConditionalSubjects = (
|
||||
subject === ProjectPermissionSub.SshHosts ||
|
||||
subject === ProjectPermissionSub.SecretRotation ||
|
||||
subject === ProjectPermissionSub.PkiSubscribers ||
|
||||
subject === ProjectPermissionSub.CertificateTemplates;
|
||||
subject === ProjectPermissionSub.CertificateTemplates ||
|
||||
subject === ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
||||
@@ -484,7 +492,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshHostGroups
|
||||
ProjectPermissionSub.SshHostGroups,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
].includes(subject)
|
||||
) {
|
||||
// from above statement we are sure it won't be undefined
|
||||
@@ -515,6 +524,36 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretSyncs) {
|
||||
const canRead = action.includes(ProjectPermissionSecretSyncActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionSecretSyncActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionSecretSyncActions.Create);
|
||||
const canSyncSecrets = action.includes(ProjectPermissionSecretSyncActions.SyncSecrets);
|
||||
const canImportSecrets = action.includes(
|
||||
ProjectPermissionSecretSyncActions.ImportSecrets
|
||||
);
|
||||
const canRemoveSecrets = action.includes(
|
||||
ProjectPermissionSecretSyncActions.RemoveSecrets
|
||||
);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{ conditions: [], inverted: false }];
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
formVal[subject]!.push({
|
||||
[ProjectPermissionSecretSyncActions.Read]: canRead,
|
||||
[ProjectPermissionSecretSyncActions.Create]: canCreate,
|
||||
[ProjectPermissionSecretSyncActions.Edit]: canEdit,
|
||||
[ProjectPermissionSecretSyncActions.Delete]: canDelete,
|
||||
[ProjectPermissionSecretSyncActions.SyncSecrets]: canSyncSecrets,
|
||||
[ProjectPermissionSecretSyncActions.ImportSecrets]: canImportSecrets,
|
||||
[ProjectPermissionSecretSyncActions.RemoveSecrets]: canRemoveSecrets,
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.DynamicSecrets) {
|
||||
const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential);
|
||||
const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential);
|
||||
@@ -777,31 +816,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretSyncs) {
|
||||
const canRead = action.includes(ProjectPermissionSecretSyncActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit);
|
||||
const canDelete = action.includes(ProjectPermissionSecretSyncActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionSecretSyncActions.Create);
|
||||
const canSyncSecrets = action.includes(ProjectPermissionSecretSyncActions.SyncSecrets);
|
||||
const canImportSecrets = action.includes(ProjectPermissionSecretSyncActions.ImportSecrets);
|
||||
const canRemoveSecrets = action.includes(ProjectPermissionSecretSyncActions.RemoveSecrets);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
|
||||
// from above statement we are sure it won't be undefined
|
||||
if (canRead) formVal[subject]![0][ProjectPermissionSecretSyncActions.Read] = true;
|
||||
if (canEdit) formVal[subject]![0][ProjectPermissionSecretSyncActions.Edit] = true;
|
||||
if (canCreate) formVal[subject]![0][ProjectPermissionSecretSyncActions.Create] = true;
|
||||
if (canDelete) formVal[subject]![0][ProjectPermissionSecretSyncActions.Delete] = true;
|
||||
if (canSyncSecrets)
|
||||
formVal[subject]![0][ProjectPermissionSecretSyncActions.SyncSecrets] = true;
|
||||
if (canImportSecrets)
|
||||
formVal[subject]![0][ProjectPermissionSecretSyncActions.ImportSecrets] = true;
|
||||
if (canRemoveSecrets)
|
||||
formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretScanningDataSources) {
|
||||
const canRead = action.includes(ProjectPermissionSecretScanningDataSourceActions.Read);
|
||||
const canEdit = action.includes(ProjectPermissionSecretScanningDataSourceActions.Edit);
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
import { SecretPermissionConditions } from "./SecretPermissionConditions";
|
||||
import { SecretSyncPermissionConditions } from "./SecretSyncPermissionConditions";
|
||||
import { SshHostPermissionConditions } from "./SshHostPermissionConditions";
|
||||
|
||||
type Props = {
|
||||
@@ -69,6 +70,10 @@ export const renderConditionalComponents = (
|
||||
return <PkiTemplatePermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.SecretSyncs) {
|
||||
return <SecretSyncPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
||||
}
|
||||
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
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 {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
getConditionOperatorHelperInfo,
|
||||
renderOperatorSelectItems
|
||||
} from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions`
|
||||
});
|
||||
|
||||
const conditionErrorMessage =
|
||||
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.message ||
|
||||
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.root?.message;
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||
</p>
|
||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{items.fields.map((el, index) => {
|
||||
const condition = watch(
|
||||
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}`
|
||||
) as {
|
||||
lhs: string;
|
||||
rhs: string;
|
||||
operator: string;
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<div className="w-1/4">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.lhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
setValue(
|
||||
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`,
|
||||
PermissionConditionOperators.$IN as never
|
||||
);
|
||||
field.onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="environment">Environment Slug</SelectItem>
|
||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{renderOperatorSelectItems(condition.lhs)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.rhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
ariaLabel="remove"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{conditionErrorMessage && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{conditionErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "environment",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { TFlyioSync } from "@app/hooks/api/secretSyncs/types/flyio-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: TFlyioSync;
|
||||
};
|
||||
|
||||
export const FlyioSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
+3
@@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationCol } from "./AzureDevOpsSyncDestinationCol";
|
||||
import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncCol";
|
||||
import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol";
|
||||
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
|
||||
import { FlyioSyncDestinationCol } from "./FlyioSyncDestinationCol";
|
||||
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
|
||||
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
|
||||
import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
|
||||
@@ -61,6 +62,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
return <AzureDevOpsSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Render:
|
||||
return <RenderSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Flyio:
|
||||
return <FlyioSyncDestinationCol secretSync={secretSync} />;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||
|
||||
+14
-5
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faBan,
|
||||
faCalendarCheck,
|
||||
@@ -117,6 +118,14 @@ export const SecretSyncRow = ({
|
||||
|
||||
const destinationDetails = SECRET_SYNC_MAP[destination];
|
||||
|
||||
const permissionSubject =
|
||||
environment && folder
|
||||
? subject(ProjectPermissionSub.SecretSyncs, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})
|
||||
: ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
onClick={() =>
|
||||
@@ -264,7 +273,7 @@ export const SecretSyncRow = ({
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.SyncSecrets}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
@@ -295,7 +304,7 @@ export const SecretSyncRow = ({
|
||||
{syncOption?.canImportSecrets && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.ImportSecrets}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
@@ -326,7 +335,7 @@ export const SecretSyncRow = ({
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.RemoveSecrets}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
@@ -356,7 +365,7 @@ export const SecretSyncRow = ({
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
@@ -373,7 +382,7 @@ export const SecretSyncRow = ({
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Delete}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
|
||||
+4
@@ -120,6 +120,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
primaryText = destinationConfig.serviceName ?? destinationConfig.serviceId;
|
||||
secondaryText = "Service";
|
||||
break;
|
||||
case SecretSync.Flyio:
|
||||
primaryText = destinationConfig.appId;
|
||||
secondaryText = "App ID";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/Overv
|
||||
|
||||
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
|
||||
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
|
||||
import {
|
||||
HIDDEN_SECRET_VALUE,
|
||||
HIDDEN_SECRET_VALUE_API_MASK
|
||||
} from "../SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
|
||||
@@ -509,15 +513,25 @@ export const OverviewPage = () => {
|
||||
env: string,
|
||||
key: string,
|
||||
value: string,
|
||||
secretValueHidden: boolean,
|
||||
type = SecretType.Shared
|
||||
) => {
|
||||
let secretValue: string | undefined = value;
|
||||
|
||||
if (
|
||||
secretValueHidden &&
|
||||
(value === HIDDEN_SECRET_VALUE_API_MASK || value === HIDDEN_SECRET_VALUE)
|
||||
) {
|
||||
secretValue = undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await updateSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretKey: key,
|
||||
secretValue: value,
|
||||
secretValue,
|
||||
type
|
||||
});
|
||||
|
||||
|
||||
+4
-1
@@ -50,6 +50,7 @@ type Props = {
|
||||
env: string,
|
||||
key: string,
|
||||
value: string,
|
||||
secretValueHidden: boolean,
|
||||
type?: SecretType,
|
||||
secretId?: string
|
||||
) => Promise<void>;
|
||||
@@ -147,6 +148,7 @@ export const SecretEditRow = ({
|
||||
environment,
|
||||
secretName,
|
||||
value,
|
||||
secretValueHidden,
|
||||
isOverride ? SecretType.Personal : SecretType.Shared,
|
||||
secretId
|
||||
);
|
||||
@@ -166,6 +168,7 @@ export const SecretEditRow = ({
|
||||
environment,
|
||||
secretName,
|
||||
secretValue,
|
||||
secretValueHidden,
|
||||
isOverride ? SecretType.Personal : SecretType.Shared,
|
||||
secretId
|
||||
);
|
||||
@@ -257,7 +260,7 @@ export const SecretEditRow = ({
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip content="save">
|
||||
<Tooltip content="Save">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="submit-value"
|
||||
|
||||
+3
-1
@@ -24,6 +24,7 @@ import { useToggle } from "@app/hooks";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { getExpandedRowStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
|
||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||
|
||||
import { SecretEditRow } from "./SecretEditRow";
|
||||
import SecretRenameRow from "./SecretRenameRow";
|
||||
@@ -40,6 +41,7 @@ type Props = {
|
||||
env: string,
|
||||
key: string,
|
||||
value: string,
|
||||
secretValueHidden: boolean,
|
||||
type?: SecretType,
|
||||
secretId?: string
|
||||
) => Promise<void>;
|
||||
@@ -96,7 +98,7 @@ export const SecretOverviewTableRow = ({
|
||||
);
|
||||
|
||||
if (secret?.secretValueHidden && !secret?.valueOverride) {
|
||||
return canEditSecretValue ? "******" : "";
|
||||
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
|
||||
}
|
||||
return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || "";
|
||||
};
|
||||
|
||||
+61
-20
@@ -11,6 +11,7 @@ import {
|
||||
faKey
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { SecretInput, Tag, Tooltip } from "@app/components/v2";
|
||||
import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types";
|
||||
@@ -105,21 +106,41 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</span>
|
||||
) : (
|
||||
<div className="relative">
|
||||
{secretVersion?.secretValueHidden && (
|
||||
<div className="absolute left-1 top-1/2 z-50 -translate-y-1/2">
|
||||
<Tooltip
|
||||
position="right"
|
||||
content="You do not have access to view the old secret value."
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="pl-2 text-mineshaft-300"
|
||||
size="sm"
|
||||
icon={faEyeSlash}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<SecretInput
|
||||
isReadOnly
|
||||
isVisible={isOldSecretValueVisible}
|
||||
valueAlwaysHidden={secretVersion?.secretValueHidden}
|
||||
value={secretVersion?.secretValue}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-700 px-2 py-1.5"
|
||||
containerClassName={twMerge(
|
||||
"border border-mineshaft-600 bg-bunker-700 py-1.5 text-bunker-300 hover:border-primary-400/50",
|
||||
secretVersion?.secretValueHidden ? "pl-8 pr-2" : "px-2"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-1 top-1"
|
||||
onClick={() => setIsOldSecretValueVisible(!isOldSecretValueVisible)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={isOldSecretValueVisible ? faEyeSlash : faEye}
|
||||
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
||||
/>
|
||||
</div>
|
||||
{!secretVersion?.secretValueHidden && (
|
||||
<div
|
||||
className="absolute right-1 top-1"
|
||||
onClick={() => setIsOldSecretValueVisible(!isOldSecretValueVisible)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={isOldSecretValueVisible ? faEyeSlash : faEye}
|
||||
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -211,21 +232,41 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</span>
|
||||
) : (
|
||||
<div className="relative">
|
||||
{newVersion?.secretValueHidden && (
|
||||
<div className="absolute left-1 top-1/2 z-50 -translate-y-1/2">
|
||||
<Tooltip
|
||||
position="right"
|
||||
content="You do not have access to view the new secret value."
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="pl-2 text-mineshaft-300"
|
||||
size="sm"
|
||||
icon={faEyeSlash}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<SecretInput
|
||||
isReadOnly
|
||||
valueAlwaysHidden={newVersion?.secretValueHidden}
|
||||
isVisible={isNewSecretValueVisible}
|
||||
value={newVersion?.secretValue}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-700 px-2 py-1.5"
|
||||
containerClassName={twMerge(
|
||||
"border border-mineshaft-600 bg-bunker-700 py-1.5 text-bunker-300 hover:border-primary-400/50",
|
||||
newVersion?.secretValueHidden ? "pl-8 pr-2" : "px-2"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-1 top-1"
|
||||
onClick={() => setIsNewSecretValueVisible(!isNewSecretValueVisible)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={isNewSecretValueVisible ? faEyeSlash : faEye}
|
||||
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
||||
/>
|
||||
</div>
|
||||
{!newVersion?.secretValueHidden && (
|
||||
<div
|
||||
className="absolute right-1 top-1"
|
||||
onClick={() => setIsNewSecretValueVisible(!isNewSecretValueVisible)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={isNewSecretValueVisible ? faEyeSlash : faEye}
|
||||
className="cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-800 p-1.5 text-mineshaft-300 hover:bg-mineshaft-700"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+4
-1
@@ -62,6 +62,7 @@ import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { camelCaseToSpaces } from "@app/lib/fn/string";
|
||||
|
||||
import { CreateReminderForm } from "./CreateReminderForm";
|
||||
import { HIDDEN_SECRET_VALUE } from "./SecretItem";
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
@@ -897,7 +898,9 @@ export const SecretDetailSidebar = ({
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
{secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
|
||||
{secretValueHidden
|
||||
? HIDDEN_SECRET_VALUE
|
||||
: secretValue?.replace(/./g, "*")}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
|
||||
+1
@@ -59,6 +59,7 @@ import {
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
|
||||
export const HIDDEN_SECRET_VALUE = "******";
|
||||
export const HIDDEN_SECRET_VALUE_API_MASK = "<hidden-by-infisical>";
|
||||
|
||||
type Props = {
|
||||
secret: SecretV3RawSanitized;
|
||||
|
||||
+16
-4
@@ -20,7 +20,7 @@ import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/c
|
||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
import { SecretDetailSidebar } from "./SecretDetailSidebar";
|
||||
import { SecretItem } from "./SecretItem";
|
||||
import { HIDDEN_SECRET_VALUE, HIDDEN_SECRET_VALUE_API_MASK, SecretItem } from "./SecretItem";
|
||||
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
@@ -85,6 +85,7 @@ export const SecretListView = ({
|
||||
type: SecretType,
|
||||
key: string,
|
||||
{
|
||||
secretValueHidden,
|
||||
value,
|
||||
comment,
|
||||
reminderRepeatDays,
|
||||
@@ -97,6 +98,7 @@ export const SecretListView = ({
|
||||
secretMetadata,
|
||||
isRotatedSecret
|
||||
}: Partial<{
|
||||
secretValueHidden: boolean;
|
||||
value: string;
|
||||
comment: string;
|
||||
reminderRepeatDays: number | null;
|
||||
@@ -123,6 +125,15 @@ export const SecretListView = ({
|
||||
}
|
||||
|
||||
if (operation === "update") {
|
||||
let secretValue = value;
|
||||
|
||||
if (
|
||||
secretValueHidden &&
|
||||
(value === HIDDEN_SECRET_VALUE_API_MASK || value === HIDDEN_SECRET_VALUE)
|
||||
) {
|
||||
secretValue = undefined;
|
||||
}
|
||||
|
||||
await updateSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
@@ -130,7 +141,7 @@ export const SecretListView = ({
|
||||
secretKey: key,
|
||||
...(!isRotatedSecret && {
|
||||
newSecretName: newKey,
|
||||
secretValue: value || ""
|
||||
secretValue: secretValueHidden ? secretValue : secretValue || ""
|
||||
}),
|
||||
type,
|
||||
tagIds: tags,
|
||||
@@ -168,7 +179,7 @@ export const SecretListView = ({
|
||||
},
|
||||
cb?: () => void
|
||||
) => {
|
||||
const { key: oldKey } = orgSecret;
|
||||
const { key: oldKey, secretValueHidden } = orgSecret;
|
||||
const {
|
||||
key,
|
||||
value,
|
||||
@@ -246,7 +257,8 @@ export const SecretListView = ({
|
||||
newKey: hasKeyChanged ? key : undefined,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding,
|
||||
secretMetadata,
|
||||
isRotatedSecret: orgSecret.isRotatedSecret
|
||||
isRotatedSecret: orgSecret.isRotatedSecret,
|
||||
secretValueHidden
|
||||
});
|
||||
if (cb) cb();
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,4 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faBan,
|
||||
faCheck,
|
||||
@@ -63,7 +64,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
|
||||
const triggerSyncSecrets = useTriggerSecretSyncSyncSecrets();
|
||||
const updateSync = useUpdateSecretSync();
|
||||
|
||||
const { destination } = secretSync;
|
||||
const { destination, environment, folder } = secretSync;
|
||||
|
||||
const destinationName = SECRET_SYNC_MAP[destination].name;
|
||||
const { syncOption } = useSecretSyncOption(destination);
|
||||
@@ -128,6 +129,14 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const permissionSubject =
|
||||
environment && folder
|
||||
? subject(ProjectPermissionSub.SecretSyncs, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})
|
||||
: ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ml-auto mt-4 flex flex-wrap items-center justify-end gap-2">
|
||||
@@ -157,7 +166,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.SyncSecrets}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<Button
|
||||
@@ -194,7 +203,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
|
||||
{syncOption?.canImportSecrets && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.ImportSecrets}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
@@ -222,7 +231,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.RemoveSecrets}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
@@ -249,7 +258,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
@@ -267,7 +276,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Delete}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TFlyioSync } from "@app/hooks/api/secretSyncs/types/flyio-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TFlyioSync;
|
||||
};
|
||||
|
||||
export const FlyioSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const {
|
||||
destinationConfig: { appId }
|
||||
} = secretSync;
|
||||
|
||||
return <GenericFieldLabel label="App ID">{appId}</GenericFieldLabel>;
|
||||
};
|
||||
+15
-5
@@ -1,4 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -18,6 +19,7 @@ import { AzureDevOpsSyncDestinationSection } from "./AzureDevOpsSyncDestinationS
|
||||
import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection";
|
||||
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
|
||||
import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection";
|
||||
import { FlyioSyncDestinationSection } from "./FlyioSyncDestinationSection";
|
||||
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
|
||||
import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection";
|
||||
import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
|
||||
@@ -35,7 +37,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: Props) => {
|
||||
const { destination, connection } = secretSync;
|
||||
const { destination, connection, folder, environment } = secretSync;
|
||||
|
||||
const app = APP_CONNECTION_MAP[connection.app].name;
|
||||
|
||||
@@ -97,18 +99,26 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
case SecretSync.Render:
|
||||
DestinationComponents = <RenderSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.Flyio:
|
||||
DestinationComponents = <FlyioSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||
}
|
||||
|
||||
const permissionSubject =
|
||||
environment && folder
|
||||
? subject(ProjectPermissionSub.SecretSyncs, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})
|
||||
: ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Destination Configuration</h3>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
|
||||
+12
-5
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
@@ -16,7 +17,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) => {
|
||||
const { syncStatus, lastSyncMessage, lastSyncedAt, name, description } = secretSync;
|
||||
const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, environment, folder } =
|
||||
secretSync;
|
||||
|
||||
const failureMessage = useMemo(() => {
|
||||
if (syncStatus === SecretSyncStatus.Failed) {
|
||||
@@ -32,14 +34,19 @@ export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) =
|
||||
return null;
|
||||
}, [syncStatus, lastSyncMessage]);
|
||||
|
||||
const permissionSubject =
|
||||
environment && folder
|
||||
? subject(ProjectPermissionSub.SecretSyncs, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})
|
||||
: ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Details</h3>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
|
||||
+14
-5
@@ -1,4 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -21,7 +22,9 @@ type Props = {
|
||||
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
|
||||
const {
|
||||
destination,
|
||||
syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema }
|
||||
syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema },
|
||||
environment,
|
||||
folder
|
||||
} = secretSync;
|
||||
|
||||
let AdditionalSyncOptionsComponent: ReactNode;
|
||||
@@ -53,21 +56,27 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
case SecretSync.OCIVault:
|
||||
case SecretSync.OnePass:
|
||||
case SecretSync.Render:
|
||||
case SecretSync.Flyio:
|
||||
AdditionalSyncOptionsComponent = null;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
|
||||
const permissionSubject =
|
||||
environment && folder
|
||||
? subject(ProjectPermissionSub.SecretSyncs, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})
|
||||
: ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
|
||||
+10
-4
@@ -1,3 +1,4 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import { faEdit, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -16,6 +17,14 @@ type Props = {
|
||||
export const SecretSyncSourceSection = ({ secretSync, onEditSource }: Props) => {
|
||||
const { folder, environment } = secretSync;
|
||||
|
||||
const permissionSubject =
|
||||
environment && folder
|
||||
? subject(ProjectPermissionSub.SecretSyncs, {
|
||||
environment: environment.slug,
|
||||
secretPath: folder.path
|
||||
})
|
||||
: ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
@@ -35,10 +44,7 @@ export const SecretSyncSourceSection = ({ secretSync, onEditSource }: Props) =>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionSecretSyncActions.Edit}
|
||||
a={ProjectPermissionSub.SecretSyncs}
|
||||
>
|
||||
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
variant="plain"
|
||||
|
||||
Reference in New Issue
Block a user