diff --git a/apps/webapp/app/components/integrations/VercelLink.tsx b/apps/webapp/app/components/integrations/VercelLink.tsx new file mode 100644 index 00000000000..4a74e599f3d --- /dev/null +++ b/apps/webapp/app/components/integrations/VercelLink.tsx @@ -0,0 +1,22 @@ +import { VercelLogo } from "./VercelLogo"; +import { LinkButton } from "~/components/primitives/Buttons"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; + +export function VercelLink({ vercelDeploymentUrl }: { vercelDeploymentUrl: string }) { + return ( + } + iconSpacing="gap-x-1" + to={vercelDeploymentUrl} + className="pl-1" + > + Vercel + + } + content="View on Vercel" + /> + ); +} diff --git a/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts index 1f5996f9967..9abc0ed0ab9 100644 --- a/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts @@ -10,7 +10,10 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { type User } from "~/models/user.server"; import { processGitMetadata } from "./BranchesPresenter.server"; import { BranchTrackingConfigSchema, getTrackedBranchForEnvironment } from "~/v3/github"; -import { VercelProjectIntegrationDataSchema } from "~/v3/vercel/vercelProjectIntegrationSchema"; +import { + VercelProjectIntegrationDataSchema, + buildVercelDeploymentUrl, +} from "~/v3/vercel/vercelProjectIntegrationSchema"; const pageSize = 20; @@ -232,8 +235,11 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`; let vercelDeploymentUrl: string | null = null; if (hasVercelIntegration && deployment.integrationDeploymentId && vercelTeamSlug && vercelProjectName) { - const vercelId = deployment.integrationDeploymentId.replace(/^dpl_/, ""); - vercelDeploymentUrl = `https://vercel.com/${vercelTeamSlug}/${vercelProjectName}/${vercelId}`; + vercelDeploymentUrl = buildVercelDeploymentUrl( + vercelTeamSlug, + vercelProjectName, + deployment.integrationDeploymentId + ); } return { diff --git a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts index bc494c118aa..ae9ed3577b8 100644 --- a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts @@ -12,6 +12,10 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { type User } from "~/models/user.server"; import { getUsername } from "~/utils/username"; import { processGitMetadata } from "./BranchesPresenter.server"; +import { + VercelProjectIntegrationDataSchema, + buildVercelDeploymentUrl, +} from "~/v3/vercel/vercelProjectIntegrationSchema"; import { S2 } from "@s2-dev/streamstore"; import { env } from "~/env.server"; import { createRedisClient } from "~/redis.server"; @@ -161,6 +165,54 @@ export class DeploymentPresenter { }); const gitMetadata = processGitMetadata(deployment.git); + + // Look up Vercel integration data to construct a deployment URL + let vercelDeploymentUrl: string | undefined; + const vercelProjectIntegration = + await this.#prismaClient.organizationProjectIntegration.findFirst({ + where: { + projectId: project.id, + deletedAt: null, + organizationIntegration: { + service: "VERCEL", + deletedAt: null, + }, + }, + select: { + integrationData: true, + }, + }); + + if (vercelProjectIntegration) { + const parsed = VercelProjectIntegrationDataSchema.safeParse( + vercelProjectIntegration.integrationData + ); + + if (parsed.success && parsed.data.vercelTeamSlug) { + const integrationDeployment = + await this.#prismaClient.integrationDeployment.findFirst({ + where: { + deploymentId: deployment.id, + integrationName: "vercel", + }, + select: { + integrationDeploymentId: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + if (integrationDeployment) { + vercelDeploymentUrl = buildVercelDeploymentUrl( + parsed.data.vercelTeamSlug, + parsed.data.vercelProjectName, + integrationDeployment.integrationDeploymentId + ); + } + } + } + const externalBuildData = deployment.externalBuildData ? ExternalBuildData.safeParse(deployment.externalBuildData) : undefined; @@ -227,6 +279,7 @@ export class DeploymentPresenter { type: deployment.type, git: gitMetadata, triggeredVia: deployment.triggeredVia, + vercelDeploymentUrl, }, }; } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index 9d32d89fd56..4a7d1df5ce8 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { GitMetadata } from "~/components/GitMetadata"; +import { VercelLink } from "~/components/integrations/VercelLink"; import { RuntimeIcon } from "~/components/RuntimeIcon"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; @@ -516,6 +517,16 @@ export default function Page() { })()} + {deployment.vercelDeploymentUrl && ( + + Linked + +
+ +
+
+
+ )} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index 61e789e138b..c93d2875238 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -19,7 +19,7 @@ import { useEffect } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { PromoteIcon } from "~/assets/icons/PromoteIcon"; -import { VercelLogo } from "~/components/integrations/VercelLogo"; +import { VercelLink } from "~/components/integrations/VercelLink"; import { DeploymentsNone, DeploymentsNoneDev } from "~/components/BlankStatePanels"; import { OctoKitty } from "~/components/GitHubLoginButton"; import { GitMetadata } from "~/components/GitMetadata"; @@ -56,7 +56,6 @@ import { TableHeaderCell, TableRow, } from "~/components/primitives/Table"; -import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { DeploymentStatus, deploymentStatusDescription, @@ -314,20 +313,14 @@ export default function Page() { {hasVercelIntegration && ( {deployment.vercelDeploymentUrl ? ( - e.stopPropagation()} - > - - - } - content="View on Vercel" - /> +
e.stopPropagation()} + > + +
) : ( "–" )} diff --git a/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts b/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts index 213e730c643..64ed5d55a3c 100644 --- a/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts +++ b/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts @@ -202,6 +202,15 @@ export function shouldSyncEnvVarForAnyEnvironment( return false; } +export function buildVercelDeploymentUrl( + vercelTeamSlug: string | undefined, + vercelProjectName: string, + integrationDeploymentId: string +): string { + const vercelId = integrationDeploymentId.replace(/^dpl_/, ""); + return `https://vercel.com/${vercelTeamSlug}/${vercelProjectName}/${vercelId}`; +} + export function isPullEnvVarsEnabledForEnvironment( pullEnvVarsBeforeBuild: EnvSlug[] | null | undefined, environmentType: TriggerEnvironmentType