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

View File

@@ -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}

View File

@@ -73,6 +73,7 @@ export type TDeleteDynamicSecretDTO = {
path: string;
environment: string;
slug: string;
isForced?: boolean;
};
export type TDetailsDynamicSecretDTO = {

View File

@@ -44,4 +44,5 @@ export type TRevokeDynamicSecretLeaseDTO = {
projectSlug: string;
path: string;
environment: string;
isForced?: boolean;
};

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"
});
}
};

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>