Skip to content

Commit 5f4c607

Browse files
authored
Add links to and from deployments (#1921)
* link from deployments tasks to filtered runs view * jump to deployment * don't add version links for dev (yet)
1 parent faf5d01 commit 5f4c607

File tree

6 files changed

+129
-11
lines changed

6 files changed

+129
-11
lines changed

apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,52 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
168168
}),
169169
};
170170
}
171+
172+
public async findPageForVersion({
173+
userId,
174+
projectSlug,
175+
organizationSlug,
176+
environmentSlug,
177+
version,
178+
}: {
179+
userId: User["id"];
180+
projectSlug: Project["slug"];
181+
organizationSlug: Organization["slug"];
182+
environmentSlug: string;
183+
version: string;
184+
}) {
185+
const project = await this.#prismaClient.project.findFirstOrThrow({
186+
select: {
187+
id: true,
188+
},
189+
where: {
190+
slug: projectSlug,
191+
organization: {
192+
slug: organizationSlug,
193+
members: {
194+
some: {
195+
userId,
196+
},
197+
},
198+
},
199+
},
200+
});
201+
202+
const environment = await findEnvironmentBySlug(project.id, environmentSlug, userId);
203+
if (!environment) {
204+
throw new Error(`Environment not found`);
205+
}
206+
207+
// Find how many deployments have been made since this version
208+
const deploymentsSinceVersion = await this.#prismaClient.$queryRaw<{ count: BigInt }[]>`
209+
SELECT COUNT(*) as count
210+
FROM ${sqlDatabaseSchema}."WorkerDeployment"
211+
WHERE "projectId" = ${project.id}
212+
AND "environmentId" = ${environment.id}
213+
AND string_to_array(version, '.')::int[] > string_to_array(${version}, '.')::int[]
214+
`;
215+
216+
const count = Number(deploymentsSinceVersion[0].count);
217+
return Math.floor(count / pageSize) + 1;
218+
}
171219
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { useUser } from "~/hooks/useUser";
2828
import { DeploymentPresenter } from "~/presenters/v3/DeploymentPresenter.server";
2929
import { requireUserId } from "~/services/session.server";
3030
import { cn } from "~/utils/cn";
31-
import { v3DeploymentParams, v3DeploymentsPath } from "~/utils/pathBuilder";
31+
import { v3DeploymentParams, v3DeploymentsPath, v3RunsPath } from "~/utils/pathBuilder";
3232
import { capitalizeWord } from "~/utils/string";
3333

3434
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
@@ -231,16 +231,19 @@ export default function Page() {
231231
</TableHeader>
232232
<TableBody>
233233
{deployment.tasks.map((t) => {
234+
const path = v3RunsPath(organization, project, environment, {
235+
tasks: [t.slug],
236+
});
234237
return (
235238
<TableRow key={t.slug}>
236-
<TableCell>
239+
<TableCell to={path}>
237240
<div className="inline-flex flex-col gap-0.5">
238241
<Paragraph variant="extra-small" className="text-text-dimmed">
239242
{t.slug}
240243
</Paragraph>
241244
</div>
242245
</TableCell>
243-
<TableCell>{t.filePath}</TableCell>
246+
<TableCell to={path}>{t.filePath}</TableCell>
244247
</TableRow>
245248
);
246249
})}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { ArrowPathIcon, ArrowUturnLeftIcon, BookOpenIcon } from "@heroicons/react/20/solid";
2-
import { type MetaFunction, Outlet, useLocation, useParams } from "@remix-run/react";
2+
import { type MetaFunction, Outlet, useLocation, useParams, useNavigate } from "@remix-run/react";
33
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
44
import { typedjson, useTypedLoaderData } from "remix-typedjson";
55
import { z } from "zod";
66
import { PromoteIcon } from "~/assets/icons/PromoteIcon";
77
import { DeploymentsNone, DeploymentsNoneDev } from "~/components/BlankStatePanels";
88
import { UserAvatar } from "~/components/UserProfilePhoto";
9-
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
109
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
1110
import { Badge } from "~/components/primitives/Badge";
1211
import { Button, LinkButton } from "~/components/primitives/Buttons";
@@ -53,6 +52,7 @@ import { EnvironmentParamSchema, docsPath, v3DeploymentPath } from "~/utils/path
5352
import { createSearchParams } from "~/utils/searchParams";
5453
import { deploymentIndexingIsRetryable } from "~/v3/deploymentStatus";
5554
import { compareDeploymentVersions } from "~/v3/utils/deploymentVersions";
55+
import { useEffect } from "react";
5656

5757
export const meta: MetaFunction = () => {
5858
return [
@@ -64,17 +64,37 @@ export const meta: MetaFunction = () => {
6464

6565
const SearchParams = z.object({
6666
page: z.coerce.number().optional(),
67+
version: z.string().optional(),
6768
});
6869

6970
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7071
const userId = await requireUserId(request);
7172
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
7273

7374
const searchParams = createSearchParams(request.url, SearchParams);
74-
const page = searchParams.success ? searchParams.params.get("page") ?? 1 : 1;
75+
76+
let page = searchParams.success ? Number(searchParams.params.get("page") ?? 1) : 1;
77+
const version = searchParams.success ? searchParams.params.get("version")?.toString() : undefined;
78+
79+
const presenter = new DeploymentListPresenter();
80+
81+
// If we have a version, find its page
82+
if (version) {
83+
try {
84+
page = await presenter.findPageForVersion({
85+
userId,
86+
organizationSlug,
87+
projectSlug: projectParam,
88+
environmentSlug: envParam,
89+
version,
90+
});
91+
} catch (error) {
92+
console.error("Error finding page for version", error);
93+
// Carry on, we'll just show the selected page
94+
}
95+
}
7596

7697
try {
77-
const presenter = new DeploymentListPresenter();
7898
const result = await presenter.call({
7999
userId,
80100
organizationSlug,
@@ -83,7 +103,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
83103
page,
84104
});
85105

86-
return typedjson(result);
106+
// If we have a version, find the deployment
107+
const selectedDeployment = version
108+
? result.deployments.find((d) => d.version === version)
109+
: undefined;
110+
111+
return typedjson({ ...result, selectedDeployment });
87112
} catch (error) {
88113
console.error(error);
89114
throw new Response(undefined, {
@@ -97,10 +122,23 @@ export default function Page() {
97122
const organization = useOrganization();
98123
const project = useProject();
99124
const environment = useEnvironment();
100-
const { deployments, currentPage, totalPages } = useTypedLoaderData<typeof loader>();
125+
const { deployments, currentPage, totalPages, selectedDeployment } =
126+
useTypedLoaderData<typeof loader>();
101127
const hasDeployments = totalPages > 0;
102128

103129
const { deploymentParam } = useParams();
130+
const location = useLocation();
131+
const navigate = useNavigate();
132+
133+
// If we have a selected deployment from the version param, show it
134+
useEffect(() => {
135+
if (selectedDeployment && !deploymentParam) {
136+
const searchParams = new URLSearchParams(location.search);
137+
searchParams.delete("version");
138+
searchParams.set("page", currentPage.toString());
139+
navigate(`${location.pathname}/${selectedDeployment.shortCode}?${searchParams.toString()}`);
140+
}
141+
}, [selectedDeployment, deploymentParam, location.search]);
104142

105143
const currentDeployment = deployments.find((d) => d.isCurrent);
106144

apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
3333
return new Response("Not found", { status: 404 });
3434
}
3535

36-
// Redirect to the project's runs page
36+
// Redirect to the project's deployments page
3737
return redirect(
3838
`/orgs/${project.organization.slug}/projects/${project.slug}/deployments/${validatedParams.deploymentParam}`
3939
);

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { formatCurrencyAccurate } from "~/utils/numberFormatter";
5959
import {
6060
docsPath,
6161
v3BatchPath,
62+
v3DeploymentVersionPath,
6263
v3RunDownloadLogsPath,
6364
v3RunPath,
6465
v3RunSpanPath,
@@ -527,7 +528,26 @@ function RunBody({
527528
<Property.Label>Version</Property.Label>
528529
<Property.Value>
529530
{run.version ? (
530-
run.version
531+
environment.type === "DEVELOPMENT" ? (
532+
run.version
533+
) : (
534+
<SimpleTooltip
535+
button={
536+
<TextLink
537+
to={v3DeploymentVersionPath(
538+
organization,
539+
project,
540+
environment,
541+
run.version
542+
)}
543+
className="group flex flex-wrap items-center gap-x-1 gap-y-0"
544+
>
545+
{run.version}
546+
</TextLink>
547+
}
548+
content={"Jump to deployment"}
549+
/>
550+
)
531551
) : (
532552
<span className="flex items-center gap-1">
533553
<span>Never started</span>

apps/webapp/app/utils/pathBuilder.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,15 @@ export function v3DeploymentPath(
395395
return `${v3DeploymentsPath(organization, project, environment)}/${deployment.shortCode}${query}`;
396396
}
397397

398+
export function v3DeploymentVersionPath(
399+
organization: OrgForPath,
400+
project: ProjectForPath,
401+
environment: EnvironmentForPath,
402+
version: string
403+
) {
404+
return `${v3DeploymentsPath(organization, project, environment)}?version=${version}`;
405+
}
406+
398407
export function v3BillingPath(organization: OrgForPath, message?: string) {
399408
return `${organizationPath(organization)}/settings/billing${
400409
message ? `?message=${encodeURIComponent(message)}` : ""

0 commit comments

Comments
 (0)