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)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -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() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{deployment.tasks.map((t) => {
|
||||
const path = v3RunsPath(organization, project, environment, {
|
||||
tasks: [t.slug],
|
||||
});
|
||||
return (
|
||||
<TableRow key={t.slug}>
|
||||
<TableCell>
|
||||
<TableCell to={path}>
|
||||
<div className="inline-flex flex-col gap-0.5">
|
||||
<Paragraph variant="extra-small" className="text-text-dimmed">
|
||||
{t.slug}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{t.filePath}</TableCell>
|
||||
<TableCell to={path}>{t.filePath}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
+44
-6
@@ -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<typeof loader>();
|
||||
const { deployments, currentPage, totalPages, selectedDeployment } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
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);
|
||||
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
+21
-1
@@ -59,6 +59,7 @@ import { formatCurrencyAccurate } from "~/utils/numberFormatter";
|
||||
import {
|
||||
docsPath,
|
||||
v3BatchPath,
|
||||
v3DeploymentVersionPath,
|
||||
v3RunDownloadLogsPath,
|
||||
v3RunPath,
|
||||
v3RunSpanPath,
|
||||
@@ -527,7 +528,26 @@ function RunBody({
|
||||
<Property.Label>Version</Property.Label>
|
||||
<Property.Value>
|
||||
{run.version ? (
|
||||
run.version
|
||||
environment.type === "DEVELOPMENT" ? (
|
||||
run.version
|
||||
) : (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<TextLink
|
||||
to={v3DeploymentVersionPath(
|
||||
organization,
|
||||
project,
|
||||
environment,
|
||||
run.version
|
||||
)}
|
||||
className="group flex flex-wrap items-center gap-x-1 gap-y-0"
|
||||
>
|
||||
{run.version}
|
||||
</TextLink>
|
||||
}
|
||||
content={"Jump to deployment"}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<span className="flex items-center gap-1">
|
||||
<span>Never started</span>
|
||||
|
||||
@@ -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)}` : ""
|
||||
|
||||
Reference in New Issue
Block a user