From 358629a93b9aa9ba9c7bde920b5e99ce41d54e6a Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Thu, 28 Mar 2024 18:47:28 -0400 Subject: [PATCH] feat: experimental support for draggable domains --- app/routes/_data.dns._index/domains.tsx | 213 ++++++++++++++++++++++++ app/routes/_data.dns._index/route.tsx | 45 +---- package.json | 3 + pnpm-lock.yaml | 56 +++++++ 4 files changed, 279 insertions(+), 38 deletions(-) create mode 100644 app/routes/_data.dns._index/domains.tsx diff --git a/app/routes/_data.dns._index/domains.tsx b/app/routes/_data.dns._index/domains.tsx new file mode 100644 index 0000000..ec2befd --- /dev/null +++ b/app/routes/_data.dns._index/domains.tsx @@ -0,0 +1,213 @@ +/* eslint-disable unicorn/no-keyword-prefix */ +import { + closestCenter, + DndContext, + DragOverlay, + PointerSensor, + TouchSensor, + useSensor, + useSensors +} from '@dnd-kit/core' +import { + arrayMove, + SortableContext, + useSortable, + verticalListSortingStrategy +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { Bars3Icon } from '@heroicons/react/24/outline' +import { useFetcher, useRevalidator } from '@remix-run/react' +import clsx from 'clsx' +import { useState } from 'react' + +type Properties = { + readonly baseDomain?: string; + readonly searchDomains: string[]; +} + +export default function Domains({ baseDomain, searchDomains }: Properties) { + // eslint-disable-next-line unicorn/no-null, @typescript-eslint/ban-types + const [activeId, setActiveId] = useState(null) + const [localDomains, setLocalDomains] = useState(searchDomains) + const [newDomain, setNewDomain] = useState('') + const fetcher = useFetcher({ key: 'search-domains' }) + const revalidator = useRevalidator() + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(TouchSensor) + ) + + return ( +
+

Search Domains

+

+ Set custom DNS search domains for your Tailnet. + When using Magic DNS, your tailnet domain is used as the first search domain. +

+
+ {baseDomain ? ( +
+

{baseDomain}

+
+ ) : undefined} + { + setActiveId(event.active.id) + }} + onDragEnd={event => { + // eslint-disable-next-line unicorn/no-null + setActiveId(null) + const { active, over } = event + if (!over) { + return + } + + const activeItem = localDomains[active.id as number - 1] + const overItem = localDomains[over.id as number - 1] + + if (!activeItem || !overItem) { + return + } + + const oldIndex = localDomains.indexOf(activeItem) + const newIndex = localDomains.indexOf(overItem) + + if (oldIndex !== newIndex) { + setLocalDomains(arrayMove(localDomains, oldIndex, newIndex)) + } + }} + > + + {localDomains.map((sd, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + + {activeId ? : undefined} + + +
+ { + setNewDomain(event.target.value) + }} + /> + +
+
+
+ ) +} + +type DomainProperties = { + readonly domain: string; + readonly id: number; + readonly isDrag?: boolean; + readonly localDomains: string[]; +} + +function Domain({ domain, id, localDomains, isDrag }: DomainProperties) { + const fetcher = useFetcher({ key: 'individual-domain' }) + const revalidator = useRevalidator() + + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id }) + + return ( +
+

+ + {domain} +

+ {isDrag ? undefined : ( + + )} +
+ ) +} diff --git a/app/routes/_data.dns._index/route.tsx b/app/routes/_data.dns._index/route.tsx index c665b06..394f09c 100644 --- a/app/routes/_data.dns._index/route.tsx +++ b/app/routes/_data.dns._index/route.tsx @@ -6,6 +6,7 @@ import { useState } from 'react' import { getConfig, patchConfig } from '~/utils/config' +import Domains from './domains' import MagicModal from './magic' import RenameModal from './rename' @@ -28,6 +29,7 @@ export async function loader() { export async function action({ request }: ActionFunctionArgs) { const data = await request.json() as Record + console.log(data) await patchConfig(data) return json({ success: true }) } @@ -163,44 +165,11 @@ export default function Page() { -
-

Search Domains

-

- Set custom DNS search domains for your Tailnet. - When using Magic DNS, your tailnet domain is used as the first search domain. -

-
- {data.magicDns ? ( -
-

{data.baseDomain}

-
- ) : undefined} - {data.searchDomains.map((sd, index) => ( -
-

{sd}

- -
- ))} -
-
+ +

Magic DNS

diff --git a/package.json b/package.json index 89d6b15..37cafe9 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "typecheck": "tsc" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.3", "@remix-run/node": "^2.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ba451e..cfbb758 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: + '@dnd-kit/core': + specifier: ^6.1.0 + version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.2.0) '@headlessui/react': specifier: ^1.7.18 version: 1.7.18(react-dom@18.2.0)(react@18.2.0) @@ -431,6 +440,49 @@ packages: to-fast-properties: 2.0.0 dev: true + /@dnd-kit/accessibility@3.1.0(react@18.2.0): + resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@dnd-kit/accessibility': 3.1.0(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + dev: false + + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@dnd-kit/utilities@3.2.2(react@18.2.0): + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + /@emotion/hash@0.9.1: resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} dev: true @@ -6054,6 +6106,10 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + /tsutils@3.21.0(typescript@5.4.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'}