diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/releases/[versionId]/FlowDiagram.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/releases/[versionId]/FlowDiagram.tsx index a7f66ed8..270a23e1 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/releases/[versionId]/FlowDiagram.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/releases/[versionId]/FlowDiagram.tsx @@ -79,7 +79,7 @@ export const FlowDiagram: React.FC<{ releaseVersion: release.version, deploymentId: release.deploymentId, environmentId: env.id, - policyType: policy?.releaseSequencing, + policy, label: `${env.name} - release sequencing`, }, }; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/releases/[versionId]/ReleaseSequencingNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/releases/[versionId]/ReleaseSequencingNode.tsx index c1d5aa3c..b7b35b56 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/releases/[versionId]/ReleaseSequencingNode.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/deployments/[deploymentSlug]/releases/[versionId]/ReleaseSequencingNode.tsx @@ -1,3 +1,4 @@ +import type * as SCHEMA from "@ctrlplane/db/schema"; import type { EnvironmentCondition, JobCondition, @@ -6,8 +7,11 @@ import type { } from "@ctrlplane/validators/jobs"; import type { ReleaseCondition } from "@ctrlplane/validators/releases"; import type { NodeProps } from "reactflow"; +import { useEffect, useState } from "react"; import { IconCheck, IconLoader2, IconMinus, IconX } from "@tabler/icons-react"; +import { differenceInMilliseconds } from "date-fns"; import _ from "lodash"; +import prettyMilliseconds from "pretty-ms"; import { Handle, Position } from "reactflow"; import colors from "tailwindcss/colors"; @@ -26,7 +30,7 @@ import { api } from "~/trpc/react"; type ReleaseSequencingNodeProps = NodeProps<{ workspaceId: string; - policyType?: "cancel" | "wait"; + policy?: SCHEMA.EnvironmentPolicy; releaseId: string; releaseVersion: string; deploymentId: string; @@ -52,8 +56,8 @@ const Waiting: React.FC = () => ( ); const Loading: React.FC = () => ( -
- +
+
); @@ -243,6 +247,72 @@ const ReleaseChannelCheck: React.FC = ({ ); }; +const MinReleaseIntervalCheck: React.FC = ({ + policy, + deploymentId, + environmentId, +}) => { + const [timeLeft, setTimeLeft] = useState(null); + + const { data: latestRelease, isLoading } = + api.release.latest.completed.useQuery( + { deploymentId, environmentId }, + { enabled: policy != null }, + ); + + useEffect(() => { + if (!latestRelease || !policy?.minimumReleaseInterval) return; + + const calculateTimeLeft = () => { + const timePassed = differenceInMilliseconds( + new Date(), + latestRelease.createdAt, + ); + return Math.max(0, policy.minimumReleaseInterval - timePassed); + }; + + setTimeLeft(calculateTimeLeft()); + + const interval = setInterval(() => { + const remaining = calculateTimeLeft(); + setTimeLeft(remaining); + + if (remaining <= 0) clearInterval(interval); + }, 1000); + + return () => clearInterval(interval); + }, [latestRelease, policy?.minimumReleaseInterval]); + + if (policy == null) return null; + const { minimumReleaseInterval } = policy; + if (minimumReleaseInterval === 0) return null; + if (isLoading) + return ( +
+ +
+ ); + + if (latestRelease == null || timeLeft === 0) { + return ( +
+ + Minimum release interval passed +
+ ); + } + + return ( +
+ + + Waiting {prettyMilliseconds(timeLeft ?? 0, { compact: true })} till next + release + +
+ ); +}; + export const ReleaseSequencingNode: React.FC = ({ data, }) => { @@ -255,6 +325,7 @@ export const ReleaseSequencingNode: React.FC = ({ > +
+ canUser.perform(Permission.ReleaseGet).on({ + type: "deployment", + id: input.deploymentId, + }), + }) + .query(({ input: { deploymentId, environmentId } }) => + db + .select() + .from(release) + .innerJoin(environment, eq(environment.id, environmentId)) + .where( + and( + eq(release.deploymentId, deploymentId), + exists( + db + .select() + .from(releaseJobTrigger) + .where( + and( + eq(releaseJobTrigger.releaseId, release.id), + eq(releaseJobTrigger.environmentId, environmentId), + ), + ) + .limit(1), + ), + notExists( + db + .select() + .from(releaseJobTrigger) + .innerJoin(job, eq(releaseJobTrigger.jobId, job.id)) + .where( + and( + eq(releaseJobTrigger.releaseId, release.id), + eq(releaseJobTrigger.environmentId, environmentId), + inArray(job.status, [...activeStatus, JobStatus.Pending]), + ), + ) + .limit(1), + ), + ), + ) + .orderBy(desc(release.createdAt)) + .limit(1) + .then(takeFirstOrNull) + .then((r) => r?.release ?? null), + ), + }), + metadataKeys: releaseMetadataKeysRouter, });