- {!hideSectionToggles && availableEnvSlugs.length > 1 && (
+ {availableEnvSlugs.length > 1 && (
- {!hideSectionToggles && availableEnvSlugs.length > 1 && (
+ {availableEnvSlugs.length > 1 && (
void;
vercelManageAccessUrl?: string;
}) {
const { capture, startSessionRecording } = usePostHogTracking();
- const navigation = useNavigation();
const fetcher = useTypedFetcher();
const envMappingFetcher = useFetcher();
const completeOnboardingFetcher = useFetcher();
- const { Form: _CompleteOnboardingForm } = completeOnboardingFetcher;
const [searchParams] = useSearchParams();
const origin = searchParams.get("origin");
const fromMarketplaceContext = origin === "marketplace";
@@ -130,7 +127,6 @@ export function VercelOnboardingModal({
() => onboardingData?.availableProjects ?? [],
[onboardingData?.availableProjects]
);
- const _hasProjectSelected = onboardingData?.hasProjectSelected ?? false;
const customEnvironments = useMemo(
() => onboardingData?.customEnvironments ?? [],
[onboardingData?.customEnvironments]
@@ -224,10 +220,6 @@ export function VercelOnboardingModal({
environmentId: string;
displayName: string;
} | null>(null);
- const _availableEnvSlugsForOnboarding = getAvailableEnvSlugs(
- hasStagingEnvironment,
- hasPreviewEnvironment
- );
const availableEnvSlugsForOnboardingBuildSettings = getAvailableEnvSlugsForBuildSettings(
hasStagingEnvironment,
hasPreviewEnvironment
@@ -375,7 +367,6 @@ export function VercelOnboardingModal({
}
break;
- case "installing":
case "project-selection":
case "env-mapping":
case "env-var-sync":
@@ -459,8 +450,6 @@ export function VercelOnboardingModal({
const overlappingEnvVarsCount = enabledEnvVars.filter((v) => existingVars[v.key]).length;
- const _isSubmitting = navigation.state === "submitting" || navigation.state === "loading";
-
const actionUrl = vercelResourcePath(organizationSlug, projectSlug, environmentSlug);
const handleToggleEnvVar = useCallback((key: string, enabled: boolean) => {
@@ -634,19 +623,6 @@ export function VercelOnboardingModal({
gitHubAppInstallations.length,
]);
- const _handleFinishOnboarding = useCallback(
- (e: React.FormEvent) => {
- e.preventDefault();
- const form = e.currentTarget;
- const formData = new FormData(form);
- completeOnboardingFetcher.submit(formData, {
- method: "post",
- action: actionUrl,
- });
- },
- [completeOnboardingFetcher, actionUrl]
- );
-
useEffect(() => {
if (
completeOnboardingFetcher.data &&
@@ -698,13 +674,6 @@ export function VercelOnboardingModal({
}
}, [state, onClose, trackOnboarding, isGitHubConnectedForOnboarding]);
- useEffect(() => {
- if (state === "installing") {
- const installUrl = vercelAppInstallPath(organizationSlug, projectSlug);
- window.location.href = installUrl;
- }
- }, [state, organizationSlug, projectSlug]);
-
useEffect(() => {
if (
envMappingFetcher.data &&
@@ -749,7 +718,6 @@ export function VercelOnboardingModal({
state === "loading-projects" ||
state === "loading-env-mapping" ||
state === "loading-env-vars" ||
- state === "installing" ||
(state === "idle" && !onboardingData);
if (isLoadingState) {
@@ -758,9 +726,7 @@ export function VercelOnboardingModal({
open={isOpen}
onOpenChange={(open) => {
if (!open && !fromMarketplaceContext) {
- if ((state as string) !== "completed") {
- trackOnboarding("vercel onboarding abandoned");
- }
+ trackOnboarding("vercel onboarding abandoned");
onClose();
}
}}
@@ -772,9 +738,30 @@ export function VercelOnboardingModal({
Set up Vercel Integration
-
-
-
+ {onboardingDataUnavailable ? (
+
+
+ We couldn't load your Vercel projects. The integration may have been removed or lost
+ access to this organization on Vercel.
+
+
+ {onDataReload && (
+
+ )}
+ {vercelManageAccessUrl && (
+
+ Manage access on Vercel
+
+ )}
+
+
+ ) : (
+
+
+
+ )}
);
diff --git a/apps/webapp/app/presenters/v3/GitHubSettingsPresenter.server.ts b/apps/webapp/app/presenters/v3/GitHubSettingsPresenter.server.ts
index 53bd034f249..162e44f8a88 100644
--- a/apps/webapp/app/presenters/v3/GitHubSettingsPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/GitHubSettingsPresenter.server.ts
@@ -19,6 +19,7 @@ export class GitHubSettingsPresenter extends BasePresenter {
connectedRepository: undefined,
installations: undefined,
isPreviewEnvironmentEnabled: undefined,
+ isStagingEnvironmentEnabled: undefined,
});
}
@@ -114,7 +115,8 @@ export class GitHubSettingsPresenter extends BasePresenter {
},
where: {
projectId: projectId,
- slug: "preview",
+ type: "PREVIEW",
+ parentEnvironmentId: null,
},
}),
(error) => ({
@@ -123,15 +125,42 @@ export class GitHubSettingsPresenter extends BasePresenter {
})
).map((previewEnvironment) => previewEnvironment !== null);
+ const isStagingEnvironmentEnabled = () =>
+ fromPromise(
+ (this._replica as PrismaClient).runtimeEnvironment.findFirst({
+ select: {
+ id: true,
+ },
+ where: {
+ projectId: projectId,
+ type: "STAGING",
+ parentEnvironmentId: null,
+ },
+ }),
+ (error) => ({
+ type: "other" as const,
+ cause: error,
+ })
+ ).map((stagingEnvironment) => stagingEnvironment !== null);
+
return ResultAsync.combine([
isPreviewEnvironmentEnabled(),
+ isStagingEnvironmentEnabled(),
findConnectedGithubRepository(),
listGithubAppInstallations(),
- ]).map(([isPreviewEnvironmentEnabled, connectedGithubRepository, githubAppInstallations]) => ({
- enabled: true,
- connectedRepository: connectedGithubRepository,
- installations: githubAppInstallations,
- isPreviewEnvironmentEnabled,
- }));
+ ]).map(
+ ([
+ isPreviewEnvironmentEnabled,
+ isStagingEnvironmentEnabled,
+ connectedGithubRepository,
+ githubAppInstallations,
+ ]) => ({
+ enabled: true,
+ connectedRepository: connectedGithubRepository,
+ installations: githubAppInstallations,
+ isPreviewEnvironmentEnabled,
+ isStagingEnvironmentEnabled,
+ })
+ );
}
}
diff --git a/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts b/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts
index 10a46c01b3a..841c929d141 100644
--- a/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts
@@ -182,6 +182,7 @@ export class VercelSettingsPresenter extends BasePresenter {
where: {
projectId,
type: "STAGING",
+ parentEnvironmentId: null,
},
}),
(error) => ({
@@ -199,6 +200,7 @@ export class VercelSettingsPresenter extends BasePresenter {
where: {
projectId,
type: "PREVIEW",
+ parentEnvironmentId: null,
},
}),
(error) => ({
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx
index 864cc300fa4..37ad3d51681 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx
@@ -59,8 +59,9 @@ export const loader = dashboardLoader(
async ({ params, user, ability }) => {
const { projectParam, organizationSlug } = params;
+ const canManageBuildSettings = ability.can("write", { type: "github" });
const canManageIntegrations =
- ability.can("write", { type: "github" }) || ability.can("write", { type: "vercel" });
+ canManageBuildSettings || ability.can("write", { type: "vercel" });
if (!canManageIntegrations) {
throwPermissionDenied("With your current role, you can't manage integrations.");
@@ -102,6 +103,7 @@ export const loader = dashboardLoader(
githubAppEnabled: gitHubApp.enabled,
buildSettings,
vercelIntegrationEnabled: OrgIntegrationRepository.isVercelSupported,
+ canManageBuildSettings,
});
}
);
@@ -208,7 +210,7 @@ export const action = dashboardAction(
);
export default function IntegrationsSettingsPage() {
- const { githubAppEnabled, buildSettings, vercelIntegrationEnabled } =
+ const { githubAppEnabled, buildSettings, vercelIntegrationEnabled, canManageBuildSettings } =
useTypedLoaderData();
const project = useProject();
const organization = useOrganization();
@@ -223,6 +225,8 @@ export default function IntegrationsSettingsPage() {
const loadVercelOnboarding = vercelFetcher.load;
const onboardingData = vercelFetcher.data?.onboardingData ?? null;
const hasVercelFetcherData = vercelFetcher.data !== undefined;
+ const onboardingDataUnavailable =
+ hasVercelFetcherData && vercelFetcher.state === "idle" && onboardingData === null;
const vercelOnboardingPath = `${vercelResourcePath(
organization.slug,
project.slug,
@@ -375,24 +379,27 @@ export default function IntegrationsSettingsPage() {
/>
)}
-
-
-
- Applies to deployments triggered from GitHub, and CLI deployments run with the{" "}
-
- --native-build-server
- {" "}
- flag.
- >
- }
- />
-
-
>
)}
+
+
+
+ Applies to deployments triggered from GitHub, and CLI deployments run with the{" "}
+
+ --native-build-server
+ {" "}
+ flag.
+ >
+ }
+ />
+
+
{/* Vercel Onboarding Modal */}
@@ -407,6 +414,7 @@ export default function IntegrationsSettingsPage() {
hasStagingEnvironment={vercelFetcher.data?.hasStagingEnvironment ?? false}
hasPreviewEnvironment={vercelFetcher.data?.hasPreviewEnvironment ?? false}
hasOrgIntegration={vercelFetcher.data?.hasOrgIntegration ?? false}
+ onboardingDataUnavailable={onboardingDataUnavailable}
nextUrl={nextUrl ?? undefined}
vercelManageAccessUrl={vercelFetcher.data?.vercelManageAccessUrl}
onDataReload={(vercelEnvironmentId) => {
@@ -424,7 +432,13 @@ export default function IntegrationsSettingsPage() {
);
}
-function BuildSettingsForm({ buildSettings }: { buildSettings: BuildSettings }) {
+function BuildSettingsForm({
+ buildSettings,
+ canManageBuildSettings = true,
+}: {
+ buildSettings: BuildSettings;
+ canManageBuildSettings?: boolean;
+}) {
const lastSubmission = useActionData() as any;
const navigation = useNavigation();
@@ -572,7 +586,12 @@ function BuildSettingsForm({ buildSettings }: { buildSettings: BuildSettings })
name="action"
value="update-build-settings"
variant="secondary/small"
- disabled={isBuildSettingsLoading || !hasBuildSettingsChanges}
+ disabled={isBuildSettingsLoading || !hasBuildSettingsChanges || !canManageBuildSettings}
+ tooltip={
+ canManageBuildSettings
+ ? undefined
+ : "You don't have permission to manage build settings"
+ }
LeadingIcon={isBuildSettingsLoading ? SpinnerWhite : undefined}
>
Save
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx
index 4421b76daf1..fba94b80bba 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx
@@ -779,6 +779,7 @@ function GitHubSettingsRows({
export function ConnectedGitHubRepoForm({
connectedGitHubRepo,
previewEnvironmentEnabled,
+ stagingEnvironmentEnabled,
organizationSlug,
projectSlug,
environmentSlug,
@@ -788,6 +789,7 @@ export function ConnectedGitHubRepoForm({
}: {
connectedGitHubRepo: ConnectedGitHubRepo;
previewEnvironmentEnabled?: boolean;
+ stagingEnvironmentEnabled?: boolean;
organizationSlug: string;
projectSlug: string;
environmentSlug: string;
@@ -956,24 +958,42 @@ export function ConnectedGitHubRepoForm({
{
- setGitSettingsValues((prev) => ({
- ...prev,
- stagingBranch: e.target.value,
- }));
- }}
- />
+ stagingEnvironmentEnabled ? (
+ {
+ setGitSettingsValues((prev) => ({
+ ...prev,
+ stagingBranch: e.target.value,
+ }));
+ }}
+ />
+ ) : (
+
+ Upgrade
+
+ )
}
>
-
+
void;
@@ -698,19 +695,6 @@ function VercelGitHubWarning() {
);
}
-function envSlugLabel(slug: EnvSlug): string {
- switch (slug) {
- case "prod":
- return "Production";
- case "stg":
- return "Staging";
- case "preview":
- return "Preview";
- case "dev":
- return "Development";
- }
-}
-
function ConnectedVercelProjectForm({
connectedProject,
hasStagingEnvironment,
@@ -774,7 +758,7 @@ function ConnectedVercelProjectForm({
stagingEnvChanged ||
autoPromoteChanged;
- const [configForm, _fields] = useForm({
+ const [configForm] = useForm({
id: "update-vercel-config",
lastResult: lastSubmission,
shouldRevalidate: "onSubmit",
@@ -833,26 +817,22 @@ function ConnectedVercelProjectForm({
const actionUrl = vercelResourcePath(organizationSlug, projectSlug, environmentSlug);
- const availableEnvSlugs = getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment);
const availableEnvSlugsForBuildSettings = getAvailableEnvSlugsForBuildSettings(
hasStagingEnvironment,
hasPreviewEnvironment
);
+ const hasVercelCustomEnvironments = customEnvironments.length > 0;
+
const disabledEnvSlugsForBuildSettings: Partial> | undefined =
hasStagingEnvironment && !configValues.vercelStagingEnvironment
- ? { stg: "Set a Vercel environment for Staging first." }
+ ? {
+ stg: hasVercelCustomEnvironments
+ ? "Set a Vercel environment for Staging first."
+ : "Add a custom environment to this project in Vercel to use Staging.",
+ }
: undefined;
- const _formatSelectedEnvs = (
- selected: EnvSlug[],
- availableSlugs: EnvSlug[] = availableEnvSlugs
- ): string => {
- if (selected.length === 0) return "None selected";
- if (selected.length === availableSlugs.length) return "All environments";
- return selected.map(envSlugLabel).join(", ");
- };
-
return (
<>
- {/* Staging environment mapping */}
- {hasStagingEnvironment && customEnvironments && customEnvironments.length > 0 && (
+ {hasStagingEnvironment && (
-
+