feat: switch to new toast provider
This commit is contained in:
parent
a19eb6bcda
commit
2c8880c84d
@ -1,5 +1,5 @@
|
||||
import { CopyIcon } from '@primer/octicons-react';
|
||||
import { toast } from './Toaster';
|
||||
import toast from '~/utils/toast';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
|
||||
87
app/components/ToastProvider.tsx
Normal file
87
app/components/ToastProvider.tsx
Normal 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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
24
app/root.tsx
24
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 (
|
||||
<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 />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
14
app/utils/toast.ts
Normal 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 });
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user