From 741f9aa6b5211f3b274e1a5d925bab1bb45673ee Mon Sep 17 00:00:00 2001
From: Aarnav Tale
Date: Sun, 26 Jan 2025 15:04:13 -0500
Subject: [PATCH] feat: switch to new dialog across all code
---
app/components/Dialog.tsx | 81 +++++---
app/routes/dns/components/magic.tsx | 59 +++---
app/routes/dns/components/rename.tsx | 67 +++----
app/routes/dns/dialogs/dns.tsx | 123 +++++-------
app/routes/dns/dialogs/nameserver.tsx | 235 +++++++++++------------
app/routes/dns/overview.tsx | 7 +-
app/routes/machines/action.tsx | 4 +-
app/routes/machines/components/menu.tsx | 89 ++++++---
app/routes/machines/dialogs/delete.tsx | 60 ++----
app/routes/machines/dialogs/expire.tsx | 59 ++----
app/routes/machines/dialogs/move.tsx | 85 +++------
app/routes/machines/dialogs/new.tsx | 117 +++++-------
app/routes/machines/dialogs/rename.tsx | 110 +++++------
app/routes/machines/dialogs/routes.tsx | 241 +++++++++++-------------
app/routes/machines/dialogs/tags.tsx | 210 +++++++++------------
app/routes/machines/overview.tsx | 12 +-
app/routes/settings/dialogs/expire.tsx | 59 +-----
app/routes/settings/dialogs/new.tsx | 192 +++++++------------
app/routes/users/components/auth.tsx | 2 +-
app/routes/users/components/oidc.tsx | 2 +-
app/routes/users/dialogs/add.tsx | 65 ++-----
app/routes/users/dialogs/remove.tsx | 57 ++----
app/routes/users/dialogs/rename.tsx | 74 +++-----
app/routes/users/overview.tsx | 19 +-
24 files changed, 833 insertions(+), 1196 deletions(-)
diff --git a/app/components/Dialog.tsx b/app/components/Dialog.tsx
index 7f8f2f3..f11dcd6 100644
--- a/app/components/Dialog.tsx
+++ b/app/components/Dialog.tsx
@@ -21,10 +21,12 @@ import Title from '~/components/Title';
import { cn } from '~/utils/cn';
export interface DialogProps extends OverlayTriggerProps {
- children: [
- React.ReactElement | React.ReactElement,
- React.ReactElement,
- ];
+ children:
+ | [
+ React.ReactElement | React.ReactElement,
+ React.ReactElement,
+ ]
+ | React.ReactElement;
}
function Dialog(props: DialogProps) {
@@ -36,34 +38,53 @@ function Dialog(props: DialogProps) {
state,
);
- const [button, panel] = props.children;
+ if (Array.isArray(props.children)) {
+ const [button, panel] = props.children;
+ return (
+ <>
+ {cloneElement(button, triggerProps)}
+ {state.isOpen && (
+
+ {cloneElement(panel, {
+ ...overlayProps,
+ close: () => state.close(),
+ })}
+
+ )}
+ >
+ );
+ }
+
return (
- <>
- {cloneElement(button, triggerProps)}
- {state.isOpen && (
-
- {cloneElement(panel, {
- ...overlayProps,
- close: () => state.close(),
- })}
-
- )}
- >
+
+ {cloneElement(props.children, {
+ ...overlayProps,
+ close: () => state.close(),
+ })}
+
);
}
export interface DialogPanelProps extends AriaDialogProps {
children: React.ReactNode;
- variant?: 'normal' | 'destructive';
+ variant?: 'normal' | 'destructive' | 'unactionable';
onSubmit?: React.FormEventHandler;
method?: HTMLFormMethod;
+ isDisabled?: boolean;
// Anonymous (passed by parent)
close?: () => void;
}
function Panel(props: DialogPanelProps) {
- const { children, onSubmit, close, variant, method = 'POST' } = props;
+ const {
+ children,
+ onSubmit,
+ isDisabled,
+ close,
+ variant,
+ method = 'POST',
+ } = props;
const ref = useRef(null);
const { dialogProps } = useDialog(
{
@@ -93,14 +114,22 @@ function Panel(props: DialogPanelProps) {
{children}
-
-
+ {variant === 'unactionable' ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
diff --git a/app/routes/dns/components/magic.tsx b/app/routes/dns/components/magic.tsx
index d97bc10..996a85d 100644
--- a/app/routes/dns/components/magic.tsx
+++ b/app/routes/dns/components/magic.tsx
@@ -1,5 +1,4 @@
import { useFetcher } from 'react-router';
-
import Dialog from '~/components/Dialog';
import Spinner from '~/components/Spinner';
@@ -8,6 +7,8 @@ type Properties = {
readonly disabled?: boolean;
};
+// TODO: Use form action instead of JSON patching
+// AND FIX JSON END OF UNEXPECTED INPUT
export default function Modal({ isEnabled, disabled }: Properties) {
const fetcher = useFetcher();
@@ -17,42 +18,26 @@ export default function Modal({ isEnabled, disabled }: Properties) {
{fetcher.state === 'idle' ? undefined : }
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
-
- {(close) => (
- <>
-
- {isEnabled ? 'Disable' : 'Enable'} Magic DNS
-
-
- Devices will no longer be accessible via your tailnet domain. The
- search domain will also be disabled.
-
-
-
- Cancel
-
- {
- fetcher.submit(
- {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- 'dns.magic_dns': !isEnabled,
- },
- {
- method: 'PATCH',
- encType: 'application/json',
- },
- );
-
- close();
- }}
- >
- {isEnabled ? 'Disable' : 'Enable'} Magic DNS
-
-
- >
- )}
+ {
+ fetcher.submit(
+ {
+ 'dns.magic_dns': !isEnabled,
+ },
+ {
+ method: 'PATCH',
+ encType: 'application/json',
+ },
+ );
+ }}
+ >
+
+ {isEnabled ? 'Disable' : 'Enable'} Magic DNS
+
+
+ Devices will no longer be accessible via your tailnet domain. The
+ search domain will also be disabled.
+
);
diff --git a/app/routes/dns/components/rename.tsx b/app/routes/dns/components/rename.tsx
index 29c0ca2..d1544af 100644
--- a/app/routes/dns/components/rename.tsx
+++ b/app/routes/dns/components/rename.tsx
@@ -1,6 +1,6 @@
-import { useFetcher } from 'react-router';
import { useState } from 'react';
import { Input } from 'react-aria-components';
+import { useFetcher } from 'react-router';
import Code from '~/components/Code';
import Dialog from '~/components/Dialog';
@@ -13,6 +13,7 @@ type Properties = {
readonly disabled?: boolean;
};
+// TODO: Switch to form submit instead of JSON patch
export default function Modal({ name, disabled }: Properties) {
const [newName, setNewName] = useState(name);
const fetcher = useFetcher();
@@ -45,46 +46,30 @@ export default function Modal({ name, disabled }: Properties) {
)}
Rename Tailnet
-
- {(close) => (
- <>
- Rename Tailnet
-
- Keep in mind that changing this can lead to all sorts of
- unexpected behavior and may break existing devices in your
- tailnet.
-
-
-
-
- Cancel
-
- {
- fetcher.submit(
- {
- 'dns.base_domain': newName,
- },
- {
- method: 'PATCH',
- encType: 'application/json',
- },
- );
-
- close();
- }}
- >
- Rename
-
-
- >
- )}
+ {
+ fetcher.submit(
+ {
+ 'dns.base_domain': newName,
+ },
+ {
+ method: 'PATCH',
+ encType: 'application/json',
+ },
+ );
+ }}
+ >
+ Rename Tailnet
+
+ Keep in mind that changing this can lead to all sorts of unexpected
+ behavior and may break existing devices in your tailnet.
+
+
diff --git a/app/routes/dns/dialogs/dns.tsx b/app/routes/dns/dialogs/dns.tsx
index 25ae8d8..099dbff 100644
--- a/app/routes/dns/dialogs/dns.tsx
+++ b/app/routes/dns/dialogs/dns.tsx
@@ -1,5 +1,5 @@
-import { Form, useSubmit } from 'react-router';
import { useMemo, useState } from 'react';
+import { useSubmit } from 'react-router';
import Code from '~/components/Code';
import Dialog from '~/components/Dialog';
@@ -23,80 +23,61 @@ export default function AddDNS({ records }: Props) {
return lookup.value === ip;
}, [records, name, ip]);
+ // TODO: Ditch useSubmit here (non JSON form)
return (
);
diff --git a/app/routes/dns/dialogs/nameserver.tsx b/app/routes/dns/dialogs/nameserver.tsx
index 2b5f98d..c021eda 100644
--- a/app/routes/dns/dialogs/nameserver.tsx
+++ b/app/routes/dns/dialogs/nameserver.tsx
@@ -1,6 +1,6 @@
import { RepoForkedIcon } from '@primer/octicons-react';
-import { Form, useSubmit } from 'react-router';
import { useState } from 'react';
+import { useSubmit } from 'react-router';
import Dialog from '~/components/Dialog';
import Switch from '~/components/Switch';
@@ -21,135 +21,116 @@ export default function AddNameserver({ nameservers }: Props) {
return (
);
diff --git a/app/routes/dns/overview.tsx b/app/routes/dns/overview.tsx
index 7a502fb..97f8292 100644
--- a/app/routes/dns/overview.tsx
+++ b/app/routes/dns/overview.tsx
@@ -49,7 +49,12 @@ export async function action({ request }: ActionFunctionArgs) {
return data({ success: false }, { status: 403 });
}
- const patch = (await request.json()) as Record;
+ const textData = await request.text();
+ if (!textData) {
+ return data({ success: true });
+ }
+
+ const patch = JSON.parse(textData) as Record;
await patchConfig(patch);
if (context.integration?.onConfigChange) {
diff --git a/app/routes/machines/action.tsx b/app/routes/machines/action.tsx
index dea6745..21012b9 100644
--- a/app/routes/machines/action.tsx
+++ b/app/routes/machines/action.tsx
@@ -1,8 +1,8 @@
import type { ActionFunctionArgs } from 'react-router';
import { del, post } 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';
export async function menuAction(request: ActionFunctionArgs['request']) {
const session = await getSession(request.headers.get('Cookie'));
diff --git a/app/routes/machines/components/menu.tsx b/app/routes/machines/components/menu.tsx
index 2edbb81..15769b7 100644
--- a/app/routes/machines/components/menu.tsx
+++ b/app/routes/machines/components/menu.tsx
@@ -1,6 +1,5 @@
import { KebabHorizontalIcon } from '@primer/octicons-react';
-import { type ReactNode, useState } from 'react';
-
+import React, { useState } from 'react';
import MenuComponent from '~/components/Menu';
import type { Machine, Route, User } from '~/types';
import { cn } from '~/utils/cn';
@@ -17,9 +16,11 @@ interface MenuProps {
routes: Route[];
users: User[];
magic?: string;
- buttonChild?: ReactNode;
+ buttonChild?: React.ReactNode;
}
+type Modal = 'rename' | 'expire' | 'remove' | 'routes' | 'move' | 'tags' | null;
+
export default function Menu({
machine,
routes,
@@ -27,12 +28,7 @@ export default function Menu({
users,
buttonChild,
}: MenuProps) {
- const renameState = useState(false);
- const expireState = useState(false);
- const removeState = useState(false);
- const routesState = useState(false);
- const moveState = useState(false);
- const tagsState = useState(false);
+ const [modal, setModal] = useState(null);
const expired =
machine.expiry === '0001-01-01 00:00:00' ||
@@ -43,12 +39,63 @@ export default function Menu({
return (
<>
-
-
- {expired ? undefined : }
-
-
-
+ {modal === 'remove' && (
+ {
+ if (!isOpen) setModal(null);
+ }}
+ />
+ )}
+ {modal === 'move' && (
+ {
+ if (!isOpen) setModal(null);
+ }}
+ />
+ )}
+ {modal === 'rename' && (
+ {
+ if (!isOpen) setModal(null);
+ }}
+ />
+ )}
+ {modal === 'routes' && (
+ {
+ if (!isOpen) setModal(null);
+ }}
+ />
+ )}
+ {modal === 'tags' && (
+ {
+ if (!isOpen) setModal(null);
+ }}
+ />
+ )}
+ {expired && modal === 'expire' ? undefined : (
+ {
+ if (!isOpen) setModal(null);
+ }}
+ />
+ )}
{buttonChild ?? (
@@ -63,26 +110,26 @@ export default function Menu({
)}
-
+ setModal('rename')}>
Edit machine name
-
+ setModal('routes')}>
Edit route settings
-
+ setModal('tags')}>
Edit ACL tags
-
+ setModal('move')}>
Change owner
{expired ? undefined : (
-
+ setModal('expire')}>
Expire
)}
setModal('remove')}
>
Remove
diff --git a/app/routes/machines/dialogs/delete.tsx b/app/routes/machines/dialogs/delete.tsx
index cb2006b..3200bd6 100644
--- a/app/routes/machines/dialogs/delete.tsx
+++ b/app/routes/machines/dialogs/delete.tsx
@@ -1,57 +1,23 @@
-import { Form, useSubmit } from 'react-router';
-import type { Dispatch, SetStateAction } from 'react';
-
import Dialog from '~/components/Dialog';
import type { Machine } from '~/types';
-import { cn } from '~/utils/cn';
interface DeleteProps {
- readonly machine: Machine;
- readonly state: [boolean, Dispatch>];
+ machine: Machine;
+ isOpen: boolean;
+ setIsOpen: (isOpen: boolean) => void;
}
-export default function Delete({ machine, state }: DeleteProps) {
- const submit = useSubmit();
-
+export default function Delete({ machine, isOpen, setIsOpen }: DeleteProps) {
return (
-
diff --git a/app/routes/users/components/oidc.tsx b/app/routes/users/components/oidc.tsx
index 509cc5a..4e5175d 100644
--- a/app/routes/users/components/oidc.tsx
+++ b/app/routes/users/components/oidc.tsx
@@ -41,7 +41,7 @@ export default function Oidc({ oidc, magic }: Props) {
manage users through your OIDC provider.
diff --git a/app/routes/users/dialogs/add.tsx b/app/routes/users/dialogs/add.tsx
index f50cac0..fe6348c 100644
--- a/app/routes/users/dialogs/add.tsx
+++ b/app/routes/users/dialogs/add.tsx
@@ -1,61 +1,24 @@
-import { Form, useSubmit } from 'react-router';
-import { useState } from 'react';
-
import Code from '~/components/Code';
import Dialog from '~/components/Dialog';
-import TextField from '~/components/TextField';
-
-interface Props {
- magic?: string;
-}
-
-export default function Add({ magic }: Props) {
- const [username, setUsername] = useState('');
- const submit = useSubmit();
+import Input from '~/components/Input';
+export default function Add() {
return (
Add a new user
-
- {(close) => (
- <>
- Add a new user
-
- Enter a username to create a new user.{' '}
- {magic ? (
- <>
- Since Magic DNS is enabled, machines will be accessible via{' '}
- [machine]. .{magic}.
- >
- ) : undefined}
-
-
- >
- )}
+ Add a new user
+
+ Enter a username to create a new user. Usernames can be addressed when
+ managing ACL policies.
+
+
+
);
diff --git a/app/routes/users/dialogs/remove.tsx b/app/routes/users/dialogs/remove.tsx
index 0fb2292..742074d 100644
--- a/app/routes/users/dialogs/remove.tsx
+++ b/app/routes/users/dialogs/remove.tsx
@@ -1,8 +1,4 @@
import { X } from 'lucide-react';
-import { Form, useSubmit } from 'react-router';
-import { useState } from 'react';
-
-import IconButton from '~/components/IconButton';
import Code from '~/components/Code';
import Dialog from '~/components/Dialog';
@@ -11,47 +7,20 @@ interface Props {
}
export default function Remove({ username }: Props) {
- const submit = useSubmit();
- const [dialog, setDialog] = useState(false);
-
return (
- <>
- setDialog(true)}
- >
+
+
-
-
-
- {(close) => (
- <>
- Delete {username}?
-
- Are you sure you want to delete {username}? A deleted user
- cannot be recovered.
-
-
- >
- )}
-
-
- >
+
+
+ Delete {username}?
+
+ Are you sure you want to delete {username}? A deleted user cannot be
+ recovered.
+
+
+
+
+
);
}
diff --git a/app/routes/users/dialogs/rename.tsx b/app/routes/users/dialogs/rename.tsx
index d50c813..48be347 100644
--- a/app/routes/users/dialogs/rename.tsx
+++ b/app/routes/users/dialogs/rename.tsx
@@ -1,65 +1,37 @@
import { Pencil } from 'lucide-react';
-import { Form, useSubmit } from 'react-router';
import { useState } from 'react';
-
-import IconButton from '~/components/IconButton';
import Dialog from '~/components/Dialog';
-import TextField from '~/components/TextField';
+import Input from '~/components/Input';
interface Props {
username: string;
- magic?: string;
}
-export default function Rename({ username, magic }: Props) {
- const submit = useSubmit();
- const [dialog, setDialog] = useState(false);
+// TODO: Server side validation before submitting
+export default function Rename({ username }: Props) {
const [newName, setNewName] = useState(username);
return (
- <>
- setDialog(true)}
- >
+
+
-
-
-
- {(close) => (
- <>
- Rename {username}?
-
- Enter a new username for {username}
-
-
- >
- )}
-
-
- >
+
+
+ Rename {username}?
+
+ Enter a new username for {username}. Changing a username will not
+ update any ACL policies that may refer to this user by their old
+ username.
+
+
+
+
+
+
);
}
diff --git a/app/routes/users/overview.tsx b/app/routes/users/overview.tsx
index 670fc0d..280518b 100644
--- a/app/routes/users/overview.tsx
+++ b/app/routes/users/overview.tsx
@@ -1,28 +1,23 @@
-import {
- DataRef,
- DndContext,
- useDraggable,
- useDroppable,
-} from '@dnd-kit/core';
+import { DataRef, DndContext, useDraggable, useDroppable } from '@dnd-kit/core';
import { PersonIcon } from '@primer/octicons-react';
+import { useEffect, useState } from 'react';
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
import { useActionData, useLoaderData, useSubmit } from 'react-router';
-import { useEffect, useState } from 'react';
import { ClientOnly } from 'remix-utils/client-only';
import Attribute from '~/components/Attribute';
import Card from '~/components/Card';
-import StatusCircle from '~/components/StatusCircle';
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';
import { loadConfig } from '~/utils/config/headscale';
import { del, post, pull } from '~/utils/headscale';
+import { send } from '~/utils/res';
import { getSession } from '~/utils/sessions.server';
import { useLiveData } from '~/utils/useLiveData';
-import { send } from '~/utils/res';
import Auth from './components/auth';
import Oidc from './components/oidc';
@@ -305,7 +300,7 @@ function UserCard({ user, magic }: CardProps) {
{user.name}
-
+
{user.machines.length === 0 ? (
) : undefined}
@@ -322,7 +317,5 @@ function UserCard({ user, magic }: CardProps) {
}
export function ErrorBoundary() {
- return (
-
- )
+ return ;
}