mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 16:26:01 +00:00
feat: added action button for notification toast and one action each for forbidden error and validation error details
This commit is contained in:
@@ -4,13 +4,15 @@ import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toas
|
|||||||
export type TNotification = {
|
export type TNotification = {
|
||||||
title?: string;
|
title?: string;
|
||||||
text: ReactNode;
|
text: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotificationContent = ({ title, text }: TNotification) => {
|
export const NotificationContent = ({ title, text, children }: TNotification) => {
|
||||||
return (
|
return (
|
||||||
<div className="msg-container">
|
<div className="msg-container">
|
||||||
{title && <div className="text-md mb-1 font-medium">{title}</div>}
|
{title && <div className="text-md mb-1 font-medium">{title}</div>}
|
||||||
<div className={title ? "text-sm" : "text-md"}>{text}</div>
|
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
|
||||||
|
{children && <div className="mt-2">{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -23,7 +25,13 @@ export const createNotification = (
|
|||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
...toastProps,
|
...toastProps,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
type: myProps?.type || "info",
|
type: myProps?.type || "info"
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NotificationContainer = () => <ToastContainer pauseOnHover toastClassName="border border-mineshaft-500" style={{ width: "400px" }} />;
|
export const NotificationContainer = () => (
|
||||||
|
<ToastContainer
|
||||||
|
pauseOnHover
|
||||||
|
toastClassName="border border-mineshaft-500"
|
||||||
|
style={{ width: "400px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ export enum PermissionConditionOperators {
|
|||||||
$GLOB = "$glob"
|
$GLOB = "$glob"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
|
||||||
|
[PermissionConditionOperators.$EQ]: "equal",
|
||||||
|
[PermissionConditionOperators.$IN]: "containing",
|
||||||
|
[PermissionConditionOperators.$ALL]: "contains all",
|
||||||
|
[PermissionConditionOperators.$NEQ]: "not equal",
|
||||||
|
[PermissionConditionOperators.$GLOB]: "glob matching",
|
||||||
|
[PermissionConditionOperators.$REGEX]: "regex"
|
||||||
|
};
|
||||||
|
|
||||||
export type TPermissionConditionOperators = {
|
export type TPermissionConditionOperators = {
|
||||||
[PermissionConditionOperators.$IN]: string[];
|
[PermissionConditionOperators.$IN]: string[];
|
||||||
[PermissionConditionOperators.$ALL]: string[];
|
[PermissionConditionOperators.$ALL]: string[];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PureAbility } from "@casl/ability";
|
||||||
import { ZodIssue } from "zod";
|
import { ZodIssue } from "zod";
|
||||||
|
|
||||||
export type { TAccessApprovalPolicy } from "./accessApproval/types";
|
export type { TAccessApprovalPolicy } from "./accessApproval/types";
|
||||||
@@ -52,9 +53,14 @@ export type TApiErrors =
|
|||||||
| {
|
| {
|
||||||
error: ApiErrorTypes.ValidationError;
|
error: ApiErrorTypes.ValidationError;
|
||||||
message: ZodIssue[];
|
message: ZodIssue[];
|
||||||
|
statusCode: 401;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
error: ApiErrorTypes.ForbiddenError;
|
||||||
|
message: string;
|
||||||
|
details: PureAbility["rules"];
|
||||||
statusCode: 403;
|
statusCode: 403;
|
||||||
}
|
}
|
||||||
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
|
|
||||||
| {
|
| {
|
||||||
statusCode: 400;
|
statusCode: 400;
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
+145
-21
@@ -3,6 +3,23 @@ import axios from "axios";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalTrigger,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "./components/v2";
|
||||||
|
import {
|
||||||
|
formatedConditionsOperatorNames,
|
||||||
|
PermissionConditionOperators
|
||||||
|
} from "./context/ProjectPermissionContext/types";
|
||||||
import { ApiErrorTypes, TApiErrors } from "./hooks/api/types";
|
import { ApiErrorTypes, TApiErrors } from "./hooks/api/types";
|
||||||
|
|
||||||
// this is saved in react-query cache
|
// this is saved in react-query cache
|
||||||
@@ -10,35 +27,142 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
|
|||||||
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
|
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
|
||||||
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
|
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
|
||||||
|
|
||||||
|
const camelCaseToSpaces = (input: string) => {
|
||||||
|
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
|
||||||
|
};
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
mutationCache: new MutationCache({
|
mutationCache: new MutationCache({
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const serverResponse = error.response?.data as TApiErrors;
|
const serverResponse = error.response?.data as TApiErrors;
|
||||||
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
||||||
createNotification({
|
createNotification(
|
||||||
title: "Validation Error",
|
{
|
||||||
type: "error",
|
title: "Validation Error",
|
||||||
text: (
|
type: "error",
|
||||||
<div>
|
text: "Please check the input and try again.",
|
||||||
{serverResponse.message?.map(({ message, path }) => (
|
children: (
|
||||||
<div className="flex space-y-2" key={path.join(".")}>
|
<Modal>
|
||||||
<div>
|
<ModalTrigger>
|
||||||
Field <i>{path.join(".")}</i> {message.toLowerCase()}
|
<Button variant="outline_bg" size="xs">
|
||||||
</div>
|
Show more
|
||||||
</div>
|
</Button>
|
||||||
))}
|
</ModalTrigger>
|
||||||
</div>
|
<ModalContent title="Validation Error Details">
|
||||||
)
|
<TableContainer>
|
||||||
});
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Field</Th>
|
||||||
|
<Th>Issue</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{serverResponse.message?.map(({ message, path }) => (
|
||||||
|
<Tr key={path.join(".")}>
|
||||||
|
<Td>{path.join(".")}</Td>
|
||||||
|
<Td>{message.toLowerCase()}</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ closeOnClick: false }
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (serverResponse.statusCode === 401) {
|
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
||||||
createNotification({
|
createNotification(
|
||||||
title: "Forbidden Access",
|
{
|
||||||
type: "error",
|
title: "Forbidden Access",
|
||||||
text: serverResponse.message
|
type: "error",
|
||||||
});
|
text: serverResponse.message,
|
||||||
|
children: serverResponse?.details?.length ? (
|
||||||
|
<Modal>
|
||||||
|
<ModalTrigger>
|
||||||
|
<Button variant="outline_bg" size="xs">
|
||||||
|
Show more
|
||||||
|
</Button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
title="Validation Rules"
|
||||||
|
subTitle="Please review the allowed rules below."
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{serverResponse.details?.map((el, index) => {
|
||||||
|
const hasConditions = Object.keys(el.conditions || {}).length;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`Forbidden-error-details-${index + 1}`}
|
||||||
|
className="rounded-md border border-gray-600 p-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{el.inverted ? "Cannot" : "Can"}{" "}
|
||||||
|
<span className="text-primary-600">{el.action.toString()}</span>{" "}
|
||||||
|
{el.subject.toString()} {hasConditions && "with conditions"}
|
||||||
|
</div>
|
||||||
|
{hasConditions && (
|
||||||
|
<div className="flex flex-col gap-1 pt-2 text-sm">
|
||||||
|
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
||||||
|
const operators = (
|
||||||
|
el.conditions as Record<
|
||||||
|
string,
|
||||||
|
| string
|
||||||
|
| { [K in PermissionConditionOperators]: string | string[] }
|
||||||
|
>
|
||||||
|
)[field];
|
||||||
|
|
||||||
|
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
||||||
|
if (typeof operators === "string") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`Forbidden-error-details-${index + 1}-${
|
||||||
|
fieldIndex + 1
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formattedFieldName} equal{" "}
|
||||||
|
<span className="text-primary-600">{operators}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(operators).map((operator, operatorIndex) => (
|
||||||
|
<div
|
||||||
|
key={`Forbidden-error-details-${index + 1}-${
|
||||||
|
fieldIndex + 1
|
||||||
|
}-${operatorIndex + 1}`}
|
||||||
|
>
|
||||||
|
<span className="capitalize">{formattedFieldName}</span>{" "}
|
||||||
|
{
|
||||||
|
formatedConditionsOperatorNames[
|
||||||
|
operator as PermissionConditionOperators
|
||||||
|
]
|
||||||
|
}{" "}
|
||||||
|
<span className="text-primary-600">
|
||||||
|
{operators[
|
||||||
|
operator as PermissionConditionOperators
|
||||||
|
].toString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
) : undefined
|
||||||
|
},
|
||||||
|
{ closeOnClick: false }
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
createNotification({ title: "Bad Request", type: "error", text: serverResponse.message });
|
createNotification({ title: "Bad Request", type: "error", text: serverResponse.message });
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ html {
|
|||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Toastify__toast-body {
|
||||||
|
@apply items-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Toastify__toast-icon {
|
||||||
|
@apply w-4 pt-1;
|
||||||
|
}
|
||||||
|
|
||||||
.rdp-day,
|
.rdp-day,
|
||||||
.rdp-nav_button {
|
.rdp-nav_button {
|
||||||
@apply rounded-md hover:text-mineshaft-500;
|
@apply rounded-md hover:text-mineshaft-500;
|
||||||
|
|||||||
Reference in New Issue
Block a user