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,31 @@
import type * as SCHEMA from "@ctrlplane/db/schema";

import { Badge } from "@ctrlplane/ui/badge";

type ReleaseBadgeListProps = {
releases: {
items: SCHEMA.Release[];
total: number;
};
};

export const ReleaseBadgeList: React.FC<ReleaseBadgeListProps> = ({
releases,
}) => (
<div className="flex gap-1">
{releases.items.map((release) => (
<Badge key={release.id} variant="outline">
<span className="truncate text-xs text-muted-foreground">
{release.name}
</span>
</Badge>
))}
{releases.total > releases.items.length && (
<Badge variant="outline">
<span className="text-xs text-muted-foreground">
+{releases.total - releases.items.length}
</span>
</Badge>
)}
</div>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type * as SCHEMA from "@ctrlplane/db/schema";
import type React from "react";
import { useRouter } from "next/navigation";
import { z } from "zod";

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

import { api } from "~/trpc/react";

type OverviewProps = {
releaseChannel: SCHEMA.ReleaseChannel;
};

const schema = z.object({
name: z.string().min(1).max(50),
description: z.string().max(1000).optional(),
});

export const Overview: React.FC<OverviewProps> = ({ releaseChannel }) => {
const defaultValues = {
name: releaseChannel.name,
description: releaseChannel.description ?? undefined,
};
const form = useForm({ schema, defaultValues });
const router = useRouter();
const utils = api.useUtils();

const updateReleaseChannel =
api.deployment.releaseChannel.update.useMutation();
const onSubmit = form.handleSubmit((data) =>
updateReleaseChannel
.mutateAsync({ id: releaseChannel.id, data })
.then(() => form.reset(data))
.then(() =>
utils.deployment.releaseChannel.byId.invalidate(releaseChannel.id),
)
.then(() => router.refresh()),
);

return (
<Form {...form}>
<form onSubmit={onSubmit} className="space-y-6 p-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<Button
type="submit"
disabled={updateReleaseChannel.isPending || !form.formState.isDirty}
>
Save
</Button>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import type React from "react";
import { IconDotsVertical, IconLoader2 } from "@tabler/icons-react";

import { Button } from "@ctrlplane/ui/button";
import { Drawer, DrawerContent, DrawerTitle } from "@ctrlplane/ui/drawer";
import { Separator } from "@ctrlplane/ui/separator";

import { api } from "~/trpc/react";
import { Overview } from "./Overview";
import { ReleaseChannelDropdown } from "./ReleaseChannelDropdown";
import { ReleaseFilter } from "./ReleaseFilter";
import { useReleaseChannelDrawer } from "./useReleaseChannelDrawer";

export const ReleaseChannelDrawer: React.FC = () => {
const { releaseChannelId, removeReleaseChannelId } =
useReleaseChannelDrawer();
const isOpen = Boolean(releaseChannelId);
const setIsOpen = removeReleaseChannelId;

const releaseChannelQ = api.deployment.releaseChannel.byId.useQuery(
releaseChannelId ?? "",
{ enabled: isOpen },
);
const releaseChannel = releaseChannelQ.data;

const filter = releaseChannel?.releaseFilter ?? undefined;
const deploymentId = releaseChannel?.deploymentId ?? "";
const releasesQ = api.release.list.useQuery(
{ deploymentId, filter },
{ enabled: isOpen && releaseChannel != null && deploymentId != "" },
);

const loading = releaseChannelQ.isLoading || releasesQ.isLoading;

return (
<Drawer open={isOpen} onOpenChange={setIsOpen}>
<DrawerContent
showBar={false}
className="scrollbar-thin scrollbar-thumb-neutral-800 scrollbar-track-neutral-900 left-auto right-0 top-0 mt-0 h-screen w-1/3 overflow-auto rounded-none focus-visible:outline-none"
>
{loading && (
<div className="flex h-full w-full items-center justify-center">
<IconLoader2 className="h-8 w-8 animate-spin" />
</div>
)}
{!loading && releaseChannel != null && (
<>
<DrawerTitle className="flex items-center gap-2 border-b p-6">
{releaseChannel.name}
<ReleaseChannelDropdown releaseChannelId={releaseChannel.id}>
<Button variant="ghost" size="icon" className="h-6 w-6">
<IconDotsVertical className="h-4 w-4" />
</Button>
</ReleaseChannelDropdown>
</DrawerTitle>

<div className="flex flex-col">
<Overview releaseChannel={releaseChannel} />
<Separator />
<ReleaseFilter releaseChannel={releaseChannel} />
</div>
</>
)}
</DrawerContent>
</Drawer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { IconTrash } from "@tabler/icons-react";

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@ctrlplane/ui/alert-dialog";
import { buttonVariants } from "@ctrlplane/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@ctrlplane/ui/dropdown-menu";

import { api } from "~/trpc/react";
import { useReleaseChannelDrawer } from "./useReleaseChannelDrawer";

type DeleteReleaseChannelDialogProps = {
releaseChannelId: string;
onClose: () => void;
children: React.ReactNode;
};

const DeleteReleaseChannelDialog: React.FC<DeleteReleaseChannelDialogProps> = ({
releaseChannelId,
onClose,
children,
}) => {
const [open, setOpen] = useState(false);
const { removeReleaseChannelId } = useReleaseChannelDrawer();
const router = useRouter();
const deleteReleaseChannel =
api.deployment.releaseChannel.delete.useMutation();
const onDelete = () =>
deleteReleaseChannel
.mutateAsync(releaseChannelId)
.then(() => removeReleaseChannelId())
.then(() => router.refresh())
.then(() => setOpen(false));
Comment on lines +42 to +48
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 error handling and loading state for deletion.

The deletion mutation chain lacks error handling and user feedback. Consider these improvements:

Apply this diff to enhance the error handling and user experience:

 const deleteReleaseChannel =
   api.deployment.releaseChannel.delete.useMutation();
+const [isDeleting, setIsDeleting] = useState(false);
 const onDelete = () => {
+  setIsDeleting(true);
   deleteReleaseChannel
     .mutateAsync(releaseChannelId)
     .then(() => removeReleaseChannelId())
     .then(() => router.refresh())
-    .then(() => setOpen(false));
+    .then(() => setOpen(false))
+    .catch((error) => {
+      console.error('Failed to delete release channel:', error);
+      // Show error toast/notification to user
+    })
+    .finally(() => setIsDeleting(false));
 };

Also update the Delete button to show loading state:

 <AlertDialogAction
   onClick={onDelete}
+  disabled={isDeleting}
   className={buttonVariants({ variant: "destructive" })}
 >
-  Delete
+  {isDeleting ? "Deleting..." : "Delete"}
 </AlertDialogAction>

Committable suggestion was skipped due to low confidence.

Comment on lines +43 to +48
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

Enhance error handling and user feedback for deletion.

The deletion operation needs better error handling and user feedback:

  1. Add error handling with user notification
  2. Update the button text to reflect the loading state
  3. Consider adding a toast notification for successful deletion

Apply this diff to enhance the user experience:

 const onDelete = () =>
   deleteReleaseChannel
     .mutateAsync(releaseChannelId)
     .then(() => removeReleaseChannelId())
     .then(() => router.refresh())
-    .then(() => setOpen(false));
+    .then(() => {
+      setOpen(false);
+      toast.success('Release channel deleted successfully');
+    })
+    .catch((error) => {
+      toast.error('Failed to delete release channel');
+      console.error('Delete error:', error);
+    });
 <AlertDialogAction
   onClick={onDelete}
   disabled={deleteReleaseChannel.isPending}
   className={buttonVariants({ variant: "destructive" })}
 >
-  Delete
+  {deleteReleaseChannel.isPending ? "Deleting..." : "Delete"}
 </AlertDialogAction>
📝 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 onDelete = () =>
deleteReleaseChannel
.mutateAsync(releaseChannelId)
.then(() => removeReleaseChannelId())
.then(() => router.refresh())
.then(() => setOpen(false));
const onDelete = () =>
deleteReleaseChannel
.mutateAsync(releaseChannelId)
.then(() => removeReleaseChannelId())
.then(() => router.refresh())
.then(() => {
setOpen(false);
toast.success('Release channel deleted successfully');
})
.catch((error) => {
toast.error('Failed to delete release channel');
console.error('Delete error:', error);
});
<AlertDialogAction
onClick={onDelete}
disabled={deleteReleaseChannel.isPending}
className={buttonVariants({ variant: "destructive" })}
>
{deleteReleaseChannel.isPending ? "Deleting..." : "Delete"}
</AlertDialogAction>


return (
<AlertDialog
open={open}
onOpenChange={(o) => {
setOpen(o);
if (!o) onClose();
}}
>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to delete this release channel?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<div className="flex-grow" />
<AlertDialogAction
onClick={onDelete}
disabled={deleteReleaseChannel.isPending}
className={buttonVariants({ variant: "destructive" })}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

type ReleaseChannelDropdownProps = {
releaseChannelId: string;
children: React.ReactNode;
};

export const ReleaseChannelDropdown: React.FC<ReleaseChannelDropdownProps> = ({
releaseChannelId,
children,
}) => {
const [open, setOpen] = useState(false);
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<DeleteReleaseChannelDialog
releaseChannelId={releaseChannelId}
onClose={() => setOpen(false)}
>
<DropdownMenuItem
className="flex cursor-pointer items-center gap-2"
onSelect={(e) => e.preventDefault()}
>
<IconTrash className="h-4 w-4 text-red-500" />
Delete
</DropdownMenuItem>
</DeleteReleaseChannelDialog>
</DropdownMenuContent>
</DropdownMenu>
);
};
Loading
Loading