feat: switch to new toast provider

This commit is contained in:
Aarnav Tale 2025-01-28 16:06:41 -05:00
parent a19eb6bcda
commit 2c8880c84d
No known key found for this signature in database
8 changed files with 151 additions and 49 deletions

View File

@ -1,5 +1,5 @@
import { CopyIcon } from '@primer/octicons-react';
import { toast } from './Toaster';
import toast from '~/utils/toast';
interface Props {
name: string;

View File

@ -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<React.ReactNode> {
state: ToastState<React.ReactNode>;
}
function Toast({ state, ...props }: ToastProps) {
const ref = useRef<HTMLDivElement | null>(null);
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
props,
state,
ref,
);
return (
<div
{...toastProps}
ref={ref}
className={cn(
'flex items-center justify-between gap-x-3 pl-4 pr-3',
'text-white shadow-lg dark:shadow-md rounded-xl py-3',
'bg-headplane-900 dark:bg-headplane-950',
)}
>
<div {...contentProps} className="flex flex-col gap-2">
<div {...titleProps}>{props.toast.content}</div>
</div>
<IconButton
{...closeButtonProps}
label="Close"
className={cn(
'bg-transparent hover:bg-headplane-700',
'dark:bg-transparent dark:hover:bg-headplane-800',
)}
>
<X className="p-1" />
</IconButton>
</div>
);
}
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<React.ReactNode>;
}
function ToastRegion({ state, ...props }: ToastRegionProps) {
const ref = useRef<HTMLDivElement | null>(null);
const { regionProps } = useToastRegion(props, state, ref);
return (
<div
{...regionProps}
ref={ref}
className={cn('fixed bottom-20 right-4', 'flex flex-col gap-4')}
>
{state.visibleToasts.map((toast) => (
<Toast key={toast.key} toast={toast} state={state} />
))}
</div>
);
}
export interface ToastProviderProps extends AriaToastRegionProps {
queue: ToastQueue<React.ReactNode>;
}
export default function ToastProvider({ queue, ...props }: ToastProviderProps) {
const state = useToastQueue(queue);
return (
<>
{state.visibleToasts.length > 0 && (
<ToastRegion {...props} state={state} />
)}
</>
);
}

View File

@ -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<typeof loader>()
const { healthy } = useLoaderData<typeof loader>();
return (
<>
{!healthy ? (
<div className={cn(
'fixed bottom-0 right-0 z-50 w-fit h-14',
'flex flex-col justify-center gap-1',
)}>
<div className={cn(
'flex items-center gap-1.5 mr-1.5 py-2 px-1.5',
'border rounded-lg text-white bg-red-500',
'border-red-600 dark:border-red-400 shadow-sm',
)}>
<div
className={cn(
'fixed bottom-0 right-0 z-50 w-fit h-14',
'flex flex-col justify-center gap-1',
)}
>
<div
className={cn(
'flex items-center gap-1.5 mr-1.5 py-2 px-1.5',
'border rounded-lg text-white bg-red-500',
'border-red-600 dark:border-red-400 shadow-sm',
)}
>
<XCircleFillIcon className="w-4 h-4 text-white" />
Headscale is unreachable
</div>

View File

@ -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 (
<html lang="en">
<head>
@ -33,7 +41,7 @@ export function Layout({ children }: { readonly children: React.ReactNode }) {
</head>
<body className="overscroll-none dark:bg-headplane-900 dark:text-headplane-50">
{children}
<Toaster />
<ToastProvider queue={toastQueue} />
<ScrollRestoration />
<Scripts />
</body>
@ -61,5 +69,5 @@ export default function App() {
</ProgressBar>
<Outlet />
</>
)
);
}

View File

@ -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() {

View File

@ -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) => (
<Domain
// eslint-disable-next-line react/no-array-index-key
key={index}
key={sd}
domain={sd}
id={index + 1}
localDomains={localDomains}

View File

@ -9,7 +9,6 @@ import Attribute from '~/components/Attribute';
import Card from '~/components/Card';
import { ErrorPopup } from '~/components/Error';
import StatusCircle from '~/components/StatusCircle';
import { toast } from '~/components/Toaster';
import type { Machine, User } from '~/types';
import { cn } from '~/utils/cn';
import { loadContext } from '~/utils/config/headplane';
@ -19,6 +18,7 @@ import { send } from '~/utils/res';
import { getSession } from '~/utils/sessions.server';
import { useLiveData } from '~/utils/useLiveData';
import toast from '~/utils/toast';
import Auth from './components/auth';
import Oidc from './components/oidc';
import Remove from './dialogs/remove';

14
app/utils/toast.ts Normal file
View File

@ -0,0 +1,14 @@
import { ToastQueue } from '@react-stately/toast';
import React from 'react';
const toastQueue = new ToastQueue<React.ReactNode>({
maxVisibleToasts: 7,
});
export function useToastQueue() {
return toastQueue;
}
export default function toast(content: React.ReactNode, duration = 3000) {
return toastQueue.add(content, { timeout: duration });
}