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,
});