feat: Add Netlify app connection and secrets sync (#4205)

* fix: save wip

* feat: final impl

* feat: docs

* Update backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* chore: remove empty conflict files

* Update backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: lint

* feat: Netlify app connection and secrets sync

* feat: docs

* fix: type check

* fix: api client

* fix: lint and types

* fix: typecheck lint

* fix: docs

* fix: lint

* fix: lint

* fix: PR changes

* fix: typecheck

* fix: PR changes

* fix PR changes

* fix: PR Change

* fix: type error

* Small tweaks

* fix: support is_secret

* fix: revert is_secret

* fix: force update existing netlify secret

---------

Co-authored-by: sidwebworks <[email protected]>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: x032205 <[email protected]>
This commit is contained in:
Sid
2025-08-01 00:24:40 +05:30
committed by GitHub
co-authored by greptile-apps[bot] sidwebworks x032205
parent 3877fe524d
commit 79de7c5f08
90 changed files with 1747 additions and 2 deletions
@@ -31,7 +31,7 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
environment: secretSync.environment ?? undefined,
secretPath: secretSync.folder?.path,
description: secretSync.description ?? ""
},
} as Partial<TSecretSyncForm>,
reValidateMode: "onChange"
});
@@ -0,0 +1,144 @@
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 } from "@app/components/v2";
import {
TNetlifyAccount,
TNetlifySite,
useNetlifyConnectionListAccounts,
useNetlifyConnectionListSites
} from "@app/hooks/api/appConnections/netlify";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { NetlifySyncContext } from "../schemas/netlify-sync-destination-schema";
export const NetlifySyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Netlify }
>();
const connectionId = useWatch({ name: "connection.id", control });
const accountId = useWatch({ name: "destinationConfig.accountId", control });
const { data: accounts = [], isPending: isAccountsLoading } = useNetlifyConnectionListAccounts(
connectionId,
{
enabled: Boolean(connectionId)
}
);
const { data: sites = [], isPending: isSitesLoading } = useNetlifyConnectionListSites(
connectionId,
accountId,
{
enabled: Boolean(accountId)
}
);
const contexts = Object.entries(NetlifySyncContext).map(([key, value]) => ({
label: key,
value
}));
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.accountId", "");
setValue("destinationConfig.accountName", "");
}}
/>
<Controller
name="destinationConfig.accountId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Account"
isRequired
tooltipClassName="max-w-md"
>
<FilterableSelect
isLoading={isAccountsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={accounts.find((p) => p.id === value) ?? null}
onChange={(option) => {
const v = option as SingleValue<TNetlifyAccount>;
onChange(v?.id ?? null);
setValue("destinationConfig.accountName", v?.name ?? "");
}}
options={accounts}
placeholder="Select an account..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.siteId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Site"
isOptional
helperText="If you do not select a site, the secrets will be synced to all sites in the account."
tooltipClassName="max-w-md"
>
<FilterableSelect
isLoading={isSitesLoading && Boolean(accountId)}
isDisabled={!accountId}
value={sites.find((p) => p.id === value) ?? null}
onChange={(option) => {
const v = option as SingleValue<TNetlifySite>;
if (v?.id === value) {
onChange(undefined);
setValue("destinationConfig.siteName", undefined);
} else {
onChange(v?.id);
setValue("destinationConfig.siteName", v?.name);
}
}}
options={sites}
placeholder="Select a site..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.context"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Context"
isOptional
helperText="If you do not select a context, the secrets will be synced to all contexts."
tooltipClassName="max-w-md"
>
<FilterableSelect
isDisabled={!accountId}
value={contexts.find((p) => p.value === value) ?? undefined}
onChange={(option) => {
const v = option as SingleValue<{ label: string; value: NetlifySyncContext }>;
if (v) onChange(v.value);
}}
options={contexts}
placeholder="Select a context..."
getOptionLabel={(option) => option.label}
getOptionValue={(option) => option.value}
/>
</FormControl>
)}
/>
</>
);
};
@@ -23,6 +23,7 @@ import { GitLabSyncFields } from "./GitLabSyncFields";
import { HCVaultSyncFields } from "./HCVaultSyncFields";
import { HerokuSyncFields } from "./HerokuSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { NetlifySyncFields } from "./NetlifySyncFields";
import { OCIVaultSyncFields } from "./OCIVaultSyncFields";
import { RailwaySyncFields } from "./RailwaySyncFields";
import { RenderSyncFields } from "./RenderSyncFields";
@@ -95,6 +96,8 @@ export const SecretSyncDestinationFields = () => {
return <SupabaseSyncFields />;
case SecretSync.DigitalOceanAppPlatform:
return <DigitalOceanAppPlatformSyncFields />;
case SecretSync.Netlify:
return <NetlifySyncFields />;
case SecretSync.Bitbucket:
return <BitbucketSyncFields />;
default:
@@ -67,6 +67,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Checkly:
case SecretSync.Supabase:
case SecretSync.DigitalOceanAppPlatform:
case SecretSync.Netlify:
case SecretSync.Bitbucket:
AdditionalSyncOptionsFieldsComponent = null;
break;
@@ -0,0 +1,22 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const NetlifySyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Netlify }>();
const accountName = watch("destinationConfig.accountName");
const accountId = watch("destinationConfig.accountId");
const siteName = watch("destinationConfig.siteName");
const siteId = watch("destinationConfig.siteId");
const context = watch("destinationConfig.context");
return (
<>
<GenericFieldLabel label="Account">{accountName || accountId}</GenericFieldLabel>
<GenericFieldLabel label="Site">{siteName || siteId || "None"}</GenericFieldLabel>
<GenericFieldLabel label="Context">{context || "None"}</GenericFieldLabel>
</>
);
};
@@ -32,6 +32,7 @@ import { GitLabSyncReviewFields } from "./GitLabSyncReviewFields";
import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields";
import { HerokuSyncReviewFields } from "./HerokuSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { NetlifySyncReviewFields } from "./NetlifySyncReviewFields";
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
@@ -150,6 +151,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.DigitalOceanAppPlatform:
DestinationFieldsComponent = <DigitalOceanAppPlatformSyncReviewFields />;
break;
case SecretSync.Netlify:
DestinationFieldsComponent = <NetlifySyncReviewFields />;
break;
case SecretSync.Bitbucket:
DestinationFieldsComponent = <BitbucketSyncReviewFields />;
break;
@@ -0,0 +1,26 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export enum NetlifySyncContext {
// All = "all", - Netlify doesn't allow "all" as the context when using is_secret: true
DeployPreview = "deploy-preview",
Production = "production",
BranchDeploy = "branch-deploy",
Dev = "dev",
Branch = "branch"
}
export const NetlifySyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Netlify),
destinationConfig: z.object({
accountId: z.string(),
accountName: z.string(),
siteId: z.string().optional(),
siteName: z.string().optional(),
context: z.nativeEnum(NetlifySyncContext).optional()
})
})
);
@@ -20,6 +20,7 @@ import { GitlabSyncDestinationSchema } from "./gitlab-sync-destination-schema";
import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema";
import { HerokuSyncDestinationSchema } from "./heroku-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { NetlifySyncDestinationSchema } from "./netlify-sync-destination-schema";
import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema";
import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema";
import { RenderSyncDestinationSchema } from "./render-sync-destination-schema";
@@ -59,6 +60,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
RailwaySyncDestinationSchema,
ChecklySyncDestinationSchema,
DigitalOceanAppPlatformSyncDestinationSchema,
NetlifySyncDestinationSchema,
BitbucketSyncDestinationSchema
]);
+6
View File
@@ -45,6 +45,7 @@ import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/b
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection";
import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection";
import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection";
import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection";
@@ -105,6 +106,10 @@ export const APP_CONNECTION_MAP: Record<
name: "Digital Ocean",
image: "Digital Ocean.png"
},
[AppConnection.Netlify]: {
name: "Netlify",
image: "Netlify.png"
},
[AppConnection.Okta]: { name: "Okta", image: "Okta.png" }
};
@@ -152,6 +157,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case AzureDevOpsConnectionMethod.AccessToken:
case WindmillConnectionMethod.AccessToken:
case FlyioConnectionMethod.AccessToken:
case NetlifyConnectionMethod.AccessToken:
return { name: "Access Token", icon: faKey };
case Auth0ConnectionMethod.ClientCredentials:
return { name: "Client Credentials", icon: faServer };
+5
View File
@@ -106,6 +106,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
name: "Digital Ocean App Platform",
image: "Digital Ocean.png"
},
[SecretSync.Netlify]: {
name: "Netlify",
image: "Netlify.png"
},
[SecretSync.Bitbucket]: {
name: "Bitbucket",
image: "Bitbucket.png"
@@ -141,6 +145,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Railway]: AppConnection.Railway,
[SecretSync.Checkly]: AppConnection.Checkly,
[SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean,
[SecretSync.Netlify]: AppConnection.Netlify,
[SecretSync.Bitbucket]: AppConnection.Bitbucket
};
@@ -34,5 +34,6 @@ export enum AppConnection {
Checkly = "checkly",
Supabase = "supabase",
DigitalOcean = "digital-ocean",
Netlify = "netlify",
Okta = "okta"
}
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,65 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import { TNetlifyAccount, TNetlifySite } from "./types";
const netlifyConnectionKeys = {
all: [...appConnectionKeys.all, "netlify"] as const,
listAccounts: (connectionId: string) =>
[...netlifyConnectionKeys.all, "accounts", connectionId] as const,
listSites: (connectionId: string, accountId: string) =>
[...netlifyConnectionKeys.all, "sites", connectionId, accountId] as const
};
export const useNetlifyConnectionListAccounts = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TNetlifyAccount[],
unknown,
TNetlifyAccount[],
ReturnType<typeof netlifyConnectionKeys.listAccounts>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: netlifyConnectionKeys.listAccounts(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<{ accounts: TNetlifyAccount[] }>(
`/api/v1/app-connections/netlify/${connectionId}/accounts`
);
return data.accounts;
},
...options
});
};
export const useNetlifyConnectionListSites = (
connectionId: string,
accountId: string,
options?: Omit<
UseQueryOptions<
TNetlifySite[],
unknown,
TNetlifySite[],
ReturnType<typeof netlifyConnectionKeys.listSites>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: netlifyConnectionKeys.listSites(connectionId, accountId),
queryFn: async () => {
const { data } = await apiRequest.get<{ sites: TNetlifySite[] }>(
`/api/v1/app-connections/netlify/${connectionId}/accounts/${accountId}/sites`
);
return data.sites;
},
...options
});
};
@@ -0,0 +1,9 @@
export type TNetlifyAccount = {
id: string;
name: string;
};
export type TNetlifySite = {
id: string;
name: string;
};
@@ -156,6 +156,10 @@ export type TDigitalOceanConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.DigitalOcean;
};
export type TNetlifyConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Netlify;
};
export type TOktaConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Okta;
};
@@ -193,6 +197,7 @@ export type TAppConnectionOption =
| TRailwayConnectionOption
| TChecklyConnectionOption
| TDigitalOceanConnectionOption
| TNetlifyConnectionOption
| TOktaConnectionOption;
export type TAppConnectionOptionMap = {
@@ -231,5 +236,6 @@ export type TAppConnectionOptionMap = {
[AppConnection.Checkly]: TChecklyConnectionOption;
[AppConnection.Supabase]: TSupabaseConnectionOption;
[AppConnection.DigitalOcean]: TDigitalOceanConnectionOption;
[AppConnection.Netlify]: TNetlifyConnectionOption;
[AppConnection.Okta]: TOktaConnectionOption;
};
@@ -24,6 +24,7 @@ import { THumanitecConnection } from "./humanitec-connection";
import { TLdapConnection } from "./ldap-connection";
import { TMsSqlConnection } from "./mssql-connection";
import { TMySqlConnection } from "./mysql-connection";
import { TNetlifyConnection } from "./netlify-connection";
import { TOCIConnection } from "./oci-connection";
import { TOktaConnection } from "./okta-connection";
import { TOracleDBConnection } from "./oracledb-connection";
@@ -109,6 +110,7 @@ export type TAppConnection =
| TChecklyConnection
| TSupabaseConnection
| TDigitalOceanConnection
| TNetlifyConnection
| TOktaConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -181,5 +183,6 @@ export type TAppConnectionMap = {
[AppConnection.Checkly]: TChecklyConnection;
[AppConnection.Supabase]: TSupabaseConnection;
[AppConnection.DigitalOcean]: TDigitalOceanConnection;
[AppConnection.Netlify]: TNetlifyConnection;
[AppConnection.Okta]: TOktaConnection;
};
@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum NetlifyConnectionMethod {
AccessToken = "access-token"
}
export type TNetlifyConnection = TRootAppConnection & {
app: AppConnection.Netlify;
method: NetlifyConnectionMethod.AccessToken;
credentials: {
accessToken: string;
};
};
@@ -27,6 +27,7 @@ export enum SecretSync {
Railway = "railway",
Checkly = "checkly",
DigitalOceanAppPlatform = "digital-ocean-app-platform",
Netlify = "netlify",
Bitbucket = "bitbucket"
}
@@ -21,6 +21,7 @@ import { TGitLabSync } from "./gitlab-sync";
import { THCVaultSync } from "./hc-vault-sync";
import { THerokuSync } from "./heroku-sync";
import { THumanitecSync } from "./humanitec-sync";
import { TNetlifySync } from "./netlify-sync";
import { TOCIVaultSync } from "./oci-vault-sync";
import { TRailwaySync } from "./railway-sync";
import { TRenderSync } from "./render-sync";
@@ -67,6 +68,7 @@ export type TSecretSync =
| TChecklySync
| TSupabaseSync
| TDigitalOceanAppPlatformSync
| TNetlifySync
| TBitbucketSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-empty-object-type */
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TNetlifySync = TRootSecretSync & {
destination: SecretSync.Netlify;
destinationConfig: {
accountId: string;
accountName: string;
siteId?: string;
siteName?: string;
context?: string;
};
connection: {
app: AppConnection.Netlify;
name: string;
id: string;
};
};
@@ -33,6 +33,7 @@ import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { MySqlConnectionForm } from "./MySqlConnectionForm";
import { NetlifyConnectionForm } from "./NetlifyConnectionForm";
import { OCIConnectionForm } from "./OCIConnectionForm";
import { OktaConnectionForm } from "./OktaConnectionForm";
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
@@ -153,6 +154,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <SupabaseConnectionForm onSubmit={onSubmit} />;
case AppConnection.DigitalOcean:
return <DigitalOceanConnectionForm onSubmit={onSubmit} />;
case AppConnection.Netlify:
return <NetlifyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Okta:
return <OktaConnectionForm onSubmit={onSubmit} />;
default:
@@ -0,0 +1,136 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
NetlifyConnectionMethod,
TNetlifyConnection
} from "@app/hooks/api/appConnections/types/netlify-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TNetlifyConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Netlify)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(NetlifyConnectionMethod.AccessToken),
credentials: z.object({
accessToken: z.string().trim().min(1, "Netlify Access Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const NetlifyConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Netlify,
method: NetlifyConnectionMethod.AccessToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The auth method you would like to connect with ${
APP_CONNECTION_MAP[AppConnection.Netlify].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(NetlifyConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.accessToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Access Token Value"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Netlify"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,14 @@
import { TNetlifySync } from "@app/hooks/api/secretSyncs/types/netlify-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TNetlifySync;
};
export const NetlifySyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -20,6 +20,7 @@ import { GitLabSyncDestinationCol } from "./GitLabSyncDestinationCol";
import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { NetlifySyncDestinationCol } from "./NetlifySyncDestinationCol";
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol";
import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol";
@@ -92,6 +93,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <SupabaseSyncDestinationCol secretSync={secretSync} />;
case SecretSync.DigitalOceanAppPlatform:
return <DigitalOceanAppPlatformSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Netlify:
return <NetlifySyncDestinationCol secretSync={secretSync} />;
case SecretSync.Bitbucket:
return <BitbucketSyncDestinationCol secretSync={secretSync} />;
default:
@@ -178,6 +178,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.appName;
secondaryText = "Digital Ocean App";
break;
case SecretSync.Netlify:
primaryText = destinationConfig.siteName || destinationConfig.siteId || "All Sites";
secondaryText = destinationConfig.accountName || destinationConfig.accountId;
break;
case SecretSync.Bitbucket:
primaryText = destinationConfig.workspaceSlug;
secondaryText = destinationConfig.repositorySlug;
@@ -0,0 +1,22 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TNetlifySync } from "@app/hooks/api/secretSyncs/types/netlify-sync";
type Props = {
secretSync: TNetlifySync;
};
export const NetlifySyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
return (
<>
<GenericFieldLabel label="Account">
{destinationConfig.accountName || destinationConfig.accountId}
</GenericFieldLabel>
<GenericFieldLabel label="Site">
{destinationConfig.siteName || destinationConfig.siteId || "None"}
</GenericFieldLabel>
<GenericFieldLabel label="Context">{destinationConfig.context || "None"}</GenericFieldLabel>
</>
);
};
@@ -31,6 +31,7 @@ import { GitLabSyncDestinationSection } from "./GitLabSyncDestinationSection";
import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { NetlifySyncDestinationSection } from "./NetlifySyncDestinationSection";
import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection";
import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection";
import { RenderSyncDestinationSection } from "./RenderSyncDestinationSection";
@@ -141,6 +142,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
<DigitalOceanAppPlatformSyncDestinationSection secretSync={secretSync} />
);
break;
case SecretSync.Netlify:
DestinationComponents = <NetlifySyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Bitbucket:
DestinationComponents = <BitbucketSyncDestinationSection secretSync={secretSync} />;
break;
@@ -69,6 +69,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Supabase:
case SecretSync.Checkly:
case SecretSync.DigitalOceanAppPlatform:
case SecretSync.Netlify:
case SecretSync.Bitbucket:
AdditionalSyncOptionsComponent = null;
break;