Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/webapp/app/components/integrations/VercelLink.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SimpleTooltip
button={
<LinkButton
variant="minimal/small"
LeadingIcon={<VercelLogo className="size-3.5" />}
iconSpacing="gap-x-1"
to={vercelDeploymentUrl}
className="pl-1"
>
Vercel
</LinkButton>
}
content="View on Vercel"
/>
);
}
52 changes: 39 additions & 13 deletions apps/webapp/app/components/integrations/VercelOnboardingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
} from "~/v3/vercel/vercelProjectIntegrationSchema";
import { type VercelCustomEnvironment } from "~/models/vercelIntegration.server";
import { type VercelOnboardingData } from "~/presenters/v3/VercelSettingsPresenter.server";
import { vercelAppInstallPath, v3ProjectSettingsPath, githubAppInstallPath, vercelResourcePath } from "~/utils/pathBuilder";
import { vercelAppInstallPath, v3ProjectSettingsIntegrationsPath, githubAppInstallPath, vercelResourcePath } from "~/utils/pathBuilder";
import type { loader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel";
import { useEffect, useState, useCallback, useRef } from "react";
import { usePostHogTracking } from "~/hooks/usePostHog";
Expand Down Expand Up @@ -102,6 +102,7 @@ export function VercelOnboardingModal({
hasOrgIntegration,
nextUrl,
onDataReload,
vercelManageAccessUrl,
}: {
isOpen: boolean;
onClose: () => void;
Expand All @@ -114,6 +115,7 @@ export function VercelOnboardingModal({
hasOrgIntegration: boolean;
nextUrl?: string;
onDataReload?: (vercelStagingEnvironment?: string) => void;
vercelManageAccessUrl?: string;
}) {
const { capture, startSessionRecording } = usePostHogTracking();
const navigation = useNavigation();
Expand All @@ -122,7 +124,8 @@ export function VercelOnboardingModal({
const completeOnboardingFetcher = useFetcher();
const { Form: CompleteOnboardingForm } = completeOnboardingFetcher;
const [searchParams] = useSearchParams();
const fromMarketplaceContext = searchParams.get("origin") === "marketplace";
const origin = searchParams.get("origin");
const fromMarketplaceContext = origin === "marketplace";

const availableProjects = onboardingData?.availableProjects || [];
const hasProjectSelected = onboardingData?.hasProjectSelected ?? false;
Expand Down Expand Up @@ -543,8 +546,15 @@ export function VercelOnboardingModal({

if (!isGitHubConnectedForOnboarding) {
setState("github-connection");
capture("vercel onboarding github step viewed", {
origin: fromMarketplaceContext ? "marketplace" : "dashboard",
step: "github-connection",
organization_slug: organizationSlug,
project_slug: projectSlug,
github_app_installed: gitHubAppInstallations.length > 0,
});
}
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl, trackOnboarding]);
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl, trackOnboarding, capture, organizationSlug, projectSlug, gitHubAppInstallations.length]);

const handleFinishOnboarding = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Expand Down Expand Up @@ -639,7 +649,7 @@ export function VercelOnboardingModal({
onClose();
}
}}>
<DialogContent className="max-w-lg">
<DialogContent className="max-w-lg" onInteractOutside={(e) => e.preventDefault()}>
<DialogHeader>
<div className="flex items-center gap-2">
<VercelLogo className="size-5" />
Expand Down Expand Up @@ -727,14 +737,25 @@ export function VercelOnboardingModal({

<FormButtons
confirmButton={
<Button
variant="primary/medium"
onClick={handleProjectSelection}
disabled={!selectedVercelProject || fetcher.state !== "idle"}
LeadingIcon={fetcher.state !== "idle" ? SpinnerWhite : undefined}
>
{fetcher.state !== "idle" ? "Connecting..." : "Connect Project"}
</Button>
<div className="flex items-center gap-2">
{vercelManageAccessUrl && !origin && (
<LinkButton
to={vercelManageAccessUrl}
variant="tertiary/medium"
target="_self"
>
Manage access
</LinkButton>
)}
<Button
variant="primary/medium"
onClick={handleProjectSelection}
disabled={!selectedVercelProject || fetcher.state !== "idle"}
LeadingIcon={fetcher.state !== "idle" ? SpinnerWhite : undefined}
>
{fetcher.state !== "idle" ? "Connecting..." : "Connect Project"}
</Button>
</div>
}
cancelButton={
<Button
Expand Down Expand Up @@ -813,6 +834,7 @@ export function VercelOnboardingModal({
<Header3>Pull Environment Variables</Header3>
<Paragraph className="text-sm">
Select which environment variables to pull from Vercel now. This is a one-time pull.
Later on environment variables can be pulled before each build.
</Paragraph>

<div className="flex gap-4 text-sm">
Expand Down Expand Up @@ -1057,7 +1079,7 @@ export function VercelOnboardingModal({
</Callout>

{(() => {
const baseSettingsPath = v3ProjectSettingsPath(
const baseSettingsPath = v3ProjectSettingsIntegrationsPath(
{ slug: organizationSlug },
{ slug: projectSlug },
{ slug: environmentSlug }
Expand All @@ -1081,6 +1103,7 @@ export function VercelOnboardingModal({
)}
variant="secondary/medium"
LeadingIcon={OctoKitty}
onClick={() => trackOnboarding("vercel onboarding github app install clicked")}
>
Install GitHub app
</LinkButton>
Expand Down Expand Up @@ -1110,6 +1133,7 @@ export function VercelOnboardingModal({
<Button
variant="primary/medium"
onClick={() => {
trackOnboarding("vercel onboarding github completed");
setState("completed");
const validUrl = safeRedirectUrl(nextUrl);
if (validUrl) {
Expand All @@ -1123,6 +1147,7 @@ export function VercelOnboardingModal({
<Button
variant="tertiary/medium"
onClick={() => {
trackOnboarding("vercel onboarding github skipped");
setState("completed");
if (fromMarketplaceContext && nextUrl) {
const validUrl = safeRedirectUrl(nextUrl);
Expand All @@ -1141,6 +1166,7 @@ export function VercelOnboardingModal({
<Button
variant="tertiary/medium"
onClick={() => {
trackOnboarding("vercel onboarding github skipped");
setState("completed");
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import {
ChartBarIcon,
Cog8ToothIcon,
CreditCardIcon,
PuzzlePieceIcon,
UserGroupIcon,
} from "@heroicons/react/20/solid";
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
import { SlackIcon } from "@trigger.dev/companyicons";
import { VercelLogo } from "~/components/integrations/VercelLogo";
import { useFeatures } from "~/hooks/useFeatures";
import { type MatchedOrganization } from "~/hooks/useOrganizations";
import { cn } from "~/utils/cn";
import {
organizationSettingsPath,
organizationSlackIntegrationPath,
organizationTeamPath,
organizationVercelIntegrationPath,
rootPath,
Expand Down Expand Up @@ -115,13 +117,25 @@ export function OrganizationSettingsSideMenu({
to={organizationSettingsPath(organization)}
data-action="settings"
/>
</div>
<div className="flex flex-col">
<div className="mb-1">
<SideMenuHeader title="Integrations" />
</div>
<SideMenuItem
name="Integrations"
icon={PuzzlePieceIcon}
activeIconColor="text-blue-500"
name="Vercel"
icon={VercelLogo}
activeIconColor="text-white"
to={organizationVercelIntegrationPath(organization)}
data-action="integrations"
/>
<SideMenuItem
name="Slack"
icon={SlackIcon}
activeIconColor="text-white"
to={organizationSlackIntegrationPath(organization)}
data-action="integrations"
/>
</div>
<div className="flex flex-col gap-1">
<SideMenuHeader title="App version" />
Expand Down
31 changes: 27 additions & 4 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Cog8ToothIcon,
CogIcon,
ExclamationTriangleIcon,
PuzzlePieceIcon,
FolderIcon,
FolderOpenIcon,
GlobeAmericasIcon,
Expand Down Expand Up @@ -74,7 +75,8 @@ import {
v3LogsPath,
v3ProjectAlertsPath,
v3ProjectPath,
v3ProjectSettingsPath,
v3ProjectSettingsGeneralPath,
v3ProjectSettingsIntegrationsPath,
v3QueuesPath,
v3RunsPath,
v3SchedulesPath,
Expand Down Expand Up @@ -589,13 +591,34 @@ export function SideMenu({
data-action="limits"
isCollapsed={isCollapsed}
/>
</SideMenuSection>

<SideMenuSection
title="Project settings"
isSideMenuCollapsed={isCollapsed}
itemSpacingClassName="space-y-0"
initialCollapsed={getSectionCollapsed(
user.dashboardPreferences.sideMenu,
"project-settings"
)}
onCollapseToggle={handleSectionToggle("project-settings")}
>
<SideMenuItem
name="Project settings"
name="General"
icon={Cog8ToothIcon}
activeIconColor="text-text-bright"
inactiveIconColor="text-text-dimmed"
to={v3ProjectSettingsPath(organization, project, environment)}
data-action="project-settings"
to={v3ProjectSettingsGeneralPath(organization, project, environment)}
data-action="project-settings-general"
isCollapsed={isCollapsed}
/>
<SideMenuItem
name="Integrations"
icon={PuzzlePieceIcon}
activeIconColor="text-text-bright"
inactiveIconColor="text-text-dimmed"
to={v3ProjectSettingsIntegrationsPath(organization, project, environment)}
data-action="project-settings-integrations"
isCollapsed={isCollapsed}
/>
</SideMenuSection>
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/navigation/sideMenuTypes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from "zod";

// Valid section IDs that can have their collapsed state toggled
export const SideMenuSectionIdSchema = z.enum(["manage", "metrics"]);
export const SideMenuSectionIdSchema = z.enum(["manage", "metrics", "project-settings"]);

// Inferred type from the schema
export type SideMenuSectionId = z.infer<typeof SideMenuSectionIdSchema>;
36 changes: 36 additions & 0 deletions apps/webapp/app/models/vercelIntegration.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,13 @@ export class VercelIntegrationRepository {
return { created: 0, updated: 0, errors: [] as string[] };
}

await this.removeAllVercelEnvVarsByKey({
client,
vercelProjectId: params.vercelProjectId,
teamId: params.teamId,
key: "TRIGGER_SECRET_KEY",
});

const result = await this.batchUpsertVercelEnvVars({
client,
vercelProjectId: params.vercelProjectId,
Expand Down Expand Up @@ -1526,6 +1533,35 @@ export class VercelIntegrationRepository {
return { created, updated, errors };
}

private static async removeAllVercelEnvVarsByKey(params: {
client: Vercel;
vercelProjectId: string;
teamId: string | null;
key: string;
}): Promise<void> {
const { client, vercelProjectId, teamId, key } = params;

const existingEnvs = await client.projects.filterProjectEnvs({
idOrName: vercelProjectId,
...(teamId && { teamId }),
});

const envs = extractVercelEnvs(existingEnvs);
const idsToRemove = envs
.filter((env) => env.key === key && env.id)
.map((env) => env.id!);

if (idsToRemove.length === 0) {
return;
}

await client.projects.batchRemoveProjectEnv({
idOrName: vercelProjectId,
...(teamId && { teamId }),
requestBody: { ids: idsToRemove },
});
}

private static async upsertVercelEnvVar(params: {
client: Vercel;
vercelProjectId: string;
Expand Down
12 changes: 9 additions & 3 deletions apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
Loading