-
Notifications
You must be signed in to change notification settings - Fork 11
fix: Init release channels ui #181
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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()), | ||
| ); | ||
adityachoudhari26 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 }, | ||
| ); | ||
adityachoudhari26 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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" | ||
| > | ||
adityachoudhari26 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| {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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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>
Comment on lines
+43
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enhance error handling and user feedback for deletion. The deletion operation needs better error handling and user feedback:
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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
adityachoudhari26 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </DropdownMenu> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.