From 7a1206c3f7432cc9e7a6634a3643146827facf8c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:19:57 +0100 Subject: [PATCH 1/3] link from deployments tasks to filtered runs view --- .../route.tsx | 9 ++++++--- ...ojects.v3.$projectRef.deployments.$deploymentParam.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index ff0ccbed92..1ee6599513 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -28,7 +28,7 @@ import { useUser } from "~/hooks/useUser"; import { DeploymentPresenter } from "~/presenters/v3/DeploymentPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; -import { v3DeploymentParams, v3DeploymentsPath } from "~/utils/pathBuilder"; +import { v3DeploymentParams, v3DeploymentsPath, v3RunsPath } from "~/utils/pathBuilder"; import { capitalizeWord } from "~/utils/string"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { @@ -231,16 +231,19 @@ export default function Page() { {deployment.tasks.map((t) => { + const path = v3RunsPath(organization, project, environment, { + tasks: [t.slug], + }); return ( - +
{t.slug}
- {t.filePath} + {t.filePath}
); })} diff --git a/apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts b/apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts index e4f83a13ad..c05ad19c68 100644 --- a/apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts +++ b/apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts @@ -33,7 +33,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { return new Response("Not found", { status: 404 }); } - // Redirect to the project's runs page + // Redirect to the project's deployments page return redirect( `/orgs/${project.organization.slug}/projects/${project.slug}/deployments/${validatedParams.deploymentParam}` ); From b3f7dccdfe3320f93ec02138dea9b32213bfd619 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat, 12 Apr 2025 12:13:31 +0100 Subject: [PATCH 2/3] jump to deployment --- .../v3/DeploymentListPresenter.server.ts | 48 ++++++++++++++++++ .../route.tsx | 50 ++++++++++++++++--- .../route.tsx | 18 ++++++- apps/webapp/app/utils/pathBuilder.ts | 9 ++++ 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts index f7dbfbc75c..b044cbe070 100644 --- a/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts @@ -168,4 +168,52 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`; }), }; } + + public async findPageForVersion({ + userId, + projectSlug, + organizationSlug, + environmentSlug, + version, + }: { + userId: User["id"]; + projectSlug: Project["slug"]; + organizationSlug: Organization["slug"]; + environmentSlug: string; + version: string; + }) { + const project = await this.#prismaClient.project.findFirstOrThrow({ + select: { + id: true, + }, + where: { + slug: projectSlug, + organization: { + slug: organizationSlug, + members: { + some: { + userId, + }, + }, + }, + }, + }); + + const environment = await findEnvironmentBySlug(project.id, environmentSlug, userId); + if (!environment) { + throw new Error(`Environment not found`); + } + + // Find how many deployments have been made since this version + const deploymentsSinceVersion = await this.#prismaClient.$queryRaw<{ count: BigInt }[]>` + SELECT COUNT(*) as count + FROM ${sqlDatabaseSchema}."WorkerDeployment" + WHERE "projectId" = ${project.id} + AND "environmentId" = ${environment.id} + AND string_to_array(version, '.')::int[] > string_to_array(${version}, '.')::int[] + `; + + const count = Number(deploymentsSinceVersion[0].count); + return Math.floor(count / pageSize) + 1; + } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index a22a6ad9b8..ef8ababe36 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -1,12 +1,11 @@ import { ArrowPathIcon, ArrowUturnLeftIcon, BookOpenIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction, Outlet, useLocation, useParams } from "@remix-run/react"; +import { type MetaFunction, Outlet, useLocation, useParams, useNavigate } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { PromoteIcon } from "~/assets/icons/PromoteIcon"; import { DeploymentsNone, DeploymentsNoneDev } from "~/components/BlankStatePanels"; import { UserAvatar } from "~/components/UserProfilePhoto"; -import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; @@ -53,6 +52,7 @@ import { EnvironmentParamSchema, docsPath, v3DeploymentPath } from "~/utils/path import { createSearchParams } from "~/utils/searchParams"; import { deploymentIndexingIsRetryable } from "~/v3/deploymentStatus"; import { compareDeploymentVersions } from "~/v3/utils/deploymentVersions"; +import { useEffect } from "react"; export const meta: MetaFunction = () => { return [ @@ -64,6 +64,7 @@ export const meta: MetaFunction = () => { const SearchParams = z.object({ page: z.coerce.number().optional(), + version: z.string().optional(), }); export const loader = async ({ request, params }: LoaderFunctionArgs) => { @@ -71,10 +72,29 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); const searchParams = createSearchParams(request.url, SearchParams); - const page = searchParams.success ? searchParams.params.get("page") ?? 1 : 1; + + let page = searchParams.success ? Number(searchParams.params.get("page") ?? 1) : 1; + const version = searchParams.success ? searchParams.params.get("version")?.toString() : undefined; + + const presenter = new DeploymentListPresenter(); + + // If we have a version, find its page + if (version) { + try { + page = await presenter.findPageForVersion({ + userId, + organizationSlug, + projectSlug: projectParam, + environmentSlug: envParam, + version, + }); + } catch (error) { + console.error("Error finding page for version", error); + // Carry on, we'll just show the selected page + } + } try { - const presenter = new DeploymentListPresenter(); const result = await presenter.call({ userId, organizationSlug, @@ -83,7 +103,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { page, }); - return typedjson(result); + // If we have a version, find the deployment + const selectedDeployment = version + ? result.deployments.find((d) => d.version === version) + : undefined; + + return typedjson({ ...result, selectedDeployment }); } catch (error) { console.error(error); throw new Response(undefined, { @@ -97,10 +122,23 @@ export default function Page() { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); - const { deployments, currentPage, totalPages } = useTypedLoaderData(); + const { deployments, currentPage, totalPages, selectedDeployment } = + useTypedLoaderData(); const hasDeployments = totalPages > 0; const { deploymentParam } = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + + // If we have a selected deployment from the version param, show it + useEffect(() => { + if (selectedDeployment && !deploymentParam) { + const searchParams = new URLSearchParams(location.search); + searchParams.delete("version"); + searchParams.set("page", currentPage.toString()); + navigate(`${location.pathname}/${selectedDeployment.shortCode}?${searchParams.toString()}`); + } + }, [selectedDeployment, deploymentParam, location.search]); const currentDeployment = deployments.find((d) => d.isCurrent); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index e5e21e5ae7..c001990afb 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -59,6 +59,7 @@ import { formatCurrencyAccurate } from "~/utils/numberFormatter"; import { docsPath, v3BatchPath, + v3DeploymentVersionPath, v3RunDownloadLogsPath, v3RunPath, v3RunSpanPath, @@ -527,7 +528,22 @@ function RunBody({ Version {run.version ? ( - run.version + + {run.version} + + } + content={"Jump to deployment"} + /> ) : ( Never started diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index dc6b392dcf..6996eff9d7 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -395,6 +395,15 @@ export function v3DeploymentPath( 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 v3BillingPath(organization: OrgForPath, message?: string) { return `${organizationPath(organization)}/settings/billing${ message ? `?message=${encodeURIComponent(message)}` : "" From 70be177830bfd9500e3d1dcc20e550d7686fe67e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:43:18 +0100 Subject: [PATCH 3/3] don't add version links for dev (yet) --- .../route.tsx | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index c001990afb..ca37c76d12 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -528,22 +528,26 @@ function RunBody({ Version {run.version ? ( - - {run.version} - - } - content={"Jump to deployment"} - /> + environment.type === "DEVELOPMENT" ? ( + run.version + ) : ( + + {run.version} + + } + content={"Jump to deployment"} + /> + ) ) : ( Never started