Merge branch 'main' into feat/secretsBatchUI

This commit is contained in:
Carlos Monastyrski
2025-07-14 11:54:56 -03:00
259 changed files with 5578 additions and 534 deletions
@@ -39,10 +39,6 @@ export const OrgMembersSection = () => {
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const isMoreUsersAllowed = subscription?.memberLimit
? subscription.membersUsed < subscription.memberLimit
: true;
const isMoreIdentitiesAllowed = subscription?.identityLimit
? subscription.identitiesUsed < subscription.identityLimit
: true;
@@ -58,7 +54,7 @@ export const OrgMembersSection = () => {
return;
}
if ((!isMoreUsersAllowed || !isMoreIdentitiesAllowed) && !isEnterprise) {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description: "You can add more members if you upgrade your Infisical plan."
});
@@ -16,6 +16,7 @@ import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConn
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
import { CamundaConnectionForm } from "./CamundaConnectionForm";
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
@@ -33,6 +34,7 @@ import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm";
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { RailwayConnectionForm } from "./RailwayConnectionForm";
import { RenderConnectionForm } from "./RenderConnectionForm";
import { TeamCityConnectionForm } from "./TeamCityConnectionForm";
import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
@@ -135,8 +137,12 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <GitLabConnectionForm onSubmit={onSubmit} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} />;
case AppConnection.Bitbucket:
return <BitbucketConnectionForm onSubmit={onSubmit} />;
case AppConnection.Zabbix:
return <ZabbixConnectionForm onSubmit={onSubmit} />;
case AppConnection.Railway:
return <RailwayConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -231,8 +237,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Bitbucket:
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Zabbix:
return <ZabbixConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Railway:
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
@@ -0,0 +1,144 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { BitbucketConnectionMethod, TBitbucketConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TBitbucketConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Bitbucket)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(BitbucketConnectionMethod.ApiToken),
credentials: z.object({
email: z.string().email().trim().min(1, "Email required"),
apiToken: z.string().trim().min(1, "API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const BitbucketConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Bitbucket,
method: BitbucketConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Bitbucket].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(BitbucketConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.email"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Email">
<Input {...field} placeholder="[email protected]" />
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Bitbucket"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,135 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
RailwayConnectionMethod,
TRailwayConnection
} from "@app/hooks/api/appConnections/types/railway-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TRailwayConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Railway)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.nativeEnum(RailwayConnectionMethod),
credentials: z.object({
apiToken: z.string().trim().min(1, "Service API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const RailwayConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Railway,
method: RailwayConnectionMethod.AccountToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The type of token you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Railway].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Token Type"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(RailwayConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Token Value"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Railway"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -1,3 +1,5 @@
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, Tooltip } from "@app/components/v2";
@@ -8,25 +10,41 @@ type Props = {
label: string;
onClear: () => void;
children: React.ReactNode;
tooltipText?: string;
};
export const LogFilterItem = ({ label, onClear, hoverTooltip, children, className }: Props) => {
export const LogFilterItem = ({
label,
onClear,
hoverTooltip,
children,
className,
tooltipText
}: Props) => {
return (
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
<div className={twMerge("flex flex-col justify-between", className)}>
<div className="flex items-center justify-between pr-1">
<p className="text-xs opacity-60">{label}</p>
<Button
onClick={() => onClear()}
variant="link"
className="font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
size="xs"
>
Clear
</Button>
</div>
{children}
<div className={twMerge("flex flex-col justify-between", className)}>
<div className="flex items-center pr-1">
<p className="text-xs opacity-60">{label}</p>
{tooltipText && (
<Tooltip content={tooltipText} className="max-w-sm">
<FontAwesomeIcon
icon={faInfoCircle}
className="-mt-[0.05rem] ml-1 text-[11px] text-mineshaft-400"
/>
</Tooltip>
)}
<Button
onClick={() => onClear()}
variant="link"
className="ml-auto font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
size="xs"
>
Clear
</Button>
</div>
</Tooltip>
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
<div>{children}</div>
</Tooltip>
</div>
);
};
@@ -366,6 +366,7 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
</LogFilterItem>
<LogFilterItem
label="Secret Path"
tooltipText="Enter the exact secret path (wildcards like * are not supported)"
hoverTooltip={
!selectedProject
? "Select a project before filtering by secret path."
@@ -380,10 +381,7 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
control={control}
name="secretPath"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
className="w-full"
>
<FormControl className="w-full">
<Input
placeholder="Enter secret path"
className="disabled:cursor-not-allowed"
@@ -403,6 +401,7 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
? "Select a project before filtering by secret key."
: undefined
}
tooltipText="Enter the exact secret key name (wildcards like * are not supported)"
className={twMerge(!selectedProject && "opacity-50")}
label="Secret Key"
onClear={() => {
@@ -413,10 +412,7 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
control={control}
name="secretKey"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to a specific secret."
className="w-full"
>
<FormControl className="w-full">
<Input
isDisabled={!selectedProject}
{...field}
@@ -70,7 +70,7 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
upgrade your subscription
</a>
</a>
)}
)}{" "}
to view integration logs
</p>
</div>
@@ -0,0 +1,14 @@
import { TRailwaySync } from "@app/hooks/api/secretSyncs/types/railway-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TRailwaySync;
};
export const RailwaySyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -17,6 +17,7 @@ import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol";
import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol";
import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol";
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
@@ -76,6 +77,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <CloudflarePagesSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Zabbix:
return <ZabbixSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Railway:
return <RailwaySyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -156,6 +156,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
throw new Error(`Unhandled Zabbix Scope Destination Col Values ${destination}`);
}
break;
case SecretSync.Railway:
primaryText = "Railway Project";
secondaryText = destinationConfig.projectName;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
@@ -55,7 +55,7 @@ const formSchema = z
.object({
environment: z.object({ slug: z.string(), name: z.string() }),
name: z.string().optional(),
secretPath: z.string().optional(),
secretPath: z.string().trim().min(1),
approvals: z.number().min(1).default(1),
userApprovers: z
.object({ type: z.literal(ApproverType.User), id: z.string() })
@@ -93,20 +93,19 @@ const formSchema = z
.optional()
})
.superRefine((data, ctx) => {
if (
data.policyType === PolicyType.ChangePolicy &&
!(data.groupApprovers.length || data.userApprovers.length)
) {
ctx.addIssue({
path: ["userApprovers"],
code: z.ZodIssueCode.custom,
message: "At least one approver should be provided"
});
ctx.addIssue({
path: ["groupApprovers"],
code: z.ZodIssueCode.custom,
message: "At least one approver should be provided"
});
if (data.policyType === PolicyType.ChangePolicy) {
if (!(data.groupApprovers.length || data.userApprovers.length)) {
ctx.addIssue({
path: ["userApprovers"],
code: z.ZodIssueCode.custom,
message: "At least one approver should be provided"
});
ctx.addIssue({
path: ["groupApprovers"],
code: z.ZodIssueCode.custom,
message: "At least one approver should be provided"
});
}
}
});
@@ -127,6 +126,7 @@ const Form = ({
control,
handleSubmit,
watch,
resetField,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
@@ -177,6 +177,7 @@ const Form = ({
: undefined,
defaultValues: !editValues
? {
secretPath: "/",
sequenceApprovers: [{ approvals: 1 }]
}
: undefined
@@ -405,7 +406,10 @@ const Form = ({
<Select
isDisabled={isEditMode}
value={value}
onValueChange={(val) => onChange(val as PolicyType)}
onValueChange={(val) => {
onChange(val as PolicyType);
resetField("secretPath");
}}
className="w-full border border-mineshaft-500"
>
{Object.values(PolicyType).map((policyType) => {
@@ -465,6 +469,7 @@ const Form = ({
<FormControl
tooltipText="Secret paths support glob patterns. For example, '/**' will match all paths."
label="Secret Path"
isRequired
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
@@ -15,6 +15,7 @@ import {
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { useGetServerConfig } from "@app/hooks/api/admin";
import {
DynamicSecretAwsIamAuth,
DynamicSecretProviders
@@ -61,6 +62,23 @@ const formSchema = z.object({
})
)
.optional()
}),
z.object({
method: z.literal(DynamicSecretAwsIamAuth.IRSA),
region: z.string().trim().min(1),
awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional(),
tags: z
.array(
z.object({
key: z.string().trim().min(1).max(128),
value: z.string().trim().min(1).max(256)
})
)
.optional()
})
]),
defaultTTL: z.string().superRefine((val, ctx) => {
@@ -106,6 +124,8 @@ export const AwsIamInputForm = ({
projectSlug,
isSingleEnvironmentMode
}: Props) => {
const { data: serverConfig } = useGetServerConfig();
const {
control,
formState: { isSubmitting },
@@ -123,7 +143,7 @@ export const AwsIamInputForm = ({
});
const createDynamicSecret = useCreateDynamicSecret();
const isAccessKeyMethod = watch("provider.method") === DynamicSecretAwsIamAuth.AccessKey;
const method = watch("provider.method");
const handleCreateDynamicSecret = async ({
name,
@@ -237,11 +257,14 @@ export const AwsIamInputForm = ({
Assume Role (Recommended)
</SelectItem>
<SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
{serverConfig?.kubernetesAutoFetchServiceAccountToken && (
<SelectItem value={DynamicSecretAwsIamAuth.IRSA}>IRSA (EKS)</SelectItem>
)}
</Select>
</FormControl>
)}
/>
{isAccessKeyMethod ? (
{method === DynamicSecretAwsIamAuth.AccessKey && (
<div className="flex items-center space-x-2">
<Controller
control={control}
@@ -274,7 +297,8 @@ export const AwsIamInputForm = ({
)}
/>
</div>
) : (
)}
{method === DynamicSecretAwsIamAuth.AssumeRole && (
<div className="flex items-center space-x-2">
<Controller
control={control}
@@ -6,7 +6,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api";
import { useGetServerConfig, useUpdateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretAwsIamAuth, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { slugSchema } from "@app/lib/schemas";
@@ -40,6 +40,18 @@ const formSchema = z.object({
tags: z
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
.optional()
}),
z.object({
method: z.literal(DynamicSecretAwsIamAuth.IRSA),
region: z.string().trim().min(1),
awsPath: z.string().trim().optional(),
permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional(),
tags: z
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
.optional()
})
]),
defaultTTL: z.string().superRefine((val, ctx) => {
@@ -83,6 +95,8 @@ export const EditDynamicSecretAwsIamForm = ({
secretPath,
projectSlug
}: Props) => {
const { data: serverConfig } = useGetServerConfig();
const {
control,
watch,
@@ -100,7 +114,7 @@ export const EditDynamicSecretAwsIamForm = ({
}
}
});
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
const method = watch("inputs.method");
const updateDynamicSecret = useUpdateDynamicSecret();
@@ -214,11 +228,14 @@ export const EditDynamicSecretAwsIamForm = ({
Assume Role (Recommended)
</SelectItem>
<SelectItem value={DynamicSecretAwsIamAuth.AccessKey}>Access Key</SelectItem>
{serverConfig?.kubernetesAutoFetchServiceAccountToken && (
<SelectItem value={DynamicSecretAwsIamAuth.IRSA}>IRSA (EKS)</SelectItem>
)}
</Select>
</FormControl>
)}
/>
{isAccessKeyMethod ? (
{method === DynamicSecretAwsIamAuth.AccessKey && (
<div className="flex items-center space-x-2">
<Controller
control={control}
@@ -251,7 +268,8 @@ export const EditDynamicSecretAwsIamForm = ({
)}
/>
</div>
) : (
)}
{method === DynamicSecretAwsIamAuth.AssumeRole && (
<div className="flex items-center space-x-2">
<Controller
control={control}
@@ -9,7 +9,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
import { parseDotEnv, parseJson, parseYaml } from "@app/components/utilities/parseSecrets";
import { Button, Lottie, Modal, ModalContent } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
@@ -127,7 +127,7 @@ export const SecretDropzone = ({
}
};
const parseFile = (file?: File, isJson?: boolean) => {
const parseFile = (file?: File) => {
const reader = new FileReader();
if (!file) {
createNotification({
@@ -140,10 +140,25 @@ export const SecretDropzone = ({
setIsLoading.on();
reader.onload = (event) => {
if (!event?.target?.result) return;
// parse function's argument looks like to be ArrayBuffer
const env = isJson
? parseJson(event.target.result as ArrayBuffer)
: parseDotEnv(event.target.result as ArrayBuffer);
let env: TParsedEnv;
const src = event.target.result as ArrayBuffer;
switch (file.type) {
case "application/json":
env = parseJson(src);
break;
case "text/yaml":
case "application/x-yaml":
case "application/yaml":
env = parseYaml(src);
break;
default:
env = parseDotEnv(src);
break;
}
setIsLoading.off();
handleParsedEnv(env);
};
@@ -165,12 +180,12 @@ export const SecretDropzone = ({
e.dataTransfer.dropEffect = "copy";
setDragActive.off();
parseFile(e.dataTransfer.files[0], e.dataTransfer.files[0].type === "application/json");
parseFile(e.dataTransfer.files[0]);
};
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
parseFile(e.target?.files?.[0]);
};
const handleSaveSecrets = async () => {
@@ -9,6 +9,7 @@ import {
faInfoCircle,
faKey,
faRotate,
faSearch,
faUpDown,
faWarning,
faXmark
@@ -140,6 +141,10 @@ export const SecretImportItem = ({
}
};
const filteredImportedSecrets = importedSecrets.filter((secret) =>
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
);
return (
<>
<div
@@ -305,21 +310,26 @@ export const SecretImportItem = ({
</td>
</tr>
)}
{importedSecrets
.filter((secret) => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
.map(({ key, value }, index) => (
<tr key={`${id}-${key}-${index + 1}`}>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{key}
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<SecretInput value={value} isReadOnly />
</td>
{/* <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{filteredImportedSecrets.length === 0 && importedSecrets?.length !== 0 && (
<tr>
<td colSpan={3}>
<EmptyState title="No secrets match search" icon={faSearch} />
</td>
</tr>
)}
{filteredImportedSecrets.map(({ key, value }, index) => (
<tr key={`${id}-${key}-${index + 1}`}>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{key}
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<SecretInput value={value} isReadOnly />
</td>
{/* <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
</td> */}
</tr>
))}
</tr>
))}
</tbody>
</table>
</TableContainer>
@@ -0,0 +1,17 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TRailwaySync } from "@app/hooks/api/secretSyncs/types/railway-sync";
type Props = {
secretSync: TRailwaySync;
};
export const RailwaySyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
return (
<>
<GenericFieldLabel label="Project">{destinationConfig.projectName}</GenericFieldLabel>
<GenericFieldLabel label="Environment">{destinationConfig.environmentName}</GenericFieldLabel>
</>
);
};
@@ -28,6 +28,7 @@ import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection";
import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection";
import { RenderSyncDestinationSection } from "./RenderSyncDestinationSection";
import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection";
import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
@@ -118,6 +119,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Zabbix:
DestinationComponents = <ZabbixSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Railway:
DestinationComponents = <RailwaySyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
@@ -61,6 +61,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
case SecretSync.Zabbix:
case SecretSync.Railway:
AdditionalSyncOptionsComponent = null;
break;
default:
@@ -20,9 +20,9 @@ import { useWorkspace } from "@app/context";
import {
useCreateIntegration,
useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces
useGetIntegrationAuthBitbucketWorkspaces
} from "@app/hooks/api";
import { useGetIntegrationAuthBitBucketEnvironments } from "@app/hooks/api/integrationAuth/queries";
import { useGetIntegrationAuthBitbucketEnvironments } from "@app/hooks/api/integrationAuth/queries";
import { IntegrationsListPageTabs } from "@app/types/integrations";
enum BitbucketScope {
@@ -89,8 +89,8 @@ export const BitbucketConfigurePage = () => {
}
});
const bitBucketWorkspace = watch("targetWorkspace");
const bitBucketRepo = watch("targetRepo");
const bitbucketWorkspace = watch("targetWorkspace");
const bitbucketRepo = watch("targetRepo");
const integrationAuthId = useSearch({
from: ROUTE_PATHS.SecretManager.Integratons.BitbucketConfigurePage.id,
@@ -99,21 +99,21 @@ export const BitbucketConfigurePage = () => {
const { currentWorkspace } = useWorkspace();
const { data: bitbucketWorkspaces, isPending: isBitbucketWorkspacesLoading } =
useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? "");
useGetIntegrationAuthBitbucketWorkspaces((integrationAuthId as string) ?? "");
const { data: bitbucketRepos, isPending: isBitbucketReposLoading } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: bitBucketWorkspace?.slug
workspaceSlug: bitbucketWorkspace?.slug
});
const { data: bitbucketEnvironments, isPending: isBitbucketEnvironmentsLoading } =
useGetIntegrationAuthBitBucketEnvironments(
useGetIntegrationAuthBitbucketEnvironments(
{
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: bitBucketWorkspace?.slug ?? "",
repoSlug: bitBucketRepo?.appId ?? ""
workspaceSlug: bitbucketWorkspace?.slug ?? "",
repoSlug: bitbucketRepo?.appId ?? ""
},
{ enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) }
{ enabled: Boolean(bitbucketWorkspace?.slug && bitbucketRepo?.appId) }
);
const onSubmit = async ({
@@ -167,8 +167,8 @@ export const BitbucketConfigurePage = () => {
useEffect(() => {
if (
bitBucketWorkspace ||
bitBucketRepo ||
bitbucketWorkspace ||
bitbucketRepo ||
!bitbucketRepos ||
!bitbucketWorkspaces ||
!currentWorkspace
@@ -325,7 +325,7 @@ export const BitbucketConfigurePage = () => {
? "Select an environment..."
: "No environments found..."
}
isLoading={isBitbucketEnvironmentsLoading && Boolean(bitBucketRepo)}
isLoading={isBitbucketEnvironmentsLoading && Boolean(bitbucketRepo)}
isDisabled={!bitbucketEnvironments?.length || isBitbucketEnvironmentsLoading}
/>
</FormControl>
@@ -0,0 +1,21 @@
import { GenericFieldLabel } from "@app/components/v2";
import { TBitbucketDataSource } from "@app/hooks/api/secretScanningV2/types/bitbucket-data-source";
type Props = {
dataSource: TBitbucketDataSource;
};
export const BitbucketDataSourceConfigDisplay = ({ dataSource }: Props) => {
const {
config: { includeRepos, workspaceSlug }
} = dataSource;
return (
<>
<GenericFieldLabel label="Workspace Slug">{workspaceSlug}</GenericFieldLabel>
<GenericFieldLabel label="Scan Repositories">
{includeRepos.includes("*") ? "All" : includeRepos.join(", ")}
</GenericFieldLabel>
</>
);
};
@@ -3,6 +3,7 @@ import {
TSecretScanningDataSource
} from "@app/hooks/api/secretScanningV2";
import { BitbucketDataSourceConfigDisplay } from "./BitbucketDataSourceConfigDisplay";
import { GitHubDataSourceConfigDisplay } from "./GitHubDataSourceConfigDisplay";
type Props = {
@@ -13,6 +14,8 @@ export const DataSourceConfigDisplay = ({ dataSource }: Props) => {
switch (dataSource.type) {
case SecretScanningDataSource.GitHub:
return <GitHubDataSourceConfigDisplay dataSource={dataSource} />;
case SecretScanningDataSource.Bitbucket:
return <BitbucketDataSourceConfigDisplay dataSource={dataSource} />;
default:
throw new Error(
`Unhandled dataSource type ${(dataSource as TSecretScanningDataSource).type}`
@@ -112,10 +112,12 @@ export const SecretScanningResourcesTable = ({ dataSource }: Props) => {
const isActive = includeRepos.includes("*") || includeRepos.includes(name);
if (filters.status.length === 1) {
if (filters.status.includes(ResourceStatus.Active)) {
return isActive;
if (filters.status.includes(ResourceStatus.Active) && !isActive) {
return false;
}
if (filters.status.includes(ResourceStatus.Inactive) && isActive) {
return false;
}
return !isActive;
}
const searchValue = search.trim().toLowerCase();
@@ -294,7 +294,7 @@ export const SecretScanningDataSourcesTable = ({ dataSources }: Props) => {
<Table>
<THead>
<Tr>
<Th className="w-60">Platform</Th>
<Th className="min-w-36">Platform</Th>
<Th className="w-1/3">
<div className="flex items-center">
Name