Files
Saadi Myftija 5567f49846 feat(webapp): expose project git settings (#2464)
* Fix settigns page delete project width issue

* Apply a couple of touch-ups to the project settings page

* Add UI flow to connect gh repos

* Enabling adding another gh account in the ui

* Enable connecting a repo to a project

* Enable updating git settings

* Enable disconnecting gh repos from a project

* Remove prisma migration drifts

* Hide git settings when github app is disabled

* Fix migration order

* Avoid using `location` to avoid SSR issues

* Make branch tracking optional

* Disable save buttons when there are no field changes

* Disable delete project button unless the input matches the project slug

* Show connected repo connectedAt date

* Check that tracking branch exists when updating git settings

* Show tracking branch hint in the deployments page

* Fix positioning issue of the pagination pane in the deployments page

* Use mono font for branch names

* Add link to git settings

* Show tracking branch hint for the preview env too

* Add a confirmation prompt on repo disconnect

* Add link to configure repo access in gh

* Add rel prop to github links

* Automatically open repo connection modal after app installation

* Apply some fixes suggested by mr rabbit

* Fix flash cookie issue

* Extract project settings actions into a service

* Extract project settings loader into a presenter service

* Introduce neverthrow for error handling

* Try out neverthrow for error handling in the project setting flows

* Move env gh branch resolution to the presenter service
2025-09-09 13:03:43 +02:00

504 lines
13 KiB
TypeScript

import type { RuntimeEnvironment, TaskRun, WorkerDeployment } from "@trigger.dev/database";
import { z } from "zod";
import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import { objectToSearchParams } from "./searchParams";
import { type WaitpointSearchParams } from "~/components/runs/v3/WaitpointTokenFilters";
export type OrgForPath = Pick<Organization, "slug">;
export type ProjectForPath = Pick<Project, "slug">;
export type EnvironmentForPath = Pick<RuntimeEnvironment, "slug">;
export type v3RunForPath = Pick<TaskRun, "friendlyId">;
export type v3SpanForPath = Pick<TaskRun, "spanId">;
export type DeploymentForPath = Pick<WorkerDeployment, "shortCode">;
export type TaskForPath = {
taskIdentifier: string;
};
export const OrganizationParamsSchema = z.object({
organizationSlug: z.string(),
});
export const ProjectParamSchema = OrganizationParamsSchema.extend({
projectParam: z.string(),
});
export const EnvironmentParamSchema = ProjectParamSchema.extend({
envParam: z.string(),
});
//v3
export const v3TaskParamsSchema = EnvironmentParamSchema.extend({
taskParam: z.string(),
});
export const v3RunParamsSchema = EnvironmentParamSchema.extend({
runParam: z.string(),
});
export const v3SpanParamsSchema = v3RunParamsSchema.extend({
spanParam: z.string(),
});
export const v3DeploymentParams = EnvironmentParamSchema.extend({
deploymentParam: z.string(),
});
export const v3ScheduleParams = EnvironmentParamSchema.extend({
scheduleParam: z.string(),
});
export function rootPath() {
return `/`;
}
export function accountPath() {
return `/account`;
}
export function personalAccessTokensPath() {
return `/account/tokens`;
}
export function accountSecurityPath() {
return `/account/security`;
}
export function invitesPath() {
return `/invites`;
}
export function confirmBasicDetailsPath() {
return `/confirm-basic-details`;
}
export function acceptInvitePath(token: string) {
return `/invite-accept?token=${token}`;
}
export function resendInvitePath() {
return `/invite-resend`;
}
export function logoutPath() {
return `/logout`;
}
export function revokeInvitePath() {
return `/invite-revoke`;
}
// Org
export function organizationPath(organization: OrgForPath) {
return `/orgs/${organizationParam(organization)}`;
}
export function newOrganizationPath() {
return `/orgs/new`;
}
export function selectPlanPath(organization: OrgForPath) {
return `${organizationPath(organization)}/select-plan`;
}
export function organizationTeamPath(organization: OrgForPath) {
return `${organizationPath(organization)}/settings/team`;
}
export function inviteTeamMemberPath(organization: OrgForPath) {
return `${organizationPath(organization)}/invite`;
}
export function organizationBillingPath(organization: OrgForPath) {
return `${organizationPath(organization)}/billing`;
}
export function organizationSettingsPath(organization: OrgForPath) {
return `${organizationPath(organization)}/settings`;
}
function organizationParam(organization: OrgForPath) {
return organization.slug;
}
// Project
export function newProjectPath(organization: OrgForPath, message?: string) {
return `${organizationPath(organization)}/projects/new${
message ? `?message=${encodeURIComponent(message)}` : ""
}`;
}
function projectParam(project: ProjectForPath) {
return project.slug;
}
function environmentParam(environment: EnvironmentForPath) {
return environment.slug;
}
//v3 project
export function v3ProjectPath(organization: OrgForPath, project: ProjectForPath) {
return `/orgs/${organizationParam(organization)}/projects/${projectParam(project)}`;
}
export function githubAppInstallPath(organizationSlug: string, redirectTo: string) {
return `/github/install?org_slug=${organizationSlug}&redirect_to=${encodeURIComponent(
redirectTo
)}`;
}
export function v3EnvironmentPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `/orgs/${organizationParam(organization)}/projects/${projectParam(
project
)}/env/${environmentParam(environment)}`;
}
export function v3TasksStreamingPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/tasks/stream`;
}
export function v3ApiKeysPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/apikeys`;
}
export function v3BulkActionsPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/bulk-actions`;
}
export function v3BulkActionPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
bulkAction: { friendlyId: string }
) {
return `${v3BulkActionsPath(organization, project, environment)}/${bulkAction.friendlyId}`;
}
export function v3EnvironmentVariablesPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/environment-variables`;
}
export function v3NewEnvironmentVariablesPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentVariablesPath(organization, project, environment)}/new`;
}
export function v3ProjectAlertsPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/alerts`;
}
export function v3NewProjectAlertPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3ProjectAlertsPath(organization, project, environment)}/new`;
}
export function v3NewProjectAlertPathConnectToSlackPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3ProjectAlertsPath(organization, project, environment)}/new/connect-to-slack`;
}
export function v3TestPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/test`;
}
export function v3TestTaskPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
task: TaskForPath
) {
return `${v3TestPath(organization, project, environment)}/tasks/${encodeURIComponent(
task.taskIdentifier
)}`;
}
export function v3RunsPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
filters?: TaskRunListSearchFilters
) {
const searchParams = objectToSearchParams(filters);
const query = searchParams ? `?${searchParams.toString()}` : "";
return `${v3EnvironmentPath(organization, project, environment)}/runs${query}`;
}
export function v3CreateBulkActionPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
filters?: TaskRunListSearchFilters,
mode?: "selected" | "filters",
action?: "replay" | "cancel"
) {
const searchParams = objectToSearchParams(filters) ?? new URLSearchParams();
searchParams.set("bulkInspector", "show");
if (mode) {
searchParams.set("mode", mode);
}
if (action) {
searchParams.set("action", action);
}
const query = `?${searchParams.toString()}`;
return `${v3RunsPath(organization, project, environment)}${query}`;
}
export function v3RunPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
run: v3RunForPath
) {
return `${v3RunsPath(organization, project, environment)}/${run.friendlyId}`;
}
export function v3RunRedirectPath(
organization: OrgForPath,
project: ProjectForPath,
run: v3RunForPath
) {
return `${v3ProjectPath(organization, project)}/runs/${run.friendlyId}`;
}
export function v3RunDownloadLogsPath(run: v3RunForPath) {
return `/resources/runs/${run.friendlyId}/logs/download`;
}
export function v3RunSpanPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
run: v3RunForPath,
span: v3SpanForPath
) {
return `${v3RunPath(organization, project, environment, run)}?span=${span.spanId}`;
}
export function v3RunStreamingPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
run: v3RunForPath
) {
return `${v3RunPath(organization, project, environment, run)}/stream`;
}
export function v3SchedulesPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/schedules`;
}
export function v3SchedulePath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
schedule: { friendlyId: string }
) {
return `${v3EnvironmentPath(organization, project, environment)}/schedules/${
schedule.friendlyId
}`;
}
export function v3EditSchedulePath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
schedule: { friendlyId: string }
) {
return `${v3EnvironmentPath(organization, project, environment)}/schedules/edit/${
schedule.friendlyId
}`;
}
export function v3NewSchedulePath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/schedules/new`;
}
export function v3QueuesPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/queues`;
}
export function v3WaitpointTokensPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
filters?: WaitpointSearchParams
) {
const searchParams = objectToSearchParams(filters);
const query = searchParams ? `?${searchParams.toString()}` : "";
return `${v3EnvironmentPath(organization, project, environment)}/waitpoints/tokens${query}`;
}
export function v3WaitpointTokenPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
token: { id: string },
filters?: WaitpointSearchParams
) {
const searchParams = objectToSearchParams(filters);
const query = searchParams ? `?${searchParams.toString()}` : "";
return `${v3WaitpointTokensPath(organization, project, environment)}/${token.id}${query}`;
}
export function v3BatchesPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/batches`;
}
export function v3BatchPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
batch: { friendlyId: string }
) {
return `${v3EnvironmentPath(organization, project, environment)}/batches?id=${batch.friendlyId}`;
}
export function v3BatchRunsPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
batch: { friendlyId: string }
) {
return `${v3RunsPath(organization, project, environment, { batchId: batch.friendlyId })}`;
}
export function v3ProjectSettingsPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/settings`;
}
export function v3DeploymentsPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/deployments`;
}
export function v3DeploymentPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
deployment: DeploymentForPath,
currentPage: number
) {
const query = currentPage ? `?page=${currentPage}` : "";
return `${v3DeploymentsPath(organization, project, environment)}/${deployment.shortCode}${query}`;
}
export function v3DeploymentVersionPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath,
version: string
) {
return `${v3DeploymentsPath(organization, project, environment)}?version=${version}`;
}
export function branchesPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/branches`;
}
export function regionsPath(
organization: OrgForPath,
project: ProjectForPath,
environment: EnvironmentForPath
) {
return `${v3EnvironmentPath(organization, project, environment)}/regions`;
}
export function v3BillingPath(organization: OrgForPath, message?: string) {
return `${organizationPath(organization)}/settings/billing${
message ? `?message=${encodeURIComponent(message)}` : ""
}`;
}
export function v3BillingAlertsPath(organization: OrgForPath) {
return `${organizationPath(organization)}/settings/billing-alerts`;
}
export function v3StripePortalPath(organization: OrgForPath) {
return `/resources/${organization.slug}/subscription/portal`;
}
export function v3UsagePath(organization: OrgForPath) {
return `${organizationPath(organization)}/settings/usage`;
}
// Docs
export function docsRoot() {
return "https://trigger.dev/docs";
}
export function docsPath(path: string) {
return `${docsRoot()}/${path}`;
}
export function docsTroubleshootingPath(path: string) {
return `${docsRoot()}/v3/troubleshooting`;
}
export function adminPath() {
return `/@`;
}