From 2c8880c84df64c403970022e4d545abeca716dba Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Tue, 28 Jan 2025 16:06:41 -0500 Subject: [PATCH] feat: switch to new toast provider --- app/components/Attribute.tsx | 2 +- app/components/ToastProvider.tsx | 87 +++++++++++++++++++++++++++ app/layouts/dashboard.tsx | 42 ++++++------- app/root.tsx | 24 +++++--- app/routes/acls/editor.tsx | 20 +++--- app/routes/dns/components/domains.tsx | 9 ++- app/routes/users/overview.tsx | 2 +- app/utils/toast.ts | 14 +++++ 8 files changed, 151 insertions(+), 49 deletions(-) create mode 100644 app/components/ToastProvider.tsx create mode 100644 app/utils/toast.ts diff --git a/app/components/Attribute.tsx b/app/components/Attribute.tsx index 256a16e..de35830 100644 --- a/app/components/Attribute.tsx +++ b/app/components/Attribute.tsx @@ -1,5 +1,5 @@ import { CopyIcon } from '@primer/octicons-react'; -import { toast } from './Toaster'; +import toast from '~/utils/toast'; interface Props { name: string; diff --git a/app/components/ToastProvider.tsx b/app/components/ToastProvider.tsx new file mode 100644 index 0000000..9c4df53 --- /dev/null +++ b/app/components/ToastProvider.tsx @@ -0,0 +1,87 @@ +import { + AriaToastProps, + AriaToastRegionProps, + useToast, + useToastRegion, +} from '@react-aria/toast'; +import { ToastQueue, ToastState, useToastQueue } from '@react-stately/toast'; +import { X } from 'lucide-react'; +import React, { useRef } from 'react'; +import IconButton from '~/components/IconButton'; +import cn from '~/utils/cn'; + +interface ToastProps extends AriaToastProps { + state: ToastState; +} + +function Toast({ state, ...props }: ToastProps) { + const ref = useRef(null); + const { toastProps, contentProps, titleProps, closeButtonProps } = useToast( + props, + state, + ref, + ); + + return ( +
+
+
{props.toast.content}
+
+ + + +
+ ); +} + +interface ToastRegionProps extends AriaToastRegionProps { + state: ToastState; +} + +function ToastRegion({ state, ...props }: ToastRegionProps) { + const ref = useRef(null); + const { regionProps } = useToastRegion(props, state, ref); + + return ( +
+ {state.visibleToasts.map((toast) => ( + + ))} +
+ ); +} + +export interface ToastProviderProps extends AriaToastRegionProps { + queue: ToastQueue; +} + +export default function ToastProvider({ queue, ...props }: ToastProviderProps) { + const state = useToastQueue(queue); + + return ( + <> + {state.visibleToasts.length > 0 && ( + + )} + + ); +} diff --git a/app/layouts/dashboard.tsx b/app/layouts/dashboard.tsx index 7e57389..181bbcd 100644 --- a/app/layouts/dashboard.tsx +++ b/app/layouts/dashboard.tsx @@ -1,19 +1,11 @@ +import { XCircleFillIcon } from '@primer/octicons-react'; import { type LoaderFunctionArgs, redirect } from 'react-router'; import { Outlet, useLoaderData } from 'react-router'; -import { useEffect } from 'react' - -import { ErrorPopup } from '~/components/Error'; -import Header from '~/components/Header'; -import { toast } from '~/components/Toaster'; -import Footer from '~/components/Footer'; -import Link from '~/components/Link'; -import { useLiveData } from '~/utils/useLiveData' import { cn } from '~/utils/cn'; -import { loadContext } from '~/utils/config/headplane'; -import { HeadscaleError, pull, healthcheck } from '~/utils/headscale'; -import { destroySession, getSession } from '~/utils/sessions.server'; -import { XCircleFillIcon } from '@primer/octicons-react'; +import { HeadscaleError, healthcheck, pull } from '~/utils/headscale'; import log from '~/utils/log'; +import { destroySession, getSession } from '~/utils/sessions.server'; +import { useLiveData } from '~/utils/useLiveData'; export async function loader({ request }: LoaderFunctionArgs) { let healthy = false; @@ -45,25 +37,29 @@ export async function loader({ request }: LoaderFunctionArgs) { return { healthy, - } + }; } export default function Layout() { useLiveData({ interval: 3000 }); - const { healthy } = useLoaderData() + const { healthy } = useLoaderData(); return ( <> {!healthy ? ( -
-
+
+
Headscale is unreachable
diff --git a/app/root.tsx b/app/root.tsx index bf2f38e..b74d066 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,14 +1,20 @@ -import type { LoaderFunctionArgs, LinksFunction, MetaFunction } from 'react-router'; -import { Links, Meta, Outlet, Scripts, ScrollRestoration, useNavigation } from 'react-router'; -import { loadContext } from '~/utils/config/headplane'; -import '@fontsource-variable/inter' +import type { LinksFunction, MetaFunction } from 'react-router'; +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useNavigation, +} from 'react-router'; +import '@fontsource-variable/inter'; import { ProgressBar } from 'react-aria-components'; import { ErrorPopup } from '~/components/Error'; -// TODO: Make this a default export -import { Toaster } from '~/components/Toaster'; +import ToastProvider from '~/components/ToastProvider'; import stylesheet from '~/tailwind.css?url'; import { cn } from '~/utils/cn'; +import { useToastQueue } from '~/utils/toast'; export const meta: MetaFunction = () => [ { title: 'Headplane' }, @@ -23,6 +29,8 @@ export const links: LinksFunction = () => [ ]; export function Layout({ children }: { readonly children: React.ReactNode }) { + const toastQueue = useToastQueue(); + return ( @@ -33,7 +41,7 @@ export function Layout({ children }: { readonly children: React.ReactNode }) { {children} - + @@ -61,5 +69,5 @@ export default function App() { - ) + ); } diff --git a/app/routes/acls/editor.tsx b/app/routes/acls/editor.tsx index dda09b6..67a89bb 100644 --- a/app/routes/acls/editor.tsx +++ b/app/routes/acls/editor.tsx @@ -1,33 +1,33 @@ +import { setTimeout } from 'node:timers/promises'; import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon, } from '@primer/octicons-react'; -import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; -import { useLoaderData, useRevalidator, useFetcher } from 'react-router'; //import { useDebounceFetcher } from 'remix-utils/use-debounce-fetcher'; -import { useEffect, useState, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; -import { setTimeout } from 'node:timers/promises'; +import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; +import { useFetcher, useLoaderData, useRevalidator } from 'react-router'; import Button from '~/components/Button'; import Code from '~/components/Code'; import Link from '~/components/Link'; import Notice from '~/components/Notice'; import Spinner from '~/components/Spinner'; -import { toast } from '~/components/Toaster'; import { cn } from '~/utils/cn'; import { loadContext } from '~/utils/config/headplane'; import { loadConfig } from '~/utils/config/headscale'; import { HeadscaleError, pull, put } from '~/utils/headscale'; -import { getSession } from '~/utils/sessions.server'; -import { send } from '~/utils/res'; import log from '~/utils/log'; +import { send } from '~/utils/res'; +import { getSession } from '~/utils/sessions.server'; -import { Editor, Differ } from './components/cm.client'; -import { Unavailable } from './components/unavailable'; +import toast from '~/utils/toast'; +import { Differ, Editor } from './components/cm.client'; import { ErrorView } from './components/error'; +import { Unavailable } from './components/unavailable'; export async function loader({ request }: LoaderFunctionArgs) { const session = await getSession(request.headers.get('Cookie')); @@ -143,8 +143,6 @@ export async function action({ request }: ActionFunctionArgs) { }, ); } - - return { success: true, error: null }; } export default function Page() { diff --git a/app/routes/dns/components/domains.tsx b/app/routes/dns/components/domains.tsx index 50411cb..a54b83e 100644 --- a/app/routes/dns/components/domains.tsx +++ b/app/routes/dns/components/domains.tsx @@ -1,20 +1,20 @@ /* eslint-disable unicorn/no-keyword-prefix */ -import { closestCorners, DndContext, DragOverlay } from '@dnd-kit/core'; +import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core'; import { restrictToParentElement, restrictToVerticalAxis, } from '@dnd-kit/modifiers'; import { - arrayMove, SortableContext, + arrayMove, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { LockIcon, ThreeBarsIcon } from '@primer/octicons-react'; -import { type FetcherWithComponents, useFetcher } from 'react-router'; import { useEffect, useState } from 'react'; import { Button, Input } from 'react-aria-components'; +import { type FetcherWithComponents, useFetcher } from 'react-router'; import Spinner from '~/components/Spinner'; import TableList from '~/components/TableList'; @@ -91,8 +91,7 @@ export default function Domains({ > {localDomains.map((sd, index) => ( ({ + maxVisibleToasts: 7, +}); + +export function useToastQueue() { + return toastQueue; +} + +export default function toast(content: React.ReactNode, duration = 3000) { + return toastQueue.add(content, { timeout: duration }); +}