Skip to content

Commit

Permalink
feat(dashboard): Add workflow page action menu (#7222)
Browse files Browse the repository at this point in the history
  • Loading branch information
scopsy authored Dec 5, 2024
1 parent da531b2 commit 3637026
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 15 deletions.
38 changes: 38 additions & 0 deletions apps/dashboard/src/components/delete-workflow-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { WorkflowListResponseDto, WorkflowResponseDto } from '@novu/shared';
import { ConfirmationModal } from './confirmation-modal';
import TruncatedText from './truncated-text';

type DeleteWorkflowDialogProps = {
workflow: WorkflowResponseDto | WorkflowListResponseDto;
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
isLoading?: boolean;
};

export const DeleteWorkflowDialog = ({
workflow,
open,
onOpenChange,
onConfirm,
isLoading,
}: DeleteWorkflowDialogProps) => {
return (
<ConfirmationModal
open={open}
onOpenChange={onOpenChange}
onConfirm={onConfirm}
title="Are you sure?"
description={
<>
You're about to delete the <TruncatedText className="max-w-[32ch] font-bold">{workflow.name}</TruncatedText>{' '}
workflow, this action is permanent. <br />
<br />
You won't be able to trigger this workflow anymore.
</>
}
confirmButtonText="Delete"
isLoading={isLoading}
/>
);
};
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/success-button-toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface SuccessToastProps {
export function SuccessButtonToast({ title, description, actionLabel, onAction, onClose }: SuccessToastProps) {
return (
<>
<ToastIcon variant="default" />
<ToastIcon variant="success" />
<div className="flex flex-1 flex-col items-start gap-2.5">
<div className="flex flex-col items-start justify-center gap-1 self-stretch">
<div className="text-foreground-950 text-sm font-medium">{title}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { motion } from 'motion/react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useNavigate } from 'react-router-dom';
import type { ExternalToast } from 'sonner';

import { PAUSE_MODAL_TITLE, PauseModalDescription } from '@/components/pause-workflow-dialog';
import { SidebarContent, SidebarHeader } from '@/components/side-navigation/sidebar';
Expand All @@ -21,17 +23,83 @@ import { TagInput } from '../primitives/tag-input';
import { Textarea } from '../primitives/textarea';
import { MAX_DESCRIPTION_LENGTH, workflowSchema } from '@/components/workflow-editor/schema';
import { useFormAutosave } from '@/hooks/use-form-autosave';
import { RiDeleteBin2Line, RiGitPullRequestFill, RiMore2Fill } from 'react-icons/ri';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/primitives/dropdown-menu';
import { Button } from '../primitives/button';
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from '../primitives/tooltip';
import { useEnvironment } from '@/context/environment/hooks';
import { useSyncWorkflow } from '@/hooks/use-sync-workflow';
import { useDeleteWorkflow } from '@/hooks/use-delete-workflow';
import { showToast } from '@/components/primitives/sonner-helpers';
import { ToastIcon } from '@/components/primitives/sonner';
import { DeleteWorkflowDialog } from '../delete-workflow-dialog';
import { ROUTES } from '@/utils/routes';

type ConfigureWorkflowFormProps = {
workflow: WorkflowResponseDto;
update: (data: UpdateWorkflowDto) => void;
};

const toastOptions: ExternalToast = {
position: 'bottom-right',
classNames: {
toast: 'mb-4 right-0',
},
};

export const ConfigureWorkflowForm = (props: ConfigureWorkflowFormProps) => {
const { workflow, update } = props;
const navigate = useNavigate();
const isReadOnly = workflow.origin === WorkflowOriginEnum.EXTERNAL;
const [isPauseModalOpen, setIsPauseModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const tagsQuery = useTags();
const { currentEnvironment } = useEnvironment();
const { safeSync, isSyncable, tooltipContent, PromoteConfirmModal } = useSyncWorkflow(workflow);

const { deleteWorkflow, isPending: isDeleteWorkflowPending } = useDeleteWorkflow({
onSuccess: () => {
showToast({
children: () => (
<>
<ToastIcon variant="success" />
<span className="text-sm">
Deleted workflow <span className="font-bold">{workflow.name}</span>.
</span>
</>
),
options: toastOptions,
});
navigate(ROUTES.WORKFLOWS);
},
onError: () => {
showToast({
children: () => (
<>
<ToastIcon variant="error" />
<span className="text-sm">
Failed to delete workflow <span className="font-bold">{workflow.name}</span>.
</span>
</>
),
options: toastOptions,
});
},
});

const onDeleteWorkflow = async () => {
await deleteWorkflow({
workflowId: workflow._id,
});
};

const form = useForm<z.infer<typeof workflowSchema>>({
defaultValues: {
Expand All @@ -57,6 +125,8 @@ export const ConfigureWorkflowForm = (props: ConfigureWorkflowFormProps) => {
saveForm();
};

const syncToLabel = `Sync to ${currentEnvironment?.name === 'Production' ? 'Development' : 'Production'}`;

return (
<>
<ConfirmationModal
Expand All @@ -70,6 +140,13 @@ export const ConfigureWorkflowForm = (props: ConfigureWorkflowFormProps) => {
description={<PauseModalDescription workflowName={workflow.name} />}
confirmButtonText="Proceed"
/>
<DeleteWorkflowDialog
workflow={workflow}
open={isDeleteModalOpen}
onOpenChange={setIsDeleteModalOpen}
onConfirm={onDeleteWorkflow}
isLoading={isDeleteWorkflowPending}
/>
<PageMeta title={workflow.name} />
<motion.div
className={cn('relative flex h-full w-full flex-col')}
Expand All @@ -83,6 +160,50 @@ export const ConfigureWorkflowForm = (props: ConfigureWorkflowFormProps) => {
<RouteFill />
<span>Configure workflow</span>
</div>
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="ml-auto">
<RiMore2Fill />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
{isSyncable ? (
<DropdownMenuItem onClick={safeSync}>
<RiGitPullRequestFill />
{syncToLabel}
</DropdownMenuItem>
) : (
<Tooltip>
<TooltipTrigger>
<DropdownMenuItem disabled>
<RiGitPullRequestFill />
{syncToLabel}
</DropdownMenuItem>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>{tooltipContent}</TooltipContent>
</TooltipPortal>
</Tooltip>
)}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup className="*:cursor-pointer">
<DropdownMenuItem
className="text-destructive"
disabled={workflow.origin === WorkflowOriginEnum.EXTERNAL}
onClick={() => {
setIsDeleteModalOpen(true);
setIsDropdownOpen(false);
}}
>
<RiDeleteBin2Line />
Delete workflow
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<PromoteConfirmModal />
</SidebarHeader>
<Separator />
<Form {...form}>
Expand Down
15 changes: 3 additions & 12 deletions apps/dashboard/src/components/workflow-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { showToast } from './primitives/sonner-helpers';
import { ToastIcon } from './primitives/sonner';
import { usePatchWorkflow } from '@/hooks/use-patch-workflow';
import { PauseModalDescription, PAUSE_MODAL_TITLE } from '@/components/pause-workflow-dialog';
import { DeleteWorkflowDialog } from './delete-workflow-dialog';

type WorkflowRowProps = {
workflow: WorkflowListResponseDto;
Expand Down Expand Up @@ -207,21 +208,11 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => {
</Tooltip>

<TableCell className="w-1">
<ConfirmationModal
<DeleteWorkflowDialog
workflow={workflow}
open={isDeleteModalOpen}
onOpenChange={setIsDeleteModalOpen}
onConfirm={onDeleteWorkflow}
title="Are you sure?"
description={
<>
You're about to delete the{' '}
<TruncatedText className="max-w-[32ch] font-bold">{workflow.name}</TruncatedText> workflow, this action is
permanent. <br />
<br />
You won't be able to trigger this workflow anymore.
</>
}
confirmButtonText="Delete"
isLoading={isDeleteWorkflowPending}
/>
<ConfirmationModal
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/hooks/use-sync-workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useMutation } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { toast } from 'sonner';

export function useSyncWorkflow(workflow: WorkflowListResponseDto) {
export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResponseDto) {
const { oppositeEnvironment, switchEnvironment } = useEnvironment();
const [isLoading, setIsLoading] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
Expand Down Expand Up @@ -106,7 +106,7 @@ export function useSyncWorkflow(workflow: WorkflowListResponseDto) {
setIsLoading(true);
loadingToast = toast.loading(
<>
<ToastIcon variant="default" />
<ToastIcon variant="default" className="animate-spin" />
<span className="text-sm">
Syncing workflow <span className="font-bold">{workflow.name}</span> to {oppositeEnvironment?.name}...
</span>
Expand Down

0 comments on commit 3637026

Please sign in to comment.