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
@@ -0,0 +1,194 @@
"use client";

import type { TooltipProps } from "recharts";
import type {
NameType,
ValueType,
} from "recharts/types/component/DefaultTooltipContent";
import { useParams } from "next/navigation";
import { formatDistanceToNowStrict, isAfter } from "date-fns";
import prettyMilliseconds from "pretty-ms";
import {
Line,
LineChart,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";

import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ctrlplane/ui/card";

import type { RolloutInfo } from "../_utils/rollout";
import { RolloutTypeToOffsetFunction } from "~/app/[workspaceSlug]/(app)/policies/[policyId]/edit/rollouts/_components/equations";
import { api } from "~/trpc/react";
import { getCurrentRolloutPosition } from "../_utils/rollout";

const PrettyYAxisTick = (props: any) => {
const { payload } = props;
const { value } = payload;

const minutes = Number.parseFloat(value);
const ms = Math.round(minutes * 60_000);

const prettyString = prettyMilliseconds(ms, {
unitCount: 2,
compact: true,
verbose: false,
});

return (
<g>
<text {...props} fontSize={14} dy={5}>
{prettyString}
</text>
</g>
);
};

const getRolloutTimeMessage = (rolloutTime: Date | null) => {
if (rolloutTime == null) return "Rollout not started";

const now = new Date();
const isInFuture = isAfter(rolloutTime, now);

const distanceToNow = formatDistanceToNowStrict(rolloutTime, {
addSuffix: true,
});
if (isInFuture) return `Version rolls out in ${distanceToNow}`;

return `Version rolled out ${distanceToNow}`;
};

const PrettyTooltip = (
props: TooltipProps<ValueType, NameType> & {
rolloutInfoList: RolloutInfo["releaseTargetRolloutInfo"];
},
) => {
const { label: position } = props;

const releaseTarget = props.rolloutInfoList.at(Number(position));

if (releaseTarget == null) return null;

const resourceName = releaseTarget.resource.name;
const rolloutTimeMessage = getRolloutTimeMessage(releaseTarget.rolloutTime);

return (
<div className="rounded-md border bg-neutral-900 p-2 text-sm">
<p>Resource: {resourceName}</p>
<p>Rollout position: {position}</p>
<p>{rolloutTimeMessage}</p>
</div>
);
};

const RolloutCurve: React.FC<{
chartData: { x: number; y: number }[];
currentRolloutPosition: number;
rolloutInfoList: RolloutInfo["releaseTargetRolloutInfo"];
}> = ({ chartData, currentRolloutPosition, rolloutInfoList }) => {
return (
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={chartData}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
<XAxis
dataKey="x"
label={{
value: "Rollout position (0-indexed)",
dy: 20,
}}
tick={{ fontSize: 14 }}
/>
<YAxis
label={{
value: "Time (minutes)",
angle: -90,
dx: -20,
}}
tick={PrettyYAxisTick}
/>
<Line type="monotone" dataKey="y" stroke="#8884d8" dot={false} />
<ReferenceLine
x={currentRolloutPosition}
stroke="#9ca3af"
strokeDasharray="5 5"
strokeWidth={2}
label={{
value: "Current rollout position",
position: "top",
fill: "#9ca3af",
fontSize: 12,
}}
/>
<Tooltip
content={(props) => PrettyTooltip({ ...props, rolloutInfoList })}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
};

export const RolloutCurveChart: React.FC = () => {
const { releaseId: versionId, environmentId } = useParams<{
releaseId: string;
environmentId: string;
}>();

const { data: rolloutInfo } = api.policy.rollout.list.useQuery(
{ environmentId, versionId },
{ refetchInterval: 10_000 },
);

const rolloutPolicy = rolloutInfo?.rolloutPolicy;
const numReleaseTargets = rolloutInfo?.releaseTargetRolloutInfo.length ?? 0;

const rolloutType = rolloutPolicy?.rolloutType ?? "linear";
const timeScaleInterval = rolloutPolicy?.timeScaleInterval ?? 0;
const positionGrowthFactor = rolloutPolicy?.positionGrowthFactor ?? 1;

const offsetFunction = RolloutTypeToOffsetFunction[rolloutType](
positionGrowthFactor,
timeScaleInterval,
numReleaseTargets,
);

const chartData = Array.from({ length: numReleaseTargets }, (_, i) => ({
x: i,
y: offsetFunction(i),
}));

const currentRolloutPosition =
getCurrentRolloutPosition(rolloutInfo?.releaseTargetRolloutInfo ?? []) ?? 0;

console.log(currentRolloutPosition);

return (
<Card className="p-2">
<CardHeader>
<CardTitle>Rollout curve</CardTitle>
<CardDescription>
View the rollout curve for the deployment.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4 p-4">
<RolloutCurve
chartData={chartData}
currentRolloutPosition={Number(currentRolloutPosition)}
rolloutInfoList={rolloutInfo?.releaseTargetRolloutInfo ?? []}
/>
</CardContent>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import { Card, CardContent, CardHeader, CardTitle } from "@ctrlplane/ui/card";

import { RolloutPieChart } from "./RolloutPieChart";

export const RolloutDistributionCard: React.FC<{ deploymentId: string }> = (
props,
) => {
return (
<Card className="p-2">
<CardHeader>
<CardTitle>Version distribution</CardTitle>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4 p-4">
<RolloutPieChart {...props} />
</CardContent>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { useParams } from "next/navigation";

import { Card, CardContent, CardHeader, CardTitle } from "@ctrlplane/ui/card";

import { api } from "~/trpc/react";
import { getCurrentRolloutPosition } from "../_utils/rollout";

export const RolloutPercentCard: React.FC = () => {
const { releaseId: versionId, environmentId } = useParams<{
releaseId: string;
environmentId: string;
}>();

const { data: rolloutInfo } = api.policy.rollout.list.useQuery(
{ environmentId, versionId },
{ refetchInterval: 10_000 },
);

const currentRolloutPosition =
getCurrentRolloutPosition(rolloutInfo?.releaseTargetRolloutInfo ?? []) ?? 0;

const maxPosition = rolloutInfo?.releaseTargetRolloutInfo.length ?? 0;

const percentComplete =
maxPosition === 0
? 0
: Math.round((currentRolloutPosition / maxPosition) * 100);

return (
<Card className="flex h-full flex-col gap-16 p-4">
<CardHeader>
<CardTitle>Rollout progress</CardTitle>
</CardHeader>
<CardContent className="flex h-full flex-col items-center justify-between">
<span className="text-5xl font-bold">{percentComplete}%</span>
<div className="w-full">
<div className="mb-2 flex justify-between text-sm text-muted-foreground">
<span>Progress</span>
<span>
{currentRolloutPosition} / {maxPosition}
</span>
</div>
<div className="h-2 w-full rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary transition-all duration-300"
style={{ width: `${percentComplete}%` }}
/>
</div>
</div>
</CardContent>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { useParams } from "next/navigation";
import { Cell, Pie, PieChart } from "recharts";

import { ChartContainer, ChartTooltip } from "@ctrlplane/ui/chart";

import { COLORS } from "../_utils/colors";
import { useChartData } from "../_utils/useChartData";

export const RolloutPieChart: React.FC<{ deploymentId: string }> = ({
deploymentId,
}) => {
const { environmentId } = useParams<{ environmentId: string }>();
const versionCounts = useChartData(deploymentId, environmentId);

return (
<ChartContainer config={{}} className="h-full w-full flex-grow">
<PieChart>
<ChartTooltip
content={({ active, payload }) => {
if (active && payload?.length) {
return (
<div className="flex items-center gap-4 rounded-lg border bg-background p-2 text-xs shadow-sm">
<div className="font-semibold">{payload[0]?.name}</div>
<div className="text-sm text-neutral-400">
{payload[0]?.value}
</div>
</div>
);
}
}}
/>
<Pie data={versionCounts} dataKey="count" nameKey="versionTag">
{versionCounts.map((_, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
</PieChart>
</ChartContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import colors from "tailwindcss/colors";

export const COLORS = [
colors.blue[500],
colors.green[500],
colors.yellow[500],
colors.red[500],
colors.purple[500],
colors.amber[500],
colors.cyan[500],
colors.fuchsia[500],
colors.lime[500],
colors.orange[500],
colors.pink[500],
colors.teal[500],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { RouterOutputs } from "@ctrlplane/api";
import { isAfter } from "date-fns";

export type RolloutInfo = RouterOutputs["policy"]["rollout"]["list"];

export const getCurrentRolloutPosition = (
rolloutInfoList: RolloutInfo["releaseTargetRolloutInfo"],
) => {
const now = new Date();
const next = rolloutInfoList.find(
(info) => info.rolloutTime != null && isAfter(info.rolloutTime, now),
);

if (next == null) return null;

return next.rolloutPosition;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { api } from "~/trpc/react";

export const useChartData = (deploymentId: string, environmentId: string) => {
const { data } =
api.dashboard.widget.data.deploymentVersionDistribution.useQuery(
{ deploymentId, environmentIds: [environmentId] },
{ refetchInterval: 10_000 },
);

return data ?? [];
};
Loading
Loading