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
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import {
githubAppInstallPath,
EnvironmentParamSchema,
v3ProjectSettingsPath,
docsPath,
v3BillingPath,
} from "~/utils/pathBuilder";
import React, { useEffect, useState } from "react";
import { Select, SelectItem } from "~/components/primitives/Select";
Expand All @@ -77,6 +79,7 @@ import { TextLink } from "~/components/primitives/TextLink";
import { cn } from "~/utils/cn";
import { ProjectSettingsPresenter } from "~/services/projectSettingsPresenter.server";
import { type BuildSettings } from "~/v3/buildSettings";
import { InfoIconTooltip } from "~/components/primitives/Tooltip";

export const meta: MetaFunction = () => {
return [
Expand Down Expand Up @@ -126,6 +129,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
githubAppEnabled: gitHubApp.enabled,
githubAppInstallations: gitHubApp.installations,
connectedGithubRepository: gitHubApp.connectedRepository,
isPreviewEnvironmentEnabled: gitHubApp.isPreviewEnvironmentEnabled,
buildSettings,
});
};
Expand Down Expand Up @@ -433,8 +437,13 @@ export const action: ActionFunction = async ({ request, params }) => {
};

export default function Page() {
const { githubAppInstallations, connectedGithubRepository, githubAppEnabled, buildSettings } =
useTypedLoaderData<typeof loader>();
const {
githubAppInstallations,
connectedGithubRepository,
githubAppEnabled,
buildSettings,
isPreviewEnvironmentEnabled,
} = useTypedLoaderData<typeof loader>();
const project = useProject();
const organization = useOrganization();
const environment = useEnvironment();
Expand Down Expand Up @@ -561,7 +570,10 @@ export default function Page() {
<Header2 spacing>Git settings</Header2>
<div className="w-full rounded-sm border border-grid-dimmed p-4">
{connectedGithubRepository ? (
<ConnectedGitHubRepoForm connectedGitHubRepo={connectedGithubRepository} />
<ConnectedGitHubRepoForm
connectedGitHubRepo={connectedGithubRepository}
previewEnvironmentEnabled={isPreviewEnvironmentEnabled}
/>
) : (
<GitHubConnectionPrompt
gitHubAppInstallations={githubAppInstallations ?? []}
Expand Down Expand Up @@ -903,11 +915,14 @@ type ConnectedGitHubRepo = {

function ConnectedGitHubRepoForm({
connectedGitHubRepo,
previewEnvironmentEnabled,
}: {
connectedGitHubRepo: ConnectedGitHubRepo;
previewEnvironmentEnabled?: boolean;
}) {
const lastSubmission = useActionData() as any;
const navigation = useNavigation();
const organization = useOrganization();

const [hasGitSettingsChanges, setHasGitSettingsChanges] = useState(false);
const [gitSettingsValues, setGitSettingsValues] = useState({
Expand Down Expand Up @@ -1003,10 +1018,10 @@ function ConnectedGitHubRepoForm({
<Fieldset>
<InputGroup fullWidth>
<Hint>
Every commit on the selected tracking branch creates a deployment in the corresponding
Every push to the selected tracking branch creates a deployment in the corresponding
environment.
</Hint>
<div className="grid grid-cols-[120px_1fr] gap-3">
<div className="mt-1 grid grid-cols-[120px_1fr] gap-3">
<div className="flex items-center gap-1.5">
<EnvironmentIcon environment={{ type: "PRODUCTION" }} className="size-4" />
<span className={`text-sm ${environmentTextClassName({ type: "PRODUCTION" })}`}>
Expand Down Expand Up @@ -1054,19 +1069,34 @@ function ConnectedGitHubRepoForm({
{environmentFullTitle({ type: "PREVIEW" })}
</span>
</div>
<Switch
name="previewDeploymentsEnabled"
defaultChecked={connectedGitHubRepo.previewDeploymentsEnabled}
variant="small"
label="create preview deployments for pull requests"
labelPosition="right"
onCheckedChange={(checked) => {
setGitSettingsValues((prev) => ({
...prev,
previewDeploymentsEnabled: checked,
}));
}}
/>
<div className="flex items-center gap-1.5">
<Switch
name="previewDeploymentsEnabled"
disabled={!previewEnvironmentEnabled}
defaultChecked={
connectedGitHubRepo.previewDeploymentsEnabled && previewEnvironmentEnabled
}
variant="small"
label="Create preview deployments for pull requests"
labelPosition="right"
onCheckedChange={(checked) => {
setGitSettingsValues((prev) => ({
...prev,
previewDeploymentsEnabled: checked,
}));
}}
/>
{!previewEnvironmentEnabled && (
<InfoIconTooltip
content={
<span className="text-xs">
<TextLink to={v3BillingPath(organization)}>Upgrade</TextLink> your plan to
enable preview branches
</span>
}
/>
)}
</div>
</div>
<FormError>{fields.productionBranch?.error}</FormError>
<FormError>{fields.stagingBranch?.error}</FormError>
Expand Down
60 changes: 45 additions & 15 deletions apps/webapp/app/services/projectSettings.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DeleteProjectService } from "~/services/deleteProject.server";
import { BranchTrackingConfigSchema, type BranchTrackingConfig } from "~/v3/github";
import { checkGitHubBranchExists } from "~/services/gitHub.server";
import { errAsync, fromPromise, okAsync, ResultAsync } from "neverthrow";
import { BuildSettings } from "~/v3/buildSettings";
import { type BuildSettings } from "~/v3/buildSettings";

export class ProjectSettingsService {
#prismaClient: PrismaClient;
Expand Down Expand Up @@ -82,7 +82,7 @@ export class ProjectSettingsService {
(error) => ({ type: "other" as const, cause: error })
);

const createConnectedRepo = (defaultBranch: string) =>
const createConnectedRepo = (defaultBranch: string, previewDeploymentsEnabled: boolean) =>
fromPromise(
this.#prismaClient.connectedGithubRepository.create({
data: {
Expand All @@ -92,21 +92,23 @@ export class ProjectSettingsService {
prod: { branch: defaultBranch },
staging: {},
} satisfies BranchTrackingConfig,
previewDeploymentsEnabled: true,
previewDeploymentsEnabled,
},
}),
(error) => ({ type: "other" as const, cause: error })
);

return ResultAsync.combine([getRepository(), findExistingConnection()]).andThen(
([repository, existingConnection]) => {
if (existingConnection) {
return errAsync({ type: "project_already_has_connected_repository" as const });
}

return createConnectedRepo(repository.defaultBranch);
return ResultAsync.combine([
getRepository(),
findExistingConnection(),
this.isPreviewEnvironmentEnabled(projectId),
]).andThen(([repository, existingConnection, previewEnvironmentEnabled]) => {
if (existingConnection) {
return errAsync({ type: "project_already_has_connected_repository" as const });
}
);

return createConnectedRepo(repository.defaultBranch, previewEnvironmentEnabled);
});
}

disconnectGitHubRepo(projectId: string) {
Expand Down Expand Up @@ -208,18 +210,22 @@ export class ProjectSettingsService {
return okAsync(stagingBranch);
};

const updateConnectedRepo = () =>
const updateConnectedRepo = (data: {
productionBranch: string | undefined;
stagingBranch: string | undefined;
previewDeploymentsEnabled: boolean | undefined;
}) =>
fromPromise(
this.#prismaClient.connectedGithubRepository.update({
where: {
projectId: projectId,
},
data: {
branchTracking: {
prod: productionBranch ? { branch: productionBranch } : {},
staging: stagingBranch ? { branch: stagingBranch } : {},
prod: data.productionBranch ? { branch: data.productionBranch } : {},
staging: data.stagingBranch ? { branch: data.stagingBranch } : {},
} satisfies BranchTrackingConfig,
previewDeploymentsEnabled: previewDeploymentsEnabled,
previewDeploymentsEnabled: data.previewDeploymentsEnabled,
},
}),
(error) => ({ type: "other" as const, cause: error })
Expand All @@ -240,8 +246,14 @@ export class ProjectSettingsService {
fullRepoName: connectedRepo.repository.fullName,
oldStagingBranch: connectedRepo.branchTracking?.staging?.branch,
}),
this.isPreviewEnvironmentEnabled(projectId),
]);
})
.map(([productionBranch, stagingBranch, previewEnvironmentEnabled]) => ({
productionBranch,
stagingBranch,
previewDeploymentsEnabled: previewDeploymentsEnabled && previewEnvironmentEnabled,
}))
.andThen(updateConnectedRepo);
}

Expand Down Expand Up @@ -296,4 +308,22 @@ export class ProjectSettingsService {
});
});
}

