feat(ui): made changes from feedback and force delete feature

This commit is contained in:
Akhil Mohan
2024-03-25 00:50:26 +05:30
parent 69297bc16e
commit b5d7699b8d
9 changed files with 237 additions and 146 deletions
@@ -73,6 +73,7 @@ export type TDeleteDynamicSecretDTO = {
path: string; path: string;
environment: string; environment: string;
slug: string; slug: string;
isForced?: boolean;
}; };
export type TDetailsDynamicSecretDTO = { export type TDetailsDynamicSecretDTO = {
@@ -44,4 +44,5 @@ export type TRevokeDynamicSecretLeaseDTO = {
projectSlug: string; projectSlug: string;
path: string; path: string;
environment: string; environment: string;
isForced?: boolean;
}; };
@@ -53,8 +53,8 @@ export const CreateDynamicSecretForm = ({
animate={{ opacity: 1, translateX: 0 }} animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }} exit={{ opacity: 0, translateX: -30 }}
> >
<div className="mb-4 text-center">Pick a service to connect</div> <div className="mb-4 ">Pick a service to connect</div>
<div className="flex items-center justify-center space-x-4"> <div className="flex items-center space-x-4">
<div <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" 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" role="button"
@@ -6,6 +6,10 @@ import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button, Button,
FormControl, FormControl,
Input, Input,
@@ -74,7 +78,16 @@ export const SqlDatabaseInputForm = ({
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit
} = useForm<TForm>({ } = 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 { createNotification } = useNotificationContext();
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
@@ -265,12 +278,17 @@ export const SqlDatabaseInputForm = ({
</FormControl> </FormControl>
)} )}
/> />
<Accordion
type="single"
collapsible
className="w-full bg-mineshaft-700"
>
<AccordionItem value="advance-statements">
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
<AccordionContent>
<Controller <Controller
control={control} control={control}
name="provider.creationStatement" 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 } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Creation Statement" label="Creation Statement"
@@ -290,9 +308,6 @@ export const SqlDatabaseInputForm = ({
<Controller <Controller
control={control} control={control}
name="provider.revocationStatement" 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 } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Revocation Statement" label="Revocation Statement"
@@ -312,7 +327,6 @@ export const SqlDatabaseInputForm = ({
<Controller <Controller
control={control} control={control}
name="provider.renewStatement" name="provider.renewStatement"
defaultValue={"ALTER ROLE \"{{username}}\" VALID UNTIL '{{expiration}}';"}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Renew Statement" label="Renew Statement"
@@ -329,6 +343,9 @@ export const SqlDatabaseInputForm = ({
</FormControl> </FormControl>
)} )}
/> />
</AccordionContent>
</AccordionItem>
</Accordion>
</div> </div>
</div> </div>
</div> </div>
@@ -118,13 +118,13 @@ export const CreateDynamicSecretLease = ({
}); });
createNotification({ createNotification({
type: "success", type: "success",
text: "Successfully deleted folder" text: "Successfully created dynamic secret"
}); });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
createNotification({ createNotification({
type: "error", type: "error",
text: "Failed to delete folder" text: "Failed to deleted dynamic secret"
}); });
} }
}; };
@@ -1,5 +1,11 @@
import { subject } from "@casl/ability"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistance } from "date-fns"; import { format, formatDistance } from "date-fns";
@@ -23,6 +29,7 @@ import {
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api"; import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
import { RenewDynamicSecretLease } from "./RenewDynamicSecretLease"; import { RenewDynamicSecretLease } from "./RenewDynamicSecretLease";
@@ -59,13 +66,17 @@ export const DynamicSecretLease = ({
const handleDynamicSecretDeleteLease = async () => { const handleDynamicSecretDeleteLease = async () => {
try { try {
const { leaseId } = popUp.deleteSecret.data as { leaseId: string }; const { leaseId, isForced } = popUp.deleteSecret.data as {
leaseId: string;
isForced?: boolean;
};
await deleteDynamicSecretLease.mutateAsync({ await deleteDynamicSecretLease.mutateAsync({
environment, environment,
projectSlug, projectSlug,
path: secretPath, path: secretPath,
slug, slug,
leaseId leaseId,
isForced
}); });
handlePopUpClose("deleteSecret"); handlePopUpClose("deleteSecret");
createNotification({ createNotification({
@@ -115,10 +126,7 @@ export const DynamicSecretLease = ({
{id} {id}
{Boolean(status) && ( {Boolean(status) && (
<Tooltip content={statusDetails || status || ""}> <Tooltip content={statusDetails || status || ""}>
<FontAwesomeIcon <FontAwesomeIcon className="ml-2 text-yellow-600" icon={faWarning} />
className="relative bottom-2 left-1 text-red-600"
icon={faInfoCircle}
/>
</Tooltip> </Tooltip>
)} )}
</Td> </Td>
@@ -169,6 +177,29 @@ export const DynamicSecretLease = ({
</IconButton> </IconButton>
)} )}
</ProjectPermissionCan> </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> </div>
</Td> </Td>
</Tr> </Tr>
@@ -2,8 +2,8 @@ import { subject } from "@casl/ability";
import { import {
faClose, faClose,
faFingerprint, faFingerprint,
faInfoCircle, faPencilSquare,
faPencilSquare faWarning
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -64,23 +64,26 @@ export const DynamicSecretListView = ({
const handleDynamicSecretDelete = async () => { const handleDynamicSecretDelete = async () => {
try { try {
const { slug } = popUp.deleteDynamicSecret.data as TDynamicSecret; const { slug, isForced } = popUp.deleteDynamicSecret.data as TDynamicSecret & {
isForced?: boolean;
};
await deleteDynamicSecret.mutateAsync({ await deleteDynamicSecret.mutateAsync({
environment, environment,
projectSlug, projectSlug,
path: secretPath, path: secretPath,
slug slug,
isForced
}); });
handlePopUpClose("deleteDynamicSecret"); handlePopUpClose("deleteDynamicSecret");
createNotification({ createNotification({
type: "success", type: "success",
text: "Successfully deleted secret" text: "Successfully deleted dynamic secret"
}); });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
createNotification({ createNotification({
type: "error", type: "error",
text: "Failed to delete secret" text: "Failed to delete dynamic secret"
}); });
} }
}; };
@@ -132,13 +135,17 @@ export const DynamicSecretListView = ({
{Boolean(secret.status) && ( {Boolean(secret.status) && (
<Tooltip content={secret?.statusDetails || secret.status || ""}> <Tooltip content={secret?.statusDetails || secret.status || ""}>
<FontAwesomeIcon <FontAwesomeIcon
className="relative bottom-2 text-red-600" className={
icon={faInfoCircle} secret.status === DynamicSecretStatus.Deleting
? "text-yellow-600"
: "text-red-600"
}
icon={faWarning}
/> />
</Tooltip> </Tooltip>
)} )}
</div> </div>
<div className="flex items-center px-4 py-2"> <div className="flex items-center space-x-2 px-4 py-2">
<Button <Button
size="xs" size="xs"
className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100" className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100"
@@ -148,8 +155,27 @@ export const DynamicSecretListView = ({
handlePopUpOpen("createDynamicSecretLease", secret); handlePopUpOpen("createDynamicSecretLease", secret);
}} }}
> >
lease Generate
</Button> </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>
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3"> <div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
<ProjectPermissionCan <ProjectPermissionCan
@@ -250,7 +276,11 @@ export const DynamicSecretListView = ({
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteDynamicSecret.isOpen} isOpen={popUp.deleteDynamicSecret.isOpen}
deleteKey={(popUp.deleteDynamicSecret?.data as TDynamicSecret)?.slug} 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)} onChange={(isOpen) => handlePopUpToggle("deleteDynamicSecret", isOpen)}
onDeleteApproved={handleDynamicSecretDelete} onDeleteApproved={handleDynamicSecretDelete}
/> />
@@ -6,6 +6,10 @@ import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button, Button,
FormControl, FormControl,
Input, Input,
@@ -280,6 +284,10 @@ export const EditDynamicSecretSqlProviderForm = ({
</FormControl> </FormControl>
)} )}
/> />
<Accordion type="multiple" className="w-full bg-mineshaft-700">
<AccordionItem value="modify-sql-statement">
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
<AccordionContent>
<Controller <Controller
control={control} control={control}
name="inputs.creationStatement" name="inputs.creationStatement"
@@ -344,6 +352,9 @@ export const EditDynamicSecretSqlProviderForm = ({
</FormControl> </FormControl>
)} )}
/> />
</AccordionContent>
</AccordionItem>
</Accordion>
</div> </div>
</div> </div>
</div> </div>