mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 13:22:08 +00:00
Merge branch 'main' into ENG-3009
This commit is contained in:
@@ -58,6 +58,7 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
|
||||
}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className="max-w-2xl"
|
||||
bodyClassName="overflow-visible"
|
||||
subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."}
|
||||
>
|
||||
<Content
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Spinner, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { SecretSync, useSecretSyncOptions } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { UpgradePlanModal } from "../license/UpgradePlanModal";
|
||||
@@ -19,6 +20,26 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
|
||||
initPerPage: 16
|
||||
});
|
||||
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
secretSyncOptions?.filter(
|
||||
({ name, destination }) =>
|
||||
name?.toLowerCase().includes(search.trim().toLowerCase()) ||
|
||||
destination.toLowerCase().includes(search.toLowerCase())
|
||||
) ?? [],
|
||||
[secretSyncOptions, search]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredOptions.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
@@ -29,75 +50,103 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{secretSyncOptions?.map(({ destination, enterprise }) => {
|
||||
const { image, name } = SECRET_SYNC_MAP[destination];
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseSecretSyncs
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
: onSelect(destination)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
height={40}
|
||||
width={40}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search options..."
|
||||
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
|
||||
/>
|
||||
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
|
||||
{filteredOptions.slice(offset, perPage * page)?.map(({ destination, enterprise }) => {
|
||||
const { image, name } = SECRET_SYNC_MAP[destination];
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseSecretSyncs
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
: onSelect(destination)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
height={40}
|
||||
width={40}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!filteredOptions?.length && (
|
||||
<EmptyState
|
||||
className="col-span-full mt-40"
|
||||
title="No Secret Syncs match search"
|
||||
icon={faSearch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{Boolean(filteredOptions.length) && (
|
||||
<Pagination
|
||||
startAdornment={
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more services.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
service you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
|
||||
<span className="text-xs">
|
||||
Don't see the third-party service you're looking for?
|
||||
</span>
|
||||
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
count={filteredOptions.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
perPageList={[16]}
|
||||
/>
|
||||
)}
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use every Secret Sync if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more services.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
service you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4 hover:bg-mineshaft-900/50">
|
||||
<FontAwesomeIcon className="mt-auto text-3xl" icon={faWrench} />
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+23
-1
@@ -30,7 +30,29 @@ export const AwsParameterStoreSyncFields = () => {
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Path">
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Path"
|
||||
tooltipText={
|
||||
<>
|
||||
The path is required and will be prepended to the key schema. For example, if you
|
||||
have a path of{" "}
|
||||
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
|
||||
/demo/path/
|
||||
</code>{" "}
|
||||
and a key schema of{" "}
|
||||
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
|
||||
INFISICAL_{"{{secretKey}}"}
|
||||
</code>
|
||||
, then the result will be{" "}
|
||||
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
|
||||
/demo/path/INFISICAL_{"{{secretKey}}"}
|
||||
</code>
|
||||
</>
|
||||
}
|
||||
tooltipClassName="max-w-lg"
|
||||
>
|
||||
<Input value={value} onChange={onChange} placeholder="Path..." />
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ export const GcpSyncFields = () => {
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content="Ensure that you've enabled the Secret Manager API and Cloud Resource Manager API on your GCP project. Additionally, make sure that the service account is assigned the appropriate GCP roles."
|
||||
content="Ensure that you've enabled the Secret Manager API, Cloud Resource Manager API, and Service Usage API on your GCP project. Additionally, make sure that the service account is assigned the appropriate GCP roles."
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the project you're looking for?</span>{" "}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props = {
|
||||
@@ -7,6 +9,7 @@ type Props = {
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
truncate?: boolean;
|
||||
icon?: IconDefinition;
|
||||
};
|
||||
|
||||
export const GenericFieldLabel = ({
|
||||
@@ -14,11 +17,15 @@ export const GenericFieldLabel = ({
|
||||
children,
|
||||
className,
|
||||
labelClassName,
|
||||
truncate
|
||||
truncate,
|
||||
icon
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className={twMerge("min-w-0", className)}>
|
||||
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{icon && <FontAwesomeIcon icon={icon} className="text-mineshaft-400" size="sm" />}
|
||||
<p className={twMerge("text-xs font-medium text-mineshaft-400", labelClassName)}>{label}</p>
|
||||
</div>
|
||||
{children ? (
|
||||
<p className={twMerge("text-sm text-mineshaft-100", truncate && "truncate")}>{children}</p>
|
||||
) : (
|
||||
|
||||
@@ -163,7 +163,7 @@ const PasswordGeneratorModal = ({
|
||||
<div className="mb-6 flex flex-row justify-between gap-2">
|
||||
<Checkbox
|
||||
id="useUppercase"
|
||||
className="mr-2 data-[state=checked]:bg-primary"
|
||||
className="mr-2"
|
||||
isChecked={passwordOptions.useUppercase}
|
||||
onCheckedChange={(checked) =>
|
||||
setPasswordOptions({ ...passwordOptions, useUppercase: checked as boolean })
|
||||
@@ -174,7 +174,7 @@ const PasswordGeneratorModal = ({
|
||||
|
||||
<Checkbox
|
||||
id="useLowercase"
|
||||
className="mr-2 data-[state=checked]:bg-primary"
|
||||
className="mr-2"
|
||||
isChecked={passwordOptions.useLowercase}
|
||||
onCheckedChange={(checked) =>
|
||||
setPasswordOptions({ ...passwordOptions, useLowercase: checked as boolean })
|
||||
@@ -185,7 +185,7 @@ const PasswordGeneratorModal = ({
|
||||
|
||||
<Checkbox
|
||||
id="useNumbers"
|
||||
className="mr-2 data-[state=checked]:bg-primary"
|
||||
className="mr-2"
|
||||
isChecked={passwordOptions.useNumbers}
|
||||
onCheckedChange={(checked) =>
|
||||
setPasswordOptions({ ...passwordOptions, useNumbers: checked as boolean })
|
||||
@@ -196,7 +196,7 @@ const PasswordGeneratorModal = ({
|
||||
|
||||
<Checkbox
|
||||
id="useSpecialChars"
|
||||
className="mr-2 data-[state=checked]:bg-primary"
|
||||
className="mr-2"
|
||||
isChecked={passwordOptions.useSpecialChars}
|
||||
onCheckedChange={(checked) =>
|
||||
setPasswordOptions({ ...passwordOptions, useSpecialChars: checked as boolean })
|
||||
|
||||
@@ -11,5 +11,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
|
||||
[IdentityAuthMethod.OCI_AUTH]: "OCI Auth",
|
||||
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
|
||||
[IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth",
|
||||
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
|
||||
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth",
|
||||
[IdentityAuthMethod.TLS_CERT_AUTH]: "TLS Certificate Auth"
|
||||
};
|
||||
|
||||
@@ -9,7 +9,8 @@ export enum IdentityAuthMethod {
|
||||
OCI_AUTH = "oci-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
LDAP_AUTH = "ldap-auth",
|
||||
JWT_AUTH = "jwt-auth"
|
||||
JWT_AUTH = "jwt-auth",
|
||||
TLS_CERT_AUTH = "tls-cert-auth"
|
||||
}
|
||||
|
||||
export enum IdentityJwtConfigurationType {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AddIdentityLdapAuthDTO,
|
||||
AddIdentityOciAuthDTO,
|
||||
AddIdentityOidcAuthDTO,
|
||||
AddIdentityTlsCertAuthDTO,
|
||||
AddIdentityTokenAuthDTO,
|
||||
AddIdentityUniversalAuthDTO,
|
||||
ClientSecretData,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
DeleteIdentityLdapAuthDTO,
|
||||
DeleteIdentityOciAuthDTO,
|
||||
DeleteIdentityOidcAuthDTO,
|
||||
DeleteIdentityTlsCertAuthDTO,
|
||||
DeleteIdentityTokenAuthDTO,
|
||||
DeleteIdentityUniversalAuthClientSecretDTO,
|
||||
DeleteIdentityUniversalAuthDTO,
|
||||
@@ -46,6 +48,7 @@ import {
|
||||
IdentityLdapAuth,
|
||||
IdentityOciAuth,
|
||||
IdentityOidcAuth,
|
||||
IdentityTlsCertAuth,
|
||||
IdentityTokenAuth,
|
||||
IdentityUniversalAuth,
|
||||
RevokeTokenDTO,
|
||||
@@ -60,6 +63,7 @@ import {
|
||||
UpdateIdentityLdapAuthDTO,
|
||||
UpdateIdentityOciAuthDTO,
|
||||
UpdateIdentityOidcAuthDTO,
|
||||
UpdateIdentityTlsCertAuthDTO,
|
||||
UpdateIdentityTokenAuthDTO,
|
||||
UpdateIdentityUniversalAuthDTO,
|
||||
UpdateTokenIdentityTokenAuthDTO
|
||||
@@ -655,6 +659,107 @@ export const useDeleteIdentityAliCloudAuth = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityTlsCertAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityTlsCertAuth, object, AddIdentityTlsCertAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
allowedCommonNames,
|
||||
caCertificate,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityTlsCertAuth }
|
||||
} = await apiRequest.post<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
|
||||
`/api/v1/auth/tls-cert-auth/identities/${identityId}`,
|
||||
{
|
||||
allowedCommonNames,
|
||||
caCertificate,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityTlsCertAuth;
|
||||
},
|
||||
onSuccess: (_, { identityId, organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityTlsCertAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityTlsCertAuth, object, UpdateIdentityTlsCertAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
allowedCommonNames,
|
||||
caCertificate,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityTlsCertAuth }
|
||||
} = await apiRequest.patch<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
|
||||
`/api/v1/auth/tls-cert-auth/identities/${identityId}`,
|
||||
{
|
||||
caCertificate,
|
||||
allowedCommonNames,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityTlsCertAuth;
|
||||
},
|
||||
onSuccess: (_, { identityId, organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIdentityTlsCertAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityTlsCertAuth, object, DeleteIdentityTlsCertAuthDTO>({
|
||||
mutationFn: async ({ identityId }) => {
|
||||
const {
|
||||
data: { identityTlsCertAuth }
|
||||
} = await apiRequest.delete(`/api/v1/auth/tls-cert-auth/identities/${identityId}`);
|
||||
return identityTlsCertAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId, identityId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityOidcAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityOidcAuth, object, UpdateIdentityOidcAuthDTO>({
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
IdentityMembershipOrg,
|
||||
IdentityOciAuth,
|
||||
IdentityOidcAuth,
|
||||
IdentityTlsCertAuth,
|
||||
IdentityTokenAuth,
|
||||
IdentityUniversalAuth,
|
||||
TSearchIdentitiesDTO
|
||||
@@ -34,6 +35,8 @@ export const identitiesKeys = {
|
||||
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
|
||||
getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const,
|
||||
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
|
||||
getIdentityTlsCertAuth: (identityId: string) =>
|
||||
[{ identityId }, "identity-tls-cert-auth"] as const,
|
||||
getIdentityAliCloudAuth: (identityId: string) =>
|
||||
[{ identityId }, "identity-alicloud-auth"] as const,
|
||||
getIdentityOciAuth: (identityId: string) => [{ identityId }, "identity-oci-auth"] as const,
|
||||
@@ -175,6 +178,27 @@ export const useGetIdentityAwsAuth = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityTlsCertAuth = (
|
||||
identityId: string,
|
||||
options?: TReactQueryOptions["options"]
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityTlsCertAuth }
|
||||
} = await apiRequest.get<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
|
||||
`/api/v1/auth/tls-cert-auth/identities/${identityId}`
|
||||
);
|
||||
return identityTlsCertAuth;
|
||||
},
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
...options,
|
||||
enabled: Boolean(identityId) && (options?.enabled ?? true)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityOciAuth = (
|
||||
identityId: string,
|
||||
options?: TReactQueryOptions["options"]
|
||||
|
||||
@@ -485,6 +485,47 @@ export type DeleteIdentityKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type IdentityTlsCertAuth = {
|
||||
identityId: string;
|
||||
caCertificate: string;
|
||||
allowedCommonNames: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
};
|
||||
|
||||
export type AddIdentityTlsCertAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
caCertificate: string;
|
||||
allowedCommonNames?: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateIdentityTlsCertAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
caCertificate: string;
|
||||
allowedCommonNames?: string | null;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type DeleteIdentityTlsCertAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type CreateIdentityUniversalAuthClientSecretDTO = {
|
||||
identityId: string;
|
||||
description?: string;
|
||||
|
||||
@@ -446,7 +446,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isDisabled={Boolean(cert)}
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
@@ -481,7 +480,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isDisabled={Boolean(cert)}
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
|
||||
-2
@@ -405,7 +405,6 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
@@ -439,7 +438,6 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
|
||||
+1
-8
@@ -408,7 +408,6 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
@@ -443,7 +442,6 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
@@ -477,12 +475,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
errorText={error?.message}
|
||||
tooltipText="If enabled, a new certificate will be issued automatically X days before the current certificate expires."
|
||||
>
|
||||
<Checkbox
|
||||
id="enableAutoRenewal"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
>
|
||||
<Checkbox id="enableAutoRenewal" isChecked={value} onCheckedChange={onChange}>
|
||||
Enable Certificate Auto Renewal
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
|
||||
@@ -333,7 +333,6 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
@@ -367,7 +366,6 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
|
||||
@@ -145,7 +145,6 @@ const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
|
||||
+11
@@ -17,6 +17,7 @@ import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
|
||||
import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm";
|
||||
import { IdentityOciAuthForm } from "./IdentityOciAuthForm";
|
||||
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
|
||||
import { IdentityTlsCertAuthForm } from "./IdentityTlsCertAuthForm";
|
||||
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
|
||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||
|
||||
@@ -52,6 +53,7 @@ const identityAuthMethods = [
|
||||
{ label: "OCI Auth", value: IdentityAuthMethod.OCI_AUTH },
|
||||
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH },
|
||||
{ label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH },
|
||||
{ label: "TLS Certificate Auth", value: IdentityAuthMethod.TLS_CERT_AUTH },
|
||||
{
|
||||
label: "JWT Auth",
|
||||
value: IdentityAuthMethod.JWT_AUTH
|
||||
@@ -123,6 +125,15 @@ export const IdentityAuthMethodModalContent = ({
|
||||
/>
|
||||
)
|
||||
},
|
||||
[IdentityAuthMethod.TLS_CERT_AUTH]: {
|
||||
render: () => (
|
||||
<IdentityTlsCertAuthForm
|
||||
identityId={identityAuthMethodData.identityId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.OIDC_AUTH]: {
|
||||
render: () => (
|
||||
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityTlsCertAuth,
|
||||
useGetIdentityTlsCertAuth,
|
||||
useUpdateIdentityTlsCertAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityFormTab } from "./types";
|
||||
|
||||
const schema = z.object({
|
||||
allowedCommonNames: z.string().optional(),
|
||||
caCertificate: z.string().min(1),
|
||||
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||
message: "Access Token TTL cannot be greater than 315360000"
|
||||
}),
|
||||
accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||
message: "Access Token Max TTL cannot be greater than 315360000"
|
||||
}),
|
||||
accessTokenNumUsesLimit: z.string(),
|
||||
accessTokenTrustedIps: z
|
||||
.array(
|
||||
z.object({
|
||||
ipAddress: z.string().max(50)
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
identityId?: string;
|
||||
isUpdate?: boolean;
|
||||
};
|
||||
|
||||
export const IdentityTlsCertAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityId,
|
||||
isUpdate
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityTlsCertAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTlsCertAuth();
|
||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||
|
||||
const { data } = useGetIdentityTlsCertAuth(identityId ?? "", {
|
||||
enabled: isUpdate
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
caCertificate: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
remove: removeAccessTokenTrustedIp
|
||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
caCertificate: data.caCertificate,
|
||||
allowedCommonNames: data.allowedCommonNames || undefined,
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
|
||||
({ ipAddress, prefix }: IdentityTrustedIp) => {
|
||||
return {
|
||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
};
|
||||
}
|
||||
)
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
caCertificate: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
caCertificate,
|
||||
allowedCommonNames,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!identityId) return;
|
||||
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
organizationId: orgId,
|
||||
caCertificate,
|
||||
allowedCommonNames: allowedCommonNames || null,
|
||||
identityId,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
caCertificate,
|
||||
allowedCommonNames: allowedCommonNames || undefined,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||
<TabList>
|
||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCertificate"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="A PEM-encoded CA certificate. This will be used to validate client certificate."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedCommonNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Common Names"
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
tooltipText="Comma separated common names allowed to authenticate against the identity. Leave empty to allow any certificate."
|
||||
>
|
||||
<Input {...field} placeholder="" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
tooltipText="The lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Add"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+131
-76
@@ -1,11 +1,12 @@
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { Spinner, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useAppConnectionOptions } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
@@ -19,6 +20,26 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
|
||||
initPerPage: 16
|
||||
});
|
||||
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
appConnectionOptions?.filter(
|
||||
({ name, app }) =>
|
||||
name?.toLowerCase().includes(search.trim().toLowerCase()) ||
|
||||
app.toLowerCase().includes(search.toLowerCase())
|
||||
) ?? [],
|
||||
[appConnectionOptions, search]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredOptions.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
@@ -29,86 +50,120 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{appConnectionOptions?.map((option) => {
|
||||
const { image, name, size = 50, enterprise = false, icon } = APP_CONNECTION_MAP[option.app];
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search options..."
|
||||
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
|
||||
/>
|
||||
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
|
||||
{filteredOptions.slice(offset, perPage * page)?.map((option) => {
|
||||
const {
|
||||
image,
|
||||
name,
|
||||
size = 50,
|
||||
enterprise = false,
|
||||
icon
|
||||
} = APP_CONNECTION_MAP[option.app];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseAppConnections
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
: onSelect(option.app)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
style={{
|
||||
width: `${size}px`
|
||||
}}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
className="absolute -bottom-1.5 -right-1.5 text-primary-700"
|
||||
size="xl"
|
||||
icon={icon}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseAppConnections
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
: onSelect(option.app)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
style={{
|
||||
width: `${size}px`
|
||||
}}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
className="absolute -bottom-1.5 -right-1.5 text-primary-700"
|
||||
size="xl"
|
||||
icon={icon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!filteredOptions?.length && (
|
||||
<EmptyState
|
||||
className="col-span-full mt-40"
|
||||
title="No App Connections match search"
|
||||
icon={faSearch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{Boolean(filteredOptions.length) && (
|
||||
<Pagination
|
||||
startAdornment={
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more services.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
service you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
|
||||
<span className="text-xs">
|
||||
Don't see the third-party service you're looking for?
|
||||
</span>
|
||||
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
count={filteredOptions.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
perPageList={[16]}
|
||||
/>
|
||||
)}
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use every App Connection if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more connections.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
app you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+7
@@ -15,6 +15,7 @@ import {
|
||||
useDeleteIdentityLdapAuth,
|
||||
useDeleteIdentityOciAuth,
|
||||
useDeleteIdentityOidcAuth,
|
||||
useDeleteIdentityTlsCertAuth,
|
||||
useDeleteIdentityTokenAuth,
|
||||
useDeleteIdentityUniversalAuth
|
||||
} from "@app/hooks/api";
|
||||
@@ -29,6 +30,7 @@ import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthC
|
||||
import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent";
|
||||
import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent";
|
||||
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
|
||||
import { ViewIdentityTlsCertAuthContent } from "./ViewIdentityTlsCertAuthContent";
|
||||
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
|
||||
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
|
||||
|
||||
@@ -63,6 +65,7 @@ export const Content = ({
|
||||
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
|
||||
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
|
||||
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
|
||||
const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth();
|
||||
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
|
||||
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
|
||||
const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth();
|
||||
@@ -93,6 +96,10 @@ export const Content = ({
|
||||
revokeMethod = revokeGcpAuth;
|
||||
Component = ViewIdentityGcpAuthContent;
|
||||
break;
|
||||
case IdentityAuthMethod.TLS_CERT_AUTH:
|
||||
revokeMethod = revokeTlsCertAuth;
|
||||
Component = ViewIdentityTlsCertAuthContent;
|
||||
break;
|
||||
case IdentityAuthMethod.AWS_AUTH:
|
||||
revokeMethod = revokeAwsAuth;
|
||||
Component = ViewIdentityAwsAuthContent;
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useGetIdentityTlsCertAuth } from "@app/hooks/api";
|
||||
import { IdentityTlsCertAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm";
|
||||
|
||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
||||
import { ViewAuthMethodProps } from "./types";
|
||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
||||
|
||||
export const ViewIdentityTlsCertAuthContent = ({
|
||||
identityId,
|
||||
handlePopUpToggle,
|
||||
handlePopUpOpen,
|
||||
onDelete,
|
||||
popUp
|
||||
}: ViewAuthMethodProps) => {
|
||||
const { data, isPending } = useGetIdentityTlsCertAuth(identityId);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Spinner className="text-mineshaft-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={faBan}
|
||||
title="Could not find TLS Certificate Auth associated with this Identity."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (popUp.identityAuthMethod.isOpen) {
|
||||
return (
|
||||
<IdentityTlsCertAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
{data.accessTokenNumUsesLimit}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCertificate}</p>}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Common Names">
|
||||
{data.allowedCommonNames
|
||||
?.split(",")
|
||||
.map((cn) => cn.trim())
|
||||
.join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
</ViewIdentityContentWrapper>
|
||||
);
|
||||
};
|
||||
+29
-2
@@ -18,6 +18,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { isCustomProjectRole } from "@app/helpers/roles";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
@@ -53,7 +55,8 @@ import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/R
|
||||
|
||||
enum RolesOrderBy {
|
||||
Name = "name",
|
||||
Slug = "slug"
|
||||
Slug = "slug",
|
||||
Type = "type"
|
||||
}
|
||||
|
||||
export const ProjectRoleList = () => {
|
||||
@@ -98,7 +101,7 @@ export const ProjectRoleList = () => {
|
||||
setPerPage,
|
||||
setPage,
|
||||
offset
|
||||
} = usePagination<RolesOrderBy>(RolesOrderBy.Name, {
|
||||
} = usePagination<RolesOrderBy>(RolesOrderBy.Type, {
|
||||
initPerPage: getUserTablePreference("projectRolesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
@@ -125,6 +128,12 @@ export const ProjectRoleList = () => {
|
||||
switch (orderBy) {
|
||||
case RolesOrderBy.Slug:
|
||||
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
|
||||
case RolesOrderBy.Type: {
|
||||
const roleOneValue = isCustomProjectRole(roleOne.slug) ? -1 : 1;
|
||||
const roleTwoValue = isCustomProjectRole(roleTwo.slug) ? -1 : 1;
|
||||
|
||||
return roleTwoValue - roleOneValue;
|
||||
}
|
||||
case RolesOrderBy.Name:
|
||||
default:
|
||||
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
|
||||
@@ -210,6 +219,19 @@ export const ProjectRoleList = () => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Type
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Type)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Type)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Type)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th aria-label="actions" className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -237,6 +259,11 @@ export const ProjectRoleList = () => {
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td>
|
||||
<Badge className="w-min whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
|
||||
{isCustomProjectRole(slug) ? "Custom" : "Default"}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
|
||||
-1
@@ -284,7 +284,6 @@ const ServiceTokenForm = () => {
|
||||
<Checkbox
|
||||
id={String(value[optionValue])}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
isDisabled={optionValue === "read"}
|
||||
onCheckedChange={(state) => {
|
||||
|
||||
+1
-3
@@ -247,9 +247,7 @@ export const AccessApprovalRequest = ({
|
||||
};
|
||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||
displayData = {
|
||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||
}`,
|
||||
label: "Pending Additional Reviews",
|
||||
type: "primary",
|
||||
icon: faClipboardCheck
|
||||
};
|
||||
|
||||
+201
-174
@@ -1,10 +1,9 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faCircle,
|
||||
faTriangleExclamation,
|
||||
faUsers,
|
||||
faXmarkCircle
|
||||
faBan,
|
||||
faCheck,
|
||||
faHourglass,
|
||||
faTriangleExclamation
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import ms from "ms";
|
||||
@@ -15,12 +14,10 @@ import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
GenericFieldLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Badge } from "@app/components/v2/Badge";
|
||||
@@ -38,10 +35,22 @@ import { groupBy } from "@app/lib/fn/array";
|
||||
|
||||
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
|
||||
if (status === ApprovalStatus.APPROVED)
|
||||
return <FontAwesomeIcon icon={faCheckCircle} size="xs" style={{ color: "#15803d" }} />;
|
||||
return (
|
||||
<Badge variant="success" className="flex h-4 items-center justify-center">
|
||||
<FontAwesomeIcon icon={faCheck} size="xs" />
|
||||
</Badge>
|
||||
);
|
||||
if (status === ApprovalStatus.REJECTED)
|
||||
return <FontAwesomeIcon icon={faXmarkCircle} size="xs" style={{ color: "#b91c1c" }} />;
|
||||
return <FontAwesomeIcon icon={faCircle} size="xs" style={{ color: "#c2410c" }} />;
|
||||
return (
|
||||
<Badge variant="danger" className="flex h-4 items-center justify-center">
|
||||
<FontAwesomeIcon icon={faBan} size="xs" />
|
||||
</Badge>
|
||||
);
|
||||
return (
|
||||
<Badge variant="primary" className="flex h-4 items-center justify-center">
|
||||
<FontAwesomeIcon icon={faHourglass} size="xs" />
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReviewAccessRequestModal = ({
|
||||
@@ -267,139 +276,160 @@ export const ReviewAccessRequestModal = ({
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="mb-2 mt-4 text-mineshaft-200">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Environment</div>
|
||||
<div>{accessDetails.env || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Secret Path</div>
|
||||
<div>{accessDetails.secretPath || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Access Type</div>
|
||||
<div>{getAccessLabel()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Permission</div>
|
||||
<div>{requestedAccess}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Note</div>
|
||||
<div>{request.note || "-"}</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-8">
|
||||
<GenericFieldLabel label="Environment">{accessDetails.env}</GenericFieldLabel>
|
||||
<GenericFieldLabel truncate label="Secret Path">
|
||||
{accessDetails.secretPath}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Access Type">{getAccessLabel()}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Permission">{requestedAccess}</GenericFieldLabel>
|
||||
{request.note && (
|
||||
<GenericFieldLabel className="col-span-full" label="Note">
|
||||
{request.note}
|
||||
</GenericFieldLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
||||
<div className="thin-scrollbar max-h-64 overflow-y-auto rounded p-2">
|
||||
{approverSequence?.approvers?.map((approver, index) => (
|
||||
<div
|
||||
key={`approval-list-${index + 1}`}
|
||||
className={twMerge(
|
||||
"relative mb-2 flex items-center rounded border border-mineshaft-500 bg-mineshaft-700 p-4",
|
||||
approverSequence?.currentSequence !== approver.sequence &&
|
||||
!hasApproved &&
|
||||
"text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between border-b-2 border-mineshaft-500 py-2">
|
||||
<span>Approvers</span>
|
||||
{approverSequence.isMyReviewInThisSequence &&
|
||||
request.status === ApprovalStatus.PENDING && (
|
||||
<Badge variant="primary" className="h-min">
|
||||
Awaiting Your Review
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="thin-scrollbar max-h-[40vh] overflow-y-auto rounded py-2">
|
||||
{approverSequence?.approvers &&
|
||||
approverSequence.approvers.map((approver, index) => {
|
||||
const isInactive =
|
||||
approverSequence?.currentSequence <
|
||||
(approver.sequence ?? approverSequence.approvers!.length);
|
||||
|
||||
const isPending = approverSequence?.currentSequence === approver.sequence;
|
||||
|
||||
let StepComponent: ReactNode;
|
||||
let BadgeComponent: ReactNode = null;
|
||||
if (approver.hasRejected) {
|
||||
StepComponent = (
|
||||
<Badge
|
||||
variant="danger"
|
||||
className="flex h-6 min-w-6 items-center justify-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
</Badge>
|
||||
);
|
||||
BadgeComponent = <Badge variant="danger">Rejected</Badge>;
|
||||
} else if (approver.hasApproved) {
|
||||
StepComponent = (
|
||||
<Badge
|
||||
variant="success"
|
||||
className="flex h-6 min-w-6 items-center justify-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</Badge>
|
||||
);
|
||||
BadgeComponent = <Badge variant="success">Approved</Badge>;
|
||||
} else if (isPending) {
|
||||
StepComponent = (
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="flex h-6 min-w-6 items-center justify-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faHourglass} />
|
||||
</Badge>
|
||||
);
|
||||
BadgeComponent = <Badge variant="primary">Pending</Badge>;
|
||||
} else {
|
||||
StepComponent = (
|
||||
<Badge
|
||||
className={
|
||||
isInactive
|
||||
? "py-auto my-auto flex h-6 min-w-6 items-center justify-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-center text-bunker-200"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<span>{index + 1}</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"mr-8 flex h-8 w-8 items-center justify-center text-3xl font-medium",
|
||||
approver.hasApproved && "border-green-400 text-green-400",
|
||||
approver.hasRejected && "border-red-500 text-red-500"
|
||||
key={`approval-list-${index + 1}`}
|
||||
className={twMerge("flex", isInactive && "opacity-50")}
|
||||
>
|
||||
{approverSequence.approvers!.length > 1 && (
|
||||
<div className="flex w-12 flex-col items-center gap-2 pr-4">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-grow border-mineshaft-600",
|
||||
index !== 0 && "border-r"
|
||||
)}
|
||||
/>
|
||||
{StepComponent}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-grow border-mineshaft-600",
|
||||
index < approverSequence.approvers!.length - 1 && "border-r"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
{index !== (approverSequence?.approvers?.length || 0) - 1 && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute bottom-0 left-8 h-5 border-r-2 border-gray-400",
|
||||
approver.hasApproved && "border-green-400",
|
||||
approver.hasRejected && "border-red-500"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute left-8 top-0 h-5 border-r-2 border-gray-400",
|
||||
approver.hasApproved && "border-green-400",
|
||||
approver.hasRejected && "border-red-500"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid flex-grow grid-cols-3">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
||||
<div>
|
||||
{approver?.user
|
||||
?.map(
|
||||
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
||||
)
|
||||
.join(",") || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
||||
<div>
|
||||
{approver?.group
|
||||
?.map(
|
||||
(el) =>
|
||||
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
|
||||
)
|
||||
.join(",") || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
||||
<div>{approver.approvals || "-"}</div>
|
||||
</div>
|
||||
<div className="ml-16">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent hideCloseBtn className="pt-3">
|
||||
<div>
|
||||
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
||||
<div className="thin-scrollbar flex max-h-64 flex-col gap-1 overflow-y-auto rounded">
|
||||
{approver.reviewers.map((el, idx) => (
|
||||
<div
|
||||
key={`reviewer-${idx + 1}`}
|
||||
className="flex items-center gap-2 bg-mineshaft-700 p-1 text-sm"
|
||||
>
|
||||
<div className="flex-grow">{el.username}</div>
|
||||
<Tooltip
|
||||
content={`Status: ${el?.status || ApprovalStatus.PENDING}`}
|
||||
>
|
||||
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
||||
</Tooltip>
|
||||
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||
<GenericFieldLabel className="col-span-2" label="Users">
|
||||
{approver?.user
|
||||
?.map(
|
||||
(el) => approverSequence?.membersGroupById?.[el.id]?.[0]?.user?.username
|
||||
)
|
||||
.join(", ")}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-2" label="Groups">
|
||||
{approver?.group
|
||||
?.map(
|
||||
(el) =>
|
||||
approverSequence?.projectGroupsGroupById?.[el.id]?.[0]?.group?.name
|
||||
)
|
||||
.join(", ")}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Approvals Required">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{approver.approvals}</span>
|
||||
{BadgeComponent && (
|
||||
<Tooltip
|
||||
className="max-w-lg"
|
||||
content={
|
||||
<div>
|
||||
<div className="mb-1 text-sm text-bunker-300">Reviewers</div>
|
||||
<div className="thin-scrollbar flex max-h-64 flex-col divide-y divide-mineshaft-500 overflow-y-auto rounded">
|
||||
{approver.reviewers.map((el, idx) => (
|
||||
<div
|
||||
key={`reviewer-${idx + 1}`}
|
||||
className="flex items-center gap-2 px-2 py-2 text-sm"
|
||||
>
|
||||
<div className="flex-1">{el.username}</div>
|
||||
{getReviewedStatusSymbol(el?.status as ApprovalStatus)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<div>{BadgeComponent}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{approverSequence.isMyReviewInThisSequence &&
|
||||
request.status === ApprovalStatus.PENDING && (
|
||||
<div className="mb-4 rounded-r border-l-2 border-l-primary-400 bg-mineshaft-300/5 px-4 py-2.5 text-sm">
|
||||
Awaiting review from you.
|
||||
</div>
|
||||
)}
|
||||
{shouldBlockRequestActions ? (
|
||||
<div
|
||||
className={twMerge(
|
||||
"mb-4 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
||||
"mt-2 rounded-r border-l-2 border-l-red-500 bg-mineshaft-300/5 px-4 py-2.5 text-sm",
|
||||
isReviewedByMe && "border-l-green-400",
|
||||
!approverSequence.isMyReviewInThisSequence && "border-l-primary-400",
|
||||
hasRejected && "border-l-red-500"
|
||||
@@ -409,41 +439,6 @@ export const ReviewAccessRequestModal = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
isLoading={isLoading === "approved"}
|
||||
isDisabled={
|
||||
Boolean(isLoading) ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("approved")}
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
|
||||
>
|
||||
Approve Request
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading === "rejected"}
|
||||
isDisabled={
|
||||
!!isLoading ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("rejected")}
|
||||
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||
size="sm"
|
||||
>
|
||||
Reject Request
|
||||
</Button>
|
||||
</div>
|
||||
{isSoftEnforcement &&
|
||||
request.isRequestedByCurrentUser &&
|
||||
!(request.isApprover && request.isSelfApproveAllowed) &&
|
||||
@@ -453,11 +448,7 @@ export const ReviewAccessRequestModal = ({
|
||||
onCheckedChange={(checked) => setBypassApproval(checked === true)}
|
||||
isChecked={bypassApproval}
|
||||
id="byPassApproval"
|
||||
checkIndicatorBg="text-white"
|
||||
className={twMerge(
|
||||
"mr-2",
|
||||
bypassApproval ? "border-red bg-red hover:bg-red-600" : ""
|
||||
)}
|
||||
className={twMerge("mr-2", bypassApproval ? "border-red/30 bg-red/10" : "")}
|
||||
>
|
||||
<span className="text-xs text-red">
|
||||
Approve without waiting for requirements to be met (bypass policy
|
||||
@@ -481,6 +472,42 @@ export const ReviewAccessRequestModal = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
isLoading={isLoading === "approved"}
|
||||
isDisabled={
|
||||
Boolean(isLoading) ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("approved")}
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
variant="outline_bg"
|
||||
colorSchema={!request.isApprover && isSoftEnforcement ? "danger" : "primary"}
|
||||
>
|
||||
Approve Request
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading === "rejected"}
|
||||
isDisabled={
|
||||
!!isLoading ||
|
||||
(!(
|
||||
request.isApprover &&
|
||||
(!request.isRequestedByCurrentUser || request.isSelfApproveAllowed)
|
||||
) &&
|
||||
!bypassApproval)
|
||||
}
|
||||
onClick={() => handleReview("rejected")}
|
||||
className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200"
|
||||
size="sm"
|
||||
>
|
||||
Reject Request
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+40
-19
@@ -1,5 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faClipboardCheck,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faTrash,
|
||||
faUser,
|
||||
faUserGroup
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -179,26 +186,40 @@ export const ApprovalPolicyRow = ({
|
||||
}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="mb-4 border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
||||
<div className="border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
||||
{labels?.map((el, index) => (
|
||||
<div
|
||||
key={`approval-list-${index + 1}`}
|
||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="my-auto mr-8 flex h-8 w-8 items-center justify-center rounded border border-mineshaft-400 bg-bunker-500/50 text-white">
|
||||
<div>{index + 1}</div>
|
||||
</div>
|
||||
{index !== labels.length - 1 && (
|
||||
<div className="absolute bottom-0 left-8 h-[1.25rem] border-r border-mineshaft-400" />
|
||||
<div key={`approval-list-${index + 1}`} className="flex">
|
||||
{labels.length > 1 && (
|
||||
<div className="flex w-12 flex-col items-center gap-2 pr-4">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-grow border-mineshaft-600",
|
||||
index !== 0 && "border-r"
|
||||
)}
|
||||
/>
|
||||
{labels.length > 1 && (
|
||||
<Badge className="my-auto flex h-5 w-min min-w-5 items-center justify-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-center text-bunker-200">
|
||||
<span>{index + 1}</span>
|
||||
</Badge>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-grow border-mineshaft-600",
|
||||
index < labels.length - 1 && "border-r"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<div className="absolute left-8 top-0 h-[1.25rem] border-r border-mineshaft-400" />
|
||||
)}
|
||||
|
||||
<div className="grid flex-grow grid-cols-3">
|
||||
<GenericFieldLabel label="Users">{el.userLabels}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Groups">{el.groupLabels}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Approvals Required">{el.approvals}</GenericFieldLabel>
|
||||
<div className="grid flex-1 grid-cols-5 border-b border-mineshaft-600 p-4">
|
||||
<GenericFieldLabel className="col-span-2" icon={faUser} label="Users">
|
||||
{el.userLabels}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel className="col-span-2" icon={faUserGroup} label="Groups">
|
||||
{el.groupLabels}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel icon={faClipboardCheck} label="Approvals Required">
|
||||
{el.approvals}
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
-2
@@ -133,7 +133,6 @@ const Folder: React.FC<FolderProps> = ({
|
||||
{!isDisabled && (
|
||||
<Checkbox
|
||||
id="folder-root"
|
||||
className="data-[state=indeterminate]:bg-secondary data-[state=checked]:bg-primary"
|
||||
isChecked={allSelected || someSelected}
|
||||
onCheckedChange={handleFolderSelect}
|
||||
isIndeterminate={someSelected && !allSelected}
|
||||
@@ -167,7 +166,6 @@ const Folder: React.FC<FolderProps> = ({
|
||||
{!isDisabled && (
|
||||
<Checkbox
|
||||
id={`folder-${item.id}`}
|
||||
className="data-[state=indeterminate]:bg-secondary data-[state=checked]:bg-primary"
|
||||
isChecked={selectedItemIds.includes(item.id)}
|
||||
onCheckedChange={(checked) => onItemSelect(item, !!checked)}
|
||||
isDisabled={isDisabled}
|
||||
|
||||
-1
@@ -42,7 +42,6 @@ export const AutoCapitalizationSection = () => {
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isDisabled={!isAllowed}
|
||||
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
||||
|
||||
-1
@@ -38,7 +38,6 @@ export const DeleteProjectProtection = () => {
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="hasDeleteProtection"
|
||||
isDisabled={!isAllowed}
|
||||
isChecked={currentWorkspace?.hasDeleteProtection ?? false}
|
||||
|
||||
-1
@@ -48,7 +48,6 @@ export const SecretSharingSection = () => {
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="secretSharing"
|
||||
isDisabled={!isAllowed || isLoading}
|
||||
isChecked={currentWorkspace?.secretSharing ?? true}
|
||||
|
||||
-1
@@ -48,7 +48,6 @@ export const SecretSnapshotsLegacySection = () => {
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="showSnapshotsLegacy"
|
||||
isDisabled={!isAllowed || isLoading}
|
||||
isChecked={currentWorkspace?.showSnapshotsLegacy ?? false}
|
||||
|
||||
Reference in New Issue
Block a user