v3 deployments page (#945)
* Set the “current” deployment in WorkerDeploymentPromotion when a new deployment is completed * Deployments page added to sidenav * Basic deployments page working * Pagination for deployments * Right-align the pagination controls * Improved the blank state * Selected deployment panel * Improved the side panel
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
CursorArrowRaysIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
ServerStackIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
projectSetupPath,
|
||||
projectTriggersPath,
|
||||
v3ApiKeysPath,
|
||||
v3DeploymentsPath,
|
||||
v3EnvironmentVariablesPath,
|
||||
v3ProjectPath,
|
||||
v3ProjectSettingsPath,
|
||||
@@ -551,6 +553,13 @@ function V3ProjectSideMenu({
|
||||
to={v3EnvironmentVariablesPath(organization, project)}
|
||||
data-action="environment variables"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Deployments"
|
||||
icon={ServerStackIcon}
|
||||
iconColor="text-blue-500"
|
||||
to={v3DeploymentsPath(organization, project)}
|
||||
data-action="deployments"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Project settings"
|
||||
icon="settings"
|
||||
|
||||
@@ -10,7 +10,7 @@ const variants = {
|
||||
"grid place-items-center rounded-sm px-1.5 h-5 tracking-wider text-xxs border border-dimmed text-text-dimmed uppercase whitespace-nowrap",
|
||||
v3: "grid place-items-center rounded-full px-[0.4rem] h-5 tracking-wider text-xxs bg-charcoal-750 text-primary whitespace-nowrap",
|
||||
"outline-rounded":
|
||||
"grid place-items-center rounded-full px-1.5 h-5 tracking-wider text-xxs border border-blue-500 text-blue-500 uppercase whitespace-nowrap",
|
||||
"grid place-items-center rounded-full px-1 h-4 tracking-wider text-xxs border border-blue-500 text-blue-500 uppercase whitespace-nowrap",
|
||||
};
|
||||
|
||||
type BadgeProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
|
||||
@@ -3,17 +3,14 @@ import { ChevronLeftIcon } from "@heroicons/react/24/solid";
|
||||
import { Link, useLocation } from "@remix-run/react";
|
||||
import { LinkDisabled } from "./LinkWithDisabled";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ButtonContent, LinkButton } from "./Buttons";
|
||||
|
||||
export function PaginationControls({
|
||||
currentPage,
|
||||
totalPages,
|
||||
pageSize,
|
||||
totalResults,
|
||||
}: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
pageSize: number;
|
||||
totalResults: number;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
if (totalPages <= 1) {
|
||||
@@ -21,65 +18,33 @@ export function PaginationControls({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-b-lg border-t border-charcoal-850 bg-charcoal-700/20 py-3 pl-4 pr-3 text-charcoal-400">
|
||||
<div className="flex flex-1 justify-between sm:hidden">
|
||||
{currentPage > 1 && (
|
||||
<Link
|
||||
to={pageUrl(location, currentPage - 1)}
|
||||
className="hover:bg-charcoal-50 relative inline-flex items-center rounded-md border border-charcoal-300 bg-charcoal-700/20 px-4 py-2 text-xs"
|
||||
>
|
||||
Previous
|
||||
</Link>
|
||||
)}
|
||||
{currentPage < totalPages && (
|
||||
<Link
|
||||
to={pageUrl(location, currentPage + 1)}
|
||||
className="hover:bg-charcoal-50 relative ml-3 inline-flex items-center rounded-md border border-charcoal-300 bg-charcoal-700/20 px-4 py-2 text-xs"
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-charcoal-400">
|
||||
Showing <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> to{" "}
|
||||
<span className="font-medium">{currentPage * pageSize}</span> of{" "}
|
||||
<span className="font-medium">{totalResults}</span> results
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav
|
||||
className="isolate inline-flex -space-x-px rounded-md shadow-sm"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<LinkDisabled
|
||||
to={pageUrl(location, currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="relative inline-flex items-center rounded-l border border-charcoal-500 bg-charcoal-700/20 px-2 text-xs font-medium text-charcoal-400 transition hover:border-charcoal-400 hover:bg-charcoal-400 hover:text-charcoal-800 focus:z-20"
|
||||
disabledClassName="opacity-30 cursor-default hover:bg-charcoal-700/20 hover:border-charcoal-500 hover:!text-charcoal-400"
|
||||
>
|
||||
<span className="sr-only">Previous</span>
|
||||
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</LinkDisabled>
|
||||
<nav className="flex items-center gap-1" aria-label="Pagination">
|
||||
<LinkDisabled
|
||||
to={pageUrl(location, currentPage - 1)}
|
||||
className={currentPage > 1 ? "group" : ""}
|
||||
disabled={currentPage === 1}
|
||||
disabledClassName="opacity-30 cursor-default"
|
||||
>
|
||||
<ButtonContent variant="minimal/medium" LeadingIcon={ChevronLeftIcon}>
|
||||
Previous
|
||||
</ButtonContent>
|
||||
</LinkDisabled>
|
||||
|
||||
{calculatePageLinks(currentPage, totalPages).map((page, i) => (
|
||||
<PageLinkComponent page={page} key={i} location={location} />
|
||||
))}
|
||||
{calculatePageLinks(currentPage, totalPages).map((page, i) => (
|
||||
<PageLinkComponent page={page} key={i} location={location} />
|
||||
))}
|
||||
|
||||
<LinkDisabled
|
||||
to={pageUrl(location, currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="relative inline-flex items-center rounded-r border border-charcoal-500 bg-charcoal-700/20 px-2 text-xs font-medium text-charcoal-400 transition hover:border-charcoal-400 hover:bg-charcoal-400 hover:text-charcoal-800 focus:z-20"
|
||||
disabledClassName="opacity-30 cursor-default hover:bg-charcoal-700/20 hover:border-charcoal-500 hover:!text-charcoal-400"
|
||||
>
|
||||
<span className="sr-only">Next</span>
|
||||
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</LinkDisabled>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LinkDisabled
|
||||
to={pageUrl(location, currentPage + 1)}
|
||||
className={currentPage !== totalPages ? "group" : ""}
|
||||
disabled={currentPage === totalPages}
|
||||
disabledClassName="opacity-30 cursor-default"
|
||||
>
|
||||
<ButtonContent variant="minimal/medium" TrailingIcon={ChevronRightIcon}>
|
||||
Next
|
||||
</ButtonContent>
|
||||
</LinkDisabled>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,11 +57,9 @@ function pageUrl(location: ReturnType<typeof useLocation>, page: number): string
|
||||
}
|
||||
|
||||
const baseClass =
|
||||
"relative inline-flex items-center border px-3.5 py-2 text-xs font-medium focus:z-20 transition";
|
||||
const unselectedClass =
|
||||
"bg-charcoal-700/20 border-charcoal-500 text-charcoal-400 hover:bg-charcoal-400 hover:text-charcoal-900";
|
||||
const selectedClass =
|
||||
"z-10 bg-charcoal-500 border-charcoal-500 hover:bg-charcoal-400 text-charcoal-900";
|
||||
"flex items-center justify-center border border-transparent h-8 w-8 text-xs font-medium transition text-text-dimmed rounded-sm";
|
||||
const unselectedClass = "hover:bg-tertiary hover:text-text-bright";
|
||||
const selectedClass = "border-text-dimmed text-text-bright hover:bg-tertiary";
|
||||
|
||||
function PageLinkComponent({
|
||||
page,
|
||||
@@ -106,25 +69,16 @@ function PageLinkComponent({
|
||||
location: ReturnType<typeof useLocation>;
|
||||
}) {
|
||||
if (page.type === "specific") {
|
||||
if (page.isCurrent) {
|
||||
return (
|
||||
<Link to={pageUrl(location, page.page)} className={cn(baseClass, selectedClass)}>
|
||||
{page.page}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link to={pageUrl(location, page.page)} className={cn(baseClass, unselectedClass)}>
|
||||
{page.page}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<span className="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-xs font-medium text-charcoal-500">
|
||||
...
|
||||
</span>
|
||||
<Link
|
||||
to={pageUrl(location, page.page)}
|
||||
className={cn(baseClass, page.isCurrent ? selectedClass : unselectedClass)}
|
||||
>
|
||||
{page.page}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return <span className={baseClass}>...</span>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function PropertyTable({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("grid grid-cols-[auto,1fr] items-center gap-x-4 gap-y-2", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type PropertyProps = {
|
||||
label: ReactNode;
|
||||
labelClassName?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Property({ label, labelClassName, children }: PropertyProps) {
|
||||
return (
|
||||
<>
|
||||
<div className={labelClassName}>
|
||||
{typeof label === "string" ? <Paragraph variant="small">{label}</Paragraph> : label}
|
||||
</div>
|
||||
<div>
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant="small/bright">{children}</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
BoltSlashIcon,
|
||||
BugAntIcon,
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
NoSymbolIcon,
|
||||
PauseCircleIcon,
|
||||
RectangleStackIcon,
|
||||
ServerStackIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { TaskRunStatus, WorkerDeploymentStatus } from "@trigger.dev/database";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function DeploymentStatus({
|
||||
status,
|
||||
className,
|
||||
}: {
|
||||
status: WorkerDeploymentStatus;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn("flex items-center gap-1", className)}>
|
||||
<DeploymentStatusIcon status={status} className="h-4 w-4" />
|
||||
<DeploymentStatusLabel status={status} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeploymentStatusLabel({ status }: { status: WorkerDeploymentStatus }) {
|
||||
return (
|
||||
<span className={deploymentStatusClassNameColor(status)}>{deploymentStatusTitle(status)}</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeploymentStatusIcon({
|
||||
status,
|
||||
className,
|
||||
}: {
|
||||
status: WorkerDeploymentStatus;
|
||||
className: string;
|
||||
}) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
case "BUILDING":
|
||||
case "DEPLOYING":
|
||||
return <Spinner className={cn(deploymentStatusClassNameColor(status), className)} />;
|
||||
case "DEPLOYED":
|
||||
return <CheckCircleIcon className={cn(deploymentStatusClassNameColor(status), className)} />;
|
||||
case "CANCELED":
|
||||
return <NoSymbolIcon className={cn(deploymentStatusClassNameColor(status), className)} />;
|
||||
case "FAILED":
|
||||
return <XCircleIcon className={cn(deploymentStatusClassNameColor(status), className)} />;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
case "BUILDING":
|
||||
case "DEPLOYING":
|
||||
return "text-pending";
|
||||
case "CANCELED":
|
||||
return "text-charcoal-500";
|
||||
case "DEPLOYED":
|
||||
return "text-success";
|
||||
case "FAILED":
|
||||
return "text-error";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deploymentStatusTitle(status: WorkerDeploymentStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "Pending…";
|
||||
case "BUILDING":
|
||||
return "Building…";
|
||||
case "DEPLOYING":
|
||||
return "Deploying…";
|
||||
case "DEPLOYED":
|
||||
return "Deployed";
|
||||
case "CANCELED":
|
||||
return "Canceled";
|
||||
case "FAILED":
|
||||
return "Failed";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ type TaskFunctionNameProps = {
|
||||
export function TaskFunctionName({ variant, functionName, className }: TaskFunctionNameProps) {
|
||||
return (
|
||||
<InlineCode variant={variant} className={cn("text-sun-100", className)}>
|
||||
{functionName}()
|
||||
{`${functionName}()`}
|
||||
</InlineCode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const LIVE_ENVIRONMENT = "live";
|
||||
export const DEV_ENVIRONMENT = "development";
|
||||
export const CURRENT_DEPLOYMENT_LABEL = "current";
|
||||
export const MAX_LIVE_PROJECTS = 1;
|
||||
export const DEFAULT_MAX_CONCURRENT_RUNS = 10;
|
||||
export const MAX_CONCURRENT_RUNS_LIMIT = 20;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { WorkerDeploymentStatus } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
export class DeploymentListPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
projectSlug,
|
||||
organizationSlug,
|
||||
page = 1,
|
||||
}: {
|
||||
userId: User["id"];
|
||||
projectSlug: Project["slug"];
|
||||
organizationSlug: Organization["slug"];
|
||||
page?: number;
|
||||
}) {
|
||||
const project = await this.#prismaClient.project.findFirstOrThrow({
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
slug: true,
|
||||
orgMember: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
displayName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
slug: projectSlug,
|
||||
organization: {
|
||||
slug: organizationSlug,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const totalCount = await this.#prismaClient.workerDeployment.count({
|
||||
where: {
|
||||
projectId: project.id,
|
||||
},
|
||||
});
|
||||
|
||||
const labeledDeployments = await this.#prismaClient.workerDeploymentPromotion.findMany({
|
||||
where: {
|
||||
environmentId: {
|
||||
in: project.environments.map((env) => env.id),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
deploymentId: true,
|
||||
label: true,
|
||||
},
|
||||
});
|
||||
|
||||
const deployments = await this.#prismaClient.$queryRaw<
|
||||
{
|
||||
id: string;
|
||||
shortCode: string;
|
||||
version: string;
|
||||
status: WorkerDeploymentStatus;
|
||||
environmentId: string;
|
||||
deployedAt: Date | null;
|
||||
tasksCount: BigInt | null;
|
||||
userId: string | null;
|
||||
userName: string | null;
|
||||
userDisplayName: string | null;
|
||||
userAvatarUrl: string | null;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
wd."id",
|
||||
wd."shortCode",
|
||||
wd."version",
|
||||
(SELECT COUNT(*) FROM "BackgroundWorkerTask" WHERE "BackgroundWorkerTask"."workerId" = wd."workerId") AS "tasksCount",
|
||||
wd."environmentId",
|
||||
wd."status",
|
||||
u."id" AS "userId",
|
||||
u."name" AS "userName",
|
||||
u."displayName" AS "userDisplayName",
|
||||
u."avatarUrl" AS "userAvatarUrl",
|
||||
wd."deployedAt"
|
||||
FROM
|
||||
"WorkerDeployment" as wd
|
||||
INNER JOIN
|
||||
"User" as u ON wd."triggeredById" = u."id"
|
||||
WHERE
|
||||
wd."projectId" = ${project.id}
|
||||
ORDER BY
|
||||
wd."version" DESC
|
||||
LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
|
||||
|
||||
return {
|
||||
currentPage: page,
|
||||
totalPages: Math.ceil(totalCount / pageSize),
|
||||
deployments: deployments.map((deployment) => {
|
||||
const environment = project.environments.find((env) => env.id === deployment.environmentId);
|
||||
if (!environment) {
|
||||
throw new Error(`Environment not found for deployment ${deployment.id}`);
|
||||
}
|
||||
|
||||
const label = labeledDeployments.find(
|
||||
(labeledDeployment) => labeledDeployment.deploymentId === deployment.id
|
||||
);
|
||||
|
||||
return {
|
||||
id: deployment.id,
|
||||
shortCode: deployment.shortCode,
|
||||
version: deployment.version,
|
||||
status: deployment.status,
|
||||
deployedAt: deployment.deployedAt,
|
||||
tasksCount: deployment.tasksCount ? Number(deployment.tasksCount) : null,
|
||||
label: label?.label,
|
||||
environment: {
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
slug: environment.slug,
|
||||
userId: environment.orgMember?.user.id,
|
||||
userName: getUsername(environment.orgMember?.user),
|
||||
},
|
||||
deployedBy: deployment.userId
|
||||
? {
|
||||
id: deployment.userId,
|
||||
name: deployment.userName,
|
||||
displayName: deployment.userDisplayName,
|
||||
avatarUrl: deployment.userAvatarUrl,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { WorkerDeployment, WorkerDeploymentStatus } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
|
||||
export class DeploymentPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
projectSlug,
|
||||
organizationSlug,
|
||||
deploymentShortCode,
|
||||
}: {
|
||||
userId: User["id"];
|
||||
projectSlug: Project["slug"];
|
||||
organizationSlug: Organization["slug"];
|
||||
deploymentShortCode: WorkerDeployment["shortCode"];
|
||||
}) {
|
||||
const project = await this.#prismaClient.project.findFirstOrThrow({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
slug: projectSlug,
|
||||
organization: {
|
||||
slug: organizationSlug,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const deployment = await this.#prismaClient.workerDeployment.findUniqueOrThrow({
|
||||
where: {
|
||||
projectId_shortCode: {
|
||||
projectId: project.id,
|
||||
shortCode: deploymentShortCode,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
shortCode: true,
|
||||
version: true,
|
||||
environment: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
slug: true,
|
||||
orgMember: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
displayName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
status: true,
|
||||
deployedAt: true,
|
||||
promotions: {
|
||||
select: {
|
||||
label: true,
|
||||
},
|
||||
},
|
||||
worker: {
|
||||
select: {
|
||||
tasks: {
|
||||
select: {
|
||||
slug: true,
|
||||
exportName: true,
|
||||
filePath: true,
|
||||
},
|
||||
orderBy: {
|
||||
exportName: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
triggeredBy: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
displayName: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deployment: {
|
||||
id: deployment.id,
|
||||
shortCode: deployment.shortCode,
|
||||
version: deployment.version,
|
||||
status: deployment.status,
|
||||
deployedAt: deployment.deployedAt,
|
||||
tasks: deployment.worker?.tasks,
|
||||
label: deployment.promotions?.[0]?.label,
|
||||
environment: {
|
||||
id: deployment.environment.id,
|
||||
type: deployment.environment.type,
|
||||
slug: deployment.environment.slug,
|
||||
userId: deployment.environment.orgMember?.user.id,
|
||||
userName: getUsername(deployment.environment.orgMember?.user),
|
||||
},
|
||||
deployedBy: deployment.triggeredBy,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
import { CommandLineIcon, ServerIcon } from "@heroicons/react/20/solid";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { TerminalIcon, TerminalSquareIcon } from "lucide-react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
import { UserAvatar } from "~/components/UserProfilePhoto";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { ResizablePanel, ResizablePanelGroup } from "~/components/primitives/Resizable";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellChevron,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { DeploymentStatus } from "~/components/runs/v3/DeploymentStatus";
|
||||
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { DeploymentListPresenter } from "~/presenters/v3/DeploymentListPresenter.server";
|
||||
import { DeploymentPresenter } from "~/presenters/v3/DeploymentPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
ProjectParamSchema,
|
||||
docsPath,
|
||||
runParam,
|
||||
v3DeploymentParams,
|
||||
v3DeploymentPath,
|
||||
v3DeploymentsPath,
|
||||
v3RunPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { createSearchParams } from "~/utils/searchParams";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam, deploymentParam } = v3DeploymentParams.parse(params);
|
||||
|
||||
try {
|
||||
const presenter = new DeploymentPresenter();
|
||||
const { deployment } = await presenter.call({
|
||||
userId,
|
||||
organizationSlug,
|
||||
projectSlug: projectParam,
|
||||
deploymentShortCode: deploymentParam,
|
||||
});
|
||||
|
||||
return typedjson({ deployment });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Response(undefined, {
|
||||
status: 400,
|
||||
statusText: "Something went wrong, if this problem persists please contact support.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const location = useLocation();
|
||||
const user = useUser();
|
||||
const { deployment } = useTypedLoaderData<typeof loader>();
|
||||
const page = new URLSearchParams(location.search).get("page");
|
||||
|
||||
const usernameForEnv =
|
||||
user.id !== deployment.environment.userId ? deployment.environment.userName : undefined;
|
||||
|
||||
return (
|
||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright">
|
||||
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
||||
<Header2 className={cn("whitespace-nowrap")}>Deploy: {deployment.shortCode}</Header2>
|
||||
<LinkButton
|
||||
to={`${v3DeploymentsPath(organization, project)}${page ? `?page=${page}` : ""}`}
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={ExitIcon}
|
||||
shortcut={{ key: "esc" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-y-auto px-3 pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="flex flex-col gap-4">
|
||||
<PropertyTable>
|
||||
<Property label="Deploy">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="small/bright">{deployment.shortCode}</Paragraph>
|
||||
{deployment.label && <Badge variant="outline-rounded">{deployment.label}</Badge>}
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Environment">
|
||||
<EnvironmentLabel environment={deployment.environment} userName={usernameForEnv} />
|
||||
</Property>
|
||||
<Property label="Version">{deployment.version}</Property>
|
||||
<Property label="Status">
|
||||
<DeploymentStatus status={deployment.status} className="text-sm" />
|
||||
</Property>
|
||||
<Property label="Tasks">{deployment.tasks ? deployment.tasks.length : "–"}</Property>
|
||||
<Property label="Deployed at">
|
||||
<Paragraph variant="small/bright">
|
||||
{deployment.deployedAt ? (
|
||||
<>
|
||||
<DateTimeAccurate date={deployment.deployedAt} /> UTC
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
</Property>
|
||||
<Property label="Deployed by">
|
||||
{deployment.deployedBy ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<UserAvatar
|
||||
avatarUrl={deployment.deployedBy.avatarUrl}
|
||||
name={deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Paragraph variant="small">
|
||||
{deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property>
|
||||
</PropertyTable>
|
||||
|
||||
{deployment.tasks ? (
|
||||
<div className="divide-y divide-charcoal-800 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell className="px-2">Task</TableHeaderCell>
|
||||
<TableHeaderCell className="px-2">File path</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{deployment.tasks.map((t) => {
|
||||
return (
|
||||
<TableRow key={t.slug}>
|
||||
<TableCell>
|
||||
<div className="inline-flex flex-col gap-0.5">
|
||||
<TaskFunctionName
|
||||
variant="extra-small"
|
||||
functionName={t.exportName}
|
||||
className="-ml-1 inline-flex"
|
||||
/>
|
||||
<Paragraph variant="extra-small" className="text-text-dimmed">
|
||||
{t.slug}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{t.filePath}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
import { CommandLineIcon, ServerIcon } from "@heroicons/react/20/solid";
|
||||
import { Outlet, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { TerminalIcon, TerminalSquareIcon } from "lucide-react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
import { UserAvatar } from "~/components/UserProfilePhoto";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "~/components/primitives/Resizable";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellChevron,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { DeploymentStatus } from "~/components/runs/v3/DeploymentStatus";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { DeploymentListPresenter } from "~/presenters/v3/DeploymentListPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ProjectParamSchema, docsPath, v3DeploymentPath } from "~/utils/pathBuilder";
|
||||
import { createSearchParams } from "~/utils/searchParams";
|
||||
|
||||
const SearchParams = z.object({
|
||||
page: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
const searchParams = createSearchParams(request.url, SearchParams);
|
||||
const page = searchParams.success ? searchParams.params.get("page") ?? 1 : 1;
|
||||
|
||||
try {
|
||||
const presenter = new DeploymentListPresenter();
|
||||
const result = await presenter.call({
|
||||
userId,
|
||||
organizationSlug,
|
||||
projectSlug: projectParam,
|
||||
page,
|
||||
});
|
||||
|
||||
return typedjson(result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Response(undefined, {
|
||||
status: 400,
|
||||
statusText: "Something went wrong, if this problem persists please contact support.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
const { deployments, currentPage, totalPages } = useTypedLoaderData<typeof loader>();
|
||||
const hasDeployments = totalPages > 0;
|
||||
|
||||
const { deploymentParam } = useParams();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<NavBar>
|
||||
<PageTitle title="Deployments" />
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
<ResizablePanelGroup direction="horizontal" className="h-full max-h-full">
|
||||
<ResizablePanel order={1} minSize={20} defaultSize={60}>
|
||||
{hasDeployments ? (
|
||||
<div className="flex flex-col gap-4 p-3">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Deploy</TableHeaderCell>
|
||||
<TableHeaderCell>Env</TableHeaderCell>
|
||||
<TableHeaderCell>Version</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Tasks</TableHeaderCell>
|
||||
<TableHeaderCell>Deployed at</TableHeaderCell>
|
||||
<TableHeaderCell>Deployed by</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{deployments.length > 0 ? (
|
||||
deployments.map((deployment) => {
|
||||
const usernameForEnv =
|
||||
user.id !== deployment.environment.userId
|
||||
? deployment.environment.userName
|
||||
: undefined;
|
||||
const path = v3DeploymentPath(organization, project, deployment);
|
||||
return (
|
||||
<TableRow key={deployment.id} className="group">
|
||||
<TableCell to={path}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small">{deployment.shortCode}</Paragraph>
|
||||
{deployment.label && (
|
||||
<Badge variant="outline-rounded">{deployment.label}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<EnvironmentLabel
|
||||
environment={deployment.environment}
|
||||
userName={usernameForEnv}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path}>{deployment.version}</TableCell>
|
||||
<TableCell to={path}>
|
||||
<DeploymentStatus status={deployment.status} />
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{deployment.tasksCount !== null ? deployment.tasksCount : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{deployment.deployedAt ? (
|
||||
<DateTime date={deployment.deployedAt} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{deployment.deployedBy ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<UserAvatar
|
||||
avatarUrl={deployment.deployedBy.avatarUrl}
|
||||
name={
|
||||
deployment.deployedBy.name ??
|
||||
deployment.deployedBy.displayName
|
||||
}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Paragraph variant="extra-small">
|
||||
{deployment.deployedBy.name ??
|
||||
deployment.deployedBy.displayName}
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} />
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={6}>
|
||||
<Paragraph variant="small" className="flex items-center justify-center">
|
||||
No deploys match your filters
|
||||
</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex justify-end">
|
||||
<PaginationControls currentPage={currentPage} totalPages={totalPages} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<CreateDeploymentInstructions />
|
||||
)}
|
||||
</ResizablePanel>
|
||||
|
||||
{deploymentParam && (
|
||||
<>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel order={2} minSize={20} defaultSize={40}>
|
||||
<Outlet />
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateDeploymentInstructions() {
|
||||
return (
|
||||
<MainCenteredContainer className="max-w-prose">
|
||||
<BlankstateInstructions title="Deploy for the first time">
|
||||
<Paragraph spacing>
|
||||
There are several ways to deploy your tasks. You can use the CLI, Continuous Integration
|
||||
(like GitHub Actions), or an integration with a service like Netlify or Vercel. Make sure
|
||||
you{" "}
|
||||
<TextLink href={docsPath("v3/deploy-environment-variables")}>
|
||||
set your environment variables
|
||||
</TextLink>{" "}
|
||||
first.
|
||||
</Paragraph>
|
||||
<div className="flex gap-3">
|
||||
<LinkButton
|
||||
to={docsPath("v3/cli-deploy")}
|
||||
variant="tertiary/medium"
|
||||
LeadingIcon={CommandLineIcon}
|
||||
className="inline-flex"
|
||||
>
|
||||
Deploy with the CLI
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
to={docsPath("v3/github-actions")}
|
||||
variant="tertiary/medium"
|
||||
LeadingIcon={ServerIcon}
|
||||
className="inline-flex"
|
||||
>
|
||||
Deploy with GitHub actions
|
||||
</LinkButton>
|
||||
</div>
|
||||
</BlankstateInstructions>
|
||||
</MainCenteredContainer>
|
||||
);
|
||||
}
|
||||
+1
-27
@@ -10,6 +10,7 @@ import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { DateTimeAccurate } from "~/components/primitives/DateTime";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
|
||||
import { RunIcon } from "~/components/runs/v3/RunIcon";
|
||||
import { SpanEvents } from "~/components/runs/v3/SpanEvents";
|
||||
@@ -138,33 +139,6 @@ export default function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
function PropertyTable({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return <div className="grid grid-cols-[auto,1fr] items-baseline gap-x-4 gap-y-2">{children}</div>;
|
||||
}
|
||||
|
||||
type PropertyProps = {
|
||||
label: ReactNode;
|
||||
labelClassName?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function Property({ label, labelClassName, children }: PropertyProps) {
|
||||
return (
|
||||
<>
|
||||
<div className={labelClassName}>
|
||||
{typeof label === "string" ? <Paragraph variant="small">{label}</Paragraph> : label}
|
||||
</div>
|
||||
<div>
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant="small/bright">{children}</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type TimelineProps = {
|
||||
startTime: Date;
|
||||
duration: number;
|
||||
|
||||
+1
-7
@@ -1,17 +1,13 @@
|
||||
import { Link, Outlet, useLocation, useNavigation, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
import {
|
||||
environmentBorderClassName,
|
||||
environmentTextClassName,
|
||||
environmentTitle,
|
||||
} from "~/components/environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
@@ -30,7 +26,6 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
|
||||
import { useLinkStatus } from "~/hooks/useLinkStatus";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
@@ -43,8 +38,7 @@ import {
|
||||
} from "~/presenters/v3/TestPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ProjectParamSchema, v3ProjectPath, v3TestPath, v3TestTaskPath } from "~/utils/pathBuilder";
|
||||
import { createSearchParams } from "~/utils/searchParams";
|
||||
import { ProjectParamSchema, v3TestPath, v3TestTaskPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const TestSearchParams = z.object({
|
||||
environment: z.string().optional(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
TriggerHttpEndpoint,
|
||||
TriggerSource,
|
||||
Webhook,
|
||||
WorkerDeployment,
|
||||
} from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
|
||||
@@ -26,6 +27,7 @@ export type HttpEndpointForPath = Pick<TriggerHttpEndpoint, "key">;
|
||||
export type TaskForPath = Pick<BackgroundWorkerTask, "friendlyId">;
|
||||
export type v3RunForPath = Pick<TaskRun, "friendlyId">;
|
||||
export type v3SpanForPath = Pick<TaskRun, "spanId">;
|
||||
export type DeploymentForPath = Pick<WorkerDeployment, "shortCode">;
|
||||
|
||||
export const OrganizationParamsSchema = z.object({
|
||||
organizationSlug: z.string(),
|
||||
@@ -84,6 +86,10 @@ export const v3SpanParamsSchema = ProjectParamSchema.extend({
|
||||
spanParam: z.string(),
|
||||
});
|
||||
|
||||
export const v3DeploymentParams = ProjectParamSchema.extend({
|
||||
deploymentParam: z.string(),
|
||||
});
|
||||
|
||||
export function trimTrailingSlash(path: string) {
|
||||
return path.replace(/\/$/, "");
|
||||
}
|
||||
@@ -363,6 +369,18 @@ export function v3ProjectSettingsPath(organization: OrgForPath, project: Project
|
||||
return `${v3ProjectPath(organization, project)}/settings`;
|
||||
}
|
||||
|
||||
export function v3DeploymentsPath(organization: OrgForPath, project: ProjectForPath) {
|
||||
return `${v3ProjectPath(organization, project)}/deployments`;
|
||||
}
|
||||
|
||||
export function v3DeploymentPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
deployment: DeploymentForPath
|
||||
) {
|
||||
return `${v3DeploymentsPath(organization, project)}/${deployment.shortCode}`;
|
||||
}
|
||||
|
||||
// Integration
|
||||
export function integrationClientPath(organization: OrgForPath, client: IntegrationForPath) {
|
||||
return `${organizationIntegrationsPath(organization)}/${clientParam(client)}`;
|
||||
|
||||
@@ -51,7 +51,7 @@ type SearchParamsResult<TParams extends ParamType> =
|
||||
| { success: true; params: SearchParams<TParams> }
|
||||
| { success: false; error: string };
|
||||
|
||||
type ParamType = Record<string, string>;
|
||||
type ParamType = Record<string, any>;
|
||||
|
||||
export function createSearchParams<TParams extends ParamType>(
|
||||
url: string,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { createBackgroundTasks } from "./createBackgroundWorker.server";
|
||||
import { CURRENT_DEPLOYMENT_LABEL } from "~/consts";
|
||||
|
||||
export class CreateDeployedBackgroundWorkerService extends BaseService {
|
||||
public async call(
|
||||
@@ -46,12 +47,30 @@ export class CreateDeployedBackgroundWorkerService extends BaseService {
|
||||
id: deployment.id,
|
||||
},
|
||||
data: {
|
||||
workerId: backgroundWorker.id,
|
||||
status: "DEPLOYED",
|
||||
workerId: backgroundWorker.id,
|
||||
deployedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
//set this deployment as the current deployment for this environment
|
||||
await this._prisma.workerDeploymentPromotion.upsert({
|
||||
where: {
|
||||
environmentId_label: {
|
||||
environmentId: environment.id,
|
||||
label: CURRENT_DEPLOYMENT_LABEL,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
deploymentId: deployment.id,
|
||||
environmentId: environment.id,
|
||||
label: CURRENT_DEPLOYMENT_LABEL,
|
||||
},
|
||||
update: {
|
||||
deploymentId: deployment.id,
|
||||
},
|
||||
});
|
||||
|
||||
return backgroundWorker;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user