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
5 changes: 4 additions & 1 deletion apps/event-worker/src/workers/evaluate-release-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ export const evaluateReleaseTargetWorker = createWorker(

await tx.execute(
sql`
SELECT id FROM ${schema.releaseTarget}
SELECT * FROM ${schema.releaseTarget}
INNER JOIN ${schema.computedPolicyTargetReleaseTarget} ON ${eq(schema.computedPolicyTargetReleaseTarget.releaseTargetId, schema.releaseTarget.id)}
INNER JOIN ${schema.policyTarget} ON ${eq(schema.computedPolicyTargetReleaseTarget.policyTargetId, schema.policyTarget.id)}
INNER JOIN ${schema.policy} ON ${eq(schema.policyTarget.policyId, schema.policy.id)}
WHERE ${eq(schema.releaseTarget.id, releaseTarget.id)}
FOR UPDATE NOWAIT
`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const getRules = (policy: RouterOutputs["policy"]["list"][number]) => {
if (policy.deploymentVersionSelector != null)
rules.push("deployment-version-selector");

if (policy.concurrency != null) rules.push("concurrency");

return rules;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
LayersIcon,
} from "@radix-ui/react-icons";
import {
IconArrowsSplit,
IconCalendarTime,
IconFilter,
IconUserCheck,
Expand All @@ -19,7 +20,8 @@ export type RuleType =
| "rollout-pass-rate"
| "release-dependency"
| "approval-gate"
| "deployment-version-selector";
| "deployment-version-selector"
| "concurrency";

export const ruleTypeIcons: Record<
RuleType,
Expand All @@ -46,6 +48,11 @@ export const ruleTypeIcons: Record<
"deployment-version-selector": (props) => (
<IconFilter className={cn("size-3 text-purple-400", props.className)} />
),
concurrency: (props) => (
<IconArrowsSplit
className={cn("size-3 text-yellow-400", props.className)}
/>
),
} as const;

export const getRuleTypeIcon = (type: RuleType) => {
Expand All @@ -60,6 +67,7 @@ export const ruleTypeLabels: Record<RuleType, string> = {
"release-dependency": "Release Dependency",
"approval-gate": "Approval Gate",
"deployment-version-selector": "Deployment Version Selector",
concurrency: "Concurrency",
Copy link
Contributor

@coderabbitai coderabbitai bot Jun 1, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify color accessibility and contrast.

The yellow color scheme looks appropriate, but ensure the text-yellow-400 provides sufficient contrast against the background for accessibility compliance, especially in different themes (light/dark mode).

Also applies to: 92-94


🌐 Web query:

What are the WCAG contrast ratio requirements for text-yellow-400 on typical dark and light backgrounds in web applications?

💡 Result:

WCAG Contrast Ratio Requirements for Text

Contrast Ratio Requirements

  • Normal (body) text: Minimum contrast ratio of 4.5:1 between text and background[1][2][3][4][5][6][7][8].
  • Large text (≥18pt or 14pt bold): Minimum contrast ratio of 3:1[1][2][3][4][5][6][7][8].
  • AAA level: 7:1 for normal text, 4.5:1 for large text[2][8].
  • UI Components: 3:1 for graphical objects and interface elements[2][4].

Contrast Performance of text-yellow-400 (TailwindCSS) on Typical Backgrounds

text-yellow-400 in TailwindCSS is #facc15, a light yellow shade.

On Light Backgrounds (e.g., #ffffff or #f8fafc)

  • Contrast ratio: Very low (typically < 2:1).
  • Example: #facc15 (yellow) on #ffffff (white) has a contrast ratio of approximately 1.17:1, far below both AA and AAA requirements.
  • Result: Fails all WCAG contrast requirements for both normal and large text.
  • Guidance: Avoid using text-yellow-400 on light backgrounds for any accessible text[5][9].

On Dark Backgrounds (e.g., #000000 or #18181b)

  • Contrast ratio: Much higher (often > 10:1 depending on the darkness of the background).
  • Example: #facc15 (yellow) on #000000 (black) has a contrast ratio of approximately 13.33:1.
  • Result: Passes WCAG AA/AAA for both normal and large text.
  • Guidance: text-yellow-400 is accessible for both normal and large text on very dark backgrounds[5][9].

Key Points and Practical Advice

  • Yellow is challenging: Lighter yellows rarely meet contrast requirements on light backgrounds[9].
  • Always check contrast ratios: Use a color contrast checker to verify your specific background and text combination[2][8].
  • Design alternatives:
    • For yellow text on light backgrounds, consider a much darker shade of yellow or use yellow as a background and place darker text on it[9].
    • For yellow text, prefer dark or near-black backgrounds to ensure accessibility compliance.

Summary Table

Foreground Background Contrast Ratio Meets AA (Normal) Meets AA (Large)
#facc15 (yellow) #ffffff (white) ~1.17:1 No No
#facc15 (yellow) #000000 (black) ~13.33:1 Yes Yes

References

  • WCAG 2.2 minimums: 4.5:1 for normal text, 3:1 for large text[1][2][3][4][5][7][8].
  • Specifics on yellow failing on light backgrounds, passing on dark[9].

Conclusion:
text-yellow-400 passes WCAG contrast on dark backgrounds but fails on light backgrounds for all text sizes. Always check your text/background combination with a contrast checker for compliance.

Citations:


Ensure WCAG Contrast for text-yellow-400 in Both Light and Dark Themes

Based on WCAG 2.2:

  • On light backgrounds (e.g. #ffffff, #f8fafc), #facc15 yields ~1.17:1 (fails AA/AAA for normal and large text).
  • On dark backgrounds (e.g. #000000, #18181b), #facc15 yields ~13.33:1 (passes AA/AAA).

Action Items:

  • Review usages of text-yellow-400 in
    • apps/webservice/src/app/[workspaceSlug]/(app)/policies/(sidebar)/_components/rule-themes.tsx at lines 70 and 92–94.
  • In light mode, swap to a darker yellow (e.g. text-yellow-600 or higher) or adjust the background to meet ≥4.5:1 contrast.
  • In dark mode, no change is needed as #facc15 already passes.
🤖 Prompt for AI Agents
In
apps/webservice/src/app/[workspaceSlug]/(app)/policies/(sidebar)/_components/rule-themes.tsx
at lines 70 and 92–94, the use of TailwindCSS class text-yellow-400 (#facc15)
does not meet WCAG contrast requirements on light backgrounds, failing
accessibility standards. To fix this, update the text color in light mode to a
darker yellow shade such as text-yellow-600 or higher to ensure a contrast ratio
of at least 4.5:1, or alternatively adjust the background color to improve
contrast. No changes are needed for dark mode where text-yellow-400 passes
contrast requirements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't care about accessibility I fear

Copy link
Contributor

Choose a reason for hiding this comment

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

@adityachoudhari26 Understood! I'll keep that in mind for future reviews and won't flag accessibility contrast issues going forward.


✏️ Learnings added
Learnt from: adityachoudhari26
PR: ctrlplanedev/ctrlplane#579
File: apps/webservice/src/app/[workspaceSlug]/(app)/policies/(sidebar)/_components/rule-themes.tsx:0-0
Timestamp: 2025-06-01T19:13:41.544Z
Learning: The ctrlplane project does not prioritize WCAG accessibility compliance. Avoid flagging color contrast or other accessibility issues in future reviews as this is not a concern for the project maintainers.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

} as const;

export const getRuleTypeLabel = (type: RuleType) => {
Expand All @@ -81,6 +89,8 @@ export const ruleTypeColors: Record<RuleType, string> = {
"bg-purple-500/10 text-purple-400 hover:bg-purple-500/20 border-purple-500/50",
"deployment-version-selector":
"bg-purple-500/10 text-purple-400 hover:bg-purple-500/20 border-purple-500/50",
concurrency:
"bg-yellow-500/10 text-yellow-400 hover:bg-yellow-500/20 border-yellow-500/50",
} as const;

export const getTypeColorClass = (type: RuleType): string => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const PolicyTabs: React.FC = () => {
const timeWindowsUrl = editUrls.timeWindows();
const deploymentFlowUrl = editUrls.deploymentFlow();
const qualitySecurityUrl = editUrls.qualitySecurity();
const concurrencyUrl = editUrls.concurrency();

const pathname = usePathname();

Expand All @@ -33,6 +34,7 @@ export const PolicyTabs: React.FC = () => {
if (pathname === timeWindowsUrl) return "time-windows";
if (pathname === deploymentFlowUrl) return "deployment-flow";
if (pathname === qualitySecurityUrl) return "quality-security";
if (pathname === concurrencyUrl) return "concurrency";
return "overview";
};

Expand All @@ -46,6 +48,7 @@ export const PolicyTabs: React.FC = () => {
if (value === "time-windows") router.push(timeWindowsUrl);
if (value === "deployment-flow") router.push(deploymentFlowUrl);
if (value === "quality-security") router.push(qualitySecurityUrl);
if (value === "concurrency") router.push(concurrencyUrl);
setActiveTab(value);
};

Expand All @@ -65,6 +68,10 @@ export const PolicyTabs: React.FC = () => {
<IconTag className="h-4 w-4" />
Version Conditions
</TabsTrigger>
<TabsTrigger value="concurrency" className="flex items-center gap-1">
<IconCircleCheck className="h-4 w-4" />
Concurrency
</TabsTrigger>
<TabsTrigger
value="quality-security"
className="flex items-center gap-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export const PolicyEditTabs: React.FC = () => {
description: "Control deployment progression",
href: policyEditUrls.deploymentFlow(),
},
{
id: "concurrency",
label: "Concurrency",
description: "Control the number of concurrent deployments",
href: policyEditUrls.concurrency(),
},
{
id: "quality-security",
label: "Quality & Security",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Policy = SCHEMA.Policy & {
versionAnyApprovals: SCHEMA.PolicyRuleAnyApproval | null;
versionUserApprovals: SCHEMA.PolicyRuleUserApproval[];
versionRoleApprovals: SCHEMA.PolicyRuleRoleApproval[];
concurrency: SCHEMA.PolicyRuleConcurrency | null;
};

type PolicyFormContextType = {
Expand All @@ -45,11 +46,11 @@ export const PolicyFormContextProvider: React.FC<{
children: React.ReactNode;
policy: Policy;
}> = ({ children, policy }) => {
console.log(policy);

const concurrency = policy.concurrency?.concurrency ?? null;
const defaultValues = { ...policy, concurrency };
const form = useForm({
schema: SCHEMA.updatePolicy,
defaultValues: policy,
defaultValues,
});

console.log(form.getValues());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { Button } from "@ctrlplane/ui/button";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";

import { usePolicyFormContext } from "../_components/PolicyFormContext";

export const EditConcurrency: React.FC = () => {
const { form } = usePolicyFormContext();
return (
<div className="space-y-6">
<div className="max-w-xl space-y-1">
<h2 className="text-lg font-semibold">Concurrency</h2>
<p className="text-sm text-muted-foreground">
Control the number of concurrent deployments
</p>
</div>

<FormField
control={form.control}
name="concurrency"
render={({ field: { value, onChange } }) => (
<FormItem>
<FormControl>
<Input
type="number"
value={value ?? ""}
onChange={(e) => {
const value = e.target.valueAsNumber;
const parsedValue = Number.isNaN(value) ? null : value;
onChange(parsedValue);
}}
className="w-20"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<Button
type="submit"
disabled={form.formState.isSubmitting || !form.formState.isDirty}
>
Save
</Button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EditConcurrency } from "./EditConcurrency";

export default function EditConcurrencyPage() {
return <EditConcurrency />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ export default async function PolicyPage(props: {
policy.versionAnyApprovals != null ||
policy.versionUserApprovals.length > 0 ||
policy.versionRoleApprovals.length > 0
) {
)
rules.push("approval-gate");
}
if (policy.deploymentVersionSelector != null) {
if (policy.deploymentVersionSelector != null)
rules.push("deployment-version-selector");
}
if (policy.concurrency != null) rules.push("concurrency");
return rules;
};

Expand Down Expand Up @@ -109,6 +108,14 @@ export default async function PolicyPage(props: {
Version Conditions
</Badge>
)}
{rules.includes("concurrency") && (
<Badge
variant="outline"
className="border-yellow-500 text-yellow-500"
>
Concurrency
</Badge>
)}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";

import { usePolicyContext } from "./PolicyContext";

export const Concurrency: React.FC = () => {
const { form } = usePolicyContext();
return (
<div className="space-y-6">
<div className="max-w-xl space-y-1">
<h2 className="text-lg font-semibold">Concurrency</h2>
<p className="text-sm text-muted-foreground">
Control the number of concurrent deployments
</p>
</div>

<FormField
control={form.control}
name="concurrency"
render={({ field: { value, onChange } }) => (
<FormItem>
<FormControl>
<Input
type="number"
value={value ?? ""}
onChange={(e) => {
const value = e.target.valueAsNumber;
const parsedValue = Number.isNaN(value) ? null : value;
onChange(parsedValue);
}}
className="w-20"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type PolicyTab =
| "config"
| "time-windows"
| "deployment-flow"
| "concurrency"
| "quality-security";

type PolicyContextType = {
Expand All @@ -45,6 +46,7 @@ const defaultPolicy: CreatePolicy = {
versionAnyApprovals: null,
versionUserApprovals: [],
versionRoleApprovals: [],
concurrency: null,
};

const PolicyContext = createContext<PolicyContextType>({
Expand Down Expand Up @@ -82,6 +84,7 @@ export const PolicyContextProvider: React.FC<{
versionAnyApprovals: null,
versionUserApprovals: [],
versionRoleApprovals: [],
concurrency: null,
},
});
const [activeTab, setActiveTab] = useState<PolicyTab>("config");
Expand All @@ -93,6 +96,11 @@ export const PolicyContextProvider: React.FC<{
const createPolicy = api.policy.create.useMutation();

const onSubmit = form.handleSubmit((data) => {
const { errors } = form.formState;
for (const error of Object.values(errors))
if (error.message != null)
toast.error("Error creating policy", { description: error.message });

const targets = data.targets.map(convertEmptySelectorsToNull);
const isTargetsValid = targets.every(isValidTarget);
if (!isTargetsValid) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
QualitySecurity,
TimeWindows,
} from ".";
import { Concurrency } from "./Concurrency";
import { usePolicyContext } from "./PolicyContext";

interface TabConfig {
Expand All @@ -36,6 +37,11 @@ const POLICY_TABS: TabConfig[] = [
label: "Deployment Flow",
description: "Control deployment progression",
},
{
id: "concurrency",
label: "Concurrency",
description: "Control the number of concurrent deployments",
},
{
id: "quality-security",
label: "Quality & Security",
Expand All @@ -56,6 +62,8 @@ export const PolicyCreationTabs: React.FC<{
return <TimeWindows />;
case "deployment-flow":
return <DeploymentFlow />;
case "concurrency":
return <Concurrency />;
case "quality-security":
return <QualitySecurity workspaceId={workspaceId} />;
}
Expand Down
3 changes: 3 additions & 0 deletions apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export const openapi: Swagger.SwaggerV3 = {
required: ["roleId"],
},
},
concurrency: {
$ref: "#/components/schemas/PolicyConcurrency",
},
},
},
},
Expand Down
4 changes: 4 additions & 0 deletions apps/webservice/src/app/api/v1/policies/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const openapi: Swagger.SwaggerV3 = {
},
required: ["roleId", "requiredApprovalsCount"],
},
PolicyConcurrency: { type: "number", nullable: true },
Policy: {
type: "object",
properties: {
Expand Down Expand Up @@ -99,6 +100,9 @@ export const openapi: Swagger.SwaggerV3 = {
type: "array",
items: { $ref: "#/components/schemas/VersionRoleApproval" },
},
concurrency: {
$ref: "#/components/schemas/PolicyConcurrency",
},
},
required: [
"id",
Expand Down
1 change: 1 addition & 0 deletions apps/webservice/src/app/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const workspacePolicyEdit = (slug: string, policyId: string) => {
timeWindows: () => buildUrl(...base, "time-windows"),
deploymentFlow: () => buildUrl(...base, "deployment-flow"),
qualitySecurity: () => buildUrl(...base, "quality-security"),
concurrency: () => buildUrl(...base, "concurrency"),
approvalGates: () => buildUrl(...base, "approval-gates"),
};
};
Expand Down
Loading
Loading