feat: add ssh buttons in the ui using hostinfo checks
This commit is contained in:
parent
b34a3f0ca1
commit
1e86b0e95b
21
app/components/tags/TailscaleSSH.tsx
Normal file
21
app/components/tags/TailscaleSSH.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import cn from '~/utils/cn';
|
||||||
|
import Chip from '../Chip';
|
||||||
|
import Tooltip from '../Tooltip';
|
||||||
|
|
||||||
|
export function TailscaleSSHTag() {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<Chip
|
||||||
|
text="Tailscale SSH"
|
||||||
|
className={cn(
|
||||||
|
'bg-lime-500 text-lime-900 dark:bg-lime-900 dark:text-lime-500',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Tooltip.Body>
|
||||||
|
This machine advertises Tailscale SSH, which allows you to authenticate
|
||||||
|
SSH credentials using your Tailscale account and via the Headplane web
|
||||||
|
UI.
|
||||||
|
</Tooltip.Body>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ import { ExitNodeTag } from '~/components/tags/ExitNode';
|
|||||||
import { ExpiryTag } from '~/components/tags/Expiry';
|
import { ExpiryTag } from '~/components/tags/Expiry';
|
||||||
import { HeadplaneAgentTag } from '~/components/tags/HeadplaneAgent';
|
import { HeadplaneAgentTag } from '~/components/tags/HeadplaneAgent';
|
||||||
import { SubnetTag } from '~/components/tags/Subnet';
|
import { SubnetTag } from '~/components/tags/Subnet';
|
||||||
|
import { TailscaleSSHTag } from '~/components/tags/TailscaleSSH';
|
||||||
import { PopulatedNode } from '~/utils/node-info';
|
import { PopulatedNode } from '~/utils/node-info';
|
||||||
import toast from '~/utils/toast';
|
import toast from '~/utils/toast';
|
||||||
import MenuOptions from './menu';
|
import MenuOptions from './menu';
|
||||||
@ -177,6 +178,10 @@ export function uiTagsForNode(node: PopulatedNode, isAgent?: boolean) {
|
|||||||
uiTags.push('subnet-approved');
|
uiTags.push('subnet-approved');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.hostInfo?.sshHostKeys && node.hostInfo?.sshHostKeys.length > 0) {
|
||||||
|
uiTags.push('tailscale-ssh');
|
||||||
|
}
|
||||||
|
|
||||||
if (isAgent === true) {
|
if (isAgent === true) {
|
||||||
uiTags.push('headplane-agent');
|
uiTags.push('headplane-agent');
|
||||||
}
|
}
|
||||||
@ -205,6 +210,9 @@ export function mapTagsToComponents(node: PopulatedNode, uiTags: string[]) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'tailscale-ssh':
|
||||||
|
return <TailscaleSSHTag />;
|
||||||
|
|
||||||
case 'headplane-agent':
|
case 'headplane-agent':
|
||||||
return <HeadplaneAgentTag />;
|
return <HeadplaneAgentTag />;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Cog, Ellipsis } from 'lucide-react';
|
import { Cog, Ellipsis, SquareTerminal } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Menu from '~/components/Menu';
|
import Menu from '~/components/Menu';
|
||||||
import type { User } from '~/types';
|
import type { User } from '~/types';
|
||||||
@ -10,7 +10,6 @@ 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';
|
||||||
import Tags from '../dialogs/tags';
|
import Tags from '../dialogs/tags';
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
node: PopulatedNode;
|
node: PopulatedNode;
|
||||||
users: User[];
|
users: User[];
|
||||||
@ -29,6 +28,9 @@ export default function MachineMenu({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
}: MenuProps) {
|
}: MenuProps) {
|
||||||
const [modal, setModal] = useState<Modal>(null);
|
const [modal, setModal] = useState<Modal>(null);
|
||||||
|
const supportsTailscaleSSH =
|
||||||
|
node.hostInfo?.sshHostKeys && node.hostInfo?.sshHostKeys.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{modal === 'remove' && (
|
{modal === 'remove' && (
|
||||||
@ -90,21 +92,69 @@ export default function MachineMenu({
|
|||||||
|
|
||||||
<Menu isDisabled={isDisabled}>
|
<Menu isDisabled={isDisabled}>
|
||||||
{isFullButton ? (
|
{isFullButton ? (
|
||||||
<Menu.Button className="flex items-center gap-x-2">
|
<div className="flex items-center justify-end gap-1.5 pr-4">
|
||||||
<Cog className="h-5" />
|
{supportsTailscaleSSH ? (
|
||||||
<p>Machine Settings</p>
|
<Menu.Button
|
||||||
</Menu.Button>
|
className="flex items-center gap-x-2"
|
||||||
|
variant="heavy"
|
||||||
|
onPress={() => {
|
||||||
|
// We need to use JS to open the SSH URL
|
||||||
|
// in a new WINDOW since href can only
|
||||||
|
// do a new TAB.
|
||||||
|
window.open(
|
||||||
|
// TODO: Use the actual real username lol
|
||||||
|
`${__PREFIX__}/ssh?hostname=${node.name}&username=tale`,
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer,width=800,height=600',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SquareTerminal className="h-5" />
|
||||||
|
<p>SSH</p>
|
||||||
|
</Menu.Button>
|
||||||
|
) : undefined}
|
||||||
|
<Menu.Button className="flex items-center gap-x-2">
|
||||||
|
<Cog className="h-5" />
|
||||||
|
<p>Machine Settings</p>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Menu.IconButton
|
<div className="flex items-center justify-end gap-1.5 pr-4">
|
||||||
label="Machine Options"
|
{supportsTailscaleSSH ? (
|
||||||
className={cn(
|
<Menu.Button
|
||||||
'py-0.5 w-10 bg-transparent border-transparent',
|
onPress={() => {
|
||||||
'border group-hover:border-headplane-200',
|
// We need to use JS to open the SSH URL
|
||||||
'dark:group-hover:border-headplane-700',
|
// in a new WINDOW since href can only
|
||||||
)}
|
// do a new TAB.
|
||||||
>
|
window.open(
|
||||||
<Ellipsis className="h-5" />
|
// TODO: Use the actual real username lol
|
||||||
</Menu.IconButton>
|
`${__PREFIX__}/ssh?hostname=${node.name}&username=tale`,
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer,width=800,height=600',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'py-0.5 w-fit bg-transparent border-transparent',
|
||||||
|
'border group-hover:border-headplane-200',
|
||||||
|
'dark:group-hover:border-headplane-700',
|
||||||
|
'opacity-0 pointer-events-none group-hover:opacity-100',
|
||||||
|
'group-hover:pointer-events-auto',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
SSH
|
||||||
|
</Menu.Button>
|
||||||
|
) : undefined}
|
||||||
|
<Menu.IconButton
|
||||||
|
label="Machine Options"
|
||||||
|
className={cn(
|
||||||
|
'py-0.5 w-10 bg-transparent border-transparent',
|
||||||
|
'border group-hover:border-headplane-200',
|
||||||
|
'dark:group-hover:border-headplane-700',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Ellipsis className="h-5" />
|
||||||
|
</Menu.IconButton>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Menu.Panel
|
<Menu.Panel
|
||||||
onAction={(key) => setModal(key as Modal)}
|
onAction={(key) => setModal(key as Modal)}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { CheckCircle, CircleSlash, Info, UserCircle } from 'lucide-react';
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
|
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
|
||||||
import { Link as RemixLink, useLoaderData } from 'react-router';
|
import { Link as RemixLink, useLoaderData } from 'react-router';
|
||||||
import { mapTag } from 'yaml/util';
|
|
||||||
import Attribute from '~/components/Attribute';
|
import Attribute from '~/components/Attribute';
|
||||||
import Button from '~/components/Button';
|
import Button from '~/components/Button';
|
||||||
import Card from '~/components/Card';
|
import Card from '~/components/Card';
|
||||||
|
|||||||
@ -71,11 +71,8 @@ export async function loader({
|
|||||||
const qp = new URL(request.url).searchParams;
|
const qp = new URL(request.url).searchParams;
|
||||||
const username = qp.get('username') || undefined;
|
const username = qp.get('username') || undefined;
|
||||||
const hostname = qp.get('hostname') || undefined;
|
const hostname = qp.get('hostname') || undefined;
|
||||||
const port = qp.get('port')
|
if (!username || !hostname) {
|
||||||
? Number.parseInt(qp.get('port')!, 10)
|
throw data('Missing required parameters: username, hostname', 400);
|
||||||
: undefined;
|
|
||||||
if (!username || !hostname || !port) {
|
|
||||||
throw data('Missing required parameters: username, hostname, port', 400);
|
|
||||||
}
|
}
|
||||||
// TODO: Verify the Host headers to ensure CORS friendly
|
// TODO: Verify the Host headers to ensure CORS friendly
|
||||||
// TODO: Check if the URL actually resolves correctly
|
// TODO: Check if the URL actually resolves correctly
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user