improvements: address feedback

This commit is contained in:
Scott Wilson
2025-01-22 10:50:24 -08:00
parent c92c160709
commit 8f2a504fd0
22 changed files with 144 additions and 143 deletions
@@ -10,7 +10,7 @@ export async function up(knex: Knex): Promise<void> {
t.string("name", 32).notNullable(); t.string("name", 32).notNullable();
t.string("description"); t.string("description");
t.string("destination").notNullable(); t.string("destination").notNullable();
t.boolean("isEnabled").notNullable().defaultTo(true); t.boolean("isAutoSyncEnabled").notNullable().defaultTo(true);
t.integer("version").defaultTo(1).notNullable(); t.integer("version").defaultTo(1).notNullable();
t.jsonb("destinationConfig").notNullable(); t.jsonb("destinationConfig").notNullable();
t.jsonb("syncOptions").notNullable(); t.jsonb("syncOptions").notNullable();
+1 -1
View File
@@ -12,7 +12,7 @@ export const SecretSyncsSchema = z.object({
name: z.string(), name: z.string(),
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
destination: z.string(), destination: z.string(),
isEnabled: z.boolean().default(true), isAutoSyncEnabled: z.boolean().default(true),
version: z.number().default(1), version: z.number().default(1),
destinationConfig: z.unknown(), destinationConfig: z.unknown(),
syncOptions: z.unknown(), syncOptions: z.unknown(),
+2 -2
View File
@@ -1671,7 +1671,7 @@ export const SecretSyncs = {
connectionId: `The ID of the ${ connectionId: `The ID of the ${
APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]] APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]]
} Connection to use for syncing.`, } Connection to use for syncing.`,
isEnabled: `Whether secrets should be synced automatically or not.`, isAutoSyncEnabled: `Whether secrets should be automatically synced when changes occur at the source location or not.`,
syncOptions: "Optional parameters to modify how secrets are synced." syncOptions: "Optional parameters to modify how secrets are synced."
}; };
}, },
@@ -1686,7 +1686,7 @@ export const SecretSyncs = {
environment: `The updated slug of the project environment to sync secrets from.`, environment: `The updated slug of the project environment to sync secrets from.`,
secretPath: `The updated folder path to sync secrets from.`, secretPath: `The updated folder path to sync secrets from.`,
description: `The updated description of the ${destinationName} Sync.`, description: `The updated description of the ${destinationName} Sync.`,
isEnabled: `Whether secrets should be synced automatically or not.`, isAutoSyncEnabled: `Whether secrets should be automatically synced when changes occur at the source location or not.`,
syncOptions: "Optional parameters to modify how secrets are synced." syncOptions: "Optional parameters to modify how secrets are synced."
}; };
}, },
@@ -28,7 +28,7 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
destinationConfig: I["destinationConfig"]; destinationConfig: I["destinationConfig"];
syncOptions: I["syncOptions"]; syncOptions: I["syncOptions"];
description?: string | null; description?: string | null;
isEnabled?: boolean; isAutoSyncEnabled?: boolean;
}>; }>;
updateSchema: z.ZodType<{ updateSchema: z.ZodType<{
connectionId?: string; connectionId?: string;
@@ -38,6 +38,7 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
destinationConfig?: I["destinationConfig"]; destinationConfig?: I["destinationConfig"];
syncOptions?: I["syncOptions"]; syncOptions?: I["syncOptions"];
description?: string | null; description?: string | null;
isAutoSyncEnabled?: boolean;
}>; }>;
responseSchema: z.ZodTypeAny; responseSchema: z.ZodTypeAny;
}) => { }) => {
@@ -16,7 +16,7 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
.trim() .trim()
.min(1, "Parameter Store Path required") .min(1, "Parameter Store Path required")
.max(2048, "Cannot exceed 2048 characters") .max(2048, "Cannot exceed 2048 characters")
.regex(/^\/([/]|(([\w-]+\/)+))?$/) .regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format')
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.PATH) .describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.PATH)
}); });
@@ -73,7 +73,7 @@ export const SecretSyncFns = {
return GithubSyncFns.syncSecrets(secretSync, secretMap); return GithubSyncFns.syncSecrets(secretSync, secretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for push secrets: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
); );
} }
}, },
@@ -88,7 +88,7 @@ export const SecretSyncFns = {
break; break;
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for push secrets: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
); );
} }
@@ -105,7 +105,7 @@ export const SecretSyncFns = {
return GithubSyncFns.removeSecrets(secretSync, secretMap); return GithubSyncFns.removeSecrets(secretSync, secretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for removing secrets: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
); );
} }
} }
@@ -827,7 +827,7 @@ export const secretSyncQueueFactory = ({
`Could not find folder at path "${secretPath}" for environment with slug "${environmentSlug}" in project with ID "${projectId}"` `Could not find folder at path "${secretPath}" for environment with slug "${environmentSlug}" in project with ID "${projectId}"`
); );
const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isEnabled: true }); const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true });
await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id }))); await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id })));
}; };
@@ -65,7 +65,7 @@ export const GenericCreateSecretSyncFieldsSchema = (destination: SecretSync, syn
.min(1, "Secret path required") .min(1, "Secret path required")
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(SecretSyncs.CREATE(destination).secretPath), .describe(SecretSyncs.CREATE(destination).secretPath),
isEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isEnabled), isAutoSyncEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(destination).isAutoSyncEnabled),
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig).describe(SecretSyncs.CREATE(destination).syncOptions) syncOptions: SyncOptionsSchema(destination, syncOptionsConfig).describe(SecretSyncs.CREATE(destination).syncOptions)
}); });
@@ -89,7 +89,7 @@ export const GenericUpdateSecretSyncFieldsSchema = (destination: SecretSync, syn
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.optional() .optional()
.describe(SecretSyncs.UPDATE(destination).secretPath), .describe(SecretSyncs.UPDATE(destination).secretPath),
isEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isEnabled), isAutoSyncEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(destination).isAutoSyncEnabled),
syncOptions: SyncOptionsSchema(destination, syncOptionsConfig) syncOptions: SyncOptionsSchema(destination, syncOptionsConfig)
.optional() .optional()
.describe(SecretSyncs.UPDATE(destination).syncOptions) .describe(SecretSyncs.UPDATE(destination).syncOptions)
@@ -218,14 +218,14 @@ export const secretSyncServiceFactory = ({
const sync = await secretSyncDAL.create({ const sync = await secretSyncDAL.create({
folderId: folder.id, folderId: folder.id,
...params, ...params,
...(params.isEnabled && { syncStatus: SecretSyncStatus.Pending }), ...(params.isAutoSyncEnabled && { syncStatus: SecretSyncStatus.Pending }),
projectId projectId
}); });
return sync; return sync;
}); });
if (secretSync.isEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id }); if (secretSync.isAutoSyncEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
return secretSync as TSecretSync; return secretSync as TSecretSync;
}; };
@@ -317,18 +317,19 @@ export const secretSyncServiceFactory = ({
}); });
} }
const isEnabled = params.isEnabled ?? secretSync.isEnabled; const isAutoSyncEnabled = params.isAutoSyncEnabled ?? secretSync.isAutoSyncEnabled;
const updatedSync = await secretSyncDAL.updateById(syncId, { const updatedSync = await secretSyncDAL.updateById(syncId, {
...params, ...params,
...(isEnabled && folderId && { syncStatus: SecretSyncStatus.Pending }), ...(isAutoSyncEnabled && folderId && { syncStatus: SecretSyncStatus.Pending }),
folderId folderId
}); });
return updatedSync; return updatedSync;
}); });
if (updatedSecretSync.isEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id }); if (updatedSecretSync.isAutoSyncEnabled)
await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
return updatedSecretSync as TSecretSync; return updatedSecretSync as TSecretSync;
}; };
@@ -51,7 +51,7 @@ export type TCreateSecretSyncDTO = Pick<TSecretSync, "syncOptions" | "destinatio
projectId: string; projectId: string;
secretPath: string; secretPath: string;
environment: string; environment: string;
isEnabled?: boolean; isAutoSyncEnabled?: boolean;
}; };
export type TUpdateSecretSyncDTO = Partial<Omit<TCreateSecretSyncDTO, "projectId">> & { export type TUpdateSecretSyncDTO = Partial<Omit<TCreateSecretSyncDTO, "projectId">> & {
@@ -40,7 +40,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Infisical if secrets conflict.
- **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Parameter Store if secrets conflict. - **Import Secrets (Prioritize Parameter Store)**: Imports secrets from the destination endpoint prior to syncing, prioritizing values present in Parameter Store if secrets conflict.
- **Enabled**: If enabled, secrets will automatically be synced from the source. Disable to prevent syncing until enabled. - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
6. Configure the **Details** of your Parameter Store Sync, then click **Next**. 6. Configure the **Details** of your Parameter Store Sync, then click **Next**.
![Configure Details](/images/secret-syncs/aws-parameter-store/aws-parameter-store-details.png) ![Configure Details](/images/secret-syncs/aws-parameter-store/aws-parameter-store-details.png)
+1 -1
View File
@@ -62,7 +62,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
<Note> <Note>
GitHub does not support importing secrets. GitHub does not support importing secrets.
</Note> </Note>
- **Enabled**: If enabled, secrets will automatically be synced from the source. Disable to prevent syncing until enabled. - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
6. Configure the **Details** of your GitHub Sync, then click **Next**. 6. Configure the **Details** of your GitHub Sync, then click **Next**.
![Configure Details](/images/secret-syncs/github/github-details.png) ![Configure Details](/images/secret-syncs/github/github-details.png)
@@ -51,7 +51,7 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
resolver: zodResolver(SecretSyncFormSchema), resolver: zodResolver(SecretSyncFormSchema),
defaultValues: { defaultValues: {
destination, destination,
isEnabled: true, isAutoSyncEnabled: true,
syncOptions: { syncOptions: {
initialSyncBehavior: syncOption?.canImportSecrets initialSyncBehavior: syncOption?.canImportSecrets
? undefined ? undefined
@@ -161,26 +161,26 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
<SecretSyncOptionsFields /> <SecretSyncOptionsFields />
<Controller <Controller
control={control} control={control}
name="isEnabled" name="isAutoSyncEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => { render={({ field: { value, onChange }, fieldState: { error } }) => {
return ( return (
<FormControl <FormControl
helperText={ helperText={
value value
? "Secrets will automatically be synced when changes occur in the source location" ? "Secrets will automatically be synced when changes occur in the source location."
: "Secrets will not be synced until enabled" : "Secrets will not automatically be synced when changes occur in the source location. You can still trigger syncs manually."
} }
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
<Switch <Switch
className="bg-red/50 shadow-inner data-[state=checked]:bg-green/50" className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
id="secret-sync-enabled" id="auto-sync-enabled"
thumbClassName="bg-mineshaft-800" thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange} onCheckedChange={onChange}
isChecked={value} isChecked={value}
> >
<p className="w-14">{value ? "Enabled" : "Disabled"}</p> <p className="w-[8.4rem]">Auto-Sync {value ? "Enabled" : "Disabled"}</p>
</Switch> </Switch>
</FormControl> </FormControl>
); );
@@ -204,9 +204,11 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
containerClassName="-mt-5" containerClassName="-mt-5"
onCheckedChange={(isChecked) => setConfirmOverwrite(Boolean(isChecked))} onCheckedChange={(isChecked) => setConfirmOverwrite(Boolean(isChecked))}
> >
<p className={`mt-5 text-wrap ${confirmOverwrite ? "text-mineshaft-200" : "text-red"}`}> <p
className={`mt-5 text-wrap text-xs ${confirmOverwrite ? "text-mineshaft-200" : "text-red"}`}
>
I understand all secrets present in the configured {destinationName} destination will I understand all secrets present in the configured {destinationName} destination will
be removed that are not present within Infisical. be removed if they are not present within Infisical.
</p> </p>
</Checkbox> </Checkbox>
)} )}
@@ -222,7 +224,6 @@ export const CreateSecretSyncForm = ({ destination, onComplete, onCancel }: Prop
> >
{isFinalStep ? "Create Sync" : "Next"} {isFinalStep ? "Create Sync" : "Next"}
</Button> </Button>
{}
{selectedTabIndex > 0 && ( {selectedTabIndex > 0 && (
<Button onClick={handlePrev} colorSchema="secondary"> <Button onClick={handlePrev} colorSchema="secondary">
Back Back
@@ -26,7 +26,7 @@ export const SecretSyncReviewFields = () => {
initialSyncBehavior initialSyncBehavior
}, },
destination, destination,
isEnabled isAutoSyncEnabled
} = watch(); } = watch();
const destinationName = SECRET_SYNC_MAP[destination].name; const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -67,9 +67,9 @@ export const SecretSyncReviewFields = () => {
<span className="text-sm text-mineshaft-300">Options</span> <span className="text-sm text-mineshaft-300">Options</span>
</div> </div>
<div className="flex flex-wrap gap-x-8 gap-y-2"> <div className="flex flex-wrap gap-x-8 gap-y-2">
<SecretSyncLabel label="Sync Enabled"> <SecretSyncLabel label="Auto-Sync">
<Badge variant={isEnabled ? "success" : "danger"}> <Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
{isEnabled ? "Enabled" : "Disabled"} {isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge> </Badge>
</SecretSyncLabel> </SecretSyncLabel>
<SecretSyncLabel label="Initial Sync Behavior"> <SecretSyncLabel label="Initial Sync Behavior">
@@ -10,7 +10,7 @@ export const AwsParameterStoreSyncDestinationSchema = z.object({
.trim() .trim()
.min(1, "Parameter Store Path required") .min(1, "Parameter Store Path required")
.max(2048, "Cannot exceed 2048 characters") .max(2048, "Cannot exceed 2048 characters")
.regex(/^\/([/]|(([\w-]+\/)+))?$/), .regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format'),
region: z.string().min(1, "Region required") region: z.string().min(1, "Region required")
}) })
}); });
@@ -26,7 +26,7 @@ const BaseSecretSyncSchema = z.object({
// .transform((str) => str.toUpperCase()) // .transform((str) => str.toUpperCase())
// .optional() // .optional()
}), }),
isEnabled: z.boolean() isAutoSyncEnabled: z.boolean()
}); });
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
@@ -24,7 +24,7 @@ export type TCreateSecretSyncDTO = DiscriminativePick<
| "connectionId" | "connectionId"
| "syncOptions" | "syncOptions"
| "destination" | "destination"
| "isEnabled" | "isAutoSyncEnabled"
> & { environment: string; secretPath: string; projectId: string }; > & { environment: string; secretPath: string; projectId: string };
export type TUpdateSecretSyncDTO = Partial< export type TUpdateSecretSyncDTO = Partial<
@@ -10,7 +10,7 @@ export type TRootSecretSync = {
connectionId: string; connectionId: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
isEnabled: boolean; isAutoSyncEnabled: boolean;
projectId: string; projectId: string;
syncStatus: SecretSyncStatus | null; syncStatus: SecretSyncStatus | null;
lastSyncJobId: string | null; lastSyncJobId: string | null;
@@ -76,7 +76,7 @@ export const SecretSyncRow = ({
name, name,
description, description,
syncStatus, syncStatus,
isEnabled, isAutoSyncEnabled,
projectId projectId
} = secretSync; } = secretSync;
@@ -131,8 +131,7 @@ export const SecretSyncRow = ({
} }
className={twMerge( className={twMerge(
"group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700", "group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700",
syncStatus === SecretSyncStatus.Failed && "bg-red/5 hover:bg-red/10", syncStatus === SecretSyncStatus.Failed && "bg-red/5 hover:bg-red/10"
!isEnabled && "bg-mineshaft-400/15 opacity-50 hover:opacity-100"
)} )}
key={`sync-${id}`} key={`sync-${id}`}
> >
@@ -180,58 +179,59 @@ export const SecretSyncRow = ({
<SecretSyncDestinationCol secretSync={secretSync} /> <SecretSyncDestinationCol secretSync={secretSync} />
<Td> <Td>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{isEnabled ? ( {syncStatus && (
syncStatus && ( <Tooltip
<Tooltip position="left"
position="left" className="max-w-sm"
className="max-w-sm" content={
content={ [SecretSyncStatus.Succeeded, SecretSyncStatus.Failed].includes(syncStatus) ? (
[SecretSyncStatus.Succeeded, SecretSyncStatus.Failed].includes(syncStatus) ? ( <div className="flex flex-col gap-2 whitespace-normal py-1">
<div className="flex flex-col gap-2 whitespace-normal py-1"> {lastSyncedAt && (
{lastSyncedAt && ( <div>
<div> <div
<div className={`mb-2 flex self-start ${syncStatus === SecretSyncStatus.Failed ? "text-yellow" : "text-green"}`}
className={`mb-2 flex self-start ${syncStatus === SecretSyncStatus.Failed ? "text-yellow" : "text-green"}`} >
> <FontAwesomeIcon
<FontAwesomeIcon icon={faCalendarCheck}
icon={faCalendarCheck} className="ml-1 pr-1.5 pt-0.5 text-sm"
className="ml-1 pr-1.5 pt-0.5 text-sm" />
/> <div className="text-xs">Last Synced</div>
<div className="text-xs">Last Synced</div>
</div>
<div className="rounded bg-mineshaft-600 p-2 text-xs">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
</div>
</div> </div>
)} <div className="rounded bg-mineshaft-600 p-2 text-xs">
{failureMessage && ( {format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon
icon={faXmark}
className="ml-1 pr-1.5 pt-0.5 text-sm"
/>
<div className="text-xs">Failure Reason</div>
</div>
<div className="rounded bg-mineshaft-600 p-2 text-xs">
{failureMessage}
</div>
</div> </div>
)} </div>
</div> )}
) : undefined {failureMessage && (
} <div>
> <div className="mb-2 flex self-start text-red">
<div> <FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
<SecretSyncStatusBadge status={syncStatus} /> <div className="text-xs">Failure Reason</div>
</div> </div>
</Tooltip> <div className="rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</div>
) </div>
) : ( )}
<Badge className="flex w-min items-center gap-1.5 bg-mineshaft-400/50 text-bunker-300"> </div>
<FontAwesomeIcon icon={faBan} /> ) : undefined
<span>Disabled</span> }
</Badge> >
<div>
<SecretSyncStatusBadge status={syncStatus} />
</div>
</Tooltip>
)}
{!isAutoSyncEnabled && (
<Tooltip
className="text-xs"
content="Auto-Sync is disabled. Changes to the source location will not be automatically synced to the destination."
>
<div>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
{!syncStatus && "Auto-Sync Disabled"}
</Badge>
</div>
</Tooltip>
)} )}
<SecretSyncImportStatusBadge mini secretSync={secretSync} /> <SecretSyncImportStatusBadge mini secretSync={secretSync} />
<SecretSyncRemoveStatusBadge mini secretSync={secretSync} /> <SecretSyncRemoveStatusBadge mini secretSync={secretSync} />
@@ -359,13 +359,13 @@ export const SecretSyncRow = ({
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
isDisabled={!isAllowed} isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={isEnabled ? faToggleOff : faToggleOn} />} icon={<FontAwesomeIcon icon={isAutoSyncEnabled ? faToggleOff : faToggleOn} />}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onToggleEnable(secretSync); onToggleEnable(secretSync);
}} }}
> >
{isEnabled ? "Disable" : "Enable"} Sync {isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -2,7 +2,6 @@ import { useMemo, useState } from "react";
import { import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faBan,
faCheck, faCheck,
faCheckCircle, faCheckCircle,
faFilter, faFilter,
@@ -61,17 +60,10 @@ enum SecretSyncsOrderBy {
type SecretSyncFilters = { type SecretSyncFilters = {
destinations: SecretSync[]; destinations: SecretSync[];
status: SecretSyncStatusCol[]; status: SecretSyncStatus[];
environmentIds: string[]; environmentIds: string[];
}; };
enum SecretSyncStatusCol {
Pending = "pending",
Success = "success",
Failed = "failed",
Disabled = "disabled"
}
const getSyncStatusOrderValue = (syncStatus: SecretSyncStatus | null) => { const getSyncStatusOrderValue = (syncStatus: SecretSyncStatus | null) => {
switch (syncStatus) { switch (syncStatus) {
case SecretSyncStatus.Failed: case SecretSyncStatus.Failed:
@@ -91,10 +83,10 @@ type Props = {
}; };
const STATUS_ICON_MAP = { const STATUS_ICON_MAP = {
[SecretSyncStatusCol.Success]: { icon: faCheck, className: "text-green", name: "Synced" }, [SecretSyncStatus.Succeeded]: { icon: faCheck, className: "text-green", name: "Synced" },
[SecretSyncStatusCol.Failed]: { icon: faWarning, className: "text-red", name: "Not Synced" }, [SecretSyncStatus.Failed]: { icon: faWarning, className: "text-red", name: "Not Synced" },
[SecretSyncStatusCol.Pending]: { icon: faRotate, className: "text-yellow", name: "Syncing" }, [SecretSyncStatus.Pending]: { icon: faRotate, className: "text-yellow", name: "Syncing" },
[SecretSyncStatusCol.Disabled]: { icon: faBan, className: "text-mineshaft-400", name: "Disabled" } [SecretSyncStatus.Running]: { icon: faRotate, className: "text-yellow", name: "Syncing" }
}; };
export const SecretSyncsTable = ({ secretSyncs }: Props) => { export const SecretSyncsTable = ({ secretSyncs }: Props) => {
@@ -133,8 +125,7 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
() => () =>
secretSyncs secretSyncs
.filter((secretSync) => { .filter((secretSync) => {
const { destination, name, connection, folder, environment, syncStatus, isEnabled } = const { destination, name, connection, folder, environment, syncStatus } = secretSync;
secretSync;
if (filters.destinations.length && !filters.destinations.includes(destination)) if (filters.destinations.length && !filters.destinations.includes(destination))
return false; return false;
@@ -146,12 +137,7 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
) )
return false; return false;
const status = isEnabled ? syncStatus : SecretSyncStatusCol.Disabled; if (filters.status.length && (!syncStatus || !filters.status.includes(syncStatus))) {
if (
filters.status.length &&
(!status || !filters.status.includes(status as SecretSyncStatusCol))
) {
return false; return false;
} }
@@ -184,9 +170,6 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
getSecretSyncDestinationColValues(syncTwo).primaryText.toLowerCase() getSecretSyncDestinationColValues(syncTwo).primaryText.toLowerCase()
); );
case SecretSyncsOrderBy.Status: case SecretSyncsOrderBy.Status:
if (!syncOne.isEnabled && syncTwo.isEnabled) return 1;
if (syncOne.isEnabled && !syncTwo.isEnabled) return -1;
if (!syncOne.syncStatus && syncTwo.syncStatus) return 1; if (!syncOne.syncStatus && syncTwo.syncStatus) return 1;
if (syncOne.syncStatus && !syncTwo.syncStatus) return -1; if (syncOne.syncStatus && !syncTwo.syncStatus) return -1;
if (!syncOne.syncStatus && !syncTwo.syncStatus) return 0; if (!syncOne.syncStatus && !syncTwo.syncStatus) return 0;
@@ -238,22 +221,22 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
const handleToggleEnableSync = async (secretSync: TSecretSync) => { const handleToggleEnableSync = async (secretSync: TSecretSync) => {
const destinationName = SECRET_SYNC_MAP[secretSync.destination].name; const destinationName = SECRET_SYNC_MAP[secretSync.destination].name;
const isEnabled = !secretSync.isEnabled; const isAutoSyncEnabled = !secretSync.isAutoSyncEnabled;
try { try {
await updateSync.mutateAsync({ await updateSync.mutateAsync({
syncId: secretSync.id, syncId: secretSync.id,
destination: secretSync.destination, destination: secretSync.destination,
isEnabled isAutoSyncEnabled
}); });
createNotification({ createNotification({
text: `Successfully ${isEnabled ? "enabled" : "disabled"} ${destinationName} Sync`, text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`,
type: "success" type: "success"
}); });
} catch { } catch {
createNotification({ createNotification({
text: `Failed to ${isEnabled ? "enable" : "disable"} ${destinationName} Sync`, text: `Failed to ${isAutoSyncEnabled ? "enable" : "disable"} auto-sync for ${destinationName} Sync`,
type: "error" type: "error"
}); });
} }
@@ -306,7 +289,7 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end"> <DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuLabel>Status</DropdownMenuLabel> <DropdownMenuLabel>Status</DropdownMenuLabel>
{Object.values(SecretSyncStatusCol).map((status) => ( {Object.values(SecretSyncStatus).map((status) => (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@@ -442,7 +425,7 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
</IconButton> </IconButton>
</div> </div>
</Th> </Th>
<Th className="min-w-[10rem]"> <Th className="min-w-[10.5rem]">
<div className="flex items-center"> <div className="flex items-center">
Status Status
<IconButton <IconButton
@@ -1,5 +1,6 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { import {
faBan,
faCheck, faCheck,
faCopy, faCopy,
faDownload, faDownload,
@@ -24,6 +25,7 @@ import {
SecretSyncRemoveStatusBadge SecretSyncRemoveStatusBadge
} from "@app/components/secret-syncs"; } from "@app/components/secret-syncs";
import { import {
Badge,
Button, Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -84,22 +86,22 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
}, [isIdCopied]); }, [isIdCopied]);
const handleToggleEnableSync = async () => { const handleToggleEnableSync = async () => {
const isEnabled = !secretSync.isEnabled; const isAutoSyncEnabled = !secretSync.isAutoSyncEnabled;
try { try {
await updateSync.mutateAsync({ await updateSync.mutateAsync({
syncId: secretSync.id, syncId: secretSync.id,
destination: secretSync.destination, destination: secretSync.destination,
isEnabled isAutoSyncEnabled
}); });
createNotification({ createNotification({
text: `Successfully ${isEnabled ? "enabled" : "disabled"} ${destinationName} Sync`, text: `Successfully ${isAutoSyncEnabled ? "enabled" : "disabled"} auto-sync for ${destinationName} Sync`,
type: "success" type: "success"
}); });
} catch { } catch {
createNotification({ createNotification({
text: `Failed to ${isEnabled ? "enable" : "disable"} ${destinationName} Sync`, text: `Failed to ${isAutoSyncEnabled ? "enable" : "disable"} auto-sync for ${destinationName} Sync`,
type: "error" type: "error"
}); });
} }
@@ -129,6 +131,27 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
<div className="ml-auto mt-4 flex flex-wrap items-center justify-end gap-2"> <div className="ml-auto mt-4 flex flex-wrap items-center justify-end gap-2">
<SecretSyncImportStatusBadge secretSync={secretSync} /> <SecretSyncImportStatusBadge secretSync={secretSync} />
<SecretSyncRemoveStatusBadge secretSync={secretSync} /> <SecretSyncRemoveStatusBadge secretSync={secretSync} />
{secretSync.isAutoSyncEnabled ? (
<Badge
variant="success"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} />
<span>Auto-Sync Enabled</span>
</Badge>
) : (
<Tooltip
className="text-xs"
content="Auto-Sync is disabled. Changes to the source location will not be automatically synced to the destination."
>
<div>
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
<span>Auto-Sync Disabled</span>
</Badge>
</div>
</Tooltip>
)}
<div> <div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.SyncSecrets} I={ProjectPermissionSecretSyncActions.SyncSecrets}
@@ -230,11 +253,13 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
<DropdownMenuItem <DropdownMenuItem
isDisabled={!isAllowed} isDisabled={!isAllowed}
icon={ icon={
<FontAwesomeIcon icon={secretSync.isEnabled ? faToggleOff : faToggleOn} /> <FontAwesomeIcon
icon={secretSync.isAutoSyncEnabled ? faToggleOff : faToggleOn}
/>
} }
onClick={handleToggleEnableSync} onClick={handleToggleEnableSync}
> >
{secretSync.isEnabled ? "Disable" : "Enable"} Sync {secretSync.isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -1,11 +1,11 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { faBan, faEdit } from "@fortawesome/free-solid-svg-icons"; import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel, SecretSyncStatusBadge } from "@app/components/secret-syncs"; import { SecretSyncLabel } from "@app/components/secret-syncs";
import { Badge, IconButton } from "@app/components/v2"; import { IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
@@ -16,7 +16,7 @@ type Props = {
}; };
export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) => { export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) => {
const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, isEnabled } = secretSync; const { syncStatus, lastSyncMessage, lastSyncedAt, name, description } = secretSync;
const failureMessage = useMemo(() => { const failureMessage = useMemo(() => {
if (syncStatus === SecretSyncStatus.Failed) { if (syncStatus === SecretSyncStatus.Failed) {
@@ -57,16 +57,6 @@ export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) =
<div className="space-y-3"> <div className="space-y-3">
<SecretSyncLabel label="Name">{name}</SecretSyncLabel> <SecretSyncLabel label="Name">{name}</SecretSyncLabel>
<SecretSyncLabel label="Description">{description}</SecretSyncLabel> <SecretSyncLabel label="Description">{description}</SecretSyncLabel>
<SecretSyncLabel label="Status">
{isEnabled ? (
syncStatus && <SecretSyncStatusBadge status={syncStatus} />
) : (
<Badge className="flex w-min items-center gap-1.5 bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faBan} />
<span>Disabled</span>
</Badge>
)}
</SecretSyncLabel>
{lastSyncedAt && ( {lastSyncedAt && (
<SecretSyncLabel label="Last Synced"> <SecretSyncLabel label="Last Synced">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")} {format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}