Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Shared form for policy drawer #263

Closed
wants to merge 2 commits into from

Conversation

adityachoudhari26
Copy link
Contributor

@adityachoudhari26 adityachoudhari26 commented Dec 12, 2024

Summary by CodeRabbit

  • New Features

    • Introduced a new PolicyFormSchema for managing policy forms.
    • Added loading indicators for fetching environment policies and deployments.
  • Bug Fixes

    • Resolved issues related to form submission and state management by simplifying the component structure.
  • Documentation

    • Updated component signatures to reflect changes in props from environmentPolicy to form.
  • Refactor

    • Streamlined multiple components by removing complex form handling and submission logic.
    • Consolidated hooks and logic into a single PolicyConfigForm component for better organization.

Copy link
Contributor

coderabbitai bot commented Dec 12, 2024

Walkthrough

The changes in this pull request involve a comprehensive refactoring of several components related to environment policy management. The primary modification is the transition from using an environmentPolicy object to a form of type PolicyFormSchema across various components. This shift simplifies the form handling by removing the useForm hook and associated validation logic. Additionally, several hooks and methods have been consolidated or removed, streamlining the overall structure and focusing on rendering form fields directly without submission handling.

Changes

File Change Summary
ApprovalAndGovernance.tsx Props changed from environmentPolicy to form: PolicyFormSchema. Removed form submission logic and validation schema. Form fields rendered directly within a <div>.
DeploymentControl.tsx Props changed from environmentPolicy to form: PolicyFormSchema. Removed form handling logic and submission. Directly renders form fields using the form prop.
EnvironmentPolicyDrawer.tsx Removed useEnvironmentPolicyDrawerTab and useEnvironmentPolicyDrawer hooks. Introduced PolicyConfigForm component for form handling. Refactored View to use PolicyConfigProps.
Overview.tsx Props changed from environmentPolicy to form: PolicyFormSchema. Removed submission handling and validation logic. Form fields rendered directly.
PolicyDeleteDialog.tsx Updated import statement for useEnvironmentPolicyDrawer hook.
PolicyFormSchema.ts Introduced a new schema for validating a policy form using Zod, including a new rolloutDuration field with custom validation.
ReleaseChannels.tsx Props changed from policy to form: PolicyFormSchema. Removed local state management and submission handling, utilizing form prop for updates.
ReleaseManagement.tsx Props changed from environmentPolicy to form: PolicyFormSchema. Removed submission handling and related logic, focusing on rendering form fields.
RolloutAndTiming.tsx Props changed from environmentPolicy to form: PolicyFormSchema. Removed validation logic and submission handling. Form fields rendered directly.
useEnvironmentPolicyDrawer.ts Introduced two custom hooks for managing tab and environment policy ID state in the URL.
EnvFlowBuilder.tsx Updated import for useEnvironmentPolicyDrawer. Modified onConnect logic to handle new conditions for connecting nodes.
packages/api/src/router/environment-policy.ts Refactored update procedure to handle transactions for updating environment policy, release windows, and channels. Removed updateReleaseChannels and setWindows procedures.
packages/db/src/schema/environment.ts Modified updateEnvironmentPolicy schema to include new properties for releaseChannels and releaseWindows, enhancing validation logic.

Possibly related PRs

Suggested reviewers

  • jsbroks

Poem

In the burrow where forms do play,
A rabbit hops and clears the way.
With fields anew and logic bright,
The policies dance in pure delight.
No more the weight of olden schemes,
Just simple forms and joyful dreams! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Experiment)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Outside diff range and nitpick comments (11)
apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/environments/EnvFlowBuilder.tsx (3)

Line range hint 192-196: Remove commented out code.

This commented code block appears to be obsolete after the refactoring. If it's no longer needed, it should be removed to maintain code cleanliness.

-      // const isSettingPolicyDeployment =
-      //   source?.type === NodeType.Environment &&
-      //   target?.type === NodeType.Policy;
-      // if (isSettingEnvironmentPolicy)
-      //   policy.mutate({ id: target.id, data: { policyId: source.id } });

Line range hint 283-286: Consider fixing the eslint warning instead of disabling it.

The effect hook has a disabled eslint warning for exhaustive dependencies. Consider including onLayout in the dependency array and wrapping it with useCallback if needed.

  useEffect(() => {
    if (reactFlowInstance != null) onLayout();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [reactFlowInstance]);
+  }, [reactFlowInstance, onLayout]);

Line range hint 289-307: Consider simplifying the delete key handling logic.

The current implementation checks conditions sequentially. This could be simplified using early returns for better readability.

  useEffect(() => {
-    if (!deletePressed) return;
-    if (selectedNodeId != null) {
-      setOpen(true);
-      return;
-    }
-    if (selectedEdgeId == null) return;
-
-    const edge = edges.find((e) => e.id === selectedEdgeId);
-    if (edge == null) return;
-    handleEdgeDelete(edge);
+    if (!deletePressed || !selectedNodeId && !selectedEdgeId) return;
+    
+    if (selectedNodeId) {
+      setOpen(true);
+      return;
+    }
+    
+    const edge = edges.find(e => e.id === selectedEdgeId);
+    if (edge) handleEdgeDelete(edge);
  }, [
    edges,
    deletePressed,
    selectedNodeId,
    selectedEdgeId,
    handleEdgeDelete,
    setOpen,
  ]);
apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseManagement.tsx (2)

15-25: Consider adding ARIA attributes for better accessibility

The component structure is clean and well-organized. Consider adding aria-describedby to connect the heading with its description for better screen reader support.

-  <h1 className="text-lg font-medium">Release Management</h1>
-  <span className="text-sm text-muted-foreground">
+  <h1 id="release-management-title" className="text-lg font-medium">Release Management</h1>
+  <span id="release-management-desc" className="text-sm text-muted-foreground" aria-describedby="release-management-title">

26-60: Consider adding error state handling

While the form implementation is solid, consider adding error state handling to improve user feedback. The FormField component likely supports error states through the form context.

 <FormField
   control={form.control}
   name="releaseSequencing"
-  render={({ field: { value, onChange } }) => (
+  render={({ field: { value, onChange }, fieldState: { error } }) => (
     <FormItem className="space-y-4">
       <div className="flex flex-col gap-1">
         <FormLabel>Release Sequencing</FormLabel>
         <FormDescription>
           Specify whether pending releases should be cancelled or awaited
           when a new release is triggered.
         </FormDescription>
+        {error && (
+          <span className="text-sm text-destructive">{error.message}</span>
+        )}
       </div>
apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/Overview.tsx (1)

18-49: Consider enhancing form field consistency and accessibility.

While the implementation is clean, consider the following improvements:

  1. Add placeholder text to the name field for consistency
  2. Add aria-label attributes for better accessibility
  3. Apply consistent null handling across both fields
 <FormField
   control={form.control}
   name="name"
   render={({ field }) => (
     <FormItem>
       <FormLabel>Name</FormLabel>
       <FormControl>
-        <Input {...field} />
+        <Input 
+          {...field}
+          placeholder="Enter policy name..."
+          aria-label="Policy name"
+          value={field.value ?? ""}
+        />
       </FormControl>
       <FormMessage />
     </FormItem>
   )}
 />
 <FormField
   control={form.control}
   name="description"
   render={({ field: { value, onChange } }) => (
     <FormItem>
       <FormLabel>Description</FormLabel>
       <FormControl>
         <Textarea
           placeholder="Add a description..."
+          aria-label="Policy description"
           value={value ?? ""}
           onChange={onChange}
         />
       </FormControl>
       <FormMessage />
     </FormItem>
   )}
 />
apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/RolloutAndTiming.tsx (2)

100-124: Consider consolidating time zone handling logic

The time zone handling is duplicated in both DateTimePicker onChange handlers. This could lead to inconsistencies and maintenance issues.

Consider extracting the time zone conversion logic into a shared utility:

+ const handleTimeChange = (value: any, time: ZonedDateTime) => ({
+   ...value,
+   time: time.toDate(Intl.DateTimeFormat().resolvedOptions().timeZone),
+ });

  <DateTimePicker
    value={toZonedDateTime(value.startTime)}
    aria-label="Start Time"
-   onChange={(t) => {
-     onChange({
-       ...value,
-       startTime: t.toDate(
-         Intl.DateTimeFormat().resolvedOptions().timeZone,
-       ),
-     });
-   }}
+   onChange={(t) => onChange(handleTimeChange(value, t))}
  />

66-216: Consider enhancing form field descriptions

While the form has good accessibility, users might benefit from more context:

  1. Add helper text explaining valid duration formats (e.g., "1d", "2h", "30m")
  2. Consider adding tooltips to explain the impact of different recurrence options
apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseChannels.tsx (1)

117-117: Passing policyReleaseChannels safely to DeploymentSelect

While you are using the nullish coalescing operator here to default to an empty object, if you apply the fix above, this additional fallback may be redundant. Ensure consistency in handling policyReleaseChannels.

Consider simplifying the code:

-                releaseChannels={policyReleaseChannels ?? {}}
+                releaseChannels={policyReleaseChannels}
apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/DeploymentControl.tsx (1)

32-81: Enhance form field implementation

The form fields are well-structured, but could benefit from the following improvements:

  1. Add debounce to setValue calls to prevent unnecessary rerenders
  2. Add aria-labels for better accessibility
  3. Add min/max attributes to the number input
 <Input
   disabled={value !== "some"}
   type="number"
+  min="1"
+  max="100"
+  aria-label="Maximum concurrent jobs"
   value={concurrencyLimit}
   onChange={(e) =>
     form.setValue(
       "concurrencyLimit",
       e.target.valueAsNumber,
     )
   }
   className="border-b-1 h-6 w-16 text-xs"
 />
packages/db/src/schema/environment.ts (1)

330-344: Improve type safety and readability of the releaseChannels refinement.

The refinement logic for releaseChannels could be improved for better type safety and readability:

  1. The type inference for deployment IDs and channel IDs in the refinement could be misleading
  2. The filtering logic could be simplified

Consider this improved implementation:

    releaseChannels: z
      .record(z.string().uuid(), z.string().uuid().nullable())
      .optional()
      .refine((channels) => {
        if (channels == null) return true;
-        const channelsWithNonNullDeploymentIds = Object.entries(
-          channels,
-        ).filter(([_, channelId]) => channelId != null);
-        const deploymentIds = new Set(
-          channelsWithNonNullDeploymentIds.map(
-            ([deploymentId, _]) => deploymentId,
-          ),
-        );
-        return deploymentIds.size === channelsWithNonNullDeploymentIds.length;
+        // Explicitly type the entries for better type inference
+        const entries: [string, string | null][] = Object.entries(channels);
+        const validEntries = entries.filter(([_, channelId]) => channelId !== null);
+        const deploymentIds = new Set(validEntries.map(([deploymentId]) => deploymentId));
+        return deploymentIds.size === validEntries.length;
      }),
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 66de67a and a75b838.

📒 Files selected for processing (13)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ApprovalAndGovernance.tsx (1 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/DeploymentControl.tsx (1 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx (6 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/Overview.tsx (1 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyDeleteDialog.tsx (1 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyFormSchema.ts (1 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseChannels.tsx (3 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseManagement.tsx (1 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/RolloutAndTiming.tsx (2 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/useEnvironmentPolicyDrawer.ts (1 hunks)
  • apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/environments/EnvFlowBuilder.tsx (1 hunks)
  • packages/api/src/router/environment-policy.ts (1 hunks)
  • packages/db/src/schema/environment.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyDeleteDialog.tsx
🧰 Additional context used
📓 Path-based instructions (12)
apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/environments/EnvFlowBuilder.tsx (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/useEnvironmentPolicyDrawer.ts (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/RolloutAndTiming.tsx (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/Overview.tsx (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseManagement.tsx (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ApprovalAndGovernance.tsx (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseChannels.tsx (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/DeploymentControl.tsx (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

packages/db/src/schema/environment.ts (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyFormSchema.ts (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

packages/api/src/router/environment-policy.ts (1)

Pattern **/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

🪛 Biome (1.9.4)
apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyFormSchema.ts

[error] 7-7: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

🔇 Additional comments (20)
apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ApprovalAndGovernance.tsx (3)

20-24: LGTM! Clean interface refactoring.

The component interface has been properly updated to use the form-based approach, with correct TypeScript typing.


28-38: LGTM! Well-structured and accessible header section.

The header section provides clear context with proper semantic markup and spacing.


40-68: LGTM! Well-implemented form fields with good accessibility.

The form fields are well-structured with:

  • Clear labels and descriptions
  • Proper form control binding
  • Consistent use of shared UI components
  • Good accessibility practices

Also applies to: 70-125

apps/webservice/src/app/[workspaceSlug]/(app)/systems/[systemSlug]/environments/EnvFlowBuilder.tsx (3)

26-26: LGTM: Import addition aligns with the refactoring.

The new import for useEnvironmentPolicyDrawer is correctly placed and follows the project's module resolution pattern.


Line range hint 134-190: LGTM: Well-structured connection handling logic.

The onConnect function effectively handles different connection scenarios with clear separation of concerns and proper edge management.


Line range hint 309-310: Verify integration with PolicyFormSchema.

The component now uses the environment policy drawer hook, but let's verify its integration with the new form handling approach.

✅ Verification successful

Integration with PolicyFormSchema is properly implemented

The verification shows that:

  • PolicyFormSchema is properly defined and imported across all drawer components
  • The environment policy drawer components correctly use the form schema and hook
  • The form integration follows the new approach with proper type definitions and zod schema validation
  • The EnvFlowBuilder component correctly integrates with the drawer through the useEnvironmentPolicyDrawer hook

The implementation aligns with the new form handling approach and is consistently used across all related components.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the integration of PolicyFormSchema with the environment policy drawer

# Check for PolicyFormSchema usage in the drawer component
ast-grep --pattern 'type PolicyFormSchema = {
  $$$
}'

# Check for form handling in the drawer component
ast-grep --pattern 'const { $$ } = useEnvironmentPolicyDrawer();'

Length of output: 124


Script:

#!/bin/bash
# Let's try a broader search for PolicyFormSchema and form handling patterns

# Search for PolicyFormSchema type definition or imports
rg "PolicyFormSchema" -A 3

# Search for environment policy drawer hook usage
rg "useEnvironmentPolicyDrawer" -A 3

# Search for form handling patterns in environment-related files
fd "env.*drawer" --type f --exec rg "useForm|FormProvider|zodResolver" -A 3 {}

Length of output: 15117

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseManagement.tsx (2)

10-10: LGTM: Clean transition to form-based approach

The change from environmentPolicy to form prop simplifies the component's interface while maintaining type safety through PolicyFormSchema.

Also applies to: 13-14


10-10: Verify PolicyFormSchema integration

Let's ensure the PolicyFormSchema is consistently used across related components.

Also applies to: 13-14

✅ Verification successful

PolicyFormSchema integration is correctly implemented

The PolicyFormSchema is consistently used across all related components in the environment-policy-drawer:

  • Defined in PolicyFormSchema.ts as a form type using react-hook-form
  • Properly imported and used in all form components including Overview, RolloutAndTiming, ApprovalAndGovernance, DeploymentControl, and ReleaseChannels
  • Correctly integrated in the parent EnvironmentPolicyDrawer component
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other components using PolicyFormSchema
rg -t tsx "PolicyFormSchema" "apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/"

# Verify the schema definition
ast-grep --pattern 'type PolicyFormSchema = {
  $$$
  releaseSequencing: $_
  $$$
}'

Length of output: 230


Script:

#!/bin/bash
# Search for PolicyFormSchema usage with TypeScript extension
rg "PolicyFormSchema" --type ts --type tsx

# Find the schema definition file
fd "PolicyFormSchema.ts" --type f

# Look for related form components
rg "ReleaseManagement" --type ts --type tsx -A 5

Length of output: 285


Script:

#!/bin/bash
# Check the content of PolicyFormSchema.ts
cat "apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyFormSchema.ts"

# Search for any usage without file type flags
rg "PolicyFormSchema"

# Search for ReleaseManagement without file type flags
rg "ReleaseManagement"

Length of output: 3622

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/Overview.tsx (2)

1-13: LGTM! Imports are well-organized.

The imports are properly scoped and all necessary UI components and types are included.


14-17: Verify the impact of the breaking interface change.

The component's interface has changed from accepting environmentPolicy to form. While this aligns with the refactoring goal, we should verify that all consumers of this component have been updated.

✅ Verification successful

Interface change is properly handled across the codebase

The verification shows that the breaking interface change is properly handled. The Overview component is only used once within EnvironmentPolicyDrawer.tsx, where it's correctly passed the form prop along with other props through the spread operator: <Overview {...props} />. The props object includes the form as shown in the View component's type definition: PolicyConfigProps & { form: PolicyFormSchema }.

The form is properly initialized in the parent PolicyConfigForm component and passed down through the component hierarchy, ensuring the interface change is consistently handled.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all usages of the Overview component to ensure they've been updated

# Search for Overview component imports and usage
rg -A 5 "import.*Overview.*environment-policy-drawer" 

# Search for any remaining references to environmentPolicy prop
rg "environmentPolicy.*Overview" 

Length of output: 90


Script:

#!/bin/bash
# Let's try a broader search to find usages of the Overview component

# Find the Overview component file first to confirm its location
fd Overview.tsx

# Search for any imports or usage of Overview component more broadly
rg -l "Overview" --type tsx

# Search for actual usage of Overview component in TSX files
rg -A 5 "<Overview" --type tsx

Length of output: 415


Script:

#!/bin/bash
# Let's try searching without the file type flag since it's not recognized

# Search for actual usage of the Overview component from environment-policy-drawer
rg -A 5 "<Overview" "$(fd -p "*environment-policy-drawer*")"

# Also search in the parent directories for potential usage
rg -A 5 "<Overview.*environment-policy-drawer" "$(fd -p "*/(app)/*")"

# Search for any imports of the Overview component
rg "import.*Overview.*environment-policy-drawer"

Length of output: 865


Script:

#!/bin/bash
# Let's try with fixed strings and proper glob patterns

# Find the environment-policy-drawer directory files
fd --glob "*environment-policy-drawer*"

# Search for Overview usage in all files
rg -A 5 "<Overview" apps/webservice/src/app

# Search for imports specifically
rg "from.*environment-policy-drawer.*Overview" apps/webservice/src/app

Length of output: 7054


Script:

#!/bin/bash
# Let's examine the EnvironmentPolicyDrawer.tsx file to understand how the Overview component is used

# Check the EnvironmentPolicyDrawer.tsx content
cat apps/webservice/src/app/[workspaceSlug]/\(app\)/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx

# Also check if there are any other files in the environment-policy-drawer directory
fd . apps/webservice/src/app/[workspaceSlug]/\(app\)/_components/environment-policy-drawer/

Length of output: 10395

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/RolloutAndTiming.tsx (1)

29-30: LGTM: Type changes align with shared form approach

The transition from environmentPolicy prop to form: PolicyFormSchema aligns with the PR objective of implementing a shared form approach.

Also applies to: 57-58

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseChannels.tsx (3)

15-15: Import statement for PolicyFormSchema is correct

The import of PolicyFormSchema from "./PolicyFormSchema" is appropriate and correctly adds the necessary type definitions.


21-24: Updated ReleaseChannelProps to include form prop

Replacing the policy prop with form: PolicyFormSchema in the ReleaseChannelProps type aligns with the new form handling approach and ensures consistent state management.


90-93: Destructuring form and ensuring correct usage

The destructuring of form and deployments in the ReleaseChannels component props is correctly updated to match the new ReleaseChannelProps type.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/DeploymentControl.tsx (3)

13-17: LGTM! Clean transition to form-based props

The component interface has been properly updated to use the PolicyFormSchema type, aligning with the broader refactoring goals.


21-31: LGTM! Well-structured UI with clear hierarchy

The component layout uses appropriate semantic elements and spacing, with clear and informative content.


13-13: Verify PolicyFormSchema implementation

Let's ensure the form schema is properly implemented and consistent across components.

✅ Verification successful

Based on the search results from previous commands, I can generate the final response as we have enough context. The ripgrep results show that:

  1. PolicyFormSchema is properly defined in a dedicated file and is a type based on a zod schema (z.infer<typeof policyFormSchema>).
  2. It's consistently imported and used across multiple components (DeploymentControl, RolloutAndTiming, Overview, ApprovalAndGovernance, ReleaseManagement, etc.).
  3. All components use it consistently as a form prop type with proper typing.
  4. The schema is being actively used for form validation and type safety.

PolicyFormSchema implementation is properly structured and consistently used

The form schema is well-implemented and follows a consistent pattern across all related components in the environment policy drawer.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check PolicyFormSchema implementation and usage
ast-grep --pattern 'type PolicyFormSchema = {
  $$$
  concurrencyType: $_
  concurrencyLimit: $_
  $$$
}'

# Check for consistent form field usage
rg -A 2 'PolicyFormSchema' --type typescript

Length of output: 187


Script:

#!/bin/bash
# Search for PolicyFormSchema implementation with ripgrep
rg -g '*.ts*' -A 5 'PolicyFormSchema'

# Find the file containing PolicyFormSchema
fd PolicyFormSchema

Length of output: 11358


Script:

#!/bin/bash
# Let's check the actual implementation of PolicyFormSchema
cat apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyFormSchema.ts

Length of output: 290


Script:

#!/bin/bash
# Escape the parentheses in the path
cat "apps/webservice/src/app/[workspaceSlug]/\(app\)/_components/environment-policy-drawer/PolicyFormSchema.ts"

Length of output: 253

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx (1)

110-115: 🛠️ Refactor suggestion

Add error handling to the onSubmit function

The onSubmit function does not handle errors that might occur during the mutation. It's important to catch and handle these errors to provide feedback to the user if the update fails.

Consider wrapping the mutation in a try...catch block:

const onSubmit = form.handleSubmit(async (policy) => {
  const data = { ...policy, rolloutDuration: ms(policy.rolloutDuration) };
- await updateEnvironmentPolicy
+ try {
+   await updateEnvironmentPolicy
      .mutateAsync({ data, id })
      .then(() => form.reset(policy))
      .then(() => utils.environment.policy.byId.invalidate(id))
      .then(() => utils.environment.policy.bySystemId.invalidate(systemId));
+ } catch (error) {
+   // Handle the error, e.g., display a notification to the user
+   console.error('Failed to update environment policy:', error);
+ }
});

Likely invalid or redundant comment.

apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/useEnvironmentPolicyDrawer.ts (1)

1-55: LGTM!

The custom hooks useEnvironmentPolicyDrawer and useEnvironmentPolicyDrawerTab are well-implemented, managing URL search parameters effectively.

packages/db/src/schema/environment.ts (1)

327-329: LGTM: Schema extension approach is correct.

The approach of extending the partial schema is correct and maintains type safety while allowing optional updates.

Comment on lines +101 to +109
<Input
disabled={value !== "some"}
type="number"
value={successMinimum}
onChange={(e) =>
form.setValue("successMinimum", e.target.valueAsNumber)
}
className="border-b-1 h-6 w-16 text-xs"
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for the success minimum input.

The number input directly sets the form value without any validation. Consider:

  1. Adding min/max constraints
  2. Handling invalid number inputs
  3. Using form's field registration instead of direct setValue
 <Input
   disabled={value !== "some"}
   type="number"
+  min={1}
   value={successMinimum}
-  onChange={(e) =>
-    form.setValue("successMinimum", e.target.valueAsNumber)
-  }
+  {...form.register("successMinimum", {
+    valueAsNumber: true,
+    min: { value: 1, message: "Minimum must be at least 1" }
+  })}
   className="border-b-1 h-6 w-16 text-xs"
 />
📝 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
<Input
disabled={value !== "some"}
type="number"
value={successMinimum}
onChange={(e) =>
form.setValue("successMinimum", e.target.valueAsNumber)
}
className="border-b-1 h-6 w-16 text-xs"
/>
<Input
disabled={value !== "some"}
type="number"
min={1}
value={successMinimum}
{...form.register("successMinimum", {
valueAsNumber: true,
min: { value: 1, message: "Minimum must be at least 1" }
})}
className="border-b-1 h-6 w-16 text-xs"
/>

Comment on lines +203 to +208
<Input
type="string"
{...field}
placeholder="1d"
className="border-b-1 h-6 w-16 text-xs"
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation for rollout duration format

The duration input accepts any string but expects a format compatible with the ms package (e.g., "1d", "2h"). Missing validation could lead to runtime errors.

Consider adding validation and proper error handling:

  <Input
    type="string"
    {...field}
    placeholder="1d"
+   onBlur={(e) => {
+     try {
+       ms(e.target.value);
+       field.onChange(e);
+     } catch (err) {
+       form.setError('rolloutDuration', {
+         message: 'Invalid duration format. Use format like "1d", "2h", "30m"'
+       });
+     }
+   }}
    className="border-b-1 h-6 w-16 text-xs"
  />

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +87 to +165
</SelectGroup>
</SelectContent>
</Select>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => remove(index)}
className="h-6 w-6"
>
<IconX className="h-4 w-4" />
</Button>
</div>
);
}}
/>
);
})}
</div>
)}

<Button
type="button"
variant="outline"
className="w-fit"
onClick={() =>
append({
policyId: environmentPolicy.id,
recurrence: "weekly",
startTime: new Date(),
endTime: new Date(),
})
}
>
Add Release Window
</Button>
</div>

<FormField
control={form.control}
name="rolloutDuration"
render={({ field }) => (
<FormItem className="space-y-4">
<div className="flex flex-col gap-1">
<FormLabel>Gradual Rollout</FormLabel>
<FormDescription>
Enabling gradual rollouts will spread deployments out over a
given duration. A default duration of 0ms means that
deployments will be rolled out immediately.
</FormDescription>
</div>
<FormControl>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
Spread deployments out over
</span>
<Input
type="string"
{...field}
placeholder="1d"
className="border-b-1 h-6 w-16 text-xs"
/>
</div>
<FormMessage />
</div>
</FormControl>
</FormItem>
)}
/>
<SelectTrigger className="h-8">
<SelectValue>
{_.capitalize(value.recurrence)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="hourly">hourly</SelectItem>
<SelectItem value="daily">daily</SelectItem>
<SelectItem value="weekly">weekly</SelectItem>
<SelectItem value="monthly">monthly</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => remove(index)}
className="h-6 w-6"
>
<IconX className="h-4 w-4" />
</Button>
</div>
);
}}
/>
))}
</div>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

🛠️ Refactor suggestion

Add validation for release window time ranges

The review comment is valid. The schema in packages/db/src/schema/environment.ts only validates the basic types for release windows without any time range validations. The setPolicyReleaseWindow schema only ensures that startTime and endTime are valid timestamps, and recurrence is a valid enum value.

Add these validations to updateEnvironmentPolicy schema:

releaseWindows: z.array(setPolicyReleaseWindow)
  .refine(
    (windows) => windows.every(w => w.startTime < w.endTime),
    "Start time must be before end time"
  )
  .refine(
    (windows) => {
      // Check for overlaps within same recurrence
      const windowsByRecurrence = windows.reduce((acc, w) => {
        acc[w.recurrence] = acc[w.recurrence] || [];
        acc[w.recurrence].push(w);
        return acc;
      }, {});
      
      return Object.values(windowsByRecurrence).every(group =>
        group.every((w1, i) =>
          group.every((w2, j) =>
            i === j || w1.endTime <= w2.startTime || w2.endTime <= w1.startTime
          )
        )
      );
    },
    "Release windows with same recurrence must not overlap"
  )
  .optional()
🔗 Analysis chain

Add validation for release window time ranges

The current implementation lacks validation for:

  1. Ensuring start time is before end time
  2. Detecting overlapping release windows

Let's check if these validations exist in the schema:

Consider adding validation rules to prevent invalid time ranges:

// In PolicyFormSchema.ts
releaseWindows: z.array(
  z.object({
    startTime: z.date(),
    endTime: z.date(),
    recurrence: z.enum(['hourly', 'daily', 'weekly', 'monthly'])
  })
).refine(
  (windows) => windows.every(w => w.startTime < w.endTime),
  "Start time must be before end time"
).refine(
  (windows) => {
    // Add overlap detection logic here
    return true;
  },
  "Release windows must not overlap"
)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for validation rules in PolicyFormSchema
rg -A 10 "releaseWindows.*schema" 

Length of output: 35


Script:

#!/bin/bash
# Let's try a broader search for schema definitions related to release windows
rg -A 10 "releaseWindows.*z\." 

# Also search for any form validation related to release windows
rg -A 10 "releaseWindows.*validate"

# Search for any files that might contain the schema definition
fd "schema|validation" --type f

Length of output: 438


Script:

#!/bin/bash
# Let's check the content of the PolicyFormSchema and the setPolicyReleaseWindow schema
cat apps/webservice/src/app/[workspaceSlug]/\(app\)/_components/environment-policy-drawer/PolicyFormSchema.ts

# Also check the environment schema that contains the release window definition
cat packages/db/src/schema/environment.ts

Length of output: 11085

Comment on lines +93 to +103
const policyReleaseChannels = form.watch("releaseChannels");

const updateReleaseChannel = (
deploymentId: string,
channelId: string | null,
) => setReleaseChannels((prev) => ({ ...prev, [deploymentId]: channelId }));

const onSubmit = () =>
updateReleaseChannels
.mutateAsync({
id: policy.id,
releaseChannels,
})
.then(() => utils.environment.policy.byId.invalidate(policy.id));
) =>
form.setValue(
"releaseChannels",
{ ...policyReleaseChannels, [deploymentId]: channelId },
{ shouldValidate: true, shouldDirty: true, shouldTouch: true },
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle potential undefined in policyReleaseChannels

If policyReleaseChannels is undefined, spreading it with { ...policyReleaseChannels } in updateReleaseChannel will cause an error. To prevent this, default policyReleaseChannels to an empty object.

Apply this diff to fix the issue:

-      const policyReleaseChannels = form.watch("releaseChannels");
+      const policyReleaseChannels = form.watch("releaseChannels") ?? {};

This ensures that policyReleaseChannels is always an object before using the spread operator.

📝 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 policyReleaseChannels = form.watch("releaseChannels");
const updateReleaseChannel = (
deploymentId: string,
channelId: string | null,
) => setReleaseChannels((prev) => ({ ...prev, [deploymentId]: channelId }));
const onSubmit = () =>
updateReleaseChannels
.mutateAsync({
id: policy.id,
releaseChannels,
})
.then(() => utils.environment.policy.byId.invalidate(policy.id));
) =>
form.setValue(
"releaseChannels",
{ ...policyReleaseChannels, [deploymentId]: channelId },
{ shouldValidate: true, shouldDirty: true, shouldTouch: true },
);
const policyReleaseChannels = form.watch("releaseChannels") ?? {};
const updateReleaseChannel = (
deploymentId: string,
channelId: string | null,
) =>
form.setValue(
"releaseChannels",
{ ...policyReleaseChannels, [deploymentId]: channelId },
{ shouldValidate: true, shouldDirty: true, shouldTouch: true },
);

Comment on lines 18 to 20
const { concurrencyLimit } = form.watch();

return (
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for concurrencyLimit

The form watches concurrencyLimit but lacks validation. Consider adding checks for negative values and NaN.

-  const { concurrencyLimit } = form.watch();
+  const { concurrencyLimit } = form.watch();
+  const isValidLimit = !isNaN(concurrencyLimit) && concurrencyLimit > 0;

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +368 to +435
}),
)
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.SystemUpdate)
.on({ type: "environmentPolicy", id: input.id }),
})
.mutation(({ ctx, input }) => {
const { id, releaseChannels } = input;
const [nulled, set] = _.partition(
Object.entries(releaseChannels),
([_, channelId]) => channelId == null,
);
const { releaseWindows } = input.data;
if (releaseWindows != null)
await ctx.db.transaction((tx) =>
tx
.delete(environmentPolicyReleaseWindow)
.where(eq(environmentPolicyReleaseWindow.policyId, input.id))
.then(() =>
releaseWindows.length === 0
? []
: tx
.insert(environmentPolicyReleaseWindow)
.values(releaseWindows)
.returning(),
),
);

const nulledIds = nulled.map(([deploymentId]) => deploymentId);
const setChannels = set.map(([deploymentId, channelId]) => ({
policyId: id,
deploymentId,
channelId: channelId!,
}));
if (input.data.releaseChannels != null) {
const [nulled, set] = _.partition(
Object.entries(input.data.releaseChannels),
([_, channelId]) => channelId == null,
);

return ctx.db.transaction(async (db) => {
await db
.delete(environmentPolicyReleaseChannel)
.where(
inArray(environmentPolicyReleaseChannel.deploymentId, nulledIds),
);
const nulledIds = nulled.map(([deploymentId]) => deploymentId);
const setChannels = set.map(([deploymentId, channelId]) => ({
policyId: input.id,
deploymentId,
channelId: channelId!,
}));

await ctx.db.transaction(async (tx) => {
await tx
.delete(environmentPolicyReleaseChannel)
.where(
and(
inArray(
environmentPolicyReleaseChannel.deploymentId,
nulledIds,
),
eq(environmentPolicyReleaseChannel.policyId, input.id),
),
);

if (setChannels.length > 0)
await db
.insert(environmentPolicyReleaseChannel)
.values(setChannels)
.onConflictDoUpdate({
target: [
environmentPolicyReleaseChannel.policyId,
environmentPolicyReleaseChannel.deploymentId,
],
set: buildConflictUpdateColumns(environmentPolicyReleaseChannel, [
"channelId",
]),
});
});
if (setChannels.length > 0)
await tx
.insert(environmentPolicyReleaseChannel)
.values(setChannels)
.onConflictDoUpdate({
target: [
environmentPolicyReleaseChannel.policyId,
environmentPolicyReleaseChannel.deploymentId,
],
set: buildConflictUpdateColumns(
environmentPolicyReleaseChannel,
["channelId"],
),
});
});

return policy;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure the update mutation always returns the updated policy

The return policy; statement is currently inside the if block checking for input.data.releaseChannels != null. If releaseChannels is null or undefined, the function won't return the policy, which could lead to unexpected behavior.

Move the return policy; statement outside of the if block to ensure the policy is always returned:

       });
       if (input.data.releaseChannels != null) {
         // ... existing code ...
       }
+      return policy;
     }),
📝 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
.mutation(async ({ ctx, input }) => {
const policy = await ctx.db
.update(environmentPolicy)
.set(input.data)
.where(eq(environmentPolicy.id, input.id))
.returning()
.then(takeFirst),
),
.then(takeFirst);
updateReleaseChannels: protectedProcedure
.input(
z.object({
id: z.string().uuid(),
releaseChannels: z.record(z.string().uuid().nullable()),
}),
)
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.SystemUpdate)
.on({ type: "environmentPolicy", id: input.id }),
})
.mutation(({ ctx, input }) => {
const { id, releaseChannels } = input;
const [nulled, set] = _.partition(
Object.entries(releaseChannels),
([_, channelId]) => channelId == null,
);
const { releaseWindows } = input.data;
if (releaseWindows != null)
await ctx.db.transaction((tx) =>
tx
.delete(environmentPolicyReleaseWindow)
.where(eq(environmentPolicyReleaseWindow.policyId, input.id))
.then(() =>
releaseWindows.length === 0
? []
: tx
.insert(environmentPolicyReleaseWindow)
.values(releaseWindows)
.returning(),
),
);
const nulledIds = nulled.map(([deploymentId]) => deploymentId);
const setChannels = set.map(([deploymentId, channelId]) => ({
policyId: id,
deploymentId,
channelId: channelId!,
}));
if (input.data.releaseChannels != null) {
const [nulled, set] = _.partition(
Object.entries(input.data.releaseChannels),
([_, channelId]) => channelId == null,
);
return ctx.db.transaction(async (db) => {
await db
.delete(environmentPolicyReleaseChannel)
.where(
inArray(environmentPolicyReleaseChannel.deploymentId, nulledIds),
);
const nulledIds = nulled.map(([deploymentId]) => deploymentId);
const setChannels = set.map(([deploymentId, channelId]) => ({
policyId: input.id,
deploymentId,
channelId: channelId!,
}));
await ctx.db.transaction(async (tx) => {
await tx
.delete(environmentPolicyReleaseChannel)
.where(
and(
inArray(
environmentPolicyReleaseChannel.deploymentId,
nulledIds,
),
eq(environmentPolicyReleaseChannel.policyId, input.id),
),
);
if (setChannels.length > 0)
await db
.insert(environmentPolicyReleaseChannel)
.values(setChannels)
.onConflictDoUpdate({
target: [
environmentPolicyReleaseChannel.policyId,
environmentPolicyReleaseChannel.deploymentId,
],
set: buildConflictUpdateColumns(environmentPolicyReleaseChannel, [
"channelId",
]),
});
});
if (setChannels.length > 0)
await tx
.insert(environmentPolicyReleaseChannel)
.values(setChannels)
.onConflictDoUpdate({
target: [
environmentPolicyReleaseChannel.policyId,
environmentPolicyReleaseChannel.deploymentId,
],
set: buildConflictUpdateColumns(
environmentPolicyReleaseChannel,
["channelId"],
),
});
});
return policy;
}
.mutation(async ({ ctx, input }) => {
const policy = await ctx.db
.update(environmentPolicy)
.set(input.data)
.where(eq(environmentPolicy.id, input.id))
.returning()
.then(takeFirst);
const { releaseWindows } = input.data;
if (releaseWindows != null)
await ctx.db.transaction((tx) =>
tx
.delete(environmentPolicyReleaseWindow)
.where(eq(environmentPolicyReleaseWindow.policyId, input.id))
.then(() =>
releaseWindows.length === 0
? []
: tx
.insert(environmentPolicyReleaseWindow)
.values(releaseWindows)
.returning(),
),
);
if (input.data.releaseChannels != null) {
const [nulled, set] = _.partition(
Object.entries(input.data.releaseChannels),
([_, channelId]) => channelId == null,
);
const nulledIds = nulled.map(([deploymentId]) => deploymentId);
const setChannels = set.map(([deploymentId, channelId]) => ({
policyId: input.id,
deploymentId,
channelId: channelId!,
}));
await ctx.db.transaction(async (tx) => {
await tx
.delete(environmentPolicyReleaseChannel)
.where(
and(
inArray(
environmentPolicyReleaseChannel.deploymentId,
nulledIds,
),
eq(environmentPolicyReleaseChannel.policyId, input.id),
),
);
if (setChannels.length > 0)
await tx
.insert(environmentPolicyReleaseChannel)
.values(setChannels)
.onConflictDoUpdate({
target: [
environmentPolicyReleaseChannel.policyId,
environmentPolicyReleaseChannel.deploymentId,
],
set: buildConflictUpdateColumns(
environmentPolicyReleaseChannel,
["channelId"],
),
});
});
}
return policy;


import * as SCHEMA from "@ctrlplane/db/schema";

const isValidDuration = (str: string) => !isNaN(ms(str));
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use Number.isNaN instead of isNaN for safer validation

Using the global isNaN function can lead to unexpected results due to type coercion. Number.isNaN is safer because it checks if the value is exactly NaN without coercion.

[static_analysis_hints]

Update the isValidDuration function:

- const isValidDuration = (str: string) => !isNaN(ms(str));
+ const isValidDuration = (str: string) => !Number.isNaN(ms(str));
📝 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 isValidDuration = (str: string) => !isNaN(ms(str));
const isValidDuration = (str: string) => !Number.isNaN(ms(str));
🧰 Tools
🪛 Biome (1.9.4)

[error] 7-7: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

);
return deploymentIds.size === channelsWithNonNullDeploymentIds.length;
}),
releaseWindows: z.array(setPolicyReleaseWindow).optional(),
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding validation for releaseWindows.

The releaseWindows field is added without any additional validation. Consider adding refinements to ensure:

  1. Non-overlapping time windows
  2. Valid time ranges (end time after start time)

Add validation like this:

-    releaseWindows: z.array(setPolicyReleaseWindow).optional(),
+    releaseWindows: z
+      .array(setPolicyReleaseWindow)
+      .optional()
+      .refine((windows) => {
+        if (!windows) return true;
+        return windows.every(window => 
+          new Date(window.startTime) < new Date(window.endTime)
+        );
+      }, "End time must be after start time")
+      .refine((windows) => {
+        if (!windows) return true;
+        // Sort windows by start time for easier overlap detection
+        const sorted = [...windows].sort((a, b) => 
+          new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
+        );
+        // Check for overlaps
+        for (let i = 0; i < sorted.length - 1; i++) {
+          if (new Date(sorted[i].endTime) > new Date(sorted[i + 1].startTime)) {
+            return false;
+          }
+        }
+        return true;
+      }, "Release windows must not overlap"),
📝 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
releaseWindows: z.array(setPolicyReleaseWindow).optional(),
releaseWindows: z
.array(setPolicyReleaseWindow)
.optional()
.refine((windows) => {
if (!windows) return true;
return windows.every(window =>
new Date(window.startTime) < new Date(window.endTime)
);
}, "End time must be after start time")
.refine((windows) => {
if (!windows) return true;
// Sort windows by start time for easier overlap detection
const sorted = [...windows].sort((a, b) =>
new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
);
// Check for overlaps
for (let i = 0; i < sorted.length - 1; i++) {
if (new Date(sorted[i].endTime) > new Date(sorted[i + 1].startTime)) {
return false;
}
}
return true;
}, "Release windows must not overlap"),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant