Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,8 @@ const DeploymentTable: React.FC<{
})}
>
<LazyDeploymentEnvironmentCell
environment={env}
environmentId={env.id}
deployment={r}
workspace={workspace}
systemSlug={systemSlug}
/>
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,11 @@ const DeploymentTable: React.FC<{
/>
{environments.map((env) => (
<TableCell key={env.id} className="h-[70px] w-[220px]">
<div className="flex h-full w-full justify-center">
<LazyDeploymentEnvironmentCell
environment={env}
deployment={r}
workspace={workspace}
systemSlug={system.slug}
/>
</div>
<LazyDeploymentEnvironmentCell
environmentId={env.id}
deployment={r}
systemSlug={system.slug}
/>
</TableCell>
))}
{directories.map((dir) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,195 +1,81 @@
"use client";

import type * as SCHEMA from "@ctrlplane/db/schema";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
IconAlertCircle,
IconCube,
IconProgressCheck,
} from "@tabler/icons-react";
import { useParams } from "next/navigation";
import { format } from "date-fns";
import { useInView } from "react-intersection-observer";

import { Skeleton } from "@ctrlplane/ui/skeleton";

import { ApprovalDialog } from "~/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/ApprovalDialog";
import { DeploymentVersionDropdownMenu } from "~/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/DeploymentVersionDropdownMenu";
import { urls } from "~/app/urls";
import { api } from "~/trpc/react";
import { Release } from "./ReleaseInfo";
import { StatusIcon } from "./StatusIcon";

const CellSkeleton: React.FC = () => (
<div className="flex h-full w-full items-center gap-2">
<Skeleton className="h-6 w-6 rounded-full" />
<div className="flex flex-col gap-2">
<Skeleton className="h-[16px] w-20 rounded-full" />
<Skeleton className="h-3 w-20 rounded-full" />
</div>
</div>
);

type DeploymentEnvironmentCellProps = {
environment: SCHEMA.Environment;
deployment: SCHEMA.Deployment;
workspace: SCHEMA.Workspace;
environmentId: string;
deployment: { id: string; slug: string };
systemSlug: string;
};

const DeploymentEnvironmentCell: React.FC<DeploymentEnvironmentCellProps> = ({
environment,
environmentId,
deployment,
workspace,
systemSlug,
}) => {
const { data: deploymentVersion, isLoading: isReleaseLoading } =
api.deployment.version.latest.byDeploymentAndEnvironment.useQuery({
deploymentId: deployment.id,
environmentId: environment.id,
});

const { data: statuses, isLoading: isStatusesLoading } =
api.deployment.version.status.byEnvironmentId.useQuery(
{ versionId: deploymentVersion?.id ?? "", environmentId: environment.id },
{ refetchInterval: 2_000, enabled: deploymentVersion != null },
);

const deploy = api.redeploy.useMutation();
const router = useRouter();

const isLoading = isStatusesLoading || isReleaseLoading;

if (isLoading)
return (
<div className="flex h-full w-full items-center gap-2">
<Skeleton className="h-6 w-6 rounded-full" />
<div className="flex flex-col gap-2">
<Skeleton className="h-[16px] w-20 rounded-full" />
<Skeleton className="h-3 w-20 rounded-full" />
</div>
</div>
);
const { workspaceSlug } = useParams<{ workspaceSlug: string }>();

if (deploymentVersion == null)
return (
<p className="text-xs text-muted-foreground/70">No versions released</p>
);
const { data, isLoading } = api.system.table.cell.useQuery({
environmentId,
deploymentId: deployment.id,
});

Comment on lines +37 to 41
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Surface query errors to avoid silent failures

useQuery exposes error / isError; if the request fails the UI stays in a perpetual loading skeleton or falls through to the “No jobs” branch, which can mask backend/permission issues.
Consider adding an explicit error state so users have feedback and you have telemetry.

-  const { data, isLoading } = api.system.table.cell.useQuery({
+  const { data, isLoading, isError, error } = api.system.table.cell.useQuery({
     environmentId,
     deploymentId: deployment.id,
   });
+
+  if (isError) {
+    return (
+      <div className="flex h-full w-full items-center justify-center text-destructive">
+        {error.message ?? "Failed to load"}
+      </div>
+    );
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data, isLoading } = api.system.table.cell.useQuery({
environmentId,
deploymentId: deployment.id,
});
const { data, isLoading, isError, error } = api.system.table.cell.useQuery({
environmentId,
deploymentId: deployment.id,
});
if (isError) {
return (
<div className="flex h-full w-full items-center justify-center text-destructive">
{error.message ?? "Failed to load"}
</div>
);
}

const envResourcesUrl = urls
.workspace(workspace.slug)
const deploymentUrls = urls
.workspace(workspaceSlug)
.system(systemSlug)
.environment(environment.id)
.resources();
.deployment(deployment.slug);

if (isLoading) return <CellSkeleton />;

if (deploymentVersion.resourceCount === 0)
if (data == null)
return (
<Link
href={envResourcesUrl}
className="flex w-full cursor-pointer items-center justify-between gap-2 rounded-md p-2 hover:bg-secondary/50"
target="_blank"
rel="noopener noreferrer"
href={deploymentUrls.releases()}
className="flex h-full w-full items-center justify-center p-2 text-muted-foreground"
>
<div className="flex items-center gap-2">
<div className="rounded-full bg-neutral-400 p-1 dark:text-black">
<IconCube className="h-4 w-4" strokeWidth={2} />
</div>
<div>
<div className="max-w-36 truncate font-semibold">
<span className="whitespace-nowrap">{deploymentVersion.tag}</span>
</div>
<div className="text-xs text-muted-foreground">No resources</div>
</div>
<div className="flex h-full w-full items-center justify-center hover:bg-accent">
No jobs
</div>
</Link>
);

const isAlreadyDeployed = statuses != null && statuses.length > 0;

const hasJobAgent = deployment.jobAgentId != null;

const isPendingApproval =
deploymentVersion.approval != null &&
deploymentVersion.approval.status === "pending";

const showRelease = isAlreadyDeployed && !isPendingApproval;

if (showRelease)
return (
<div className="flex w-full items-center justify-center rounded-md p-2 hover:bg-secondary/50">
<Release
deployment={deployment}
workspaceSlug={workspace.slug}
systemSlug={systemSlug}
deploymentSlug={deployment.slug}
versionId={deploymentVersion.id}
tag={deploymentVersion.tag}
environment={environment}
deployedAt={deploymentVersion.createdAt}
statuses={statuses.map((s) => s.job.status)}
/>
</div>
);

if (!hasJobAgent)
return (
<div className="text-center text-xs text-muted-foreground/70">
No job agent
</div>
);

if (deploymentVersion.approval != null && isPendingApproval)
return (
<ApprovalDialog
policyId={deploymentVersion.approval.policyId}
deploymentVersion={deploymentVersion}
environmentId={environment.id}
>
<div className="flex w-full cursor-pointer items-center justify-between gap-2 rounded-md p-2 hover:bg-secondary/50">
<div className="flex items-center gap-2">
<div className="rounded-full bg-yellow-400 p-1 dark:text-black">
<IconAlertCircle className="h-4 w-4" strokeWidth={2} />
</div>
<div>
<div className="max-w-36 truncate font-semibold">
<span className="whitespace-nowrap">
{deploymentVersion.tag}
</span>
</div>
<div className="text-xs text-muted-foreground">
Approval required
</div>
</div>
</div>

<DeploymentVersionDropdownMenu
deployment={deployment}
environment={environment}
isVersionBeingDeployed={false}
/>
</div>
</ApprovalDialog>
);
const versionUrl = deploymentUrls.release(data.versionId).baseUrl();

return (
<div className="flex w-full items-center justify-center rounded-md p-2 hover:bg-secondary/50">
<div
className="flex w-full cursor-pointer items-center justify-between gap-2 bg-transparent p-0 hover:bg-transparent"
onClick={() =>
deploy
.mutateAsync({
environmentId: environment.id,
deploymentId: deployment.id,
})
.then(() => router.refresh())
}
<div className="flex h-full w-full items-center justify-center p-1">
<Link
href={versionUrl}
className="flex w-full items-center gap-2 rounded-md p-2 hover:bg-accent"
>
<div className="flex items-center gap-2">
<div className="rounded-full bg-blue-400 p-1 dark:text-black">
<IconProgressCheck className="h-4 w-4" strokeWidth={2} />
<StatusIcon statuses={data.statuses} />
<div className="flex flex-col">
<div className="max-w-36 truncate font-semibold">
{data.versionTag}
</div>
<div className="flex flex-col items-start">
<div className="max-w-36 truncate font-semibold text-neutral-200">
<span className="whitespace-nowrap">{deploymentVersion.tag}</span>
</div>
<div className="text-xs text-muted-foreground">Click to deploy</div>
<div className="text-xs text-muted-foreground">
{format(data.versionCreatedAt, "MMM d, hh:mm aa")}
</div>
</div>

<DeploymentVersionDropdownMenu
deployment={deployment}
environment={environment}
isVersionBeingDeployed={false}
/>
</div>
</Link>
</div>
);
};
Expand All @@ -200,16 +86,11 @@ export const LazyDeploymentEnvironmentCell: React.FC<
const { ref, inView } = useInView();

return (
<div className="flex w-full items-center justify-center" ref={ref}>
{!inView && (
<div className="flex h-full w-full items-center gap-2">
<Skeleton className="h-6 w-6 rounded-full" />
<div className="flex flex-col gap-2">
<Skeleton className="h-[16px] w-20 rounded-full" />
<Skeleton className="h-3 w-20 rounded-full" />
</div>
</div>
)}
<div
className="flex h-[70px] w-[220px] items-center justify-center"
ref={ref}
>
{!inView && <CellSkeleton />}
{inView && <DeploymentEnvironmentCell {...props} />}
</div>
);
Expand Down
68 changes: 68 additions & 0 deletions packages/api/src/router/system-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { z } from "zod";

import { and, desc, eq, sql, takeFirstOrNull } from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
import { Permission } from "@ctrlplane/validators/auth";

import { createTRPCRouter, protectedProcedure } from "../trpc";

export const systemTableRouter = createTRPCRouter({
cell: protectedProcedure
.input(
z.object({
environmentId: z.string().uuid(),
deploymentId: z.string().uuid(),
}),
)
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.DeploymentGet)
.on({ type: "deployment", id: input.deploymentId }),
})
.query(({ ctx, input }) => {
const { deploymentId, environmentId } = input;

return ctx.db
.select({
statuses: sql<
schema.JobStatus[]
>`json_agg(distinct ${schema.job.status})`,
versionId: schema.deploymentVersion.id,
versionName: schema.deploymentVersion.name,
versionCreatedAt: schema.deploymentVersion.createdAt,
versionTag: schema.deploymentVersion.tag,
})
.from(schema.job)
.innerJoin(
schema.releaseJob,
eq(schema.releaseJob.jobId, schema.job.id),
)
.innerJoin(
schema.release,
eq(schema.release.id, schema.releaseJob.releaseId),
)
.innerJoin(
schema.versionRelease,
eq(schema.release.versionReleaseId, schema.versionRelease.id),
)
.innerJoin(
schema.deploymentVersion,
eq(schema.versionRelease.versionId, schema.deploymentVersion.id),
)
.innerJoin(
schema.releaseTarget,
eq(schema.versionRelease.releaseTargetId, schema.releaseTarget.id),
)
.where(
and(
eq(schema.releaseTarget.deploymentId, deploymentId),
eq(schema.releaseTarget.environmentId, environmentId),
),
)
.groupBy(schema.deploymentVersion.id)
.orderBy(desc(schema.deploymentVersion.createdAt))
.limit(1)
.then(takeFirstOrNull);
}),
});
2 changes: 2 additions & 0 deletions packages/api/src/router/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {

import { createTRPCRouter, protectedProcedure } from "../trpc";
import { directoryRouter } from "./directory";
import { systemTableRouter } from "./system-table";

export const systemRouter = createTRPCRouter({
list: protectedProcedure
Expand Down Expand Up @@ -315,4 +316,5 @@ export const systemRouter = createTRPCRouter({
}),

directory: directoryRouter,
table: systemTableRouter,
});
Loading