mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 19:28:35 +00:00
Merge branch 'main' into gitlab-sso
This commit is contained in:
+132
-115
@@ -1,125 +1,142 @@
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useOrganization,useSubscription } from "@app/context";
|
||||
import {
|
||||
useCreateCustomerPortalSession,
|
||||
useGetOrgPlanBillingInfo,
|
||||
useGetOrgTrialUrl
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import {
|
||||
useCreateCustomerPortalSession,
|
||||
useGetOrgPlanBillingInfo,
|
||||
useGetOrgTrialUrl
|
||||
} from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { ManagePlansModal } from "./ManagePlansModal";
|
||||
|
||||
export const PreviewSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
|
||||
const getOrgTrialUrl = useGetOrgTrialUrl();
|
||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"managePlan"
|
||||
] as const);
|
||||
|
||||
const formatAmount = (amount: number) => {
|
||||
const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
});
|
||||
|
||||
return formattedTotal;
|
||||
}
|
||||
|
||||
const formatDate = (date: number) => {
|
||||
const createdDate = new Date(date * 1000);
|
||||
const day: number = createdDate.getDate();
|
||||
const month: number = createdDate.getMonth() + 1;
|
||||
const year: number = createdDate.getFullYear();
|
||||
const formattedDate: string = `${day}/${month}/${year}`;
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
|
||||
const getOrgTrialUrl = useGetOrgTrialUrl();
|
||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||
|
||||
function formatPlanSlug(slug: string) {
|
||||
return slug
|
||||
.replace(/(\b[a-z])/g, match => match.toUpperCase())
|
||||
.replace(/-/g, " ");
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["managePlan"] as const);
|
||||
|
||||
const formatAmount = (amount: number) => {
|
||||
const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
});
|
||||
|
||||
return formattedTotal;
|
||||
};
|
||||
|
||||
const formatDate = (date: number) => {
|
||||
const createdDate = new Date(date * 1000);
|
||||
const day: number = createdDate.getDate();
|
||||
const month: number = createdDate.getMonth() + 1;
|
||||
const year: number = createdDate.getFullYear();
|
||||
const formattedDate: string = `${day}/${month}/${year}`;
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
function formatPlanSlug(slug: string) {
|
||||
return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " ");
|
||||
}
|
||||
|
||||
const handleUpgradeBtnClick = async () => {
|
||||
try {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
if (!subscription.has_used_trial) {
|
||||
// direct user to start pro trial
|
||||
const url = await getOrgTrialUrl.mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// open compare plans modal
|
||||
handlePopUpOpen("managePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const handleUpgradeBtnClick = async () => {
|
||||
try {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
if (!subscription.has_used_trial) {
|
||||
// direct user to start pro trial
|
||||
const url = await getOrgTrialUrl.mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// open compare plans modal
|
||||
handlePopUpOpen("managePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{subscription && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
|
||||
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
|
||||
</div>
|
||||
<Button
|
||||
// onClick={() => handlePopUpOpen("managePlan")}
|
||||
onClick={() => handleUpgradeBtnClick()}
|
||||
color="mineshaft"
|
||||
>
|
||||
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && subscription && data && (
|
||||
<div className="flex mb-6">
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
|
||||
<p className="mb-2 text-gray-400">Current plan</p>
|
||||
<p className="text-2xl text-mineshaft-50 font-semibold mb-8">
|
||||
{`${formatPlanSlug(subscription.slug)} ${subscription.status === "trialing" ? "(Trial)" : ""}`}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!currentOrg?._id) return;
|
||||
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
|
||||
window.location.href = url;
|
||||
}}
|
||||
className="text-primary"
|
||||
>
|
||||
Manage plan →
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
|
||||
<p className="mb-2 text-gray-400">Price</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||
{subscription.status === "trialing" ? "$0.00 / month" : `${formatAmount(data.amount)} / ${data.interval}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
|
||||
<p className="mb-2 text-gray-400">Subscription renews on</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||
{formatDate(data.currentPeriodEnd)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ManagePlansModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{subscription &&
|
||||
subscription?.slug !== "enterprise" &&
|
||||
subscription?.slug !== "pro" &&
|
||||
subscription?.slug !== "pro-annual" && (
|
||||
<div className="p-4 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
|
||||
<p className="text-gray-400 mt-4">
|
||||
Unlimited members, projects, RBAC, smart alerts, and so much more
|
||||
</p>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handleUpgradeBtnClick()}
|
||||
color="mineshaft"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && subscription && data && (
|
||||
<div className="flex mb-6">
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
|
||||
<p className="mb-2 text-gray-400">Current plan</p>
|
||||
<p className="text-2xl text-mineshaft-50 font-semibold mb-8">
|
||||
{`${formatPlanSlug(subscription.slug)} ${
|
||||
subscription.status === "trialing" ? "(Trial)" : ""
|
||||
}`}
|
||||
</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!currentOrg?._id) return;
|
||||
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
|
||||
window.location.href = url;
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
className="text-primary"
|
||||
>
|
||||
Manage plan →
|
||||
</button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
|
||||
<p className="mb-2 text-gray-400">Price</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||
{subscription.status === "trialing"
|
||||
? "$0.00 / month"
|
||||
: `${formatAmount(data.amount)} / ${data.interval}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
|
||||
<p className="mb-2 text-gray-400">Subscription renews on</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||
{formatDate(data.currentPeriodEnd)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<ManagePlansModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+82
-88
@@ -1,98 +1,92 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useGetOrgBillingDetails,
|
||||
useUpdateOrgBillingDetails
|
||||
} from "@app/hooks/api";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
|
||||
|
||||
const schema = yup.object({
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Company name is required")
|
||||
}).required();
|
||||
})
|
||||
.required();
|
||||
|
||||
export const CompanyNameSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
name: data?.name ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({ name }: { name: string }) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (name === "") return;
|
||||
await mutateAsync({
|
||||
name,
|
||||
organizationId: currentOrg._id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated business name",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update business name",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||
Business name
|
||||
</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Acme Corp"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
name: data?.name ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({ name }: { name: string }) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (name === "") return;
|
||||
await mutateAsync({
|
||||
name,
|
||||
organizationId: currentOrg._id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated business name",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update business name",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Business name</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Acme Corp" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+81
-85
@@ -1,97 +1,93 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useGetOrgBillingDetails,
|
||||
useUpdateOrgBillingDetails
|
||||
} from "@app/hooks/api";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
|
||||
|
||||
const schema = yup.object({
|
||||
const schema = yup
|
||||
.object({
|
||||
email: yup.string().required("Email is required")
|
||||
}).required();
|
||||
})
|
||||
.required();
|
||||
|
||||
export const InvoiceEmailSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
email: ""
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
email: ""
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
email: data?.email ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({ email }: { email: string }) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (email === "") return;
|
||||
|
||||
await mutateAsync({
|
||||
email,
|
||||
organizationId: currentOrg._id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated invoice email recipient",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update invoice email recipient",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
email: data?.email ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-white mb-8">
|
||||
Invoice email recipient
|
||||
</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="[email protected]"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="email"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
const onFormSubmit = async ({ email }: { email: string }) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (email === "") return;
|
||||
|
||||
await mutateAsync({
|
||||
email,
|
||||
organizationId: currentOrg._id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated invoice email recipient",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update invoice email recipient",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Invoice email recipient</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="[email protected]" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="email"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+39
-38
@@ -1,46 +1,47 @@
|
||||
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useAddOrgPmtMethod } from "@app/hooks/api";
|
||||
|
||||
import { PmtMethodsTable } from "./PmtMethodsTable";
|
||||
|
||||
export const PmtMethodsSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isLoading } = useAddOrgPmtMethod();
|
||||
|
||||
const handleAddPmtMethodBtnClick = async () => {
|
||||
if (!currentOrg?._id) return;
|
||||
const url = await mutateAsync({
|
||||
organizationId: currentOrg._id,
|
||||
success_url: window.location.href,
|
||||
cancel_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Payment methods
|
||||
</h2>
|
||||
<Button
|
||||
onClick={handleAddPmtMethodBtnClick}
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
</div>
|
||||
<PmtMethodsTable />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isLoading } = useAddOrgPmtMethod();
|
||||
|
||||
const handleAddPmtMethodBtnClick = async () => {
|
||||
if (!currentOrg?._id) return;
|
||||
const url = await mutateAsync({
|
||||
organizationId: currentOrg._id,
|
||||
success_url: window.location.href,
|
||||
cancel_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">Payment methods</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={handleAddPmtMethodBtnClick}
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<PmtMethodsTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+20
-11
@@ -1,6 +1,7 @@
|
||||
import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
|
||||
|
||||
export const PmtMethodsTable = () => {
|
||||
@@ -52,17 +53,25 @@ export const PmtMethodsTable = () => {
|
||||
<Td>{last4}</Td>
|
||||
<Td>{`${exp_month}/${exp_year}`}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeletePmtMethodBtnClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeletePmtMethodBtnClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+31
-27
@@ -1,38 +1,42 @@
|
||||
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { TaxIDModal } from "./TaxIDModal";
|
||||
import { TaxIDTable } from "./TaxIDTable";
|
||||
|
||||
export const TaxIDSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addTaxID"
|
||||
] as const);
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addTaxID"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Tax ID
|
||||
</h2>
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addTaxID")}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
</div>
|
||||
<TaxIDTable />
|
||||
<TaxIDModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">Tax ID</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addTaxID")}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<TaxIDTable />
|
||||
<TaxIDModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+20
-11
@@ -1,6 +1,7 @@
|
||||
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
|
||||
|
||||
const taxIDTypeLabelMap: { [key: string]: string } = {
|
||||
@@ -101,17 +102,25 @@ export const TaxIDTable = () => {
|
||||
<Td>{taxIDTypeLabelMap[type]}</Td>
|
||||
<Td>{value}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeleteTaxIdBtnClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeleteTaxIdBtnClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+46
-38
@@ -1,5 +1,8 @@
|
||||
import { Fragment } from "react"
|
||||
import { Tab } from "@headlessui/react"
|
||||
import { Fragment } from "react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { BillingCloudTab } from "../BillingCloudTab";
|
||||
import { BillingDetailsTab } from "../BillingDetailsTab";
|
||||
@@ -7,43 +10,48 @@ import { BillingReceiptsTab } from "../BillingReceiptsTab";
|
||||
import { BillingSelfHostedTab } from "../BillingSelfHostedTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Infisical Cloud", key: "tab-infisical-cloud" },
|
||||
{ name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" },
|
||||
{ name: "Receipts", key: "tab-receipts" },
|
||||
{ name: "Billing details", key: "tab-billing-details" }
|
||||
{ name: "Infisical Cloud", key: "tab-infisical-cloud" },
|
||||
{ name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" },
|
||||
{ name: "Receipts", key: "tab-receipts" },
|
||||
{ name: "Billing details", key: "tab-billing-details" }
|
||||
];
|
||||
|
||||
export const BillingTabGroup = () => {
|
||||
export const BillingTabGroup = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<BillingCloudTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingSelfHostedTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingReceiptsTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingDetailsTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
<Tab.Group>
|
||||
<Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
|
||||
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||
}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<BillingCloudTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingSelfHostedTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingReceiptsTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingDetailsTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
|
||||
);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
|
||||
export const OrgAuthTab = () => {
|
||||
export const OrgAuthTab = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<div>
|
||||
<OrgSSOSection />
|
||||
</div>
|
||||
<div>
|
||||
<OrgSSOSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Sso }
|
||||
);
|
||||
|
||||
+131
-117
@@ -2,136 +2,150 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useCreateSSOConfig,
|
||||
useGetSSOConfig,
|
||||
useUpdateSSOConfig
|
||||
} from "@app/hooks/api";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SSOModal } from "./SSOModal";
|
||||
|
||||
const ssoAuthProviderMap: { [key: string]: string } = {
|
||||
"okta-saml": "Okta SAML",
|
||||
"azure-saml": "Azure SAML",
|
||||
"jumpcloud-saml": "JumpCloud SAML"
|
||||
}
|
||||
"okta-saml": "Okta SAML",
|
||||
"azure-saml": "Azure SAML",
|
||||
"jumpcloud-saml": "JumpCloud SAML"
|
||||
};
|
||||
|
||||
export const OrgSSOSection = (): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||
const { mutateAsync } = useUpdateSSOConfig();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"upgradePlan",
|
||||
"addSSO"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateSSOConfig();
|
||||
|
||||
const handleSamlSSOToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||
const { mutateAsync } = useUpdateSSOConfig();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"upgradePlan",
|
||||
"addSSO"
|
||||
] as const);
|
||||
|
||||
await mutateAsync({
|
||||
organizationId: currentOrg?._id,
|
||||
isActive: value
|
||||
});
|
||||
const { mutateAsync: createMutateAsync } = useCreateSSOConfig();
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enabled" : "disabled"} SAML SSO`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${value ? "enable" : "disable"} SAML SSO`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
const handleSamlSSOToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
|
||||
await mutateAsync({
|
||||
organizationId: currentOrg?._id,
|
||||
isActive: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enabled" : "disabled"} SAML SSO`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${value ? "enable" : "disable"} SAML SSO`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
const addSSOBtnClick = async () => {
|
||||
try {
|
||||
if (subscription?.samlSSO && currentOrg) {
|
||||
if (!data) {
|
||||
// case: SAML SSO is not configured
|
||||
// -> initialize empty SAML SSO configuration
|
||||
await createMutateAsync({
|
||||
organizationId: currentOrg._id,
|
||||
authProvider: "okta-saml",
|
||||
isActive: false,
|
||||
entryPoint: "",
|
||||
issuer: "",
|
||||
cert: ""
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handlePopUpOpen("addSSO");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const addSSOBtnClick = async () => {
|
||||
try {
|
||||
if (subscription?.samlSSO && currentOrg) {
|
||||
if (!data) {
|
||||
// case: SAML SSO is not configured
|
||||
// -> initialize empty SAML SSO configuration
|
||||
await createMutateAsync({
|
||||
organizationId: currentOrg._id,
|
||||
authProvider: "okta-saml",
|
||||
isActive: false,
|
||||
entryPoint: "",
|
||||
issuer: "",
|
||||
cert: ""
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpOpen("addSSO");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
SAML SSO Configuration
|
||||
</h2>
|
||||
{!isLoading && (
|
||||
<Button
|
||||
onClick={addSSOBtnClick}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
{data ? "Update SAML SSO" : "Set up SAML SSO"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{data && (
|
||||
<div className="mb-4">
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
>
|
||||
Enable SAML SSO
|
||||
</Switch>
|
||||
</div>
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">SAML SSO Configuration</h2>
|
||||
{!isLoading && (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={addSSOBtnClick}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
{data ? "Update SAML SSO" : "Set up SAML SSO"}
|
||||
</Button>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
|
||||
<p className="text-gray-400 text-md">{(data && data._id !== "") ? data._id : "-"}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">Type</h3>
|
||||
<p className="text-gray-400 text-md">{(data && data.authProvider !== "") ? ssoAuthProviderMap[data.authProvider] : "-"}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
|
||||
<p className="text-gray-400 text-md">{(data && data.entryPoint !== "") ? data.entryPoint : "-"}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
|
||||
<p className="text-gray-400 text-md">{(data && data.issuer !== "") ? data.issuer : "-"}</p>
|
||||
</div>
|
||||
<SSOModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use SAML SSO if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
{data && (
|
||||
<div className="mb-4">
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Enable SAML SSO
|
||||
</Switch>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
|
||||
<p className="text-gray-400 text-md">{data && data._id !== "" ? data._id : "-"}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">Type</h3>
|
||||
<p className="text-gray-400 text-md">
|
||||
{data && data.authProvider !== "" ? ssoAuthProviderMap[data.authProvider] : "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
|
||||
<p className="text-gray-400 text-md">
|
||||
{data && data.entryPoint !== "" ? data.entryPoint : "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
|
||||
<p className="text-gray-400 text-md">{data && data.issuer !== "" ? data.issuer : "-"}</p>
|
||||
</div>
|
||||
<SSOModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use SAML SSO if you switch to Infisical's Pro plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-11
@@ -1,15 +1,11 @@
|
||||
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
|
||||
import { OrgNameChangeSection } from "../OrgNameChangeSection";
|
||||
import { OrgServiceAccountsTable } from "../OrgServiceAccountsTable";
|
||||
|
||||
export const OrgGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<OrgNameChangeSection />
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
|
||||
<OrgServiceAccountsTable />
|
||||
</div>
|
||||
<OrgIncidentContactsSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<OrgNameChangeSection />
|
||||
<OrgIncidentContactsSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+79
-94
@@ -3,17 +3,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useAddIncidentContact
|
||||
} from "@app/hooks/api";
|
||||
import { useAddIncidentContact } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -24,97 +16,90 @@ const addContactFormSchema = yup.object({
|
||||
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["addContact"]>;
|
||||
handlePopUpClose: (popUpName: keyof UsePopUpState<["addContact"]>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addContact"]>, state?: boolean) => void;
|
||||
popUp: UsePopUpState<["addContact"]>;
|
||||
handlePopUpClose: (popUpName: keyof UsePopUpState<["addContact"]>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addContact"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const AddOrgIncidentContactModal = ({
|
||||
popUp,
|
||||
handlePopUpClose,
|
||||
handlePopUpToggle
|
||||
popUp,
|
||||
handlePopUpClose,
|
||||
handlePopUpToggle
|
||||
}: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: serverDetails } = useFetchServerStatus()
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset
|
||||
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const { control, handleSubmit, reset } = useForm<TAddContactForm>({
|
||||
resolver: yupResolver(addContactFormSchema)
|
||||
});
|
||||
|
||||
const { mutateAsync, isLoading } = useAddIncidentContact();
|
||||
|
||||
const onFormSubmit = async ({ email }: TAddContactForm) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
email
|
||||
});
|
||||
const { mutateAsync, isLoading } = useAddIncidentContact();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully added incident contact",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (serverDetails?.emailConfigured){
|
||||
handlePopUpClose("addContact");
|
||||
}
|
||||
const onFormSubmit = async ({ email }: TAddContactForm) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to add incident contact",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
email
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully added incident contact",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (serverDetails?.emailConfigured) {
|
||||
handlePopUpClose("addContact");
|
||||
}
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to add incident contact",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addContact?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addContact", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add an Incident Contact"
|
||||
subTitle="This contact will be notified in the unlikely event of a severe incident."
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addContact?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addContact", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add an Incident Contact"
|
||||
subTitle="This contact will be notified in the unlikely event of a severe incident."
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="email"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center space-x-4">
|
||||
<Button size="sm" type="submit" isLoading={isLoading} isDisabled={isLoading}>
|
||||
Add Incident Contact
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpClose("addContact")}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="email"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Add Incident Contact
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpClose("addContact")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+39
-32
@@ -2,42 +2,49 @@ import { useTranslation } from "react-i18next";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionCan, PermissionDeniedBanner } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
|
||||
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
|
||||
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
|
||||
|
||||
export const OrgIncidentContactsSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addContact"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-4">
|
||||
<p className="min-w-max text-xl font-semibold">
|
||||
{t("section.incident.incident-contacts")}
|
||||
</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addContact")}
|
||||
>
|
||||
Add contact
|
||||
</Button>
|
||||
</div>
|
||||
<OrgIncidentContactsTable />
|
||||
<AddOrgIncidentContactModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { t } = useTranslation();
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addContact"
|
||||
] as const);
|
||||
const permission = useOrgPermission();
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-4">
|
||||
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addContact")}
|
||||
>
|
||||
Add contact
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
{permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount) ? (
|
||||
<OrgIncidentContactsTable />
|
||||
) : (
|
||||
<PermissionDeniedBanner />
|
||||
)}
|
||||
<AddOrgIncidentContactModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+16
-7
@@ -3,6 +3,7 @@ import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-sol
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
|
||||
|
||||
@@ -83,13 +84,21 @@ export const OrgIncidentContactsTable = () => {
|
||||
<Tr key={email}>
|
||||
<Td className="w-full">{email}</Td>
|
||||
<Td className="mr-4">
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen("removeContact", { email })}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
an={OrgPermissionSubjects.IncidentAccount}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen("removeContact", { email })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+32
-30
@@ -4,8 +4,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useRenameOrg } from "@app/hooks/api";
|
||||
|
||||
const formSchema = yup.object({
|
||||
@@ -17,11 +18,9 @@ type FormData = yup.InferType<typeof formSchema>;
|
||||
export const OrgNameChangeSection = (): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
const { mutateAsync, isLoading } = useRenameOrg();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,33 +49,36 @@ export const OrgNameChangeSection = (): JSX.Element => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
|
||||
Organization name
|
||||
</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Acme Corp" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Acme Corp" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+304
-289
@@ -34,7 +34,13 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
// useCreateServiceAccount,
|
||||
@@ -62,313 +68,322 @@ import // Controller,
|
||||
|
||||
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
||||
|
||||
export const OrgServiceAccountsTable = () => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
export const OrgServiceAccountsTable = withPermission(
|
||||
() => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const orgId = currentOrg?._id || "";
|
||||
const [step, setStep] = useState(0);
|
||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||
const [accessKey] = useState("");
|
||||
const [publicKey] = useState("");
|
||||
const [privateKey] = useState("");
|
||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addServiceAccount",
|
||||
"removeServiceAccount"
|
||||
] as const);
|
||||
const orgId = currentOrg?._id || "";
|
||||
const [step, setStep] = useState(0);
|
||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||
const [accessKey] = useState("");
|
||||
const [publicKey] = useState("");
|
||||
const [privateKey] = useState("");
|
||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addServiceAccount",
|
||||
"removeServiceAccount"
|
||||
] as const);
|
||||
|
||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
|
||||
useGetServiceAccounts(orgId);
|
||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
|
||||
useGetServiceAccounts(orgId);
|
||||
|
||||
// const createServiceAccount = useCreateServiceAccount();
|
||||
const removeServiceAccount = useDeleteServiceAccount();
|
||||
// const createServiceAccount = useCreateServiceAccount();
|
||||
const removeServiceAccount = useDeleteServiceAccount();
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isAccessKeyCopied) {
|
||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||
}
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isAccessKeyCopied) {
|
||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isPublicKeyCopied) {
|
||||
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
||||
}
|
||||
if (isPublicKeyCopied) {
|
||||
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isPrivateKeyCopied) {
|
||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||
}
|
||||
if (isPrivateKeyCopied) {
|
||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||
return () => clearTimeout(timer);
|
||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||
|
||||
// const {
|
||||
// control,
|
||||
// handleSubmit,
|
||||
// reset,
|
||||
// formState: { isSubmitting }
|
||||
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||
// const {
|
||||
// control,
|
||||
// handleSubmit,
|
||||
// reset,
|
||||
// formState: { isSubmitting }
|
||||
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||
|
||||
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||
// if (!currentOrg?._id) return;
|
||||
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||
// if (!currentOrg?._id) return;
|
||||
|
||||
// const keyPair = generateKeyPair();
|
||||
// setPublicKey(keyPair.publicKey);
|
||||
// setPrivateKey(keyPair.privateKey);
|
||||
// const keyPair = generateKeyPair();
|
||||
// setPublicKey(keyPair.publicKey);
|
||||
// setPrivateKey(keyPair.privateKey);
|
||||
|
||||
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||
// name,
|
||||
// organizationId: currentOrg?._id,
|
||||
// publicKey: keyPair.publicKey,
|
||||
// expiresIn: Number(expiresIn)
|
||||
// });
|
||||
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||
// name,
|
||||
// organizationId: currentOrg?._id,
|
||||
// publicKey: keyPair.publicKey,
|
||||
// expiresIn: Number(expiresIn)
|
||||
// });
|
||||
|
||||
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||
|
||||
// setStep(1);
|
||||
// reset();
|
||||
// }
|
||||
// setStep(1);
|
||||
// reset();
|
||||
// }
|
||||
|
||||
const onRemoveServiceAccount = async () => {
|
||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||
handlePopUpClose("removeServiceAccount");
|
||||
};
|
||||
const onRemoveServiceAccount = async () => {
|
||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||
handlePopUpClose("removeServiceAccount");
|
||||
};
|
||||
|
||||
const filteredServiceAccounts = useMemo(
|
||||
() =>
|
||||
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
|
||||
[serviceAccounts, searchServiceAccountFilter]
|
||||
);
|
||||
const filteredServiceAccounts = useMemo(
|
||||
() =>
|
||||
serviceAccounts.filter(({ name }) =>
|
||||
name.toLowerCase().includes(searchServiceAccountFilter)
|
||||
),
|
||||
[serviceAccounts, searchServiceAccountFilter]
|
||||
);
|
||||
|
||||
const renderStep = (stepToRender: number) => {
|
||||
switch (stepToRender) {
|
||||
case 0:
|
||||
return (
|
||||
<div>
|
||||
We are currently revising the service account mechanism. In the meantime, please use
|
||||
service tokens or API key to fetch secrets via API request.
|
||||
</div>
|
||||
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
|
||||
// <Controller
|
||||
// control={control}
|
||||
// defaultValue=""
|
||||
// name="name"
|
||||
// render={({ field, fieldState: { error } }) => (
|
||||
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
// <Input {...field} />
|
||||
// </FormControl>
|
||||
// )}
|
||||
// />
|
||||
// <Controller
|
||||
// control={control}
|
||||
// name="expiresIn"
|
||||
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
||||
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
// return (
|
||||
// <FormControl
|
||||
// label="Expiration"
|
||||
// errorText={error?.message}
|
||||
// isError={Boolean(error)}
|
||||
// >
|
||||
// <Select
|
||||
// defaultValue={field.value}
|
||||
// {...field}
|
||||
// onValueChange={(e) => onChange(e)}
|
||||
// className="w-full"
|
||||
// >
|
||||
// {serviceAccountExpiration.map(({ label, value }) => (
|
||||
// <SelectItem value={String(value)} key={label}>
|
||||
// {label}
|
||||
// </SelectItem>
|
||||
// ))}
|
||||
// </Select>
|
||||
// </FormControl>
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// <div className="mt-8 flex items-center">
|
||||
// <Button
|
||||
// className="mr-4"
|
||||
// size="sm"
|
||||
// type="submit"
|
||||
// isLoading={isSubmitting}
|
||||
// isDisabled={isSubmitting}
|
||||
// >
|
||||
// Create Service Account
|
||||
// </Button>
|
||||
// <Button
|
||||
// colorSchema="secondary"
|
||||
// variant="plain"
|
||||
// onClick={() => handlePopUpClose("addServiceAccount")}
|
||||
// >
|
||||
// Cancel
|
||||
// </Button>
|
||||
// </div>
|
||||
// </form>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<>
|
||||
<p>Access Key</p>
|
||||
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{accessKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(accessKey);
|
||||
setIsAccessKeyCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
const renderStep = (stepToRender: number) => {
|
||||
switch (stepToRender) {
|
||||
case 0:
|
||||
return (
|
||||
<div>
|
||||
We are currently revising the service account mechanism. In the meantime, please use
|
||||
service tokens or API key to fetch secrets via API request.
|
||||
</div>
|
||||
<p className="mt-4">Public Key</p>
|
||||
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{publicKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(publicKey);
|
||||
setIsPublicKeyCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<p className="mt-4">Private Key</p>
|
||||
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{privateKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(privateKey);
|
||||
setIsPrivateKeyCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
|
||||
// <Controller
|
||||
// control={control}
|
||||
// defaultValue=""
|
||||
// name="name"
|
||||
// render={({ field, fieldState: { error } }) => (
|
||||
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
// <Input {...field} />
|
||||
// </FormControl>
|
||||
// )}
|
||||
// />
|
||||
// <Controller
|
||||
// control={control}
|
||||
// name="expiresIn"
|
||||
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
|
||||
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
// return (
|
||||
// <FormControl
|
||||
// label="Expiration"
|
||||
// errorText={error?.message}
|
||||
// isError={Boolean(error)}
|
||||
// >
|
||||
// <Select
|
||||
// defaultValue={field.value}
|
||||
// {...field}
|
||||
// onValueChange={(e) => onChange(e)}
|
||||
// className="w-full"
|
||||
// >
|
||||
// {serviceAccountExpiration.map(({ label, value }) => (
|
||||
// <SelectItem value={String(value)} key={label}>
|
||||
// {label}
|
||||
// </SelectItem>
|
||||
// ))}
|
||||
// </Select>
|
||||
// </FormControl>
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// <div className="mt-8 flex items-center">
|
||||
// <Button
|
||||
// className="mr-4"
|
||||
// size="sm"
|
||||
// type="submit"
|
||||
// isLoading={isSubmitting}
|
||||
// isDisabled={isSubmitting}
|
||||
// >
|
||||
// Create Service Account
|
||||
// </Button>
|
||||
// <Button
|
||||
// colorSchema="secondary"
|
||||
// variant="plain"
|
||||
// onClick={() => handlePopUpClose("addServiceAccount")}
|
||||
// >
|
||||
// Cancel
|
||||
// </Button>
|
||||
// </div>
|
||||
// </form>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<>
|
||||
<p>Access Key</p>
|
||||
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{accessKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(accessKey);
|
||||
setIsAccessKeyCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<p className="mt-4">Public Key</p>
|
||||
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{publicKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(publicKey);
|
||||
setIsPublicKeyCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<p className="mt-4">Private Key</p>
|
||||
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{privateKey}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(privateKey);
|
||||
setIsPrivateKeyCopied.on();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
// reset();
|
||||
handlePopUpOpen("addServiceAccount");
|
||||
}}
|
||||
>
|
||||
Add Service Account
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={searchServiceAccountFilter}
|
||||
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search service accounts..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Th>Name</Th>
|
||||
<Th className="w-full">Valid Until</Th>
|
||||
<Th aria-label="actions" />
|
||||
</THead>
|
||||
<TBody>
|
||||
{isServiceAccountsLoading && (
|
||||
<TableSkeleton columns={5} innerKey="org-service-accounts" />
|
||||
)}
|
||||
{!isServiceAccountsLoading &&
|
||||
filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
|
||||
return (
|
||||
<Tr key={`org-service-account-${serviceAccountId}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
||||
<Td>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="edit"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
if (currentWorkspace?._id) {
|
||||
router.push(
|
||||
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
||||
<EmptyState title="No service accounts found" icon={faServer} />
|
||||
)}
|
||||
</TableContainer>
|
||||
<Modal
|
||||
isOpen={popUp?.addServiceAccount?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addServiceAccount", isOpen);
|
||||
// reset();
|
||||
handlePopUpOpen("addServiceAccount");
|
||||
}}
|
||||
>
|
||||
Add Service Account
|
||||
</Button>
|
||||
<ModalContent
|
||||
title="Add Service Account"
|
||||
subTitle="A service account represents a machine identity such as a VM or application client."
|
||||
>
|
||||
{renderStep(step)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeServiceAccount.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this service account from the org?"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
||||
onDeleteApproved={onRemoveServiceAccount}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
value={searchServiceAccountFilter}
|
||||
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search service accounts..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Th>Name</Th>
|
||||
<Th className="w-full">Valid Until</Th>
|
||||
<Th aria-label="actions" />
|
||||
</THead>
|
||||
<TBody>
|
||||
{isServiceAccountsLoading && (
|
||||
<TableSkeleton columns={5} innerKey="org-service-accounts" />
|
||||
)}
|
||||
{!isServiceAccountsLoading &&
|
||||
filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
|
||||
return (
|
||||
<Tr key={`org-service-account-${serviceAccountId}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{new Date(expiresAt).toUTCString()}</Td>
|
||||
<Td>
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="edit"
|
||||
colorSchema="secondary"
|
||||
onClick={() => {
|
||||
if (currentWorkspace?._id) {
|
||||
router.push(
|
||||
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
|
||||
<EmptyState title="No service accounts found" icon={faServer} />
|
||||
)}
|
||||
</TableContainer>
|
||||
<Modal
|
||||
isOpen={popUp?.addServiceAccount?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addServiceAccount", isOpen);
|
||||
// reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add Service Account"
|
||||
subTitle="A service account represents a machine identity such as a VM or application client."
|
||||
>
|
||||
{renderStep(step)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeServiceAccount.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this service account from the org?"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
|
||||
onDeleteApproved={onRemoveServiceAccount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionActions.Read,
|
||||
subject: OrgPermissionSubjects.Settings,
|
||||
containerClassName: "mb-4"
|
||||
}
|
||||
);
|
||||
|
||||
+35
-54
@@ -1,59 +1,40 @@
|
||||
import { Fragment } from "react"
|
||||
import { Tab } from "@headlessui/react"
|
||||
|
||||
import { useOrganization,useUser } from "@app/context";
|
||||
import {
|
||||
useGetOrgUsers
|
||||
} from "@app/hooks/api";
|
||||
import { Fragment } from "react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import { OrgAuthTab } from "../OrgAuthTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general" },
|
||||
{ name: "Authentication", key: "tab-org-auth" }
|
||||
];
|
||||
export const OrgTabGroup = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { user } = useUser();
|
||||
const { data } = useGetOrgUsers(currentOrg?._id ?? "");
|
||||
|
||||
const isRoleSufficient = data?.some((orgUser) => {
|
||||
return orgUser.role !== "member" && orgUser.user._id === user._id;
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general" },
|
||||
];
|
||||
|
||||
if (isRoleSufficient) {
|
||||
tabs.push(
|
||||
{ name: "Authentication", key: "tab-org-auth" }
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<OrgGeneralTab />
|
||||
</Tab.Panel>
|
||||
{isRoleSufficient && (
|
||||
<Tab.Panel>
|
||||
<OrgAuthTab />
|
||||
</Tab.Panel>
|
||||
)}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
|
||||
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||
}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<OrgGeneralTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<OrgAuthTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,7 +32,9 @@ export const ProjectSettingsPage = () => {
|
||||
{({ selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
|
||||
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
|
||||
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||
}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
|
||||
+22
-14
@@ -1,16 +1,17 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggleAutoCapitalization } from "@app/hooks/api";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useToggleAutoCapitalization } from "@app/hooks/api";
|
||||
|
||||
export const AutoCapitalizationSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync } = useToggleAutoCapitalization();
|
||||
|
||||
|
||||
const handleToggleCapitalizationToggle = async (state: boolean) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
@@ -32,21 +33,28 @@ export const AutoCapitalizationSection = () => {
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
||||
onCheckedChange={(state) => {
|
||||
handleToggleCapitalizationToggle(state as boolean);
|
||||
}}
|
||||
>
|
||||
{t("settings.project.auto-capitalization-description")}
|
||||
</Checkbox>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isDisabled={!isAllowed}
|
||||
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
||||
onCheckedChange={(state) => {
|
||||
handleToggleCapitalizationToggle(state as boolean);
|
||||
}}
|
||||
>
|
||||
{t("settings.project.auto-capitalization-description")}
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+52
-44
@@ -3,30 +3,34 @@ import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
useDeleteWorkspace
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useDeleteWorkspace } from "@app/hooks/api";
|
||||
|
||||
export const DeleteProjectSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization()
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [deleteProjectInput, setDeleteProjectInput] = useState("");
|
||||
const deleteWorkspace = useDeleteWorkspace();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
const [deleteProjectInput, setDeleteProjectInput] = useState("");
|
||||
const deleteWorkspace = useDeleteWorkspace();
|
||||
|
||||
const onDeleteWorkspace = async () => {
|
||||
const onDeleteWorkspace = async () => {
|
||||
setIsDeleting.on();
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
await deleteWorkspace.mutateAsync({
|
||||
if (!currentWorkspace?._id) return;
|
||||
await deleteWorkspace.mutateAsync({
|
||||
workspaceID: currentWorkspace?._id
|
||||
});
|
||||
});
|
||||
// redirect user to the org overview
|
||||
router.push(`/org/${currentOrg?._id}/overview`);
|
||||
|
||||
@@ -45,38 +49,42 @@ export const DeleteProjectSection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-red">
|
||||
<p className="mb-3 text-xl font-semibold text-red">{t("settings.project.danger-zone")}</p>
|
||||
<p className="text-gray-400 mb-8">{t("settings.project.danger-zone-note")}</p>
|
||||
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
|
||||
<FormControl
|
||||
label={
|
||||
<div className="mb-0.5 text-sm font-normal text-gray-400">
|
||||
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
|
||||
workspace
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
onChange={(e) => setDeleteProjectInput(e.target.value)}
|
||||
value={deleteProjectInput}
|
||||
placeholder="Type the project name to delete"
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-red">
|
||||
<p className="mb-3 text-xl font-semibold text-red">{t("settings.project.danger-zone")}</p>
|
||||
<p className="text-gray-400 mb-8">{t("settings.project.danger-zone-note")}</p>
|
||||
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
|
||||
<FormControl
|
||||
label={
|
||||
<div className="mb-0.5 text-sm font-normal text-gray-400">
|
||||
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
|
||||
workspace
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
onChange={(e) => setDeleteProjectInput(e.target.value)}
|
||||
value={deleteProjectInput}
|
||||
placeholder="Type the project name to delete"
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
onClick={onDeleteWorkspace}
|
||||
isDisabled={deleteProjectInput !== currentWorkspace?.name || isDeleting}
|
||||
isDisabled={!isAllowed || deleteProjectInput !== currentWorkspace?.name || isDeleting}
|
||||
isLoading={isDeleting}
|
||||
>
|
||||
{t("settings.project.delete-project")}
|
||||
</Button>
|
||||
<p className="mt-3 ml-0.5 text-xs text-gray-500">
|
||||
{t("settings.project.delete-project-note")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<p className="mt-3 ml-0.5 text-xs text-gray-500">
|
||||
{t("settings.project.delete-project-note")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+91
-80
@@ -1,99 +1,110 @@
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetUserWsKey,useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
||||
|
||||
export const E2EESection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
|
||||
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
|
||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
|
||||
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
|
||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
|
||||
/**
|
||||
/**
|
||||
* Activate bot for project by performing the following steps:
|
||||
* 1. Get the (encrypted) project key
|
||||
* 2. Decrypt project key with user's private key
|
||||
* 3. Encrypt project key with bot's public key
|
||||
* 4. Send encrypted project key to backend and set bot status to active
|
||||
*/
|
||||
const toggleBotActivate = async () => {
|
||||
let botKey;
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
const toggleBotActivate = async () => {
|
||||
let botKey;
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
if (bot && wsKey) {
|
||||
// case: there is a bot
|
||||
|
||||
if (!bot.isActive) {
|
||||
// bot is not active -> activate bot
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
if (bot && wsKey) {
|
||||
// case: there is a bot
|
||||
|
||||
if (!PRIVATE_KEY) {
|
||||
throw new Error("Private Key missing");
|
||||
}
|
||||
if (!bot.isActive) {
|
||||
// bot is not active -> activate bot
|
||||
|
||||
const WORKSPACE_KEY = decryptAssymmetric({
|
||||
ciphertext: wsKey.encryptedKey,
|
||||
nonce: wsKey.nonce,
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: WORKSPACE_KEY,
|
||||
publicKey: bot.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
if (!PRIVATE_KEY) {
|
||||
throw new Error("Private Key missing");
|
||||
}
|
||||
|
||||
botKey = {
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
};
|
||||
|
||||
await updateBotActiveStatus({
|
||||
workspaceId: currentWorkspace._id,
|
||||
botKey,
|
||||
isActive: true,
|
||||
botId: bot._id
|
||||
});
|
||||
} else {
|
||||
// bot is active -> deactivate bot
|
||||
await updateBotActiveStatus({
|
||||
isActive: false,
|
||||
botId: bot._id,
|
||||
workspaceId: currentWorkspace._id
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const WORKSPACE_KEY = decryptAssymmetric({
|
||||
ciphertext: wsKey.encryptedKey,
|
||||
nonce: wsKey.nonce,
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: WORKSPACE_KEY,
|
||||
publicKey: bot.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
botKey = {
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
};
|
||||
|
||||
await updateBotActiveStatus({
|
||||
workspaceId: currentWorkspace._id,
|
||||
botKey,
|
||||
isActive: true,
|
||||
botId: bot._id
|
||||
});
|
||||
} else {
|
||||
// bot is active -> deactivate bot
|
||||
await updateBotActiveStatus({
|
||||
isActive: false,
|
||||
botId: bot._id,
|
||||
workspaceId: currentWorkspace._id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return bot ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to cloud providers as well as HTTP calls to get secrets back raw but enables the server to read/decrypt your secret values.
|
||||
</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
||||
</p>
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={!bot.isActive}
|
||||
onCheckedChange={async () => {
|
||||
await toggleBotActivate();
|
||||
}}
|
||||
>
|
||||
End-to-end encryption enabled
|
||||
</Checkbox>
|
||||
</div>
|
||||
) : <div />;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return bot ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
|
||||
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
|
||||
read/decrypt your secret values.
|
||||
</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
||||
</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={!bot.isActive}
|
||||
isDisabled={!isAllowed}
|
||||
onCheckedChange={async () => {
|
||||
await toggleBotActivate();
|
||||
}}
|
||||
>
|
||||
End-to-end encryption enabled
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
+47
-33
@@ -2,15 +2,16 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useSubscription,useWorkspace } from "@app/context";
|
||||
import {
|
||||
useDeleteWsEnvironment
|
||||
} from "@app/hooks/api";
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDeleteWsEnvironment } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddEnvironmentModal } from "./AddEnvironmentModal";
|
||||
@@ -21,11 +22,15 @@ export const EnvironmentSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const permision = useProjectPermission();
|
||||
|
||||
const deleteWsEnvironment = useDeleteWsEnvironment();
|
||||
|
||||
const isMoreEnvironmentsAllowed = (subscription?.environmentLimit && currentWorkspace?.environments) ? (currentWorkspace.environments.length < subscription.environmentLimit) : true;
|
||||
|
||||
const isMoreEnvironmentsAllowed =
|
||||
subscription?.environmentLimit && currentWorkspace?.environments
|
||||
? currentWorkspace.environments.length < subscription.environmentLimit
|
||||
: true;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"createEnv",
|
||||
"updateEnv",
|
||||
@@ -36,7 +41,7 @@ export const EnvironmentSection = () => {
|
||||
const onEnvDeleteSubmit = async (environmentSlug: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
|
||||
await deleteWsEnvironment.mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
environmentSlug
|
||||
@@ -46,7 +51,7 @@ export const EnvironmentSection = () => {
|
||||
text: "Successfully deleted environment",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
|
||||
handlePopUpClose("deleteEnv");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -60,37 +65,46 @@ export const EnvironmentSection = () => {
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
Environments
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
|
||||
<div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isMoreEnvironmentsAllowed) {
|
||||
handlePopUpOpen("createEnv");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
Create environment
|
||||
</Button>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isMoreEnvironmentsAllowed) {
|
||||
handlePopUpOpen("createEnv");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create environment
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Choose which environments will show up in your dashboard like development, staging, production
|
||||
Choose which environments will show up in your dashboard like development, staging,
|
||||
production
|
||||
</p>
|
||||
<EnvironmentTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<AddEnvironmentModal
|
||||
{permision.can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments) ? (
|
||||
<EnvironmentTable handlePopUpOpen={handlePopUpOpen} />
|
||||
) : (
|
||||
<PermissionDeniedBanner />
|
||||
)}
|
||||
<AddEnvironmentModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<UpdateEnvironmentModal
|
||||
<UpdateEnvironmentModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
|
||||
+87
-52
@@ -1,7 +1,8 @@
|
||||
import { faArrowDown,faArrowUp, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowDown, faArrowUp, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -14,10 +15,8 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
useReorderWsEnvironment
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useReorderWsEnvironment } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -38,18 +37,23 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const reorderWsEnvironment = useReorderWsEnvironment();
|
||||
|
||||
const handleReorderEnv= async (shouldMoveUp: boolean, name: string, slug: string) => {
|
||||
const handleReorderEnv = async (shouldMoveUp: boolean, name: string, slug: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
const indexOfEnv = currentWorkspace.environments.findIndex((env) => env.name === name && env.slug === slug);
|
||||
const indexOfEnv = currentWorkspace.environments.findIndex(
|
||||
(env) => env.name === name && env.slug === slug
|
||||
);
|
||||
|
||||
// check that this reordering is possible
|
||||
if (indexOfEnv === 0 && shouldMoveUp || indexOfEnv === currentWorkspace.environments.length - 1 && !shouldMoveUp) {
|
||||
return
|
||||
if (
|
||||
(indexOfEnv === 0 && shouldMoveUp) ||
|
||||
(indexOfEnv === currentWorkspace.environments.length - 1 && !shouldMoveUp)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexToSwap = shouldMoveUp ? indexOfEnv - 1 : indexOfEnv + 1
|
||||
const indexToSwap = shouldMoveUp ? indexOfEnv - 1 : indexOfEnv + 1;
|
||||
|
||||
await reorderWsEnvironment.mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
@@ -91,52 +95,83 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handleReorderEnv(false, name, slug)
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={pos === currentWorkspace.environments.length - 1}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowDown} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handleReorderEnv(true, name, slug)
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={pos === 0}
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handleReorderEnv(false, name, slug);
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={pos === currentWorkspace.environments.length - 1 || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowDown} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowUp} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("updateEnv", { name, slug });
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handleReorderEnv(true, name, slug);
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={pos === 0 || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowUp} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteEnv", { name, slug });
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("updateEnv", { name, slug });
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Environments}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteEnv", { name, slug });
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+46
-33
@@ -1,24 +1,30 @@
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useGetUserWsKey,
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceSecrets,
|
||||
useNameWorkspaceSecrets
|
||||
useGetUserWsKey,
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceSecrets,
|
||||
useNameWorkspaceSecrets
|
||||
} from "@app/hooks/api";
|
||||
|
||||
// TODO: add check so that this only shows up if user is
|
||||
// an admin in the workspace
|
||||
|
||||
export const ProjectIndexSecretsSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(currentWorkspace?._id ?? "");
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
|
||||
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
|
||||
|
||||
const onEnableBlindIndices = async () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
|
||||
currentWorkspace?._id ?? ""
|
||||
);
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
|
||||
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
|
||||
|
||||
const onEnableBlindIndices = async () => {
|
||||
if (!currentWorkspace?._id) return;
|
||||
if (!encryptedSecrets) return;
|
||||
if (!latestFileKey) return;
|
||||
@@ -50,22 +56,29 @@ export const ProjectIndexSecretsSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
return (!isBlindIndexedLoading && (isBlindIndexed === false)) ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Your project, created before the introduction of blind indexing, contains unindexed secrets. To access individual secrets by name through the SDK and public API, please enable blind indexing.
|
||||
</p>
|
||||
<Button
|
||||
onClick={onEnableBlindIndices}
|
||||
color="mineshaft"
|
||||
size="sm"
|
||||
type="submit"
|
||||
>
|
||||
Enable Blind Indexing
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
return !isBlindIndexedLoading && !isBlindIndexed ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Your project, created before the introduction of blind indexing, contains unindexed secrets.
|
||||
To access individual secrets by name through the SDK and public API, please enable blind
|
||||
indexing.
|
||||
</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={onEnableBlindIndices}
|
||||
isDisabled={!isAllowed}
|
||||
color="mineshaft"
|
||||
size="sm"
|
||||
type="submit"
|
||||
>
|
||||
Enable Blind Indexing
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
+32
-41
@@ -4,11 +4,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
useRenameWorkspace
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useRenameWorkspace } from "@app/hooks/api";
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("Project Name")
|
||||
@@ -21,25 +20,20 @@ export const ProjectNameChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isLoading } = useRenameWorkspace();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
reset({
|
||||
name: currentWorkspace.name
|
||||
});
|
||||
}
|
||||
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
|
||||
await mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
newWorkspaceName: name
|
||||
@@ -49,7 +43,6 @@ export const ProjectNameChangeSection = () => {
|
||||
text: "Successfully renamed workspace",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
@@ -60,37 +53,35 @@ export const ProjectNameChangeSection = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
|
||||
Project Name
|
||||
</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Project Name</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Project name" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+29
-22
@@ -2,10 +2,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal
|
||||
} from "@app/components/v2";
|
||||
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteWsTag } from "@app/hooks/api";
|
||||
|
||||
@@ -20,12 +19,13 @@ export const SecretTagsSection = (): JSX.Element => {
|
||||
"CreateSecretTag",
|
||||
"deleteTagConfirmation"
|
||||
] as const);
|
||||
const permission = useProjectPermission();
|
||||
|
||||
const deleteWsTag = useDeleteWsTag();
|
||||
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
await deleteWsTag.mutateAsync({
|
||||
await deleteWsTag.mutateAsync({
|
||||
tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id
|
||||
});
|
||||
|
||||
@@ -48,26 +48,33 @@ export const SecretTagsSection = (): JSX.Element => {
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
console.log("x");
|
||||
handlePopUpOpen("CreateSecretTag");
|
||||
console.log("x2");
|
||||
}}
|
||||
>
|
||||
Create tag
|
||||
</Button>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
console.log("x");
|
||||
handlePopUpOpen("CreateSecretTag");
|
||||
console.log("x2");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create tag
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-8">
|
||||
Every secret can be assigned to one or more tags. Here you can add and remove tags for
|
||||
the current project.
|
||||
Every secret can be assigned to one or more tags. Here you can add and remove tags for the
|
||||
current project.
|
||||
</p>
|
||||
<SecretTagsTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<AddSecretTagModal
|
||||
{permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? (
|
||||
<SecretTagsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
) : (
|
||||
<PermissionDeniedBanner />
|
||||
)}
|
||||
<AddSecretTagModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
|
||||
+21
-12
@@ -1,6 +1,7 @@
|
||||
import { faTags, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetWsTags } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -53,18 +54,26 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteTagConfirmation", {
|
||||
name,
|
||||
id: _id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="update"
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Tags}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteTagConfirmation", {
|
||||
name,
|
||||
id: _id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+71
-57
@@ -3,7 +3,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteServiceToken } from "@app/hooks/api";
|
||||
|
||||
@@ -12,65 +15,76 @@ import { ServiceTokenTable } from "./ServiceTokenTable";
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const ServiceTokenSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const deleteServiceToken = useDeleteServiceToken();
|
||||
export const ServiceTokenSection = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const deleteServiceToken = useDeleteServiceToken();
|
||||
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"createAPIToken",
|
||||
"deleteAPITokenConfirmation"
|
||||
] as const);
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"createAPIToken",
|
||||
"deleteAPITokenConfirmation"
|
||||
] as const);
|
||||
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteServiceToken.mutateAsync(
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id
|
||||
);
|
||||
createNotification({
|
||||
text: "Successfully deleted service token",
|
||||
type: "success"
|
||||
});
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteServiceToken.mutateAsync(
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id
|
||||
);
|
||||
createNotification({
|
||||
text: "Successfully deleted service token",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteAPITokenConfirmation");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete service token",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
handlePopUpClose("deleteAPITokenConfirmation");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete service token",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
{t("section.token.service-tokens")}
|
||||
</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createAPIToken");
|
||||
}}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
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">
|
||||
{t("section.token.service-tokens")}
|
||||
</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createAPIToken");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
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
|
||||
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} service token?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
|
||||
onDeleteApproved={onDeleteApproved}
|
||||
/>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
|
||||
<ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddServiceTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} service token?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteAPITokenConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteAPITokenConfirmation")}
|
||||
onDeleteApproved={onDeleteApproved}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
|
||||
);
|
||||
|
||||
+21
-12
@@ -1,6 +1,7 @@
|
||||
import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetUserWsServiceTokens } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -70,18 +71,26 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Td>
|
||||
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||
name: row.name,
|
||||
id: row._id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteAPITokenConfirmation", {
|
||||
name: row.name,
|
||||
id: row._id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+7
-1
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
@@ -85,11 +86,16 @@ export const AddWebhookForm = ({
|
||||
/>
|
||||
<FormControl
|
||||
label="Secret Path"
|
||||
icon={<GlobPatternExamples />}
|
||||
isRequired
|
||||
isError={Boolean(errors?.secretPath)}
|
||||
errorText={errors?.secretPath?.message}
|
||||
helperText="Glob patterns are used to match multiple files or directories"
|
||||
>
|
||||
<Input placeholder="/, /**/*" {...register("secretPath")} />
|
||||
<Input
|
||||
placeholder="glob pattern / or /**/* or /{dir1,dir2}"
|
||||
{...register("secretPath")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Secret Key"
|
||||
|
||||
+280
-239
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -17,7 +18,8 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateWebhook,
|
||||
@@ -29,252 +31,291 @@ import {
|
||||
|
||||
import { AddWebhookForm, TFormSchema } from "./AddWebhookForm";
|
||||
|
||||
export const WebhooksTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"addWebhook",
|
||||
"deleteWebhook"
|
||||
] as const);
|
||||
export const WebhooksTab = withProjectPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"addWebhook",
|
||||
"deleteWebhook"
|
||||
] as const);
|
||||
|
||||
const { data: webhooks, isLoading: isWebhooksLoading } = useGetWebhooks(workspaceId);
|
||||
const { data: webhooks, isLoading: isWebhooksLoading } = useGetWebhooks(workspaceId);
|
||||
|
||||
// mutation
|
||||
const { mutateAsync: createWebhook } = useCreateWebhook();
|
||||
const {
|
||||
mutateAsync: testWebhook,
|
||||
variables: testWebhookVars,
|
||||
isLoading: isTestWebhookSubmitting
|
||||
} = useTestWebhook();
|
||||
const {
|
||||
mutateAsync: updateWebhook,
|
||||
variables: updateWebhookVars,
|
||||
isLoading: isUpdateWebhookSubmitting
|
||||
} = useUpdateWebhook();
|
||||
const { mutateAsync: deleteWebhook } = useDeleteWebhook();
|
||||
// mutation
|
||||
const { mutateAsync: createWebhook } = useCreateWebhook();
|
||||
const {
|
||||
mutateAsync: testWebhook,
|
||||
variables: testWebhookVars,
|
||||
isLoading: isTestWebhookSubmitting
|
||||
} = useTestWebhook();
|
||||
const {
|
||||
mutateAsync: updateWebhook,
|
||||
variables: updateWebhookVars,
|
||||
isLoading: isUpdateWebhookSubmitting
|
||||
} = useUpdateWebhook();
|
||||
const { mutateAsync: deleteWebhook } = useDeleteWebhook();
|
||||
|
||||
const handleWebhookCreate = async (data: TFormSchema) => {
|
||||
try {
|
||||
await createWebhook({
|
||||
...data,
|
||||
workspaceId
|
||||
});
|
||||
handlePopUpClose("addWebhook");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWebhookCreate = async (data: TFormSchema) => {
|
||||
try {
|
||||
await createWebhook({
|
||||
...data,
|
||||
workspaceId
|
||||
});
|
||||
handlePopUpClose("addWebhook");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebhookDisable = async (webhookId: string, isDisabled: boolean) => {
|
||||
try {
|
||||
await updateWebhook({
|
||||
webhookId,
|
||||
workspaceId,
|
||||
isDisabled
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWebhookDisable = async (webhookId: string, isDisabled: boolean) => {
|
||||
try {
|
||||
await updateWebhook({
|
||||
webhookId,
|
||||
workspaceId,
|
||||
isDisabled
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebhookDelete = async () => {
|
||||
try {
|
||||
const webhookId = popUp?.deleteWebhook?.data as string;
|
||||
await deleteWebhook({
|
||||
webhookId,
|
||||
workspaceId
|
||||
});
|
||||
handlePopUpClose("deleteWebhook");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWebhookDelete = async () => {
|
||||
try {
|
||||
const webhookId = popUp?.deleteWebhook?.data as string;
|
||||
await deleteWebhook({
|
||||
webhookId,
|
||||
workspaceId
|
||||
});
|
||||
handlePopUpClose("deleteWebhook");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebhookTest = async (webhookId: string) => {
|
||||
try {
|
||||
await testWebhook({
|
||||
webhookId,
|
||||
workspaceId
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully triggered webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to trigger webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleWebhookTest = async (webhookId: string) => {
|
||||
try {
|
||||
await testWebhook({
|
||||
webhookId,
|
||||
workspaceId
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully triggered webhook"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to trigger webhook"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addWebhook")}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("settings.webhooks.description")}</p>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Td>URL</Td>
|
||||
<Td>Environment</Td>
|
||||
<Td>Secret Path</Td>
|
||||
<Td>Status</Td>
|
||||
<Td className="text-right">Action</Td>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isWebhooksLoading && <TableSkeleton columns={5} innerKey="webhooks-loading" />}
|
||||
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
|
||||
return (
|
||||
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addWebhook")}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<p className="mb-8 text-gray-400">{t("settings.webhooks.description")}</p>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No webhooks found" icon={faPlug} />
|
||||
</Td>
|
||||
<Td>URL</Td>
|
||||
<Td>Environment</Td>
|
||||
<Td>Secret Path</Td>
|
||||
<Td>Status</Td>
|
||||
<Td className="text-right">Action</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!isWebhooksLoading &&
|
||||
webhooks?.map(
|
||||
({
|
||||
_id: id,
|
||||
url,
|
||||
environment,
|
||||
secretPath,
|
||||
lastStatus,
|
||||
isDisabled,
|
||||
updatedAt,
|
||||
lastRunErrorMessage
|
||||
}) => (
|
||||
<Tr key={id}>
|
||||
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||
{url}
|
||||
</Td>
|
||||
<Td>{environment}</Td>
|
||||
<Td>{secretPath}</Td>
|
||||
<Td>
|
||||
{!lastStatus ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="inline-flex w-min items-center rounded bg-mineshaft-600 px-2 py-0.5 text-sm">
|
||||
{lastStatus}{" "}
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="text-xs">
|
||||
<div>
|
||||
Updated At:{" "}
|
||||
{format(new Date(updatedAt), "yyyy-MM-dd, hh:mm aaa")}
|
||||
</div>
|
||||
{lastRunErrorMessage && (
|
||||
<div className="mt-2 text-red">
|
||||
Error: {lastRunErrorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={`ml-1 ${
|
||||
lastStatus === "failed" ? "text-red" : "text-green"
|
||||
}`}
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button
|
||||
variant="star"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookTest(id)}
|
||||
isDisabled={
|
||||
isTestWebhookSubmitting && testWebhookVars?.webhookId === id
|
||||
}
|
||||
isLoading={isTestWebhookSubmitting && testWebhookVars?.webhookId === id}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookDisable(id, !isDisabled)}
|
||||
isDisabled={
|
||||
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
|
||||
}
|
||||
isLoading={
|
||||
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
|
||||
}
|
||||
>
|
||||
{isDisabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
|
||||
colorSchema="danger"
|
||||
size="xs"
|
||||
onClick={() => handlePopUpOpen("deleteWebhook", id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
</THead>
|
||||
<TBody>
|
||||
{isWebhooksLoading && <TableSkeleton columns={5} innerKey="webhooks-loading" />}
|
||||
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState title="No webhooks found" icon={faPlug} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{!isWebhooksLoading &&
|
||||
webhooks?.map(
|
||||
({
|
||||
_id: id,
|
||||
url,
|
||||
environment,
|
||||
secretPath,
|
||||
lastStatus,
|
||||
isDisabled,
|
||||
updatedAt,
|
||||
lastRunErrorMessage
|
||||
}) => (
|
||||
<Tr key={id}>
|
||||
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||
{url}
|
||||
</Td>
|
||||
<Td>{environment}</Td>
|
||||
<Td>{secretPath}</Td>
|
||||
<Td>
|
||||
{!lastStatus ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="inline-flex w-min items-center rounded bg-mineshaft-600 px-2 py-0.5 text-sm">
|
||||
{lastStatus}{" "}
|
||||
<Tooltip
|
||||
content={
|
||||
<div className="text-xs">
|
||||
<div>
|
||||
Updated At:{" "}
|
||||
{format(new Date(updatedAt), "yyyy-MM-dd, hh:mm aaa")}
|
||||
</div>
|
||||
{lastRunErrorMessage && (
|
||||
<div className="mt-2 text-red">
|
||||
Error: {lastRunErrorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={`ml-1 ${
|
||||
lastStatus === "failed" ? "text-red" : "text-green"
|
||||
}`}
|
||||
icon={faInfoCircle}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="star"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookTest(id)}
|
||||
isDisabled={
|
||||
(isTestWebhookSubmitting &&
|
||||
testWebhookVars?.webhookId === id) ||
|
||||
!isAllowed
|
||||
}
|
||||
isLoading={
|
||||
isTestWebhookSubmitting && testWebhookVars?.webhookId === id
|
||||
}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => handleWebhookDisable(id, !isDisabled)}
|
||||
isDisabled={
|
||||
(isUpdateWebhookSubmitting &&
|
||||
updateWebhookVars?.webhookId === id) ||
|
||||
!isAllowed
|
||||
}
|
||||
isLoading={
|
||||
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
|
||||
}
|
||||
>
|
||||
{isDisabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Webhooks}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
|
||||
colorSchema="danger"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("deleteWebhook", id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<AddWebhookForm
|
||||
environments={currentWorkspace?.environments}
|
||||
isOpen={popUp?.addWebhook?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addWebhook", isOpen)}
|
||||
onCreateWebhook={handleWebhookCreate}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteWebhook.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Are you sure you want to delete this webhook?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteWebhook", isOpen)}
|
||||
onClose={() => handlePopUpClose("deleteWebhook")}
|
||||
onDeleteApproved={handleWebhookDelete}
|
||||
/>
|
||||
</div>
|
||||
<AddWebhookForm
|
||||
environments={currentWorkspace?.environments}
|
||||
isOpen={popUp?.addWebhook?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addWebhook", isOpen)}
|
||||
onCreateWebhook={handleWebhookCreate}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteWebhook.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Are you sure you want to delete this webhook?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteWebhook", isOpen)}
|
||||
onClose={() => handlePopUpClose("deleteWebhook")}
|
||||
onDeleteApproved={handleWebhookDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Webhooks }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user