feat(TALE-10): edit machine owner via dropdown
This commit is contained in:
parent
ddd20fe027
commit
d71e55d7af
@ -1,10 +1,11 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node'
|
import { ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node'
|
||||||
import { Link, useLoaderData } from '@remix-run/react'
|
import { Link, useLoaderData } from '@remix-run/react'
|
||||||
|
|
||||||
import Attribute from '~/components/Attribute'
|
import Attribute from '~/components/Attribute'
|
||||||
import Card from '~/components/Card'
|
import Card from '~/components/Card'
|
||||||
import StatusCircle from '~/components/StatusCircle'
|
import StatusCircle from '~/components/StatusCircle'
|
||||||
import { type Machine, Route } from '~/types'
|
import { type Machine, Route, User } from '~/types'
|
||||||
import { cn } from '~/utils/cn'
|
import { cn } from '~/utils/cn'
|
||||||
import { loadContext } from '~/utils/config/headplane'
|
import { loadContext } from '~/utils/config/headplane'
|
||||||
import { loadConfig } from '~/utils/config/headscale'
|
import { loadConfig } from '~/utils/config/headscale'
|
||||||
@ -31,16 +32,16 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [machine, routes] = await Promise.all([
|
const [machine, routes, users] = await Promise.all([
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
pull<{ node: Machine }>(`v1/node/${params.id}`, session.get('hsApiKey')!),
|
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<{ routes: Route[] }>('v1/routes', session.get('hsApiKey')!),
|
||||||
|
pull<{ users: User[] }>('v1/user', session.get('hsApiKey')!),
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
machine: machine.node,
|
machine: machine.node,
|
||||||
routes: routes.routes.filter(route => route.node.id === params.id),
|
routes: routes.routes.filter(route => route.node.id === params.id),
|
||||||
|
users: users.users,
|
||||||
magic,
|
magic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,7 +51,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { machine, magic, routes } = useLoaderData<typeof loader>()
|
const { machine, magic, routes, users } = useLoaderData<typeof loader>()
|
||||||
useLiveData({ interval: 1000 })
|
useLiveData({ interval: 1000 })
|
||||||
|
|
||||||
const expired = machine.expiry === '0001-01-01 00:00:00'
|
const expired = machine.expiry === '0001-01-01 00:00:00'
|
||||||
@ -92,6 +93,7 @@ export default function Page() {
|
|||||||
<MenuOptions
|
<MenuOptions
|
||||||
machine={machine}
|
machine={machine}
|
||||||
routes={routes}
|
routes={routes}
|
||||||
|
users={users}
|
||||||
magic={magic}
|
magic={magic}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -61,6 +61,25 @@ export async function menuAction(request: ActionFunctionArgs['request']) {
|
|||||||
return json({ message: 'Route updated' })
|
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: {
|
default: {
|
||||||
return json({ message: 'Invalid method' }, {
|
return json({ message: 'Invalid method' }, {
|
||||||
status: 400,
|
status: 400,
|
||||||
|
|||||||
119
app/routes/_data.machines._index/dialogs/move.tsx
Normal file
119
app/routes/_data.machines._index/dialogs/move.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ import { Link } from '@remix-run/react'
|
|||||||
import Menu from '~/components/Menu'
|
import Menu from '~/components/Menu'
|
||||||
import StatusCircle from '~/components/StatusCircle'
|
import StatusCircle from '~/components/StatusCircle'
|
||||||
import { toast } from '~/components/Toaster'
|
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 { cn } from '~/utils/cn'
|
||||||
|
|
||||||
import MenuOptions from './menu'
|
import MenuOptions from './menu'
|
||||||
@ -12,10 +12,11 @@ import MenuOptions from './menu'
|
|||||||
interface Props {
|
interface Props {
|
||||||
readonly machine: Machine
|
readonly machine: Machine
|
||||||
readonly routes: Route[]
|
readonly routes: Route[]
|
||||||
|
readonly users: User[]
|
||||||
readonly magic?: string
|
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'
|
const expired = machine.expiry === '0001-01-01 00:00:00'
|
||||||
|| machine.expiry === '0001-01-01T00:00:00Z'
|
|| machine.expiry === '0001-01-01T00:00:00Z'
|
||||||
? false
|
? false
|
||||||
@ -142,6 +143,7 @@ export default function MachineRow({ machine, routes, magic }: Props) {
|
|||||||
<MenuOptions
|
<MenuOptions
|
||||||
machine={machine}
|
machine={machine}
|
||||||
routes={routes}
|
routes={routes}
|
||||||
|
users={users}
|
||||||
magic={magic}
|
magic={magic}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -2,25 +2,28 @@ import { KebabHorizontalIcon } from '@primer/octicons-react'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import MenuComponent from '~/components/Menu'
|
import MenuComponent from '~/components/Menu'
|
||||||
import { Machine, Route } from '~/types'
|
import { Machine, Route, User } from '~/types'
|
||||||
import { cn } from '~/utils/cn'
|
import { cn } from '~/utils/cn'
|
||||||
|
|
||||||
import Delete from './dialogs/delete'
|
import Delete from './dialogs/delete'
|
||||||
import Expire from './dialogs/expire'
|
import Expire from './dialogs/expire'
|
||||||
|
import Move from './dialogs/move'
|
||||||
import Rename from './dialogs/rename'
|
import Rename from './dialogs/rename'
|
||||||
import Routes from './dialogs/routes'
|
import Routes from './dialogs/routes'
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
machine: Machine
|
machine: Machine
|
||||||
routes: Route[]
|
routes: Route[]
|
||||||
|
users: User[]
|
||||||
magic?: string
|
magic?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Menu({ machine, routes, magic }: MenuProps) {
|
export default function Menu({ machine, routes, magic, users }: MenuProps) {
|
||||||
const renameState = useState(false)
|
const renameState = useState(false)
|
||||||
const expireState = useState(false)
|
const expireState = useState(false)
|
||||||
const removeState = useState(false)
|
const removeState = useState(false)
|
||||||
const routesState = useState(false)
|
const routesState = useState(false)
|
||||||
|
const moveState = useState(false)
|
||||||
|
|
||||||
const expired = machine.expiry === '0001-01-01 00:00:00'
|
const expired = machine.expiry === '0001-01-01 00:00:00'
|
||||||
|| machine.expiry === '0001-01-01T00:00:00Z'
|
|| machine.expiry === '0001-01-01T00:00:00Z'
|
||||||
@ -51,6 +54,12 @@ export default function Menu({ machine, routes, magic }: MenuProps) {
|
|||||||
routes={routes}
|
routes={routes}
|
||||||
state={routesState}
|
state={routesState}
|
||||||
/>
|
/>
|
||||||
|
<Move
|
||||||
|
machine={machine}
|
||||||
|
state={moveState}
|
||||||
|
users={users}
|
||||||
|
magic={magic}
|
||||||
|
/>
|
||||||
|
|
||||||
<MenuComponent>
|
<MenuComponent>
|
||||||
<MenuComponent.Button
|
<MenuComponent.Button
|
||||||
@ -72,6 +81,9 @@ export default function Menu({ machine, routes, magic }: MenuProps) {
|
|||||||
<MenuComponent.Item className="opacity-50 hover:bg-transparent">
|
<MenuComponent.Item className="opacity-50 hover:bg-transparent">
|
||||||
Edit ACL tags
|
Edit ACL tags
|
||||||
</MenuComponent.Item>
|
</MenuComponent.Item>
|
||||||
|
<MenuComponent.ItemButton control={moveState}>
|
||||||
|
Change owner
|
||||||
|
</MenuComponent.ItemButton>
|
||||||
{expired
|
{expired
|
||||||
? undefined
|
? undefined
|
||||||
: (
|
: (
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { InfoIcon } from '@primer/octicons-react'
|
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 { useLoaderData } from '@remix-run/react'
|
||||||
import { Button, Tooltip, TooltipTrigger } from 'react-aria-components'
|
import { Button, Tooltip, TooltipTrigger } from 'react-aria-components'
|
||||||
|
|
||||||
import Code from '~/components/Code'
|
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 { cn } from '~/utils/cn'
|
||||||
import { loadContext } from '~/utils/config/headplane'
|
import { loadContext } from '~/utils/config/headplane'
|
||||||
import { loadConfig } from '~/utils/config/headscale'
|
import { loadConfig } from '~/utils/config/headscale'
|
||||||
import { del, post, pull } from '~/utils/headscale'
|
import { pull } from '~/utils/headscale'
|
||||||
import { getSession } from '~/utils/sessions'
|
import { getSession } from '~/utils/sessions'
|
||||||
import { useLiveData } from '~/utils/useLiveData'
|
import { useLiveData } from '~/utils/useLiveData'
|
||||||
|
|
||||||
@ -18,9 +18,10 @@ import MachineRow from './machine'
|
|||||||
|
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
const session = await getSession(request.headers.get('Cookie'))
|
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<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!),
|
||||||
pull<{ routes: Route[] }>('v1/routes', session.get('hsApiKey')!),
|
pull<{ routes: Route[] }>('v1/routes', session.get('hsApiKey')!),
|
||||||
|
pull<{ users: User[] }>('v1/user', session.get('hsApiKey')!),
|
||||||
])
|
])
|
||||||
|
|
||||||
const context = await loadContext()
|
const context = await loadContext()
|
||||||
@ -36,6 +37,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
return {
|
return {
|
||||||
nodes: machines.nodes,
|
nodes: machines.nodes,
|
||||||
routes: routes.routes,
|
routes: routes.routes,
|
||||||
|
users: users.users,
|
||||||
magic,
|
magic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,6 +98,7 @@ export default function Page() {
|
|||||||
key={machine.id}
|
key={machine.id}
|
||||||
machine={machine}
|
machine={machine}
|
||||||
routes={data.routes.filter(route => route.node.id === machine.id)}
|
routes={data.routes.filter(route => route.node.id === machine.id)}
|
||||||
|
users={data.users}
|
||||||
magic={data.magic}
|
magic={data.magic}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user