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

feat(dashboard): Empty workflow list state & improvements #6666

Merged
merged 1 commit into from
Oct 9, 2024
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
4 changes: 2 additions & 2 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@
"@hookform/resolvers": "^2.9.1",
"@novu/react": "^2.3.0",
"@novu/shared": "workspace:*",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@segment/analytics-next": "^1.73.0",
"@tanstack/react-query": "^4.20.4",
"@tanstack/react-query": "^5.59.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
Expand Down
6 changes: 3 additions & 3 deletions apps/dashboard/src/components/dashboard-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { SideNavigation } from './side-navigation';

export const DashboardLayout = ({ children }: { children: ReactNode }) => {
return (
<div className="relative flex h-screen w-full">
<div className="relative flex w-full">
<SideNavigation />
<div className="flex min-h-screen flex-1 flex-col overflow-y-auto overflow-x-hidden">
<div className="flex flex-1 flex-col overflow-y-auto overflow-x-hidden">
<div className="bg-background flex h-16 w-full items-center justify-between border-b p-4">
<a
href="/legacy/integrations"
Expand All @@ -22,7 +22,7 @@ export const DashboardLayout = ({ children }: { children: ReactNode }) => {
</div>
</div>

<div className="overflow-y-auto overflow-x-hidden">{children}</div>
<div className="flex min-h-[calc(100dvh-4rem)] flex-col overflow-y-auto overflow-x-hidden">{children}</div>
</div>
</div>
);
Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/src/components/default-pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const DefaultPagination = (props: DefaultPaginationProps) => {
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationStart to={hrefFromOffset(0)} isDisabled={currentPage === 1} />
<PaginationStart to={hrefFromOffset(0)} />
</PaginationItem>
<PaginationItem>
<PaginationPrevious to={hrefFromOffset(Math.max(0, offset - limit))} isDisabled={currentPage === 1} />
Expand All @@ -45,7 +45,7 @@ export const DefaultPagination = (props: DefaultPaginationProps) => {

if (startPage > 2) {
pageItems.push(
<PaginationItem>
<PaginationItem key="ellipsis">
<PaginationEllipsis />
</PaginationItem>
);
Expand Down Expand Up @@ -79,7 +79,7 @@ export const DefaultPagination = (props: DefaultPaginationProps) => {
}

pageItems.push(
<PaginationItem>
<PaginationItem key="next">
<PaginationNext
to={hrefFromOffset(Math.min(offset + limit, (totalPages - 1) * limit))}
isDisabled={currentPage === totalPages}
Expand All @@ -88,7 +88,7 @@ export const DefaultPagination = (props: DefaultPaginationProps) => {
);

pageItems.push(
<PaginationItem>
<PaginationItem key="end">
<PaginationEnd to={hrefFromOffset((totalPages - 1) * limit)} isDisabled={currentPage === totalPages} />
</PaginationItem>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/primitives/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const buttonVariants = cva(
'bg-gradient-to-b from-destructive/90 to-destructive text-destructive-foreground shadow-[inset_0_-4px_2px_-2px_hsl(var(--destructive)),inset_0_0_0_1px_rgba(255,255,255,0.16),0_0_0_1px_hsl(var(--destructive)),0px_1px_2px_0px_#0E121B3D] after:content-[""] after:absolute after:w-full after:h-full after:bg-gradient-to-b after:from-background/10 after:opacity-0 hover:after:opacity-100 after:rounded-lg after:transition-opacity after:duration-300',
outline: 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
ghost: 'hover:bg-accent',
link: 'text-primary underline-offset-4 hover:underline',
link: 'underline-offset-4 hover:underline',
light:
'bg-destructive/10 hover:bg-background hover:border hover:border-destructive text-destructive focus-visible:ring-destructive/10 focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:bg-background focus-visible:border focus-visible:border-destructive',
},
Expand Down
19 changes: 5 additions & 14 deletions apps/dashboard/src/components/primitives/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ const Table = React.forwardRef<
>(({ className, containerClassname, ...props }, ref) => (
<div
className={cn(
'border-neutral-alpha-200 relative w-full overflow-x-auto rounded-lg border shadow-sm',
'border-neutral-alpha-200 relative w-full overflow-x-auto rounded-md border shadow-sm',
containerClassname
)}
>
<table
ref={ref}
className={cn('relative w-full table-fixed caption-bottom border-separate border-spacing-0 text-sm', className)}
className={cn('relative w-full caption-bottom border-separate border-spacing-0 text-sm', className)}
{...props}
/>
</div>
Expand Down Expand Up @@ -65,7 +65,7 @@ const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<
<th
ref={ref}
className={cn(
'text-foreground-600 h-10 px-2 text-left align-middle text-xs font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
'text-foreground-600 h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
Expand All @@ -75,17 +75,8 @@ const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<
TableHead.displayName = 'TableHead';

const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td ref={ref} className={cn('overflow-hidden p-2 align-middle', className)} {...props} />
)
({ className, ...props }, ref) => <td ref={ref} className={cn('px-6 py-2 align-middle', className)} {...props} />
);
TableCell.displayName = 'TableCell';

const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn('text-foreground/50 mt-4 text-sm', className)} {...props} />
)
);
TableCaption.displayName = 'TableCaption';

export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell };
86 changes: 86 additions & 0 deletions apps/dashboard/src/components/workflow-cloud.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { HTMLAttributes } from 'react';

type WorkflowCloudProps = HTMLAttributes<HTMLOrSVGElement>;
export const WorkflowCloud = (props: WorkflowCloudProps) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="153" height="120" fill="none" {...props}>
<circle cx="76.5" cy="52" r="52" fill="#F4F5F6" />
<g filter="url(#a)">
<path
fill="#FBFBFB"
d="M78.1 16c-10.773 0-20.302 5.323-26.101 13.483A25.672 25.672 0 0 0 46.1 28.8c-14.139 0-25.6 11.461-25.6 25.6C20.5 68.539 31.962 80 46.1 80h64c12.371 0 22.4-10.029 22.4-22.4 0-12.371-10.029-22.4-22.4-22.4-.879 0-1.746.05-2.598.149C102.598 23.968 91.28 16 78.1 16Z"
/>
<circle cx="46.1" cy="54.4" r="25.6" fill="url(#b)" />
<circle cx="78.1" cy="48" r="32" fill="url(#c)" />
<circle cx="110.1" cy="57.6" r="22.4" fill="url(#d)" />
</g>
<circle cx="21.5" cy="19" r="5" fill="#FBFBFB" />
<circle cx="18.5" cy="109" r="7" fill="#FBFBFB" />
<circle cx="145.5" cy="35" r="7" fill="#FBFBFB" />
<circle cx="134.5" cy="8" r="4" fill="#FBFBFB" />
<g filter="url(#e)">
<path
fill="#99A0AE"
fillOpacity=".1"
d="M76 85.5c0-13.255 10.745-24 24-24s24 10.745 24 24-10.745 24-24 24-24-10.745-24-24Z"
/>
<path
fill="#fff"
d="M90.4 89.1v-7.8a5.4 5.4 0 0 1 10.8 0v8.4a3.002 3.002 0 0 0 3 3 3 3 0 0 0 3-3v-8.004a3.601 3.601 0 1 1 2.4 0V89.7a5.4 5.4 0 1 1-10.8 0v-8.4a3 3 0 0 0-6 0v7.8h3.6l-4.8 6-4.8-6h3.6Z"
/>
</g>
<defs>
<linearGradient id="b" x1="26.443" x2="71.7" y1="37.486" y2="80" gradientUnits="userSpaceOnUse">
<stop stopColor="#D0D5DD" stopOpacity=".75" />
<stop offset=".351" stopColor="#fff" stopOpacity="0" />
</linearGradient>
<linearGradient id="c" x1="53.529" x2="110.1" y1="26.857" y2="80" gradientUnits="userSpaceOnUse">
<stop stopColor="#D0D5DD" stopOpacity=".75" />
<stop offset=".351" stopColor="#fff" stopOpacity="0" />
</linearGradient>
<linearGradient id="d" x1="92.9" x2="132.5" y1="42.8" y2="80" gradientUnits="userSpaceOnUse">
<stop stopColor="#D0D5DD" stopOpacity=".75" />
<stop offset=".351" stopColor="#fff" stopOpacity="0" />
</linearGradient>
<filter
id="a"
width="152"
height="104"
x=".5"
y="16"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feMorphology in="SourceAlpha" radius="4" result="effect1_dropShadow_2411_21338" />
<feOffset dy="8" />
<feGaussianBlur stdDeviation="4" />
<feColorMatrix values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.03 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2411_21338" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feMorphology in="SourceAlpha" radius="4" result="effect2_dropShadow_2411_21338" />
<feOffset dy="20" />
<feGaussianBlur stdDeviation="12" />
<feColorMatrix values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.08 0" />
<feBlend in2="effect1_dropShadow_2411_21338" result="effect2_dropShadow_2411_21338" />
<feBlend in="SourceGraphic" in2="effect2_dropShadow_2411_21338" result="shape" />
</filter>
<filter
id="e"
width="64"
height="64"
x="68"
y="53.5"
colorInterpolationFilters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="4" />
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_2411_21338" />
<feBlend in="SourceGraphic" in2="effect1_backgroundBlur_2411_21338" result="shape" />
</filter>
</defs>
</svg>
);
};
114 changes: 76 additions & 38 deletions apps/dashboard/src/components/workflow-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getV2 } from '@/api/api.client';
import { DefaultPagination } from '@/components/default-pagination';
import { Badge } from '@/components/primitives/badge';
import { Button, buttonVariants } from '@/components/primitives/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select';
import { Skeleton } from '@/components/primitives/skeleton';
import {
Expand All @@ -13,14 +14,17 @@ import {
TableRow,
} from '@/components/primitives/table';
import TruncatedText from '@/components/truncated-text';
import { WorkflowCloud } from '@/components/workflow-cloud';
import { WorkflowStatus } from '@/components/workflow-status';
import { WorkflowSteps } from '@/components/workflow-steps';
import { WorkflowTags } from '@/components/workflow-tags';
import { useEnvironment } from '@/context/environment/hooks';
import { ListWorkflowResponse, WorkflowOriginEnum } from '@novu/shared';
import { useQuery } from '@tanstack/react-query';
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import { FaCode } from 'react-icons/fa6';
import { createSearchParams, useLocation, useSearchParams } from 'react-router-dom';
import { createSearchParams, Link, useLocation, useSearchParams } from 'react-router-dom';
import { RiRouteFill } from 'react-icons/ri';
import { RiBookMarkedLine } from 'react-icons/ri';

export const WorkflowList = () => {
const { currentEnvironment } = useEnvironment();
Expand All @@ -47,16 +51,48 @@ export const WorkflowList = () => {
const { data } = await getV2<{ data: ListWorkflowResponse }>(`/workflows?limit=${limit}&offset=${offset}`);
return data;
},
placeholderData: keepPreviousData,
});
const currentPage = Math.floor(offset / limit) + 1;

if (!workflowsQuery.isLoading && !workflowsQuery.data) {
if (workflowsQuery.isError) {
return null;
}

if (!workflowsQuery.isPending && workflowsQuery.data.totalCount === 0) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-6">
<div className="flex flex-col items-center gap-2 text-center">
<WorkflowCloud className="drop-shadow" />
<span className="text-foreground-900 block font-medium">
No workflows exist, create workflows to orchestrate notifications
</span>
<p className="text-foreground-600 max-w-[55ch] text-sm">
Workflows in Novu handle event-driven notifications across multiple channels in a single, version-controlled
flow, with the ability to manage preference for each subscriber.
</p>
</div>

<div className="flex items-center justify-center gap-6">
<Link
to={'https://docs.novu.co/concepts/workflows'}
className={buttonVariants({ variant: 'link', className: 'text-foreground-600 gap-1' })}
>
<RiBookMarkedLine className="size-4" />
View docs
</Link>
<Button variant="primary" className="gap-2">
<RiRouteFill className="size-5" />
Create workflow
</Button>
</div>
</div>
);
}

return (
<div className="flex h-full flex-col px-6 py-2">
<Table containerClassname="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Workflows</TableHead>
Expand All @@ -67,7 +103,7 @@ export const WorkflowList = () => {
</TableRow>
</TableHeader>
<TableBody>
{workflowsQuery.isLoading ? (
{workflowsQuery.isPending ? (
<>
{new Array(limit).fill(0).map((_, index) => (
<TableRow key={index}>
Expand Down Expand Up @@ -126,39 +162,41 @@ export const WorkflowList = () => {
</>
)}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={5}>
<div className="flex items-center justify-between">
{workflowsQuery.data ? (
<span className="text-foreground-600 block text-sm font-normal">
Page {currentPage} of {Math.ceil(workflowsQuery.data.totalCount / limit)}
</span>
) : (
<Skeleton className="h-5 w-[20ch]" />
)}
{workflowsQuery.data ? (
<DefaultPagination
hrefFromOffset={hrefFromOffset}
totalCount={workflowsQuery.data.totalCount}
limit={limit}
offset={offset}
/>
) : (
<Skeleton className="h-5 w-32" />
)}
<Select onValueChange={(v) => setLimit(parseInt(v))} defaultValue={limit.toString()}>
<SelectTrigger className="w-fit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="12">12 / page</SelectItem>
</SelectContent>
</Select>
</div>
</TableCell>
</TableRow>
</TableFooter>
{workflowsQuery.data && limit < workflowsQuery.data.totalCount && (
<TableFooter>
<TableRow>
<TableCell colSpan={5}>
<div className="flex items-center justify-between">
{workflowsQuery.data ? (
<span className="text-foreground-600 block text-sm font-normal">
Page {currentPage} of {Math.ceil(workflowsQuery.data.totalCount / limit)}
</span>
) : (
<Skeleton className="h-5 w-[20ch]" />
)}
{workflowsQuery.data ? (
<DefaultPagination
hrefFromOffset={hrefFromOffset}
totalCount={workflowsQuery.data.totalCount}
limit={limit}
offset={offset}
/>
) : (
<Skeleton className="h-5 w-32" />
)}
<Select onValueChange={(v) => setLimit(parseInt(v))} defaultValue={limit.toString()}>
<SelectTrigger className="w-fit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="12">12 / page</SelectItem>
</SelectContent>
</Select>
</div>
</TableCell>
</TableRow>
</TableFooter>
)}
</Table>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion apps/dashboard/src/context/environment/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export const useFetchEnvironments = ({ organizationId }: { organizationId?: stri
data: environments,
isInitialLoading: areEnvironmentsInitialLoading,
refetch: refetchEnvironments,
} = useQuery<IEnvironment[]>([QueryKeys.myEnvironments, organizationId], getEnvironments, {
} = useQuery<IEnvironment[]>({
queryKey: [QueryKeys.myEnvironments, organizationId],
queryFn: getEnvironments,
enabled: !!organizationId,
retry: false,
staleTime: Infinity,
Expand Down
Loading
Loading