diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index cd2d3248e..4399b4be0 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -3,12 +3,15 @@ import { Types } from "mongoose"; import { EventService, SecretService } from "../../services"; import { eventPushSecrets } from "../../events"; import { BotService } from "../../services"; -import { repackageSecretToRaw } from "../../helpers/secrets"; +import { containsGlobPatterns, repackageSecretToRaw } from "../../helpers/secrets"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { getAllImportedSecrets } from "../../services/SecretImportService"; import Folder from "../../models/folder"; import { getFolderByPath } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; +import { IServiceTokenData } from "../../models"; +import { requireWorkspaceAuth } from "../../middleware"; +import { ADMIN, MEMBER, PERMISSION_READ_SECRETS } from "../../variables"; /** * Return secrets for workspace with id [workspaceId] and environment @@ -17,11 +20,31 @@ import { BadRequestError } from "../../utils/errors"; * @param res */ export const getSecretsRaw = async (req: Request, res: Response) => { - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const secretPath = req.query.secretPath as string; + let workspaceId = req.query.workspaceId as string; + let environment = req.query.environment as string; + let secretPath = req.query.secretPath as string; const includeImports = req.query.include_imports as string; + // if the service token has single scope, it will get all secrets for that scope by default + const serviceTokenDetails: IServiceTokenData = req?.serviceTokenData + if (serviceTokenDetails) { + if (serviceTokenDetails.scopes.length == 1 && !containsGlobPatterns(serviceTokenDetails.scopes[0].secretPath)) { + const scope = serviceTokenDetails.scopes[0] + secretPath = scope.secretPath + environment = scope.environment + workspaceId = serviceTokenDetails.workspace.toString() + } else { + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "query", + locationEnvironment: "query", + requiredPermissions: [PERMISSION_READ_SECRETS], + requireBlindIndicesEnabled: true, + requireE2EEOff: true + }) + } + } + const secrets = await SecretService.getSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 1a68930bc..c770e78fb 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -44,6 +44,7 @@ import { EELogService, EESecretService } from "../ee/services"; import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth"; import { getFolderIdFromServiceToken } from "../services/FolderService"; import picomatch from "picomatch"; +import path from "path"; export const isValidScope = ( authPayload: IServiceTokenData, @@ -60,6 +61,13 @@ export const isValidScope = ( return Boolean(validScope); }; +export function containsGlobPatterns(secretPath: string) { + const globChars = ["*", "?", "[", "]", "{", "}", "**"]; + const normalizedPath = path.normalize(secretPath); + return globChars.some(char => normalizedPath.includes(char)); +} + + /** * Returns an object containing secret [secret] but with its value, key, comment decrypted. * diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 51cf43c45..69810d025 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -14,6 +14,8 @@ import { INTEGRATION_CLOUD_66_API_URL, INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH_API_URL, + INTEGRATION_DIGITAL_OCEAN_API_URL, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, @@ -164,6 +166,11 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: + apps = await getAppsDigitalOceanAppPlatform({ + accessToken + }); + break; case INTEGRATION_CLOUD_66: apps = await getAppsCloud66({ accessToken, @@ -849,6 +856,48 @@ const getAppsCodefresh = async ({ }; +/** + * Return list of applications for DigitalOcean App Platform integration + * @param {Object} obj + * @param {String} obj.accessToken - personal access token for DigitalOcean + * @returns {Object[]} apps - names of DigitalOcean apps + * @returns {String} apps.name - name of DigitalOcean app + * @returns {String} apps.appId - id of DigitalOcean app + */ +const getAppsDigitalOceanAppPlatform = async ({ accessToken }: { accessToken: string }) => { + interface DigitalOceanApp { + id: string; + owner_uuid: string; + spec: Spec; + } + + interface Spec { + name: string; + region: string; + envs: Env[]; + } + + interface Env { + key: string; + value: string; + scope: string; + } + + const res = ( + await standardRequest.get(`${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" + } + }) + ).data; + + return (res.apps ?? []).map((a: DigitalOceanApp) => ({ + name: a.spec.name, + appId: a.id + })); +} + /** * Return list of applications for Cloud66 integration * @param {Object} obj diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index aef9c52fe..9afb0e4c5 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -22,6 +22,8 @@ import { INTEGRATION_CLOUD_66_API_URL, INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH_API_URL, + INTEGRATION_DIGITAL_OCEAN_API_URL, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, @@ -222,6 +224,13 @@ const syncSecrets = async ({ accessToken, }); break; + case INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM: + await syncSecretsDigitalOceanAppPlatform({ + integration, + secrets, + accessToken, + }); + break; case INTEGRATION_CLOUD_66: await syncSecretsCloud66({ integration, @@ -2111,6 +2120,40 @@ const syncSecretsCodefresh = async ({ ); }; +/** + * Sync/push [secrets] to DigitalOcean App Platform application with name [integration.app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - personal access token for DigitalOcean + */ +const syncSecretsDigitalOceanAppPlatform = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + await standardRequest.put( + `${INTEGRATION_DIGITAL_OCEAN_API_URL}/v2/apps/${integration.appId}`, + { + spec: { + name: integration.app, + envs: Object.entries(secrets).map(([key, value]) => ({ key, value })) + } + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); +} + /** * Sync/push [secrets] to Cloud66 application with name [integration.app] * @param {Object} obj @@ -2128,6 +2171,7 @@ const syncSecretsCloud66 = async ({ secrets: any; accessToken: string; }) => { + interface Cloud66Secret { id: number; key: string; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index f3234b616..e79e85ce9 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -8,6 +8,7 @@ import { INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUD_66, INTEGRATION_CODEFRESH, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, @@ -60,6 +61,7 @@ export interface IIntegration { | "cloudflare-pages" | "bitbucket" | "codefresh" + | "digital-ocean-app-platform" | "cloud-66" integrationAuth: Types.ObjectId; } @@ -151,6 +153,7 @@ const integrationSchema = new Schema<IIntegration>( INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66, ], diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index b1f4e4c7e..8d1ea1e56 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -10,6 +10,7 @@ import { INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_CLOUD_66, INTEGRATION_CODEFRESH, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, @@ -47,6 +48,7 @@ export interface IIntegrationAuth extends Document { | "checkly" | "cloudflare-pages" | "codefresh" + | "digital-ocean-app-platform" | "bitbucket" | "cloud-66"; teamId: string; @@ -95,6 +97,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>( INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66, ], diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 69ba938e6..cea90a6ca 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -18,8 +18,8 @@ import { router.get( "/raw", - query("workspaceId").exists().isString().trim(), - query("environment").exists().isString().trim(), + query("workspaceId").optional().isString().trim(), + query("environment").optional().isString().trim(), query("secretPath").default("/").isString().trim(), query("include_imports").optional().isBoolean().default(false), validateRequest, @@ -31,14 +31,6 @@ router.get( AUTH_MODE_SERVICE_ACCOUNT ] }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "query", - locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], - requireBlindIndicesEnabled: true, - requireE2EEOff: true - }), secretsController.getSecretsRaw ); diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index c005ac715..95c5b7116 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -29,6 +29,7 @@ export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_BITBUCKET = "bitbucket"; export const INTEGRATION_CODEFRESH = "codefresh"; +export const INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform"; export const INTEGRATION_CLOUD_66 = "cloud-66"; export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, @@ -47,6 +48,7 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_HASHICORP_VAULT, INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_BITBUCKET, + INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_CODEFRESH, INTEGRATION_CLOUD_66 ]); @@ -81,6 +83,7 @@ export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com"; export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; export const INTEGRATION_BITBUCKET_API_URL = "https://api.bitbucket.org"; export const INTEGRATION_CODEFRESH_API_URL = "https://g.codefresh.io/api"; +export const INTEGRATION_DIGITAL_OCEAN_API_URL = "https://api.digitalocean.com"; export const INTEGRATION_CLOUD_66_API_URL = "https://app.cloud66.com/api"; export const getIntegrationOptions = async () => { @@ -275,6 +278,15 @@ export const getIntegrationOptions = async () => { clientId: "", docsLink: "", }, + { + name: "Digital Ocean App Platform", + slug: "digital-ocean-app-platform", + image: "Digital Ocean.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, { name: "Cloud 66", slug: "cloud-66", diff --git a/docs/api-reference/overview/examples/e2ee-disabled.mdx b/docs/api-reference/overview/examples/e2ee-disabled.mdx index 9864a1ff5..1a9e57552 100644 --- a/docs/api-reference/overview/examples/e2ee-disabled.mdx +++ b/docs/api-reference/overview/examples/e2ee-disabled.mdx @@ -7,8 +7,7 @@ in plaintext. Effectively, this means each such secret operation only requires 1 <AccordionGroup> <Accordion title="Retrieve secrets"> - Retrieve all secrets for an Infisical project and environment. - + Retrieve all secrets for an Infisical project and environment. <Tabs> <Tab title="cURL"> ```bash @@ -18,7 +17,12 @@ in plaintext. Effectively, this means each such secret operation only requires 1 ``` </Tab> </Tabs> - + #### + <Info> + When using a [service token](../../../documentation/platform/token) with access to a single environment and path, you don't need to provide request parameters because the server will automatically scope the request to the defined environment/secrets path of the service token used. + For all other cases, request parameters are required. + </Info> + #### <ParamField query="workspaceId" type="string" required> The ID of the workspace </ParamField> diff --git a/docs/images/integrations-do-dashboard.png b/docs/images/integrations-do-dashboard.png new file mode 100644 index 000000000..af9a06d07 Binary files /dev/null and b/docs/images/integrations-do-dashboard.png differ diff --git a/docs/images/integrations-do-enter-token.png b/docs/images/integrations-do-enter-token.png new file mode 100644 index 000000000..48a436278 Binary files /dev/null and b/docs/images/integrations-do-enter-token.png differ diff --git a/docs/images/integrations-do-select-projects.png b/docs/images/integrations-do-select-projects.png new file mode 100644 index 000000000..cb1878f5f Binary files /dev/null and b/docs/images/integrations-do-select-projects.png differ diff --git a/docs/images/integrations-do-success.png b/docs/images/integrations-do-success.png new file mode 100644 index 000000000..3866c4a7d Binary files /dev/null and b/docs/images/integrations-do-success.png differ diff --git a/docs/images/integrations-do-token-modal.png b/docs/images/integrations-do-token-modal.png new file mode 100644 index 000000000..47e8a27fe Binary files /dev/null and b/docs/images/integrations-do-token-modal.png differ diff --git a/docs/images/integrations.png b/docs/images/integrations.png index 89c4c6d66..af2a45125 100644 Binary files a/docs/images/integrations.png and b/docs/images/integrations.png differ diff --git a/docs/integrations/cloud/digital-ocean-app-platform.mdx b/docs/integrations/cloud/digital-ocean-app-platform.mdx new file mode 100644 index 000000000..10dd7751d --- /dev/null +++ b/docs/integrations/cloud/digital-ocean-app-platform.mdx @@ -0,0 +1,39 @@ +--- +title: "Digital Ocean App Platform" +description: "How to sync secrets from Infisical to Digital Ocean App Platform" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Get your Digital Ocean Personal Access Tokens + +On Digital Ocean dashboard, navigate to **API > Tokens** and click on "Generate New Token" + + +Name it **infisical**, choose **No expiry**, and make sure to check **Write (optional)**. Then click on "Generate Token" and copy your API token. + + +## Navigate to your project's integrations tab + +Click on the **Digital Ocean App Platform** tile and enter your API token to grant Infisical access to your Digital Ocean account. + + +<Info> + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. +</Info> + +Then enter your Digital Ocean Personal Access Token here. Then click "Connect to Digital Ocean App Platform". + + +## Start integration + +Select which Infisical environment secrets you want to sync to which Digital Ocean App and click "Create Integration". + + +Done! + diff --git a/docs/mint.json b/docs/mint.json index ba4324027..48755ea92 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -208,6 +208,12 @@ "integrations/cloud/aws-secret-manager" ] }, + { + "group": "Digital Ocean", + "pages": [ + "integrations/cloud/digital-ocean-app-platform" + ] + }, "integrations/cloud/heroku", "integrations/cloud/vercel", "integrations/cloud/netlify", diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 996860826..61490758e 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -22,6 +22,7 @@ const integrationSlugNameMapping: Mapping = { "hashicorp-vault": "Vault", "cloudflare-pages": "Cloudflare Pages", "codefresh": "Codefresh", + "digital-ocean-app-platform": "Digital Ocean App Platform", bitbucket: "BitBucket", "cloud-66": "Cloud 66" }; diff --git a/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx b/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx new file mode 100644 index 000000000..9c3912b2e --- /dev/null +++ b/frontend/src/pages/integrations/digital-ocean-app-platform/authorize.tsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; + +import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; +import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; + +export default function DigitalOceanAppPlatformCreateIntegrationPage() { + const router = useRouter(); + const [apiKey, setApiKey] = useState(""); + const [apiKeyErrorText, setApiKeyErrorText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleButtonClick = async () => { + try { + setApiKeyErrorText(""); + if (apiKey.length === 0) { + setApiKeyErrorText("API Key cannot be blank"); + return; + } + + setIsLoading(true); + + const integrationAuth = await saveIntegrationAccessToken({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "digital-ocean-app-platform", + accessId: null, + accessToken: apiKey, + url: null, + namespace: null + }); + + setIsLoading(false); + + router.push(`/integrations/digital-ocean-app-platform/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + }; + + return ( + <div className="flex h-full w-full items-center justify-center"> + <Card className="max-w-md rounded-md p-8"> + <CardTitle className="text-center">Digital Ocean App Platform Integration</CardTitle> + <FormControl + label="Digital Ocean API Key" + errorText={apiKeyErrorText} + isError={apiKeyErrorText !== "" ?? false} + > + <Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} /> + </FormControl> + <Button + onClick={handleButtonClick} + color="mineshaft" + className="mt-4" + isLoading={isLoading} + > + Connect to Digital Ocean App Platform + </Button> + </Card> + </div> + ); +} + +DigitalOceanAppPlatformCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx b/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx new file mode 100644 index 000000000..925729c94 --- /dev/null +++ b/frontend/src/pages/integrations/digital-ocean-app-platform/create.tsx @@ -0,0 +1,155 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "../../../components/v2"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "../../../hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "../../../hooks/api/workspace"; +import createIntegration from "../../api/integrations/createIntegration"; + +export default function DigitalOceanAppPlatformCreateIntegrationPage() { + const router = useRouter(); + + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + integrationAuthId: (integrationAuthId as string) ?? "" + }); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [targetApp, setTargetApp] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetApp(integrationAuthApps[0].name); + } else { + setTargetApp("none"); + } + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + if (!integrationAuth?._id) return; + + setIsLoading(true); + + await createIntegration({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: targetApp, + appId: + integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp) + ?.appId ?? null, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + targetEnvironmentId: null, + targetService: null, + targetServiceId: null, + owner: null, + path: null, + region: null, + secretPath + }); + + setIsLoading(false); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && + workspace && + selectedSourceEnvironment && + integrationAuthApps && + targetApp ? ( + <div className="flex h-full w-full items-center justify-center"> + <Card className="max-w-md rounded-md p-8"> + <CardTitle className="text-center">Digital Ocean App Platform Integration</CardTitle> + <FormControl label="Project Environment" className="mt-4"> + <Select + value={selectedSourceEnvironment} + onValueChange={(val) => setSelectedSourceEnvironment(val)} + className="w-full border border-mineshaft-500" + > + {workspace?.environments.map((sourceEnvironment) => ( + <SelectItem + value={sourceEnvironment.slug} + key={`source-environment-${sourceEnvironment.slug}`} + > + {sourceEnvironment.name} + </SelectItem> + ))} + </Select> + </FormControl> + <FormControl label="Secrets Path"> + <Input + value={secretPath} + onChange={(evt) => setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + </FormControl> + <FormControl label="Digital Ocean App Platform Service" className="mt-4"> + <Select + value={targetApp} + onValueChange={(val) => setTargetApp(val)} + className="w-full border border-mineshaft-500" + isDisabled={integrationAuthApps.length === 0} + > + {integrationAuthApps.length > 0 ? ( + integrationAuthApps.map((integrationAuthApp) => ( + <SelectItem + value={integrationAuthApp.name} + key={`target-app-${integrationAuthApp.name}`} + > + {integrationAuthApp.name} + </SelectItem> + )) + ) : ( + <SelectItem value="none" key="target-app-none"> + No services found + </SelectItem> + )} + </Select> + </FormControl> + <Button + onClick={handleButtonClick} + color="mineshaft" + className="mt-4" + isLoading={isLoading} + isDisabled={integrationAuthApps.length === 0} + > + Create Integration + </Button> + </Card> + </div> + ) : ( + <div /> + ); +} + +DigitalOceanAppPlatformCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index 9a3316822..7ec4a15a8 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -98,6 +98,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "codefresh": link = `${window.location.origin}/integrations/codefresh/authorize`; break; + case "digital-ocean-app-platform": + link = `${window.location.origin}/integrations/digital-ocean-app-platform/authorize`; + break; case "cloud-66": link = `${window.location.origin}/integrations/cloud-66/authorize`; break; diff --git a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx index 4ce1bd083..e8e761a0f 100644 --- a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx @@ -67,18 +67,9 @@ export const CloudIntegrationSection = ({ width={70} alt="integration logo" /> - {cloudIntegration.name.split(" ").length > 2 ? ( - <div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200"> - <div>{cloudIntegration.name.split(" ")[0]}</div> - <div className="text-base"> - {cloudIntegration.name.split(" ")[1]} {cloudIntegration.name.split(" ")[2]} - </div> - </div> - ) : ( - <div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200"> - {cloudIntegration.name} - </div> - )} + <div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200"> + {cloudIntegration.name} + </div> {cloudIntegration.isAvailable && Boolean(integrationAuths?.[cloudIntegration.slug]) && ( <div className="absolute top-0 right-0 z-40 h-full">