feat(TALE-10): edit machine owner via dropdown

This commit is contained in:
Aarnav Tale 2024-06-23 17:25:33 -04:00
parent ddd20fe027
commit d71e55d7af
No known key found for this signature in database
6 changed files with 170 additions and 13 deletions

View File

@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node'
import { Link, useLoaderData } from '@remix-run/react'
import Attribute from '~/components/Attribute'
import Card from '~/components/Card'
import StatusCircle from '~/components/StatusCircle'
import { type Machine, Route } from '~/types'
import { type Machine, Route, User } from '~/types'
import { cn } from '~/utils/cn'
import { loadContext } from '~/utils/config/headplane'
import { loadConfig } from '~/utils/config/headscale'
@ -31,16 +32,16 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
}
}
const [machine, routes] = await Promise.all([
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [machine, routes, users] = await Promise.all([
pull<{ node: Machine }>(`v1/node/${params.id}`, session.get('hsApiKey')!),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
pull<{ routes: Route[] }>('v1/routes', session.get('hsApiKey')!),
pull<{ users: User[] }>('v1/user', session.get('hsApiKey')!),
])
return {
machine: machine.node,
routes: routes.routes.filter(route => route.node.id === params.id),
users: users.users,
magic,
}
}
@ -50,7 +51,7 @@ export async function action({ request }: ActionFunctionArgs) {
}
export default function Page() {
const { machine, magic, routes } = useLoaderData<typeof loader>()
const { machine, magic, routes, users } = useLoaderData<typeof loader>()
useLiveData({ interval: 1000 })
const expired = machine.expiry === '0001-01-01 00:00:00'
@ -92,6 +93,7 @@ export default function Page() {
<MenuOptions
machine={machine}
routes={routes}
users={users}
magic={magic}
/>
</div>

View File

@ -61,6 +61,25 @@ export async function menuAction(request: ActionFunctionArgs['request']) {
return json({ message: 'Route updated' })
}
case 'move': {
if (!data.has('to')) {
return json({ message: 'No destination provided' }, {
status: 400,
})
}
const to = String(data.get('to'))
try {
await post(`v1/node/${id}/user?user=${to}`, session.get('hsApiKey')!)
return json({ message: `Moved node ${id} to ${to}` })
} catch {
return json({ message: `Failed to move node ${id} to ${to}` }, {
status: 500,
})
}
}
default: {
return json({ message: 'Invalid method' }, {
status: 400,

View File

@ -0,0 +1,119 @@
import { Form, useSubmit } from '@remix-run/react'
import { type Dispatch, type SetStateAction, useState } from 'react'
import Code from '~/components/Code'
import Dialog from '~/components/Dialog'
import Select from '~/components/Select'
import { type Machine, User } from '~/types'
interface MoveProps {
readonly machine: Machine
readonly users: User[]
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
readonly magic?: string
}
export default function Move({ machine, state, magic, users }: MoveProps) {
const [owner, setOwner] = useState(machine.user.name)
const submit = useSubmit()
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
<>
<Dialog.Title>
Change the owner of
{' '}
{machine.givenName}
</Dialog.Title>
<Dialog.Text>
The owner of the machine is the user associated with it.
When MagicDNS is enabled, the username of the owner
will control the hostname of the machine.
</Dialog.Text>
<Form
method="POST"
onSubmit={(e) => {
submit(e.currentTarget)
}}
>
<input type="hidden" name="_method" value="move" />
<input type="hidden" name="id" value={machine.id} />
<Select
label="Owner"
name="to"
placeholder="Select a user"
state={[owner, setOwner]}
>
{users.map(user => (
<Select.Item key={user.id} id={user.name}>
{user.name}
</Select.Item>
))}
</Select>
{magic
? (
owner === machine.user.name
? (
<p className="text-sm text-gray-500 dark:text-gray-300 leading-tight">
This machine is accessible by the hostname
{' '}
<Code className="text-sm">
{machine.givenName}
.
{owner}
.
{magic}
</Code>
.
</p>
)
: (
<p className="text-sm text-gray-500 dark:text-gray-300 leading-tight">
This machine will be accessible by the hostname
{' '}
<Code className="text-sm">
{machine.givenName}
.
{owner}
.
{magic}
</Code>
{'. '}
The hostname
{' '}
<Code className="text-sm">
{machine.givenName}
.
{machine.user.name}
.
{magic}
</Code>
{' '}
will no longer point to this machine.
</p>
)
)
: undefined}
<div className="mt-6 flex justify-end gap-2 mt-6">
<Dialog.Action
variant="cancel"
onPress={close}
>
Cancel
</Dialog.Action>
<Dialog.Action
variant="confirm"
onPress={close}
>
Change owner
</Dialog.Action>
</div>
</Form>
</>
)}
</Dialog.Panel>
</Dialog>
)
}

View File

@ -4,7 +4,7 @@ import { Link } from '@remix-run/react'
import Menu from '~/components/Menu'
import StatusCircle from '~/components/StatusCircle'
import { toast } from '~/components/Toaster'
import { type Machine, type Route } from '~/types'
import { type Machine, type Route, User } from '~/types'
import { cn } from '~/utils/cn'
import MenuOptions from './menu'
@ -12,10 +12,11 @@ import MenuOptions from './menu'
interface Props {
readonly machine: Machine
readonly routes: Route[]
readonly users: User[]
readonly magic?: string
}
export default function MachineRow({ machine, routes, magic }: Props) {
export default function MachineRow({ machine, routes, magic, users }: Props) {
const expired = machine.expiry === '0001-01-01 00:00:00'
|| machine.expiry === '0001-01-01T00:00:00Z'
? false
@ -142,6 +143,7 @@ export default function MachineRow({ machine, routes, magic }: Props) {
<MenuOptions
machine={machine}
routes={routes}
users={users}
magic={magic}
/>
</td>

View File

@ -2,25 +2,28 @@ import { KebabHorizontalIcon } from '@primer/octicons-react'
import { useState } from 'react'
import MenuComponent from '~/components/Menu'
import { Machine, Route } from '~/types'
import { Machine, Route, User } from '~/types'
import { cn } from '~/utils/cn'
import Delete from './dialogs/delete'
import Expire from './dialogs/expire'
import Move from './dialogs/move'
import Rename from './dialogs/rename'
import Routes from './dialogs/routes'
interface MenuProps {
machine: Machine
routes: Route[]
users: User[]
magic?: string
}
export default function Menu({ machine, routes, magic }: MenuProps) {
export default function Menu({ machine, routes, magic, users }: MenuProps) {
const renameState = useState(false)
const expireState = useState(false)
const removeState = useState(false)
const routesState = useState(false)
const moveState = useState(false)
const expired = machine.expiry === '0001-01-01 00:00:00'
|| machine.expiry === '0001-01-01T00:00:00Z'
@ -51,6 +54,12 @@ export default function Menu({ machine, routes, magic }: MenuProps) {
routes={routes}
state={routesState}
/>
<Move
machine={machine}
state={moveState}
users={users}
magic={magic}
/>
<MenuComponent>
<MenuComponent.Button
@ -72,6 +81,9 @@ export default function Menu({ machine, routes, magic }: MenuProps) {
<MenuComponent.Item className="opacity-50 hover:bg-transparent">
Edit ACL tags
</MenuComponent.Item>
<MenuComponent.ItemButton control={moveState}>
Change owner
</MenuComponent.ItemButton>
{expired
? undefined
: (

View File

@ -1,15 +1,15 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { InfoIcon } from '@primer/octicons-react'
import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from '@remix-run/node'
import { type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { Button, Tooltip, TooltipTrigger } from 'react-aria-components'
import Code from '~/components/Code'
import { type Machine, type Route } from '~/types'
import { type Machine, type Route, 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 { pull } from '~/utils/headscale'
import { getSession } from '~/utils/sessions'
import { useLiveData } from '~/utils/useLiveData'
@ -18,9 +18,10 @@ import MachineRow from './machine'
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'))
const [machines, routes] = await Promise.all([
const [machines, routes, users] = await Promise.all([
pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!),
pull<{ routes: Route[] }>('v1/routes', session.get('hsApiKey')!),
pull<{ users: User[] }>('v1/user', session.get('hsApiKey')!),
])
const context = await loadContext()
@ -36,6 +37,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
return {
nodes: machines.nodes,
routes: routes.routes,
users: users.users,
magic,
}
}
@ -96,6 +98,7 @@ export default function Page() {
key={machine.id}
machine={machine}
routes={data.routes.filter(route => route.node.id === machine.id)}
users={data.users}
magic={data.magic}
/>
))}