mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 11:44:31 +00:00
Add warning on same destination secret sync
This commit is contained in:
@@ -53,3 +53,34 @@ export const titleCaseToCamelCase = (obj: unknown): unknown => {
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const deepEqual = (obj1: unknown, obj2: unknown): boolean => {
|
||||
if (obj1 === obj2) return true;
|
||||
|
||||
if (obj1 === null || obj2 === null || obj1 === undefined || obj2 === undefined) {
|
||||
return obj1 === obj2;
|
||||
}
|
||||
|
||||
if (typeof obj1 !== typeof obj2) return false;
|
||||
|
||||
if (typeof obj1 !== "object") return obj1 === obj2;
|
||||
|
||||
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false;
|
||||
|
||||
if (Array.isArray(obj1)) {
|
||||
const arr1 = obj1 as unknown[];
|
||||
const arr2 = obj2 as unknown[];
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
return arr1.every((val, idx) => deepEqual(val, arr2[idx]));
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1 as Record<string, unknown>).sort();
|
||||
const keys2 = Object.keys(obj2 as Record<string, unknown>).sort();
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
if (keys1.some((key, idx) => key !== keys2[idx])) return false;
|
||||
|
||||
return keys1.every((key) =>
|
||||
deepEqual((obj1 as Record<string, unknown>)[key], (obj2 as Record<string, unknown>)[key])
|
||||
);
|
||||
};
|
||||
|
||||
@@ -425,4 +425,39 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
|
||||
return { secretSync };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/check-destination",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
tags: [ApiDocsTags.SecretSyncs],
|
||||
body: z.object({
|
||||
destinationConfig: z.unknown(),
|
||||
excludeSyncId: z.string().uuid().optional(),
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ hasDuplicate: z.boolean() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { destinationConfig, excludeSyncId, projectId } = req.body;
|
||||
|
||||
const result = await server.services.secretSync.checkDuplicateDestination(
|
||||
{
|
||||
destinationConfig,
|
||||
destination,
|
||||
excludeSyncId,
|
||||
projectId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { deepEqual } from "@app/lib/fn/object";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@@ -20,6 +21,7 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { enterpriseSyncCheck, listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import {
|
||||
SecretSyncStatus,
|
||||
TCheckDuplicateDestinationDTO,
|
||||
TCreateSecretSyncDTO,
|
||||
TDeleteSecretSyncDTO,
|
||||
TFindSecretSyncByIdDTO,
|
||||
@@ -696,6 +698,51 @@ export const secretSyncServiceFactory = ({
|
||||
return updatedSecretSync as TSecretSync;
|
||||
};
|
||||
|
||||
const checkDuplicateDestination = async (
|
||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
if (!destinationConfig || typeof destinationConfig !== "object") {
|
||||
return { hasDuplicate: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingSyncs = await secretSyncDAL.find({
|
||||
destination
|
||||
});
|
||||
|
||||
const duplicates = existingSyncs.filter((sync) => {
|
||||
if (sync.id === excludeSyncId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return deepEqual(sync.destinationConfig, destinationConfig);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return { hasDuplicate: duplicates.length > 0 };
|
||||
} catch (error) {
|
||||
return { hasDuplicate: false };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretSyncOptions,
|
||||
listSecretSyncsByProjectId,
|
||||
@@ -707,6 +754,7 @@ export const secretSyncServiceFactory = ({
|
||||
deleteSecretSync,
|
||||
triggerSecretSyncSyncSecretsById,
|
||||
triggerSecretSyncImportSecretsById,
|
||||
triggerSecretSyncRemoveSecretsById
|
||||
triggerSecretSyncRemoveSecretsById,
|
||||
checkDuplicateDestination
|
||||
};
|
||||
};
|
||||
|
||||
@@ -324,6 +324,13 @@ export type TDeleteSecretSyncDTO = {
|
||||
removeSecrets: boolean;
|
||||
};
|
||||
|
||||
export type TCheckDuplicateDestinationDTO = {
|
||||
destination: SecretSync;
|
||||
destinationConfig: unknown;
|
||||
excludeSyncId?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
Pending = "pending",
|
||||
Running = "running",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export const DuplicateDestinationConfirmationModal = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
isLoading
|
||||
}: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent className="max-w-lg" title="Duplicate Destination Configuration">
|
||||
<div className="mb-4 text-sm">
|
||||
<p>
|
||||
Another secret sync in your organization is already configured with the same
|
||||
destination. Proceeding may cause conflicts or overwrite existing data.
|
||||
</p>
|
||||
<p className="mt-2">Are you sure you want to continue?</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 pt-4">
|
||||
<ModalClose asChild>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
colorSchema="danger"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain" isDisabled={isLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
@@ -6,9 +6,14 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { SecretSyncEditFields } from "@app/components/secret-syncs/types";
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { TSecretSync, useUpdateSecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import {
|
||||
TSecretSync,
|
||||
useCheckDuplicateDestination,
|
||||
useUpdateSecretSync
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { SecretSyncOptionsFields } from "./SecretSyncOptionsFields/SecretSyncOptionsFields";
|
||||
import { DuplicateDestinationConfirmationModal } from "./DuplicateDestinationConfirmationModal";
|
||||
import { TSecretSyncForm, UpdateSecretSyncFormSchema } from "./schemas";
|
||||
import { SecretSyncDestinationFields } from "./SecretSyncDestinationFields";
|
||||
import { SecretSyncDetailsFields } from "./SecretSyncDetailsFields";
|
||||
@@ -23,6 +28,8 @@ type Props = {
|
||||
export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) => {
|
||||
const updateSecretSync = useUpdateSecretSync();
|
||||
const { name: destinationName } = SECRET_SYNC_MAP[secretSync.destination];
|
||||
const [showDuplicateConfirmation, setShowDuplicateConfirmation] = useState(false);
|
||||
const [pendingFormData, setPendingFormData] = useState<TSecretSyncForm | null>(null);
|
||||
|
||||
const formMethods = useForm<TSecretSyncForm>({
|
||||
resolver: zodResolver(UpdateSecretSyncFormSchema),
|
||||
@@ -35,11 +42,23 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ environment, connection, ...formData }: TSecretSyncForm) => {
|
||||
const [destinationConfigToCheck, setDestinationConfigToCheck] = useState<unknown>(null);
|
||||
const [checkDuplicateEnabled, setCheckDuplicateEnabled] = useState(false);
|
||||
|
||||
const { data: hasDuplicate, isLoading: isCheckingDuplicate } = useCheckDuplicateDestination(
|
||||
secretSync.destination,
|
||||
destinationConfigToCheck,
|
||||
secretSync.projectId,
|
||||
secretSync.id,
|
||||
{ enabled: checkDuplicateEnabled && Boolean(destinationConfigToCheck) }
|
||||
);
|
||||
|
||||
const performUpdate = async (formData: TSecretSyncForm) => {
|
||||
try {
|
||||
const { environment, connection, ...updateData } = formData;
|
||||
const updatedSecretSync = await updateSecretSync.mutateAsync({
|
||||
syncId: secretSync.id,
|
||||
...formData,
|
||||
...updateData,
|
||||
environment: environment?.slug,
|
||||
connectionId: connection.id,
|
||||
projectId: secretSync.projectId
|
||||
@@ -60,6 +79,73 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (checkDuplicateEnabled && !isCheckingDuplicate && destinationConfigToCheck) {
|
||||
if (hasDuplicate) {
|
||||
setShowDuplicateConfirmation(true);
|
||||
} else if (pendingFormData) {
|
||||
performUpdate(pendingFormData);
|
||||
setPendingFormData(null);
|
||||
}
|
||||
setCheckDuplicateEnabled(false);
|
||||
setDestinationConfigToCheck(null);
|
||||
}
|
||||
}, [
|
||||
checkDuplicateEnabled,
|
||||
isCheckingDuplicate,
|
||||
hasDuplicate,
|
||||
destinationConfigToCheck,
|
||||
pendingFormData,
|
||||
performUpdate
|
||||
]);
|
||||
|
||||
const normalizeConfig = (config: unknown): unknown => {
|
||||
if (config === null || config === undefined || typeof config !== "object") {
|
||||
return config;
|
||||
}
|
||||
|
||||
if (Array.isArray(config)) {
|
||||
return config.map(normalizeConfig);
|
||||
}
|
||||
|
||||
const normalized: Record<string, unknown> = {};
|
||||
Object.keys(config as Record<string, unknown>)
|
||||
.sort()
|
||||
.forEach((key) => {
|
||||
normalized[key] = normalizeConfig((config as Record<string, unknown>)[key]);
|
||||
});
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const hasDestinationConfigChanged = (formData: TSecretSyncForm) => {
|
||||
const originalConfig = normalizeConfig(secretSync.destinationConfig);
|
||||
const currentConfig = normalizeConfig(formData.destinationConfig);
|
||||
|
||||
return JSON.stringify(originalConfig) !== JSON.stringify(currentConfig);
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: TSecretSyncForm) => {
|
||||
if (fields === SecretSyncEditFields.Destination && hasDestinationConfigChanged(formData)) {
|
||||
setDestinationConfigToCheck(formData.destinationConfig);
|
||||
setPendingFormData(formData);
|
||||
setCheckDuplicateEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await performUpdate(formData);
|
||||
};
|
||||
|
||||
const handleConfirmDuplicate = async () => {
|
||||
if (pendingFormData) {
|
||||
await performUpdate(pendingFormData);
|
||||
setPendingFormData(null);
|
||||
}
|
||||
setShowDuplicateConfirmation(false);
|
||||
setCheckDuplicateEnabled(false);
|
||||
setDestinationConfigToCheck(null);
|
||||
};
|
||||
|
||||
let Component: ReactNode;
|
||||
|
||||
switch (fields) {
|
||||
@@ -83,24 +169,35 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = formMethods;
|
||||
|
||||
const isLoading = isSubmitting || isCheckingDuplicate;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...formMethods}>{Component}</FormProvider>
|
||||
<div className="flex w-full justify-between gap-4 pt-4">
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...formMethods}>{Component}</FormProvider>
|
||||
<div className="flex w-full justify-between gap-4 pt-4">
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isDisabled={!isDirty || isLoading}
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
{isCheckingDuplicate ? "Checking..." : "Update Sync"}
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={!isDirty || isSubmitting}
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Update Sync
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DuplicateDestinationConfirmationModal
|
||||
isOpen={showDuplicateConfirmation}
|
||||
onOpenChange={setShowDuplicateConfirmation}
|
||||
onConfirm={handleConfirmDuplicate}
|
||||
isLoading={updateSecretSync.isPending}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { Badge } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import {
|
||||
AwsParameterStoreDestinationReviewFields,
|
||||
@@ -46,6 +49,7 @@ import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
|
||||
|
||||
export const SecretSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm>();
|
||||
const { currentProject } = useProject();
|
||||
|
||||
let DestinationFieldsComponent: ReactNode;
|
||||
let AdditionalSyncOptionsFieldsComponent: ReactNode;
|
||||
@@ -63,6 +67,13 @@ export const SecretSyncReviewFields = () => {
|
||||
|
||||
const destinationName = SECRET_SYNC_MAP[destination].name;
|
||||
|
||||
const { hasDuplicate, isChecking } = useDuplicateDestinationCheck({
|
||||
destination,
|
||||
projectId: currentProject?.id || "",
|
||||
enabled: true,
|
||||
destinationConfig: watch("destinationConfig")
|
||||
});
|
||||
|
||||
switch (destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
DestinationFieldsComponent = <AwsParameterStoreDestinationReviewFields />;
|
||||
@@ -173,9 +184,21 @@ export const SecretSyncReviewFields = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<div className="flex w-full items-center gap-2 border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Destination</span>
|
||||
{isChecking && <span className="text-xs text-mineshaft-400">Checking...</span>}
|
||||
</div>
|
||||
{hasDuplicate && (
|
||||
<div className="mb-2 flex items-start rounded-md border border-yellow-600 bg-yellow-900/20 p-3">
|
||||
<div className="text-sm text-yellow-200">
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faWarning} className="mr-1" />
|
||||
Another secret sync in your organization is already configured with the same
|
||||
destination. This may lead to conflicts or unexpected behavior.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>
|
||||
{DestinationFieldsComponent}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./enums";
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
export * from "./useDuplicateDestinationCheck";
|
||||
|
||||
@@ -14,7 +14,15 @@ export const secretSyncKeys = {
|
||||
options: () => [...secretSyncKeys.all, "options"] as const,
|
||||
list: (projectId: string) => [...secretSyncKeys.all, "list", projectId] as const,
|
||||
byId: (destination: SecretSync, syncId: string) =>
|
||||
[...secretSyncKeys.all, destination, "by-id", syncId] as const
|
||||
[...secretSyncKeys.all, destination, "by-id", syncId] as const,
|
||||
duplicateCheck: (destination: SecretSync, destinationConfig: unknown, excludeSyncId?: string) =>
|
||||
[
|
||||
...secretSyncKeys.all,
|
||||
destination,
|
||||
"duplicate-check",
|
||||
destinationConfig,
|
||||
excludeSyncId
|
||||
] as const
|
||||
};
|
||||
|
||||
export const useSecretSyncOptions = (
|
||||
@@ -88,3 +96,32 @@ export const useGetSecretSync = (
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useCheckDuplicateDestination = (
|
||||
destination: SecretSync,
|
||||
destinationConfig: unknown,
|
||||
projectId: string,
|
||||
excludeSyncId?: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<boolean, unknown, boolean, ReturnType<typeof secretSyncKeys.duplicateCheck>>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: secretSyncKeys.duplicateCheck(destination, destinationConfig, excludeSyncId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.post<{ hasDuplicate: boolean }>(
|
||||
`/api/v1/secret-syncs/${destination}/check-destination`,
|
||||
{
|
||||
destinationConfig,
|
||||
excludeSyncId,
|
||||
projectId
|
||||
}
|
||||
);
|
||||
|
||||
return data.hasDuplicate;
|
||||
},
|
||||
enabled: Boolean(destinationConfig) && Object.keys(destinationConfig || {}).length > 0,
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { SecretSync, useCheckDuplicateDestination } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
type UseDuplicateDestinationCheckProps = {
|
||||
destination: SecretSync;
|
||||
projectId: string;
|
||||
excludeSyncId?: string;
|
||||
enabled?: boolean;
|
||||
destinationConfig?: unknown;
|
||||
};
|
||||
|
||||
export const useDuplicateDestinationCheck = ({
|
||||
destination,
|
||||
projectId,
|
||||
excludeSyncId,
|
||||
enabled = true,
|
||||
destinationConfig
|
||||
}: UseDuplicateDestinationCheckProps) => {
|
||||
const hasValidConfig = useMemo(() => {
|
||||
if (!destinationConfig || typeof destinationConfig !== "object") return false;
|
||||
|
||||
const values = Object.values(destinationConfig);
|
||||
return (
|
||||
values.length > 0 &&
|
||||
values.some((value) => value !== null && value !== undefined && value !== "")
|
||||
);
|
||||
}, [destinationConfig]);
|
||||
|
||||
const shouldCheck = enabled && hasValidConfig;
|
||||
|
||||
const {
|
||||
data: hasDuplicate,
|
||||
isLoading,
|
||||
error,
|
||||
refetch
|
||||
} = useCheckDuplicateDestination(destination, destinationConfig, projectId, excludeSyncId, {
|
||||
enabled: shouldCheck,
|
||||
staleTime: 0,
|
||||
gcTime: 0
|
||||
});
|
||||
|
||||
return {
|
||||
hasDuplicate: shouldCheck ? Boolean(hasDuplicate) : false,
|
||||
isChecking: shouldCheck && isLoading,
|
||||
hasError: Boolean(error),
|
||||
hasValidConfig,
|
||||
refetch
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user