mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: adds secret sync handlers
This commit is contained in:
@@ -2512,8 +2512,11 @@ export const SecretSyncs = {
|
||||
},
|
||||
LARAVEL_FORGE: {
|
||||
orgSlug: "The slug of the Laravel Forge org to sync secrets to.",
|
||||
orgName: "The name of the Laravel Forge org to sync secrets to.",
|
||||
serverId: "The ID of the Laravel Forge server to sync secrets to.",
|
||||
siteId: "The ID of the Laravel Forge site to sync secrets to."
|
||||
serverName: "The name of the Laravel Forge server to sync secrets to.",
|
||||
siteId: "The ID of the Laravel Forge site to sync secrets to.",
|
||||
siteName: "The name of the Laravel Forge site to sync secrets to."
|
||||
},
|
||||
WINDMILL: {
|
||||
workspace: "The Windmill workspace to sync secrets to.",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./laravel-forge-sync-constants";
|
||||
export * from "./laravel-forge-sync-fns";
|
||||
export * from "./laravel-forge-sync-schemas";
|
||||
export * from "./laravel-forge-sync-types";
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import {
|
||||
LaravelForgeSecret,
|
||||
TGetLaravelForgeSecrets,
|
||||
TLaravelForgeSecrets,
|
||||
TLaravelForgeSyncWithCredentials
|
||||
} from "./laravel-forge-sync-types";
|
||||
|
||||
const getLaravelForgeSecretsRaw = async ({ apiToken, orgSlug, serverId, siteId }: TGetLaravelForgeSecrets) => {
|
||||
const { data } = await request.get<TLaravelForgeSecrets>(
|
||||
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs/${orgSlug}/servers/${serverId}/sites/${siteId}/environment`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data.data.attributes.content;
|
||||
};
|
||||
|
||||
const parseEnv = (str: string) => {
|
||||
const lines = str.split("\n");
|
||||
const parsed: { key: string; value: string }[] = [];
|
||||
|
||||
lines.forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
|
||||
const isInvalidLine = trimmed === "" || trimmed.startsWith("#");
|
||||
|
||||
if (!isInvalidLine && trimmed.includes("=")) {
|
||||
const equalIndex = trimmed.indexOf("=");
|
||||
const key = trimmed.substring(0, equalIndex).trim();
|
||||
const valueRaw = trimmed.substring(equalIndex + 1).trim();
|
||||
let value = valueRaw;
|
||||
|
||||
if ((value.startsWith(`"`) && value.endsWith(`"`)) || (value.startsWith(`'`) && value.endsWith(`'`))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
parsed.push({
|
||||
key,
|
||||
value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const getLaravelForgeSecrets = async (secretSync: TLaravelForgeSyncWithCredentials): Promise<LaravelForgeSecret[]> => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { orgSlug, serverId, siteId }
|
||||
} = secretSync;
|
||||
|
||||
const { apiToken } = connection.credentials;
|
||||
|
||||
const secrets = await getLaravelForgeSecretsRaw({ apiToken, orgSlug, serverId, siteId });
|
||||
|
||||
const parsedSecrets = parseEnv(secrets);
|
||||
|
||||
return parsedSecrets;
|
||||
};
|
||||
|
||||
const buildEnvString = (secrets: LaravelForgeSecret[]) => {
|
||||
return secrets.map((secret) => `${secret.key}=${secret.value}`).join("\n");
|
||||
};
|
||||
|
||||
const updateLaravelForgeSecrets = async (secretSync: TLaravelForgeSyncWithCredentials, envString: string) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { orgSlug, serverId, siteId }
|
||||
} = secretSync;
|
||||
|
||||
const { apiToken } = connection.credentials;
|
||||
|
||||
await request.put(
|
||||
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs/${orgSlug}/servers/${serverId}/sites/${siteId}/environment`,
|
||||
{
|
||||
environment: envString
|
||||
},
|
||||
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const LaravelForgeSyncFns = {
|
||||
async syncSecrets(secretSync: TLaravelForgeSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const secrets = await getLaravelForgeSecrets(secretSync);
|
||||
const secretsMap = Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }]));
|
||||
|
||||
for (const [key, { value }] of Object.entries(secretMap)) {
|
||||
secretsMap[key] = { value };
|
||||
}
|
||||
|
||||
await updateLaravelForgeSecrets(secretSync, buildEnvString(secrets));
|
||||
},
|
||||
|
||||
async getSecrets(secretSync: TLaravelForgeSyncWithCredentials): Promise<TSecretMap> {
|
||||
const secrets = await getLaravelForgeSecrets(secretSync);
|
||||
return Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }]));
|
||||
},
|
||||
|
||||
async removeSecrets(secretSync: TLaravelForgeSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const existingSecrets = await getLaravelForgeSecrets(secretSync);
|
||||
|
||||
const newSecrets = existingSecrets.filter((secret) => !Object.hasOwn(secretMap, secret.key));
|
||||
|
||||
if (newSecrets.length === existingSecrets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const envString = buildEnvString(newSecrets);
|
||||
|
||||
await updateLaravelForgeSecrets(secretSync, envString);
|
||||
}
|
||||
};
|
||||
@@ -12,8 +12,14 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"
|
||||
|
||||
const LaravelForgeSyncDestinationConfigSchema = z.object({
|
||||
orgSlug: z.string().min(1, "Org Slug is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.orgSlug),
|
||||
orgName: z.string().min(1, "Org Name is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.orgName),
|
||||
serverId: z.string().min(1, "Server ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.serverId),
|
||||
siteId: z.string().min(1, "Site ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteId)
|
||||
serverName: z
|
||||
.string()
|
||||
.min(1, "Server Name is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.serverName),
|
||||
siteId: z.string().min(1, "Site ID is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteId),
|
||||
siteName: z.string().min(1, "Site Name is required").describe(SecretSyncs.DESTINATION_CONFIG.LARAVEL_FORGE.siteName)
|
||||
});
|
||||
|
||||
const LaravelForgeSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
@@ -18,24 +18,29 @@ export type TLaravelForgeSyncWithCredentials = TLaravelForgeSync & {
|
||||
connection: TLaravelForgeConnection;
|
||||
};
|
||||
|
||||
export type TGetLaravelForgeSecrets = {
|
||||
apiToken: string;
|
||||
orgSlug: string;
|
||||
serverId: string;
|
||||
siteId: string;
|
||||
};
|
||||
|
||||
export type TLaravelForgeSecrets = {
|
||||
data: {
|
||||
id: string;
|
||||
type: string;
|
||||
attributes: {
|
||||
content: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type LaravelForgeSecret = {
|
||||
description: string;
|
||||
is_secret: boolean;
|
||||
key: string;
|
||||
source: "app" | "env";
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface LaravelForgeApiSecret {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
target: string[];
|
||||
customEnvironmentIds?: string[];
|
||||
gitBranch?: string;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
configurationId?: string;
|
||||
system?: boolean;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { LARAVEL_FORGE_SYNC_LIST_OPTION } from "./laravel-forge";
|
||||
import { LaravelForgeSyncFns } from "./laravel-forge/laravel-forge-sync-fns";
|
||||
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
||||
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
|
||||
import { RailwaySyncFns } from "./railway/railway-sync-fns";
|
||||
@@ -279,6 +280,8 @@ export const SecretSyncFns = {
|
||||
return NetlifySyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Bitbucket:
|
||||
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.LaravelForge:
|
||||
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -395,6 +398,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.Bitbucket:
|
||||
secretMap = await BitbucketSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.LaravelForge:
|
||||
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -488,6 +494,8 @@ export const SecretSyncFns = {
|
||||
return NetlifySyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Bitbucket:
|
||||
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.LaravelForge:
|
||||
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
@@ -50,6 +50,9 @@ export const LaravelForgeSyncFields = () => {
|
||||
setValue("destinationConfig.orgSlug", "");
|
||||
setValue("destinationConfig.serverId", "");
|
||||
setValue("destinationConfig.siteId", "");
|
||||
setValue("destinationConfig.orgName", "");
|
||||
setValue("destinationConfig.serverName", "");
|
||||
setValue("destinationConfig.siteName", "");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -69,6 +72,7 @@ export const LaravelForgeSyncFields = () => {
|
||||
onChange={(option) => {
|
||||
const selectedOrg = option as SingleValue<TLaravelForgeOrganization>;
|
||||
onChange(selectedOrg?.slug ?? "");
|
||||
setValue("destinationConfig.orgName", selectedOrg?.name ?? "");
|
||||
setValue("destinationConfig.serverId", "");
|
||||
setValue("destinationConfig.siteId", "");
|
||||
}}
|
||||
@@ -94,6 +98,7 @@ export const LaravelForgeSyncFields = () => {
|
||||
onChange={(option) => {
|
||||
const selectedServer = option as SingleValue<TLaravelForgeServer>;
|
||||
onChange(selectedServer?.id ?? "");
|
||||
setValue("destinationConfig.serverName", selectedServer?.name ?? "");
|
||||
setValue("destinationConfig.siteId", "");
|
||||
}}
|
||||
options={servers}
|
||||
@@ -118,6 +123,7 @@ export const LaravelForgeSyncFields = () => {
|
||||
onChange={(option) => {
|
||||
const selectedSite = option as SingleValue<TLaravelForgeSite>;
|
||||
onChange(selectedSite?.id ?? "");
|
||||
setValue("destinationConfig.siteName", selectedSite?.name ?? "");
|
||||
}}
|
||||
options={sites}
|
||||
placeholder="Select a site..."
|
||||
|
||||
@@ -69,6 +69,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
case SecretSync.DigitalOceanAppPlatform:
|
||||
case SecretSync.Netlify:
|
||||
case SecretSync.Bitbucket:
|
||||
case SecretSync.LaravelForge:
|
||||
AdditionalSyncOptionsFieldsComponent = null;
|
||||
break;
|
||||
default:
|
||||
@@ -77,7 +78,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-bunker-300">Configure how secrets should be synced.</p>
|
||||
<p className="text-bunker-300 mb-4 text-sm">Configure how secrets should be synced.</p>
|
||||
{!hideInitialSync && (
|
||||
<>
|
||||
<Controller
|
||||
@@ -100,7 +101,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
return (
|
||||
<li key={name}>
|
||||
<p className="text-mineshaft-300">
|
||||
<span className="font-medium text-bunker-200">{name}</span>:{" "}
|
||||
<span className="text-bunker-200 font-medium">{name}</span>:{" "}
|
||||
{description}
|
||||
</p>
|
||||
</li>
|
||||
@@ -118,7 +119,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
isDisabled={!syncOption?.canImportSecrets}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
className="border-mineshaft-500 w-full border"
|
||||
position="popper"
|
||||
placeholder="Select an option..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
@@ -137,7 +138,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
)}
|
||||
/>
|
||||
{!syncOption?.canImportSecrets && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<p className="text-yellow -mt-2.5 mb-2.5 text-xs">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
|
||||
{destinationName} only supports overwriting destination secrets.{" "}
|
||||
{!currentSyncOption.disableSecretDeletion &&
|
||||
@@ -218,7 +219,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
className="bg-mineshaft-400/80 data-[state=checked]:bg-green/80 shadow-inner"
|
||||
id="auto-sync-enabled"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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 LaravelForgeSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.LaravelForge }>();
|
||||
const orgName = watch("destinationConfig.orgName");
|
||||
const orgSlug = watch("destinationConfig.orgSlug");
|
||||
const serverName = watch("destinationConfig.serverName");
|
||||
const serverId = watch("destinationConfig.serverId");
|
||||
const siteName = watch("destinationConfig.siteName");
|
||||
const siteId = watch("destinationConfig.siteId");
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Account">{orgName || orgSlug}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Server">{serverName || serverId || "None"}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Site">{siteName || siteId || "None"}</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -35,6 +35,7 @@ import { GitLabSyncReviewFields } from "./GitLabSyncReviewFields";
|
||||
import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields";
|
||||
import { HerokuSyncReviewFields } from "./HerokuSyncReviewFields";
|
||||
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
|
||||
import { LaravelForgeSyncReviewFields } from "./LaravelForgeSyncReviewFields";
|
||||
import { NetlifySyncReviewFields } from "./NetlifySyncReviewFields";
|
||||
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
|
||||
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
|
||||
@@ -168,6 +169,9 @@ export const SecretSyncReviewFields = () => {
|
||||
case SecretSync.Bitbucket:
|
||||
DestinationFieldsComponent = <BitbucketSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.LaravelForge:
|
||||
DestinationFieldsComponent = <LaravelForgeSyncReviewFields />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
@@ -175,8 +179,8 @@ export const SecretSyncReviewFields = () => {
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Source</span>
|
||||
<div className="border-mineshaft-600 w-full border-b">
|
||||
<span className="text-mineshaft-300 text-sm">Source</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Environment">{environment.name}</GenericFieldLabel>
|
||||
@@ -184,14 +188,14 @@ export const SecretSyncReviewFields = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<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 className="border-mineshaft-600 flex w-full items-center gap-2 border-b">
|
||||
<span className="text-mineshaft-300 text-sm">Destination</span>
|
||||
{isChecking && <span className="text-mineshaft-400 text-xs">Checking...</span>}
|
||||
</div>
|
||||
{hasDuplicate && (
|
||||
<div className="mb-2 flex items-start rounded-md border border-yellow-600 bg-yellow-900/20 px-3 py-2">
|
||||
<div className="flex text-sm text-yellow-100">
|
||||
<FontAwesomeIcon icon={faWarning} className="mt-1 mr-2 text-yellow-600" />
|
||||
<FontAwesomeIcon icon={faWarning} className="mr-2 mt-1 text-yellow-600" />
|
||||
<div>
|
||||
<p>
|
||||
Another secret sync in your organization is already configured with the same
|
||||
@@ -215,8 +219,8 @@ export const SecretSyncReviewFields = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||
<div className="border-mineshaft-600 w-full border-b">
|
||||
<span className="text-mineshaft-300 text-sm">Sync Options</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Auto-Sync">
|
||||
@@ -237,8 +241,8 @@ export const SecretSyncReviewFields = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Details</span>
|
||||
<div className="border-mineshaft-600 w-full border-b">
|
||||
<span className="text-mineshaft-300 text-sm">Details</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||
|
||||
@@ -8,8 +8,11 @@ export const LaravelForgeSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
destination: z.literal(SecretSync.LaravelForge),
|
||||
destinationConfig: z.object({
|
||||
orgSlug: z.string().trim().min(1, "Org Slug required"),
|
||||
orgName: z.string().trim().min(1, "Org Name required"),
|
||||
serverId: z.string().trim().min(1, "Server ID required"),
|
||||
siteId: z.string().trim().min(1, "Site ID required")
|
||||
serverName: z.string().trim().min(1, "Server Name required"),
|
||||
siteId: z.string().trim().min(1, "Site ID required"),
|
||||
siteName: z.string().trim().min(1, "Site Name required")
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
@@ -6,8 +6,11 @@ export type TLaravelForgeSync = TRootSecretSync & {
|
||||
destination: SecretSync.LaravelForge;
|
||||
destinationConfig: {
|
||||
orgSlug: string;
|
||||
orgName: string;
|
||||
serverId: string;
|
||||
serverName: string;
|
||||
siteId: string;
|
||||
siteName: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.LaravelForge;
|
||||
|
||||
@@ -130,9 +130,9 @@ export const CloudIntegrationSection = ({
|
||||
tabIndex={0}
|
||||
className={`group relative ${
|
||||
cloudIntegration.isAvailable
|
||||
? "cursor-pointer duration-200 hover:bg-mineshaft-700"
|
||||
? "hover:bg-mineshaft-700 cursor-pointer duration-200"
|
||||
: "opacity-50"
|
||||
} flex h-36 flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3`}
|
||||
} border-mineshaft-600 bg-mineshaft-800 flex h-36 flex-col items-center justify-center rounded-md border p-3`}
|
||||
onClick={() => {
|
||||
if (isSyncAvailable) {
|
||||
navigate({
|
||||
@@ -180,9 +180,9 @@ export const CloudIntegrationSection = ({
|
||||
</div>
|
||||
{cloudIntegration.isAvailable &&
|
||||
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
|
||||
<div className="absolute top-0 right-0 z-30 h-full">
|
||||
<div className="absolute right-0 top-0 z-30 h-full">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden rounded-tr-md rounded-bl-md bg-primary px-2 py-0.5 text-xs whitespace-nowrap text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
|
||||
<div className="bg-primary absolute right-0 top-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-bl-md rounded-tr-md px-2 py-0.5 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
|
||||
Authorized
|
||||
</div>
|
||||
@@ -197,7 +197,7 @@ export const CloudIntegrationSection = ({
|
||||
provider: cloudIntegration.slug
|
||||
});
|
||||
}}
|
||||
className="absolute top-0 right-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md bg-red text-xs opacity-50 transition-all duration-300 group-hover:h-full hover:opacity-100"
|
||||
className="bg-red absolute right-0 top-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md text-xs opacity-50 transition-all duration-300 hover:opacity-100 group-hover:h-full"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="xl" />
|
||||
</div>
|
||||
@@ -208,7 +208,7 @@ export const CloudIntegrationSection = ({
|
||||
{isSyncAvailable && (
|
||||
<div className="absolute bottom-0 left-0 z-30 h-full w-full">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute bottom-0 left-0 w-full flex-row overflow-hidden rounded-br-md rounded-bl-md bg-yellow/20 px-2 py-0.5 text-center text-xs whitespace-nowrap text-yellow">
|
||||
<div className="bg-yellow/20 text-yellow absolute bottom-0 left-0 w-full flex-row overflow-hidden whitespace-nowrap rounded-bl-md rounded-br-md px-2 py-0.5 text-center text-xs">
|
||||
Secret Sync Available
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,7 +230,7 @@ export const CloudIntegrationSection = ({
|
||||
{Array.from({ length: 16 }).map((_, index) => (
|
||||
<div
|
||||
key={`dummy-cloud-integration-${index + 1}`}
|
||||
className="h-32 animate-pulse rounded-md border border-mineshaft-600 bg-mineshaft-800"
|
||||
className="border-mineshaft-600 bg-mineshaft-800 h-32 animate-pulse rounded-md border"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { TLaravelForgeSync } from "@app/hooks/api/secretSyncs/types/laravel-forge-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: TLaravelForgeSync;
|
||||
};
|
||||
|
||||
export const LaravelForgeSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
console.log({ 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 { LaravelForgeSyncDestinationCol } from "./LaravelForgeSyncDestinationCol";
|
||||
import { NetlifySyncDestinationCol } from "./NetlifySyncDestinationCol";
|
||||
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
|
||||
import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol";
|
||||
@@ -97,6 +98,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
return <NetlifySyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Bitbucket:
|
||||
return <BitbucketSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.LaravelForge:
|
||||
return <LaravelForgeSyncDestinationCol secretSync={secretSync} />;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||
|
||||
@@ -194,6 +194,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
|
||||
primaryText = destinationConfig.workspaceSlug;
|
||||
secondaryText = destinationConfig.repositorySlug;
|
||||
break;
|
||||
case SecretSync.LaravelForge:
|
||||
primaryText = destinationConfig.siteName || destinationConfig.siteId;
|
||||
secondaryText = destinationConfig.orgName || destinationConfig.orgSlug;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Col Values ${destination}`);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ const PageContent = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
|
||||
<div className="bg-bunker-800 font-inter container mx-auto flex flex-col justify-between text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<Button
|
||||
variant="link"
|
||||
@@ -96,11 +96,11 @@ const PageContent = () => {
|
||||
<img
|
||||
alt={`${destinationDetails.name} sync`}
|
||||
src={`/images/integrations/${destinationDetails.image}`}
|
||||
className="mt-3 ml-1 w-16"
|
||||
className="ml-1 mt-3 w-16"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl font-medium text-white">{secretSync.name}</p>
|
||||
<p className="leading-3 text-bunker-300">{destinationDetails.name} Sync</p>
|
||||
<p className="text-bunker-300 leading-3">{destinationDetails.name} Sync</p>
|
||||
</div>
|
||||
<SecretSyncActionTriggers secretSync={secretSync} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
||||
import { TLaravelForgeSync } from "@app/hooks/api/secretSyncs/types/laravel-forge-sync";
|
||||
|
||||
type Props = {
|
||||
secretSync: TLaravelForgeSync;
|
||||
};
|
||||
|
||||
export const LaravelForgeSyncDestinationSection = ({ secretSync }: Props) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Account">
|
||||
{destinationConfig.orgName || destinationConfig.orgSlug}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Server">
|
||||
{destinationConfig.serverName || destinationConfig.serverId}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Site">
|
||||
{destinationConfig.siteName || destinationConfig.siteId}
|
||||
</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -31,6 +31,7 @@ import { GitLabSyncDestinationSection } from "./GitLabSyncDestinationSection";
|
||||
import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
|
||||
import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection";
|
||||
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
|
||||
import { LaravelForgeSyncDestinationSection } from "./LaravelForgeSyncDestinationSection";
|
||||
import { NetlifySyncDestinationSection } from "./NetlifySyncDestinationSection";
|
||||
import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection";
|
||||
import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection";
|
||||
@@ -148,6 +149,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
case SecretSync.Bitbucket:
|
||||
DestinationComponents = <BitbucketSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
case SecretSync.LaravelForge:
|
||||
DestinationComponents = <LaravelForgeSyncDestinationSection secretSync={secretSync} />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Section components: ${destination}`);
|
||||
}
|
||||
@@ -161,9 +165,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
|
||||
: ProjectPermissionSub.SecretSyncs;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Destination Configuration</h3>
|
||||
<div className="border-mineshaft-600 bg-mineshaft-900 flex w-full flex-col gap-3 rounded-lg border px-4 py-3">
|
||||
<div className="border-mineshaft-400 flex items-center justify-between border-b pb-2">
|
||||
<h3 className="text-mineshaft-100 font-medium">Destination Configuration</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
|
||||
@@ -71,6 +71,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
case SecretSync.DigitalOceanAppPlatform:
|
||||
case SecretSync.Netlify:
|
||||
case SecretSync.Bitbucket:
|
||||
case SecretSync.LaravelForge:
|
||||
AdditionalSyncOptionsComponent = null;
|
||||
break;
|
||||
default:
|
||||
@@ -87,9 +88,9 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<h3 className="font-medium text-mineshaft-100">Sync Options</h3>
|
||||
<div className="border-mineshaft-600 bg-mineshaft-900 flex w-full flex-col gap-3 rounded-lg border px-4 py-3">
|
||||
<div className="border-mineshaft-400 flex items-center justify-between border-b pb-2">
|
||||
<h3 className="text-mineshaft-100 font-medium">Sync Options</h3>
|
||||
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
|
||||
Reference in New Issue
Block a user