Merge branch 'main' into ENG-3041

This commit is contained in:
x032205
2025-07-03 19:55:32 -04:00
148 changed files with 3763 additions and 846 deletions
@@ -65,6 +65,7 @@ export const createNotification = (
toast(<NotificationContent {...myProps} />, {
position: "bottom-right",
...toastProps,
autoClose: toastProps.autoClose || 15000,
theme: "dark",
type: myProps?.type || "info"
});
@@ -60,7 +60,7 @@ export const OnePassSyncFields = () => {
menuPlacement="top"
isLoading={isVaultsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={vaults?.find((v) => v.id === value) ?? null}
value={vaults?.find((v) => v.id === value) || null}
onChange={(option) => onChange((option as SingleValue<TOnePassVault>)?.id ?? null)}
options={vaults}
placeholder="Select a vault..."
@@ -25,6 +25,7 @@ import { TeamCitySyncFields } from "./TeamCitySyncFields";
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
import { VercelSyncFields } from "./VercelSyncFields";
import { WindmillSyncFields } from "./WindmillSyncFields";
import { ZabbixSyncFields } from "./ZabbixSyncFields";
export const SecretSyncDestinationFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -76,6 +77,8 @@ export const SecretSyncDestinationFields = () => {
return <GitLabSyncFields />;
case SecretSync.CloudflarePages:
return <CloudflarePagesSyncFields />;
case SecretSync.Zabbix:
return <ZabbixSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -0,0 +1,147 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import {
TZabbixHost,
useZabbixConnectionListHosts,
ZABBIX_SYNC_SCOPES,
ZabbixMacroType,
ZabbixSyncScope
} from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const ZabbixSyncFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Zabbix }
>();
const connectionId = useWatch({ name: "connection.id", control });
const currentScope = watch("destinationConfig.scope");
const { data: hosts = [], isPending: isHostsPending } = useZabbixConnectionListHosts(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.scope", ZabbixSyncScope.Global);
setValue("destinationConfig.hostId", "");
setValue("destinationConfig.hostName", "");
}}
/>
<Controller
name="destinationConfig.scope"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Scope"
tooltipClassName="max-w-lg py-3"
tooltipText={
<div className="flex flex-col gap-3">
<p>
Specify how Infisical should manage secrets from Zabbix. The following options are
available:
</p>
<ul className="flex list-disc flex-col gap-3 pl-4">
{Object.values(ZABBIX_SYNC_SCOPES).map(({ name, description }) => {
return (
<li key={name}>
<p className="text-mineshaft-300">
<span className="font-medium text-bunker-200">{name}</span>: {description}
</p>
</li>
);
})}
</ul>
</div>
}
>
<Select
value={value}
onValueChange={(val) => {
onChange(val);
setValue("destinationConfig.hostId", "");
setValue("destinationConfig.hostName", "");
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select a scope..."
dropdownContainerClassName="max-w-none"
>
{Object.values(ZabbixSyncScope).map((scope) => (
<SelectItem className="capitalize" value={scope} key={scope}>
{scope.replace("-", " ")}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{currentScope === ZabbixSyncScope.Host && (
<Controller
name="destinationConfig.hostId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Host">
<FilterableSelect
menuPlacement="top"
isLoading={isHostsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={hosts.find((host) => host.hostId === value) ?? null}
onChange={(option) => {
const selectedOption = option as SingleValue<TZabbixHost>;
onChange(selectedOption?.hostId ?? null);
if (selectedOption) {
setValue("destinationConfig.hostName", selectedOption.host);
} else {
setValue("destinationConfig.hostName", "");
}
}}
options={hosts}
placeholder="Select a host..."
getOptionLabel={(option) => option.host}
getOptionValue={(option) => option.hostId}
/>
</FormControl>
)}
/>
)}
<Controller
control={control}
name="destinationConfig.macroType"
defaultValue={ZabbixMacroType.Secret}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Macro Type">
<Select
value={String(value)}
onValueChange={(val) => onChange(parseInt(val, 10))}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
placeholder="Select a macro type..."
dropdownContainerClassName="max-w-none"
>
<SelectItem value={String(ZabbixMacroType.Text)} key="text">
Text
</SelectItem>
<SelectItem value={String(ZabbixMacroType.Secret)} key="secret">
Secret
</SelectItem>
</Select>
</FormControl>
)}
/>
</>
);
};
@@ -23,6 +23,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
const { control, watch } = useFormContext<TSecretSyncForm>();
const destination = watch("destination");
const currentSyncOption = watch("syncOptions");
const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -57,6 +58,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Flyio:
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
case SecretSync.Zabbix:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
@@ -127,8 +129,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
{!syncOption?.canImportSecrets && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
{destinationName} only supports overwriting destination secrets. Secrets not present
in Infisical will be removed from the destination.
{destinationName} only supports overwriting destination secrets.{" "}
{!currentSyncOption.disableSecretDeletion &&
"Secrets not present in Infisical will be removed from the destination."}
</p>
)}
</>
@@ -35,6 +35,7 @@ import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields";
import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -124,6 +125,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.CloudflarePages:
DestinationFieldsComponent = <CloudflarePagesSyncReviewFields />;
break;
case SecretSync.Zabbix:
DestinationFieldsComponent = <ZabbixSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -0,0 +1,31 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
const isTextMacro = (macroType: number) => macroType === 0;
export const ZabbixSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Zabbix }>();
const scope = watch("destinationConfig.scope");
const hostId = watch("destinationConfig.hostId");
const hostName = watch("destinationConfig.hostName");
const macroType = watch("destinationConfig.macroType");
return (
<>
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
{scope === ZabbixSyncScope.Host && (
<>
<GenericFieldLabel label="Host ID">{hostId}</GenericFieldLabel>
<GenericFieldLabel label="Host Name">{hostName}</GenericFieldLabel>
</>
)}
<GenericFieldLabel label="Macro Type">
{isTextMacro(macroType) ? "Text" : "Secret"}
</GenericFieldLabel>
</>
);
};
@@ -22,6 +22,7 @@ import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schem
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema";
import { ZabbixSyncDestinationSchema } from "./zabbix-sync-destination-schema";
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
@@ -45,7 +46,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
RenderSyncDestinationSchema,
FlyioSyncDestinationSchema,
GitlabSyncDestinationSchema,
CloudflarePagesSyncDestinationSchema
CloudflarePagesSyncDestinationSchema,
ZabbixSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;
@@ -0,0 +1,27 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { ZabbixMacroType, ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const ZabbixSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Zabbix),
destinationConfig: z.discriminatedUnion("scope", [
z.object({
scope: z.literal(ZabbixSyncScope.Host),
hostId: z.string().trim().min(1, "Host ID required"),
hostName: z.string().trim().min(1, "Host name required"),
macroType: z.nativeEnum(ZabbixMacroType, {
errorMap: () => ({ message: "Macro type must be either 'text' or 'secret'" })
})
}),
z.object({
scope: z.literal(ZabbixSyncScope.Global),
macroType: z.nativeEnum(ZabbixMacroType, {
errorMap: () => ({ message: "Macro type must be either 'text' or 'secret'" })
})
})
])
})
);
@@ -0,0 +1,36 @@
import { MouseEventHandler } from "react";
export const HeaderResizer = ({
onMouseDown,
isActive,
scrollOffset,
heightOffset
}: {
onMouseDown: MouseEventHandler<HTMLDivElement>;
isActive: boolean;
scrollOffset: number;
heightOffset: number;
}) => {
return (
<>
<div
tabIndex={-1}
role="button"
className={`absolute left-0 z-40 h-0.5 w-full cursor-ns-resize hover:bg-blue-400/20 ${
isActive ? "bg-blue-400/75" : "bg-transparent"
}`}
onMouseDown={onMouseDown}
style={{
transform: "translateY(50%)",
top: heightOffset
}}
/>
<div
style={{ left: `calc(50% + ${scrollOffset}px)`, top: heightOffset }}
className="pointer-events-none absolute z-30 -translate-x-1/2"
>
<div className="h-1 w-8 rounded bg-gray-400 opacity-50" />
</div>
</>
);
};
@@ -0,0 +1,42 @@
export const HighlightText = ({
text,
highlight,
highlightClassName
}: {
text: string | undefined | null;
highlight: string;
highlightClassName?: string;
}) => {
if (!text) return null;
const searchTerm = highlight.toLowerCase().trim();
if (!searchTerm) return <span>{text}</span>;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(escapedSearchTerm, "gi");
text.replace(regex, (match: string, offset: number) => {
if (offset > lastIndex) {
parts.push(<span key={`pre-${lastIndex}`}>{text.substring(lastIndex, offset)}</span>);
}
parts.push(
<span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}>
{match}
</span>
);
lastIndex = offset + match.length;
return match;
});
if (lastIndex < text.length) {
parts.push(<span key={`post-${lastIndex}`}>{text.substring(lastIndex)}</span>);
}
return parts;
};
@@ -0,0 +1 @@
export { HighlightText } from "./HighlightText";
+9 -3
View File
@@ -45,10 +45,14 @@ export const Table = ({ children, className }: TableProps): JSX.Element => (
export type THeadProps = {
children: ReactNode;
className?: string;
style?: React.CSSProperties;
};
export const THead = ({ children, className }: THeadProps): JSX.Element => (
<thead className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}>
export const THead = ({ children, className, style }: THeadProps): JSX.Element => (
<thead
className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}
style={style}
>
{children}
</thead>
);
@@ -96,14 +100,16 @@ export const Tr = ({
export type ThProps = {
children?: ReactNode;
className?: string;
style?: React.CSSProperties;
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
export const Th = ({ children, className, style }: ThProps): JSX.Element => (
<th
className={twMerge(
"border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pb-3.5 pt-4 font-semibold",
className
)}
style={style}
>
{children}
</th>