mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 10:35:17 +00:00
Merge branch 'main' into feat/point-in-time-revamp
This commit is contained in:
@@ -78,11 +78,14 @@ export default function CodeInputStep({
|
||||
const resendVerificationEmail = async () => {
|
||||
setIsResendingVerificationEmail(true);
|
||||
setIsLoading(true);
|
||||
await mutateAsync({ email });
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsResendingVerificationEmail(false);
|
||||
}, 2000);
|
||||
try {
|
||||
await mutateAsync({ email });
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsResendingVerificationEmail(false);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const DeleteProjectTemplateModal = ({ isOpen, onOpenChange, template }: P
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure want to delete ${name}?`}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleDeleteProjectTemplate}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeTemplate.isOpen}
|
||||
title={`Are you sure want to delete ${projectTemplate.name}?`}
|
||||
title={`Are you sure you want to delete ${projectTemplate.name}?`}
|
||||
deleteKey="confirm"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeTemplate", isOpen)}
|
||||
onDeleteApproved={handleRemoveTemplate}
|
||||
|
||||
@@ -66,7 +66,7 @@ export const DeleteSecretRotationV2Modal = ({
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure want to delete ${name}?`}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDeleteSecretRotation}
|
||||
>
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ const Content = ({ secretRotation }: ContentProps) => {
|
||||
let Component: ReactNode;
|
||||
switch (generatedCredentialsResponse.type) {
|
||||
case SecretRotation.PostgresCredentials:
|
||||
case SecretRotation.MySqlCredentials:
|
||||
case SecretRotation.MsSqlCredentials:
|
||||
Component = (
|
||||
<ViewSqlCredentialsRotationGeneratedCredentials
|
||||
|
||||
+2
@@ -1,11 +1,13 @@
|
||||
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
|
||||
import { ViewRotationGeneratedCredentialsDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/ViewRotationGeneratedCredentialsDisplay";
|
||||
import { TMsSqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mssql-credentials-rotation";
|
||||
import { TMySqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mysql-credentials-rotation";
|
||||
import { TPostgresCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/postgres-credentials-rotation";
|
||||
|
||||
type Props = {
|
||||
generatedCredentialsResponse:
|
||||
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMySqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TPostgresCredentialsRotationGeneratedCredentialsResponse;
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import { SqlCredentialsRotationParametersFields } from "./shared";
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationParametersFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationParametersFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ import { SqlCredentialsRotationReviewFields } from "./shared";
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationReviewFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationReviewFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
|
||||
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.MySqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.AzureClientSecret]: AzureClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations
|
||||
import { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema";
|
||||
import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
|
||||
import { MySqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mysql-credentials-rotation-schema";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation";
|
||||
@@ -17,6 +18,7 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
|
||||
AzureClientSecretRotationSchema,
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema,
|
||||
MySqlCredentialsRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema
|
||||
]),
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
|
||||
import { SqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const MySqlCredentialsRotationSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretRotation.MySqlCredentials)
|
||||
})
|
||||
.merge(SqlCredentialsRotationSchema)
|
||||
.merge(BaseSecretRotationSchema);
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useState } from "react";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
TSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningDataSourceForm } from "./forms";
|
||||
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
|
||||
import { SecretScanningDataSourceSelect } from "./SecretScanningDataSourceSelect";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onComplete: (dataSource: TSecretScanningDataSource) => void;
|
||||
selectedDataSource: SecretScanningDataSource | null;
|
||||
setSelectedDataSource: (selectedDataSource: SecretScanningDataSource | null) => void;
|
||||
};
|
||||
|
||||
const Content = ({ setSelectedDataSource, selectedDataSource, ...props }: ContentProps) => {
|
||||
if (selectedDataSource) {
|
||||
return (
|
||||
<SecretScanningDataSourceForm
|
||||
onCancel={() => setSelectedDataSource(null)}
|
||||
type={selectedDataSource}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <SecretScanningDataSourceSelect onSelect={setSelectedDataSource} />;
|
||||
};
|
||||
|
||||
export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...props }: Props) => {
|
||||
const [selectedDataSource, setSelectedDataSource] = useState<SecretScanningDataSource | null>(
|
||||
null
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSelectedDataSource(null);
|
||||
onOpenChange(open);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={
|
||||
selectedDataSource ? (
|
||||
<SecretScanningDataSourceModalHeader isConfigured={false} type={selectedDataSource} />
|
||||
) : (
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
Add Data Source
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className={selectedDataSource ? "max-w-2xl" : "max-w-3xl"}
|
||||
subTitle={
|
||||
selectedDataSource ? undefined : "Select a data source to configure secret scanning for."
|
||||
}
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<Content
|
||||
onComplete={() => {
|
||||
setSelectedDataSource(null);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
selectedDataSource={selectedDataSource}
|
||||
setSelectedDataSource={setSelectedDataSource}
|
||||
{...props}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import {
|
||||
TSecretScanningDataSource,
|
||||
useDeleteSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
dataSource?: TSecretScanningDataSource;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
onComplete?: () => void;
|
||||
};
|
||||
|
||||
export const DeleteSecretScanningDataSourceModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
dataSource,
|
||||
onComplete
|
||||
}: Props) => {
|
||||
const deleteDataSource = useDeleteSecretScanningDataSource();
|
||||
|
||||
if (!dataSource) return null;
|
||||
|
||||
const { id: dataSourceId, name, type, projectId } = dataSource;
|
||||
|
||||
const handleDeleteDataSource = async () => {
|
||||
const dataSourceType = SECRET_SCANNING_DATA_SOURCE_MAP[type].name;
|
||||
|
||||
try {
|
||||
await deleteDataSource.mutateAsync({
|
||||
dataSourceId,
|
||||
type,
|
||||
projectId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully deleted ${dataSourceType} Data Source`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (onComplete) onComplete();
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to delete ${dataSourceType} Data Source`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDeleteDataSource}
|
||||
>
|
||||
<p className="mt-1 font-inter text-sm text-mineshaft-400">
|
||||
Findings associated with this data source will be preserved.
|
||||
</p>
|
||||
</DeleteActionModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { TSecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { SecretScanningDataSourceForm } from "./forms";
|
||||
import { SecretScanningDataSourceModalHeader } from "./SecretScanningDataSourceModalHeader";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
dataSource?: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
export const EditSecretScanningDataSourceModal = ({
|
||||
dataSource,
|
||||
onOpenChange,
|
||||
...props
|
||||
}: Props) => {
|
||||
if (!dataSource) return null;
|
||||
|
||||
return (
|
||||
<Modal {...props} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title={<SecretScanningDataSourceModalHeader isConfigured type={dataSource.type} />}
|
||||
className="max-w-2xl"
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<SecretScanningDataSourceForm
|
||||
onComplete={() => onOpenChange(false)}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
dataSource={dataSource}
|
||||
type={dataSource.type}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
type: SecretScanningDataSource;
|
||||
isConfigured: boolean;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Props) => {
|
||||
const dataSourceDetails = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<img
|
||||
alt={`${dataSourceDetails.name} logo`}
|
||||
src={`/images/integrations/${dataSourceDetails.image}`}
|
||||
className="h-12 rounded-md bg-bunker-500 p-2"
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center text-mineshaft-300">
|
||||
{dataSourceDetails.name} Data Source
|
||||
<a
|
||||
target="_blank"
|
||||
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`}
|
||||
className="mb-1 ml-1"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mb-[0.03rem] mr-1 text-[12px]" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm leading-4 text-mineshaft-400">
|
||||
{isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Spinner, Tooltip } from "@app/components/v2";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
useSecretScanningDataSourceOptions
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
onSelect: (type: SecretScanningDataSource) => void;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceSelect = ({ onSelect }: Props) => {
|
||||
const { isPending, data: dataSourceOptions } = useSecretScanningDataSourceOptions();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
<Spinner size="lg" className="text-mineshaft-500" />
|
||||
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{dataSourceOptions?.map(({ type }) => {
|
||||
const { image, name, size } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={type}
|
||||
onClick={() => onSelect(type)}
|
||||
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"
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
width={size}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Badge, Tooltip } from "@app/components/v2";
|
||||
import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
type Props = {
|
||||
status: SecretScanningScanStatus;
|
||||
statusMessage?: string | null;
|
||||
className?: string;
|
||||
scannedAt?: string | null;
|
||||
};
|
||||
|
||||
export const SecretScanningScanStatusBadge = ({
|
||||
status,
|
||||
statusMessage,
|
||||
className,
|
||||
scannedAt
|
||||
}: Props) => {
|
||||
if (status === SecretScanningScanStatus.Failed) {
|
||||
let errorMessage = statusMessage;
|
||||
if (statusMessage) {
|
||||
try {
|
||||
errorMessage = JSON.stringify(JSON.parse(statusMessage), null, 2);
|
||||
} catch {
|
||||
errorMessage = statusMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
position="left"
|
||||
className="max-w-sm select-text"
|
||||
content={
|
||||
<div className="flex flex-col gap-2 whitespace-normal py-1">
|
||||
<div>
|
||||
<div className="mb-2 flex self-start text-red">
|
||||
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
|
||||
<div className="text-xs">Failure Reason</div>
|
||||
</div>
|
||||
<div className="break-words rounded bg-mineshaft-600 p-2 text-xs">{errorMessage}</div>
|
||||
{scannedAt && (
|
||||
<div className="mt-1 text-xs text-mineshaft-400">
|
||||
Attempted {formatDistance(new Date(scannedAt), new Date(), { addSuffix: true })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Badge
|
||||
variant="danger"
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
Scan Error
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
|
||||
return (
|
||||
<Badge
|
||||
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
|
||||
variant="primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
|
||||
<span>Scanning</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant="success"
|
||||
className={twMerge(
|
||||
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
<span>Complete</span>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { MultiValue } from "react-select";
|
||||
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
TGitHubRadarConnectionRepository,
|
||||
useGitHubRadarConnectionListRepositories
|
||||
} from "@app/hooks/api/appConnections/github-radar";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
|
||||
|
||||
enum ScanMethod {
|
||||
AllRepositories = "all-repositories",
|
||||
SelectRepositories = "select-repositories"
|
||||
}
|
||||
|
||||
export const GitHubDataSourceConfigFields = () => {
|
||||
const { control, watch, setValue } = useFormContext<
|
||||
TSecretScanningDataSourceForm & {
|
||||
type: SecretScanningDataSource.GitHub;
|
||||
}
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ control, name: "connection.id" });
|
||||
const isUpdate = Boolean(watch("id"));
|
||||
|
||||
const { data: repositories, isPending: areRepositoriesLoading } =
|
||||
useGitHubRadarConnectionListRepositories(connectionId, { enabled: Boolean(connectionId) });
|
||||
|
||||
const includeRepos = watch("config.includeRepos");
|
||||
|
||||
const scanMethod =
|
||||
!includeRepos || includeRepos[0] === "*"
|
||||
? ScanMethod.AllRepositories
|
||||
: ScanMethod.SelectRepositories;
|
||||
|
||||
useEffect(() => {
|
||||
if (!includeRepos) {
|
||||
setValue("config.includeRepos", ["*"]);
|
||||
}
|
||||
}, [includeRepos, setValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretScanningDataSourceConnectionField
|
||||
isUpdate={isUpdate}
|
||||
onChange={() => {
|
||||
if (scanMethod === ScanMethod.SelectRepositories) {
|
||||
setValue("config.includeRepos", []);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormControl label="Scan Repositories">
|
||||
<Select
|
||||
value={scanMethod}
|
||||
onValueChange={(val) => {
|
||||
setValue("config.includeRepos", val === ScanMethod.AllRepositories ? ["*"] : []);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isDisabled={!connectionId}
|
||||
>
|
||||
{Object.values(ScanMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem className="capitalize" value={method} key={method}>
|
||||
{method.replace("-", " ")}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{scanMethod === ScanMethod.SelectRepositories && (
|
||||
<Controller
|
||||
name="config.includeRepos"
|
||||
defaultValue={["*"]}
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Include Repositories"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={<>Ensure that your connection has the correct permissions.</>}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the repository you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={areRepositoriesLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
isMulti
|
||||
value={repositories?.filter((repository) => value.includes(repository.name))}
|
||||
onChange={(newValue) => {
|
||||
onChange(
|
||||
newValue
|
||||
? (newValue as MultiValue<TGitHubRadarConnectionRepository>).map(
|
||||
(p) => p.name
|
||||
)
|
||||
: null
|
||||
);
|
||||
}}
|
||||
options={repositories}
|
||||
placeholder="Select repositories..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Switch } from "@app/components/v2";
|
||||
import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceConfigFields = () => {
|
||||
const { watch, control } = useFormContext<TSecretScanningDataSourceForm>();
|
||||
|
||||
const type = watch("type");
|
||||
|
||||
const Component = COMPONENT_MAP[type];
|
||||
const autoScanDescription = RESOURCE_DESCRIPTION_HELPER[type];
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">Connect and configure your Data Source.</p>
|
||||
<Component />
|
||||
<Controller
|
||||
control={control}
|
||||
name="isAutoScanEnabled"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
helperText={
|
||||
value
|
||||
? `Scans will automatically be triggered when a ${autoScanDescription.verb} occurs to ${autoScanDescription.pluralNoun} associated with this data source.`
|
||||
: "Manually trigger scans to detect secret leaks."
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="auto-scan-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p className="w-[9.6rem]">Auto-Scan {value ? "Enabled" : "Disabled"}</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceConfigFields";
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
|
||||
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP } from "@app/helpers/secretScanningV2";
|
||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "./schemas";
|
||||
|
||||
type Props = {
|
||||
onChange?: VoidFunction;
|
||||
isUpdate?: boolean;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceConnectionField = ({
|
||||
onChange: callback,
|
||||
isUpdate
|
||||
}: Props) => {
|
||||
const { permission } = useOrgPermission();
|
||||
const { control, watch } = useFormContext<TSecretScanningDataSourceForm>();
|
||||
|
||||
const dataSourceType = watch("type");
|
||||
const app = SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP[dataSourceType];
|
||||
|
||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
|
||||
|
||||
const connectionName = APP_CONNECTION_MAP[app].name;
|
||||
|
||||
const canCreateConnection = permission.can(
|
||||
OrgPermissionAppConnectionActions.Create,
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="App Connections can be created from the Organization Settings page."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label={`${connectionName} Connection`}
|
||||
helperText={
|
||||
isUpdate ? (
|
||||
"Cannot be updated"
|
||||
) : (
|
||||
<p>
|
||||
Check out{" "}
|
||||
<a
|
||||
href={`https://infisical.com/docs/integrations/app-connections/${app}`}
|
||||
target="_blank"
|
||||
className="underline"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
our docs
|
||||
</a>{" "}
|
||||
to ensure your connection has the required permissions for secret scanning.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
if (callback) callback();
|
||||
}}
|
||||
isLoading={isPending}
|
||||
options={availableConnections}
|
||||
isDisabled={isUpdate}
|
||||
placeholder="Select connection..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="connection"
|
||||
/>
|
||||
{!isUpdate && availableConnections?.length === 0 && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
|
||||
{canCreateConnection ? (
|
||||
<>
|
||||
You do not have access to any {connectionName} Connections. Create one from the{" "}
|
||||
<Link to="/organization/app-connections" className="underline">
|
||||
App Connections
|
||||
</Link>{" "}
|
||||
page.
|
||||
</>
|
||||
) : (
|
||||
`You do not have access to any ${connectionName} Connections. Contact an admin to create one.`
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "./schemas";
|
||||
|
||||
export const SecretScanningDataSourceDetailsFields = () => {
|
||||
const { control } = useFormContext<TSecretScanningDataSourceForm>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">
|
||||
Provide a name and description for this Data Source.
|
||||
</p>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
helperText="Must be slug-friendly"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Name"
|
||||
>
|
||||
<Input autoFocus value={value} onChange={onChange} placeholder="my-data-source" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
label="Description"
|
||||
>
|
||||
<TextArea
|
||||
value={value ?? ""}
|
||||
onChange={onChange}
|
||||
placeholder="Provide a description for this data source..."
|
||||
className="!resize-none"
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
|
||||
import {
|
||||
SecretScanningDataSource,
|
||||
TSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2";
|
||||
import {
|
||||
useCreateSecretScanningDataSource,
|
||||
useUpdateSecretScanningDataSource
|
||||
} from "@app/hooks/api/secretScanningV2/mutations";
|
||||
|
||||
import { SecretScanningDataSourceSchema, TSecretScanningDataSourceForm } from "./schemas";
|
||||
import { SecretScanningDataSourceConfigFields } from "./SecretScanningDataSourceConfigFields";
|
||||
import { SecretScanningDataSourceDetailsFields } from "./SecretScanningDataSourceDetailsFields";
|
||||
import { SecretScanningDataSourceReviewFields } from "./SecretScanningDataSourceReviewFields";
|
||||
|
||||
type Props = {
|
||||
onComplete: (dataSource: TSecretScanningDataSource) => void;
|
||||
type: SecretScanningDataSource;
|
||||
onCancel: () => void;
|
||||
dataSource?: TSecretScanningDataSource;
|
||||
};
|
||||
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TSecretScanningDataSourceForm)[] }[] =
|
||||
[
|
||||
{ name: "Configuration", key: "config", fields: ["config", "isAutoScanEnabled", "connection"] },
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
|
||||
export const SecretScanningDataSourceForm = ({ type, onComplete, onCancel, dataSource }: Props) => {
|
||||
const createDataSource = useCreateSecretScanningDataSource();
|
||||
const updateDataSource = useUpdateSecretScanningDataSource();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { name: sourceType } = SECRET_SCANNING_DATA_SOURCE_MAP[type];
|
||||
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const formMethods = useForm<TSecretScanningDataSourceForm>({
|
||||
resolver: zodResolver(SecretScanningDataSourceSchema),
|
||||
defaultValues: dataSource ?? {
|
||||
type,
|
||||
isAutoScanEnabled: true // scott: this may need to be derived from type in the future
|
||||
},
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ connection, ...formData }: TSecretScanningDataSourceForm) => {
|
||||
const mutation = dataSource
|
||||
? updateDataSource.mutateAsync({
|
||||
dataSourceId: dataSource.id,
|
||||
projectId: dataSource.projectId,
|
||||
...formData
|
||||
})
|
||||
: createDataSource.mutateAsync({
|
||||
...formData,
|
||||
connectionId: connection?.id,
|
||||
projectId: currentWorkspace.id
|
||||
});
|
||||
try {
|
||||
const source = await mutation;
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${source ? "updated" : "created"} ${sourceType} Data Source`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(source);
|
||||
} catch (err: any) {
|
||||
createNotification({
|
||||
title: `Failed to ${dataSource ? "update" : "create"} ${sourceType} Data Source`,
|
||||
text: err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (selectedTabIndex === 0) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedTabIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
trigger,
|
||||
formState: { isSubmitting }
|
||||
} = formMethods;
|
||||
|
||||
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
|
||||
|
||||
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
|
||||
|
||||
const handleNext = async () => {
|
||||
if (isFinalStep) {
|
||||
handleSubmit(onSubmit)();
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await isStepValid(selectedTabIndex);
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
setSelectedTabIndex((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const isTabEnabled = async (index: number) => {
|
||||
let isEnabled = true;
|
||||
for (let i = index - 1; i >= 0; i -= 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
isEnabled = isEnabled && (await isStepValid(i));
|
||||
}
|
||||
|
||||
return isEnabled;
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={twMerge(isFinalStep && "max-h-[70vh] overflow-y-auto")}>
|
||||
<FormProvider {...formMethods}>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List className="-pb-1 mb-6 w-full border-b-2 border-mineshaft-600">
|
||||
{FORM_TABS.map((tab, index) => (
|
||||
<Tab
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const isEnabled = await isTabEnabled(index);
|
||||
setSelectedTabIndex((prev) => (isEnabled ? index : prev));
|
||||
}}
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
key={tab.key}
|
||||
>
|
||||
{index + 1}. {tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<SecretScanningDataSourceConfigFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretScanningDataSourceDetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<SecretScanningDataSourceReviewFields />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</FormProvider>
|
||||
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
colorSchema="secondary"
|
||||
>
|
||||
{isFinalStep ? `${dataSource ? "Update" : "Create"} Data Source` : "Next"}
|
||||
</Button>
|
||||
<Button onClick={handlePrev} colorSchema="secondary">
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { SecretScanningDataSourceConfigReviewSection } from "./shared";
|
||||
|
||||
export const GitHubDataSourceReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretScanningDataSourceForm & {
|
||||
type: SecretScanningDataSource.GitHub;
|
||||
}
|
||||
>();
|
||||
|
||||
const [{ includeRepos }, connection] = watch(["config", "connection"]);
|
||||
const shouldScanAll = includeRepos[0] === "*";
|
||||
|
||||
return (
|
||||
<SecretScanningDataSourceConfigReviewSection>
|
||||
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
|
||||
<GenericFieldLabel label="Scan Repositories">
|
||||
{shouldScanAll ? "All" : includeRepos.join(", ")}
|
||||
</GenericFieldLabel>
|
||||
</SecretScanningDataSourceConfigReviewSection>
|
||||
);
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretScanningDataSourceForm>();
|
||||
|
||||
const { type, name, description } = watch();
|
||||
|
||||
const Component = COMPONENT_MAP[type];
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-6">
|
||||
<Component />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Details</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Description">{description}</GenericFieldLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceReviewFields";
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceConfigReviewSection = ({ children }: Props) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Configuration</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceConfigReviewSection";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./SecretScanningDataSourceForm";
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
type SchemaOptions = {
|
||||
isConnectionRequired: boolean;
|
||||
};
|
||||
|
||||
export const BaseSecretScanningDataSourceSchema = ({ isConnectionRequired }: SchemaOptions) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "Name" }),
|
||||
description: z.string().trim().max(256, "Cannot exceed 256 characters").nullish(),
|
||||
connection: isConnectionRequired
|
||||
? z.object({ name: z.string(), id: z.string().uuid() })
|
||||
: z.null().or(z.undefined()),
|
||||
isAutoScanEnabled: z.boolean(),
|
||||
id: z.string().uuid().optional()
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema";
|
||||
|
||||
export const GitHubDataSourceSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretScanningDataSource.GitHub),
|
||||
config: z.object({
|
||||
includeRepos: z
|
||||
.string()
|
||||
.array()
|
||||
.min(1, "One or more repositories required")
|
||||
.max(100, "Cannot configure more than 100 repositories")
|
||||
})
|
||||
})
|
||||
.merge(BaseSecretScanningDataSourceSchema({ isConnectionRequired: true }));
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitHubDataSourceSchema } from "./github-data-source-schema";
|
||||
|
||||
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
|
||||
GitHubDataSourceSchema
|
||||
]);
|
||||
|
||||
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./CreateSecretScanningDataSourceModal";
|
||||
export * from "./EditSecretScanningDataSourceModal";
|
||||
export * from "./SecretScanningScanStatus";
|
||||
@@ -52,7 +52,7 @@ export const DeleteSecretSyncModal = ({ isOpen, onOpenChange, secretSync, onComp
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
onChange={onOpenChange}
|
||||
title={`Are you sure want to delete ${name}?`}
|
||||
title={`Are you sure you want to delete ${name}?`}
|
||||
deleteKey={name}
|
||||
onDeleteApproved={handleDeleteSecretSync}
|
||||
>
|
||||
|
||||
+25
-1
@@ -131,7 +131,27 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipClassName="max-w-md"
|
||||
tooltipText="When a secret is synced, its key will be injected into the key schema before it reaches the destination. This is useful for organization."
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-3">
|
||||
<span>
|
||||
When a secret is synced, values will be injected into the key schema before it
|
||||
reaches the destination. This is useful for organization.
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<span>Available keys:</span>
|
||||
<ul className="list-disc pl-4 text-sm">
|
||||
<li>
|
||||
<code>{"{{secretKey}}"}</code> - The key of the secret
|
||||
</li>
|
||||
<li>
|
||||
<code>{"{{environment}}"}</code> - The environment which the secret is in
|
||||
(e.g. dev, staging, prod)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
@@ -146,11 +166,15 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
href="https://infisical.com/docs/integrations/secret-syncs/overview#key-schemas"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Key Schema
|
||||
</a>{" "}
|
||||
to ensure that Infisical only manages the specific keys you intend, keeping
|
||||
everything else untouched.
|
||||
<br />
|
||||
<br />
|
||||
Destination secrets that do not match the schema will not be deleted or updated.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -13,11 +13,27 @@ export const BaseSecretSyncSchema = <T extends AnyZodObject | undefined = undefi
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) =>
|
||||
!val || /^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/.test(val),
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
const allowedOptionalPlaceholders = ["{{environment}}"];
|
||||
|
||||
const allowedPlaceholdersRegexPart = ["{{secretKey}}", ...allowedOptionalPlaceholders]
|
||||
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters
|
||||
.join("|");
|
||||
|
||||
const allowedContentRegex = new RegExp(
|
||||
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
|
||||
);
|
||||
const contentIsValid = allowedContentRegex.test(val);
|
||||
|
||||
const secretKeyCount = (val.match(/\{\{secretKey\}\}/g) || []).length;
|
||||
|
||||
return contentIsValid && secretKeyCount === 1;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
|
||||
"Key schema must include exactly one {{secretKey}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { twMerge } from "tailwind-merge";
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
const badgeVariants = cva(
|
||||
@@ -23,9 +24,12 @@ const badgeVariants = cva(
|
||||
|
||||
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
|
||||
|
||||
export const Badge = ({ children, className, variant }: BadgeProps) => {
|
||||
export const Badge = ({ children, className, variant, ...props }: BadgeProps) => {
|
||||
return (
|
||||
<div className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}>
|
||||
<div
|
||||
className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ type Props = {
|
||||
isDisabled?: boolean;
|
||||
icon?: IconProp;
|
||||
isMulti?: boolean;
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
|
||||
@@ -33,6 +34,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
dropdownContainerClassName,
|
||||
position,
|
||||
containerClassName,
|
||||
iconClassName,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -58,7 +60,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{props.icon && <FontAwesomeIcon icon={props.icon} />}
|
||||
{props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />}
|
||||
<div className="flex-1 truncate">
|
||||
<SelectPrimitive.Value placeholder={placeholder} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user