private isPreviewEnvironmentEnabled(projectId: string) {
return fromPromise(
this.#prismaClient.runtimeEnvironment.findFirst({
select: {
id: true,
},
where: {
projectId: projectId,
slug: "preview",
},
}),
(error) => ({
type: "other" as const,
cause: error,
})
).map((previewEnvironment) => previewEnvironment !== null);
}
}
112 changes: 56 additions & 56 deletions apps/webapp/app/services/projectSettingsPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { prisma } from "~/db.server";
import { BranchTrackingConfigSchema } from "~/v3/github";
import { env } from "~/env.server";
import { findProjectBySlug } from "~/models/project.server";
import { err, fromPromise, ok, okAsync } from "neverthrow";
import { err, fromPromise, ok, ResultAsync } from "neverthrow";
import { BuildSettingsSchema } from "~/v3/buildSettings";

export class ProjectSettingsPresenter {
Expand All @@ -20,33 +20,31 @@ export class ProjectSettingsPresenter {
fromPromise(findProjectBySlug(organizationSlug, projectSlug, userId), (error) => ({
type: "other" as const,
cause: error,
})).andThen((project) => {
if (!project) {
return err({ type: "project_not_found" as const });
}
return ok(project);
});
}))
.andThen((project) => {
if (!project) {
return err({ type: "project_not_found" as const });
}
return ok(project);
})
.map((project) => {
const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings);
const buildSettings = buildSettingsOrFailure.success
? buildSettingsOrFailure.data
: undefined;
return { ...project, buildSettings };
});

