mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(ui): made changes from feedback and force delete feature
This commit is contained in:
@@ -27,7 +27,7 @@ export const AccordionTrigger = forwardRef<
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
className={twMerge(
|
||||
"group flex h-11 flex-1 items-center justify-between py-2 px-4 outline-none hover:text-primary data-[state=open]:text-primary ",
|
||||
"group flex h-11 flex-1 items-center justify-between py-2 px-4 outline-none hover:text-primary data-[state=open]:text-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -73,6 +73,7 @@ export type TDeleteDynamicSecretDTO = {
|
||||
path: string;
|
||||
environment: string;
|
||||
slug: string;
|
||||
isForced?: boolean;
|
||||
};
|
||||
|
||||
export type TDetailsDynamicSecretDTO = {
|
||||
|
||||
@@ -44,4 +44,5 @@ export type TRevokeDynamicSecretLeaseDTO = {
|
||||
projectSlug: string;
|
||||
path: string;
|
||||
environment: string;
|
||||
isForced?: boolean;
|
||||
};
|
||||
|
||||
@@ -53,8 +53,8 @@ export const CreateDynamicSecretForm = ({
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<div className="mb-4 text-center">Pick a service to connect</div>
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<div className="mb-4 ">Pick a service to connect</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div
|
||||
className="flex h-32 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-700 p-6 transition-all hover:bg-bunker-600"
|
||||
role="button"
|
||||
|
||||
@@ -6,6 +6,10 @@ import { z } from "zod";
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
@@ -74,7 +78,16 @@ export const SqlDatabaseInputForm = ({
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema)
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: {
|
||||
creationStatement:
|
||||
"CREATE USER \"{{username}}\" WITH SUPERUSER ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{username}}\";",
|
||||
renewStatement: "ALTER ROLE \"{{username}}\" VALID UNTIL '{{expiration}}';",
|
||||
revocationStatement:
|
||||
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";\nDROP OWNED BY "{{username}}";\nDROP ROLE "{{username}}";'
|
||||
}
|
||||
}
|
||||
});
|
||||
const { createNotification } = useNotificationContext();
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
@@ -265,70 +278,74 @@ export const SqlDatabaseInputForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
defaultValue={
|
||||
"CREATE USER \"{{username}}\" WITH SUPERUSER ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{username}}\";"
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full bg-mineshaft-700"
|
||||
>
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.revocationStatement"
|
||||
defaultValue={
|
||||
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";\nDROP OWNED BY "{{username}}"; DROP ROLE "{{username}}";'
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.revocationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.renewStatement"
|
||||
defaultValue={"ALTER ROLE \"{{username}}\" VALID UNTIL '{{expiration}}';"}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.renewStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,13 +118,13 @@ export const CreateDynamicSecretLease = ({
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted folder"
|
||||
text: "Successfully created dynamic secret"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete folder"
|
||||
text: "Failed to deleted dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import { faClose, faFileContract, faInfoCircle, faRepeat } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faClose,
|
||||
faFileContract,
|
||||
faRepeat,
|
||||
faTrash,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
|
||||
@@ -23,6 +29,7 @@ import {
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
|
||||
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
|
||||
|
||||
import { RenewDynamicSecretLease } from "./RenewDynamicSecretLease";
|
||||
|
||||
@@ -59,13 +66,17 @@ export const DynamicSecretLease = ({
|
||||
|
||||
const handleDynamicSecretDeleteLease = async () => {
|
||||
try {
|
||||
const { leaseId } = popUp.deleteSecret.data as { leaseId: string };
|
||||
const { leaseId, isForced } = popUp.deleteSecret.data as {
|
||||
leaseId: string;
|
||||
isForced?: boolean;
|
||||
};
|
||||
await deleteDynamicSecretLease.mutateAsync({
|
||||
environment,
|
||||
projectSlug,
|
||||
path: secretPath,
|
||||
slug,
|
||||
leaseId
|
||||
leaseId,
|
||||
isForced
|
||||
});
|
||||
handlePopUpClose("deleteSecret");
|
||||
createNotification({
|
||||
@@ -115,10 +126,7 @@ export const DynamicSecretLease = ({
|
||||
{id}
|
||||
{Boolean(status) && (
|
||||
<Tooltip content={statusDetails || status || ""}>
|
||||
<FontAwesomeIcon
|
||||
className="relative bottom-2 left-1 text-red-600"
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
<FontAwesomeIcon className="ml-2 text-yellow-600" icon={faWarning} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
@@ -169,6 +177,29 @@ export const DynamicSecretLease = ({
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{status === DynamicSecretLeaseStatus.FailedDeletion && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Force Delete. This action will remove the secret from internal storage, but it will remain in external systems."
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-folder"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="p-0 text-red-600"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteSecret", { leaseId: id, isForced: true })
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { subject } from "@casl/ability";
|
||||
import {
|
||||
faClose,
|
||||
faFingerprint,
|
||||
faInfoCircle,
|
||||
faPencilSquare
|
||||
faPencilSquare,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -64,23 +64,26 @@ export const DynamicSecretListView = ({
|
||||
|
||||
const handleDynamicSecretDelete = async () => {
|
||||
try {
|
||||
const { slug } = popUp.deleteDynamicSecret.data as TDynamicSecret;
|
||||
const { slug, isForced } = popUp.deleteDynamicSecret.data as TDynamicSecret & {
|
||||
isForced?: boolean;
|
||||
};
|
||||
await deleteDynamicSecret.mutateAsync({
|
||||
environment,
|
||||
projectSlug,
|
||||
path: secretPath,
|
||||
slug
|
||||
slug,
|
||||
isForced
|
||||
});
|
||||
handlePopUpClose("deleteDynamicSecret");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted secret"
|
||||
text: "Successfully deleted dynamic secret"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete secret"
|
||||
text: "Failed to delete dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -132,13 +135,17 @@ export const DynamicSecretListView = ({
|
||||
{Boolean(secret.status) && (
|
||||
<Tooltip content={secret?.statusDetails || secret.status || ""}>
|
||||
<FontAwesomeIcon
|
||||
className="relative bottom-2 text-red-600"
|
||||
icon={faInfoCircle}
|
||||
className={
|
||||
secret.status === DynamicSecretStatus.Deleting
|
||||
? "text-yellow-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
icon={faWarning}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<div className="flex items-center space-x-2 px-4 py-2">
|
||||
<Button
|
||||
size="xs"
|
||||
className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100"
|
||||
@@ -148,8 +155,27 @@ export const DynamicSecretListView = ({
|
||||
handlePopUpOpen("createDynamicSecretLease", secret);
|
||||
}}
|
||||
>
|
||||
lease
|
||||
Generate
|
||||
</Button>
|
||||
{secret.status === DynamicSecretStatus.FailedDeletion && (
|
||||
<Tooltip content="This action will remove the secret from internal storage, but it will remain in external systems. Use this option only after you've confirmed that your external leases are handled.">
|
||||
<Button
|
||||
size="xs"
|
||||
className="m-0 py-0.5 px-2"
|
||||
colorSchema="danger"
|
||||
isDisabled={isRevocking}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("deleteDynamicSecret", {
|
||||
...secret,
|
||||
isForced: true
|
||||
});
|
||||
}}
|
||||
>
|
||||
Force Delete
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
@@ -250,7 +276,11 @@ export const DynamicSecretListView = ({
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteDynamicSecret.isOpen}
|
||||
deleteKey={(popUp.deleteDynamicSecret?.data as TDynamicSecret)?.slug}
|
||||
title="Do you want to delete this dynamc secret?"
|
||||
title={
|
||||
(popUp.deleteDynamicSecret?.data as { isForced?: boolean })?.isForced
|
||||
? "Do you want to force delete this dynamic secret?"
|
||||
: "Do you want to delete this dynamic secret?"
|
||||
}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteDynamicSecret", isOpen)}
|
||||
onDeleteApproved={handleDynamicSecretDelete}
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,10 @@ import { z } from "zod";
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
@@ -280,70 +284,77 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
defaultValue={
|
||||
"CREATE USER \"{{username}}\" WITH SUPERUSER ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{username}}\";"
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
<Accordion type="multiple" className="w-full bg-mineshaft-700">
|
||||
<AccordionItem value="modify-sql-statement">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
defaultValue={
|
||||
"CREATE USER \"{{username}}\" WITH SUPERUSER ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{username}}\";"
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.revocationStatement"
|
||||
defaultValue={
|
||||
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";\nDROP OWNED BY "{{username}}"; DROP ROLE "{{username}}";'
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.revocationStatement"
|
||||
defaultValue={
|
||||
'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";\nDROP OWNED BY "{{username}}"; DROP ROLE "{{username}}";'
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.renewStatement"
|
||||
defaultValue={"ALTER ROLE \"{{username}}\" VALID UNTIL '{{expiration}}';"}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.renewStatement"
|
||||
defaultValue={"ALTER ROLE \"{{username}}\" VALID UNTIL '{{expiration}}';"}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user