Merge branch 'main' into feat/remindersImprovement

This commit is contained in:
Carlos Monastyrski
2025-07-11 08:59:21 -03:00
397 changed files with 9891 additions and 1520 deletions
@@ -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,11 +17,13 @@ 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";
import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol";
import { WindmillSyncDestinationCol } from "./WindmillSyncDestinationCol";
import { ZabbixSyncDestinationCol } from "./ZabbixSyncDestinationCol";
type Props = {
secretSync: TSecretSync;
@@ -73,6 +75,10 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <GitLabSyncDestinationCol secretSync={secretSync} />;
case SecretSync.CloudflarePages:
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}`
@@ -0,0 +1,14 @@
import { TZabbixSync } from "@app/hooks/api/secretSyncs/types/zabbix-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TZabbixSync;
};
export const ZabbixSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -1,4 +1,5 @@
import { TerraformCloudSyncScope } from "@app/hooks/api/appConnections/terraform-cloud";
import { ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { GcpSyncScope } from "@app/hooks/api/secretSyncs/types/gcp-sync";
import {
@@ -144,6 +145,21 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.projectName;
secondaryText = destinationConfig.environment;
break;
case SecretSync.Zabbix:
if (destinationConfig.scope === ZabbixSyncScope.Host) {
primaryText = destinationConfig.hostName;
secondaryText = destinationConfig.hostId;
} else if (destinationConfig.scope === ZabbixSyncScope.Global) {
primaryText = "Global";
secondaryText = "";
} else {
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}`);
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
@@ -58,6 +58,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { HeaderResizer } from "@app/components/v2/HeaderResizer/HeaderResizer";
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
@@ -73,7 +74,14 @@ import {
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper, useToggle } from "@app/hooks";
import {
useDebounce,
usePagination,
usePopUp,
useResetPageHelper,
useResizableHeaderHeight,
useToggle
} from "@app/hooks";
import {
useCreateFolder,
useCreateSecretV3,
@@ -97,6 +105,7 @@ import {
useSecretRotationOverview
} from "@app/hooks/utils";
import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow";
import { getHeaderStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
@@ -142,6 +151,8 @@ const DEFAULT_FILTER_STATE = {
[RowType.SecretRotation]: true
};
const DEFAULT_COLLAPSED_HEADER_HEIGHT = 120;
export const OverviewPage = () => {
const { t } = useTranslation();
@@ -159,7 +170,7 @@ export const OverviewPage = () => {
const [scrollOffset, setScrollOffset] = useState(0);
const [debouncedScrollOffset] = useDebounce(scrollOffset);
const { permission } = useProjectPermission();
const tableRef = useRef<HTMLDivElement>(null);
const { currentWorkspace } = useWorkspace();
const isProjectV3 = currentWorkspace?.version === ProjectVersion.V3;
const workspaceId = currentWorkspace?.id as string;
@@ -861,10 +872,26 @@ export const OverviewPage = () => {
);
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
const storedHeight = Number.parseInt(
localStorage.getItem("overview-header-height") ?? DEFAULT_COLLAPSED_HEADER_HEIGHT.toString(),
10
);
const { headerHeight, handleMouseDown, isResizing } = useResizableHeaderHeight({
initialHeight: Number.isNaN(storedHeight) ? DEFAULT_COLLAPSED_HEADER_HEIGHT : storedHeight,
minHeight: DEFAULT_COLLAPSED_HEADER_HEIGHT,
maxHeight: 288
});
const debouncedHeaderHeight = useDebounce(headerHeight);
useEffect(() => {
localStorage.setItem("overview-header-height", debouncedHeaderHeight.toString());
}, [debouncedHeaderHeight]);
if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) {
return (
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
<Lottie icon="infisical_loading" className="h-32 w-32" />
<Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
</div>
);
}
@@ -892,7 +919,7 @@ export const OverviewPage = () => {
<meta property="og:title" content={String(t("dashboard.og-title"))} />
<meta name="og:description" content={String(t("dashboard.og-description"))} />
</Helmet>
<div className="mx-auto max-w-7xl text-mineshaft-50 dark:[color-scheme:dark]">
<div className="relative mx-auto max-w-7xl text-mineshaft-50 dark:[color-scheme:dark]">
<div className="flex w-full items-baseline justify-between">
<PageHeader
title="Secrets Overview"
@@ -959,7 +986,10 @@ export const OverviewPage = () => {
</Tooltip>
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
align="end"
>
{/* <DropdownMenuItem className="px-1.5" asChild>
<Button
size="xs"
@@ -1176,21 +1206,20 @@ export const OverviewPage = () => {
secretsToDeleteKeys={secretsToDeleteKeys}
usedBySecretSyncs={usedBySecretSyncs}
/>
<div className="thin-scrollbar mt-4">
<div ref={tableRef} className="thin-scrollbar mt-4">
<TableContainer
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
className="thin-scrollbar rounded-b-none"
>
<Table>
<THead className={collapseEnvironments ? "h-24" : ""}>
<THead style={{ height: collapseEnvironments ? headerHeight : undefined }}>
<Tr
className={twMerge("sticky top-0 z-20 border-0", collapseEnvironments && "h-24")}
className="sticky top-0 z-20 border-0"
style={{ height: collapseEnvironments ? headerHeight : undefined }}
>
<Th
className={twMerge(
"sticky left-0 z-20 min-w-[20rem] border-b-0 p-0",
collapseEnvironments && "h-24"
)}
className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0"
style={{ height: collapseEnvironments ? headerHeight : undefined }}
>
<div
className={twMerge(
@@ -1264,13 +1293,23 @@ export const OverviewPage = () => {
const importedSecKeyCount = getEnvImportedSecretKeyCount(slug);
const missingKeyCount = secKeys.length - envSecKeyCount - importedSecKeyCount;
const isLast = index === visibleEnvs.length - 1;
return (
<Th
className={twMerge(
"min-table-row border-b-0 p-0 text-xs",
collapseEnvironments && index === visibleEnvs.length - 1 && "mr-8",
collapseEnvironments ? "h-24 w-[1rem]" : "min-w-[11rem] text-center"
collapseEnvironments && index === visibleEnvs.length - 1 && "!mr-8",
!collapseEnvironments && "min-w-[11rem] text-center"
)}
style={
collapseEnvironments
? {
height: headerHeight,
width: "w-[1rem]"
}
: undefined
}
key={`secret-overview-${name}-${index + 1}`}
>
<Tooltip
@@ -1293,33 +1332,41 @@ export const OverviewPage = () => {
className={twMerge(
"border-b border-mineshaft-600",
collapseEnvironments
? "relative h-24 w-[2.9rem]"
? "relative"
: "flex items-center justify-center px-5 pb-[0.82rem] pt-3.5",
collapseEnvironments &&
index === visibleEnvs.length - 1 &&
"overflow-clip"
collapseEnvironments && isLast && "overflow-clip"
)}
style={{
height: collapseEnvironments ? headerHeight : undefined,
minWidth: collapseEnvironments ? "2.9rem" : undefined,
width: collapseEnvironments && isLast ? headerHeight * 0.3 : undefined
}}
>
<div
className={twMerge(
"border-mineshaft-600",
collapseEnvironments
? "ml-[0.85rem] h-24 -skew-x-[16rad] transform border-l text-xs"
? "-skew-x-[16rad] transform border-l text-xs"
: "flex items-center justify-center"
)}
style={{
height: collapseEnvironments ? headerHeight : undefined,
marginLeft: collapseEnvironments ? headerHeight * 0.145 : undefined
}}
/>
<button
type="button"
className={twMerge(
"duration-100 hover:text-mineshaft-100",
collapseEnvironments &&
(index === visibleEnvs.length - 1
? "bottom-[1.75rem] w-14"
: "bottom-10 w-20"),
collapseEnvironments
? "absolute -rotate-[72.25deg] text-left !text-[12px] font-normal"
? "absolute -rotate-[72.75deg] text-left text-sm font-normal"
: "flex items-center text-center text-sm font-medium"
)}
style={getHeaderStyle({
collapseEnvironments,
isLast,
headerHeight
})}
onClick={() => handleExploreEnvClick(slug)}
>
<p className="truncate font-medium">{name}</p>
@@ -1340,6 +1387,14 @@ export const OverviewPage = () => {
);
})}
</Tr>
{collapseEnvironments && (
<HeaderResizer
onMouseDown={handleMouseDown}
isActive={isResizing}
scrollOffset={scrollOffset}
heightOffset={(tableRef.current?.clientTop ?? 0) + headerHeight - 2.5}
/>
)}
</THead>
<TBody>
{canViewOverviewPage && isOverviewLoading && (
@@ -1499,9 +1554,9 @@ export const OverviewPage = () => {
style={{ height: "45px" }}
/>
</Td>
{visibleEnvs?.map(({ name, slug }) => (
{visibleEnvs?.map(({ name, slug }, i) => (
<Td
key={`explore-${name}-btn`}
key={`explore-${name}-btn-${i + 1}`}
className="border-0 border-r border-mineshaft-600 p-0"
>
<div
@@ -1,8 +1,8 @@
import { subject } from "@casl/ability";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
import {
faAngleDown,
faCheck,
faCircle,
faCodeBranch,
faEye,
faEyeSlash,
@@ -148,7 +148,7 @@ export const SecretOverviewTableRow = ({
"border-r border-mineshaft-600 px-0 py-3 group-hover:bg-mineshaft-700",
isFormExpanded && "border-t-2 border-mineshaft-500",
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-mineshaft-400" : "",
!isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : ""
)}
>
@@ -174,7 +174,7 @@ export const SecretOverviewTableRow = ({
)}
{isSecretEmpty && (
<Tooltip content="Empty value">
<FontAwesomeIcon icon={faCircle} />
<FontAwesomeIcon size="sm" icon={faCircle} />
</Tooltip>
)}
</div>
@@ -166,23 +166,42 @@ export const SelectionPanel = ({
secrets: secretsToDelete
});
}
return {
environment: env.slug
};
});
const results = await Promise.allSettled(promises);
const areEntriesDeleted = results.some((result) => result.status === "fulfilled");
const areAllEntriesDeleted = results.every((result) => result.status === "fulfilled");
const areSomeEntriesDeleted = results.some((result) => result.status === "fulfilled");
const failedEnvs = userAvailableEnvs
.filter(
(env) =>
!results.some(
(result) => result.status === "fulfilled" && result.value.environment === env.slug
)
)
.map((env) => env.name);
if (processedEntries === 0) {
handlePopUpClose("bulkDeleteEntries");
createNotification({
type: "info",
text: "You don't have access to delete selected items"
});
} else if (areEntriesDeleted) {
} else if (areAllEntriesDeleted) {
handlePopUpClose("bulkDeleteEntries");
resetSelectedEntries();
createNotification({
type: "success",
text: "Successfully deleted selected secrets and folders"
});
} else if (areSomeEntriesDeleted) {
createNotification({
type: "warning",
text: `Deletion partially completed. The following environments could not be processed due to conflicts: ${failedEnvs.join(", ")}.`
});
} else {
createNotification({
type: "error",
@@ -3,3 +3,33 @@ export const getExpandedRowStyle = (scrollOffset: number) => ({
width: "calc(100vw - 355px)", // 350px accounts for sidebar and margin
maxWidth: "1270px" // largest width of table on ultra-wide
});
type GetHeaderStyleParams = {
collapseEnvironments: boolean;
isLast: boolean;
headerHeight: number;
};
export const getHeaderStyle = ({
collapseEnvironments,
isLast,
headerHeight
}: GetHeaderStyleParams) => {
if (!collapseEnvironments) return undefined;
// scott: this is mostly trial/error to keep centered with skew
if (isLast) {
return {
width: headerHeight * 0.42,
bottom: headerHeight * 0.222,
left: 2
};
}
return {
// scott: this is mostly trial/error to keep centered with skew
width: headerHeight * 0.9,
bottom: headerHeight * 0.45,
left: 24 - (headerHeight * 0.9) / 2.985
};
};
@@ -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 () => {
@@ -253,7 +268,7 @@ export const SecretDropzone = ({
>
{isLoading ? (
<div className="mb-16 flex items-center justify-center pt-16">
<Lottie icon="infisical_loading" className="h-32 w-32" />
<Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
</div>
) : (
<div className="flex flex-col items-center justify-center space-y-2">
@@ -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,11 +28,13 @@ 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";
import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection";
import { WindmillSyncDestinationSection } from "./WindmillSyncDestinationSection";
import { ZabbixSyncDestinationSection } from "./ZabbixSyncDestinationSection";
type Props = {
secretSync: TSecretSync;
@@ -114,6 +116,12 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.CloudflarePages:
DestinationComponents = <CloudflarePagesSyncDestinationSection secretSync={secretSync} />;
break;
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}`);
}
@@ -0,0 +1,33 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
import { TZabbixSync } from "@app/hooks/api/secretSyncs/types/zabbix-sync";
type Props = {
secretSync: TZabbixSync;
};
const isTextMacro = (macroType: number) => macroType === 0;
export const ZabbixSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { macroType }
} = secretSync;
return (
<>
{secretSync.destinationConfig.scope === ZabbixSyncScope.Host && (
<>
<GenericFieldLabel label="Host Name">
{secretSync.destinationConfig.hostName}
</GenericFieldLabel>
<GenericFieldLabel label="Host ID">
{secretSync.destinationConfig.hostId}
</GenericFieldLabel>
</>
)}
<GenericFieldLabel label="Macro Type">
{isTextMacro(macroType) ? "Text" : "Secret"}
</GenericFieldLabel>
</>
);
};
@@ -60,6 +60,8 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Flyio:
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>
@@ -1,14 +1,14 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { createFileRoute, redirect } from "@tanstack/react-router";
// this is done as part of migration for multi product inside project
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval',
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval"
)({
beforeLoad: ({ params, search }) => {
throw redirect({
to: '/projects/$projectId/secret-manager/approval',
to: "/projects/$projectId/secret-manager/approval",
params,
search,
})
},
})
search
});
}
});