if (!githubAppEnabled) {
return getProject().andThen((project) => {
if (!project) {
return err({ type: "project_not_found" as const });
}

const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings);
const buildSettings = buildSettingsOrFailure.success
? buildSettingsOrFailure.data
: undefined;

return ok({
gitHubApp: {
enabled: false,
connectedRepository: undefined,
installations: undefined,
},
buildSettings,
});
});
return getProject().map(({ buildSettings }) => ({
gitHubApp: {
enabled: false,
connectedRepository: undefined,
installations: undefined,
isPreviewEnvironmentEnabled: undefined,
},
buildSettings,
}));
}

const findConnectedGithubRepository = (projectId: string) =>
Expand Down Expand Up @@ -136,37 +134,39 @@ export class ProjectSettingsPresenter {
})
);

return getProject().andThen((project) =>
findConnectedGithubRepository(project.id).andThen((connectedGithubRepository) => {
const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings);
const buildSettings = buildSettingsOrFailure.success
? buildSettingsOrFailure.data
: undefined;

if (connectedGithubRepository) {
return okAsync({
gitHubApp: {
enabled: true,
connectedRepository: connectedGithubRepository,
// skip loading installations if there is a connected repository
// a project can have only a single connected repository
installations: undefined,
},
buildSettings,
});
}
const isPreviewEnvironmentEnabled = (projectId: string) =>
fromPromise(
this.#prismaClient.runtimeEnvironment.findFirst({
select: {
id: true,
},
where: {
projectId: projectId,
slug: "preview",
},
}),
(error) => ({
type: "other" as const,
cause: error,
})
).map((previewEnvironment) => previewEnvironment !== null);

return listGithubAppInstallations(project.organizationId).map((githubAppInstallations) => {
return {
gitHubApp: {
enabled: true,
connectedRepository: undefined,
installations: githubAppInstallations,
},
buildSettings,
};
});
})
return getProject().andThen((project) =>
ResultAsync.combine([
isPreviewEnvironmentEnabled(project.id),
findConnectedGithubRepository(project.id),
listGithubAppInstallations(project.organizationId),
]).map(
([isPreviewEnvironmentEnabled, connectedGithubRepository, githubAppInstallations]) => ({
gitHubApp: {
enabled: true,
connectedRepository: connectedGithubRepository,
installations: githubAppInstallations,
isPreviewEnvironmentEnabled,
},
buildSettings: project.buildSettings,
})
)
);
}
}