mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: add ssh buttons in the ui using hostinfo checks
This commit is contained in:
@@ -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 { HeadplaneAgentTag } from '~/components/tags/HeadplaneAgent';
|
||||
import { SubnetTag } from '~/components/tags/Subnet';
|
||||
import { TailscaleSSHTag } from '~/components/tags/TailscaleSSH';
|
||||
import { PopulatedNode } from '~/utils/node-info';
|
||||
import toast from '~/utils/toast';
|
||||
import MenuOptions from './menu';
|
||||
@@ -177,6 +178,10 @@ export function uiTagsForNode(node: PopulatedNode, isAgent?: boolean) {
|
||||
uiTags.push('subnet-approved');
|
||||
}
|
||||
|
||||
if (node.hostInfo?.sshHostKeys && node.hostInfo?.sshHostKeys.length > 0) {
|
||||
uiTags.push('tailscale-ssh');
|
||||
}
|
||||
|
||||
if (isAgent === true) {
|
||||
uiTags.push('headplane-agent');
|
||||
}
|
||||
@@ -205,6 +210,9 @@ export function mapTagsToComponents(node: PopulatedNode, uiTags: string[]) {
|
||||
/>
|
||||
);
|
||||
|
||||
case 'tailscale-ssh':
|
||||
return <TailscaleSSHTag />;
|
||||
|
||||
case 'headplane-agent':
|
||||
return <HeadplaneAgentTag />;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cog, Ellipsis } from 'lucide-react';
|
||||
import { Cog, Ellipsis, SquareTerminal } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Menu from '~/components/Menu';
|
||||
import type { User } from '~/types';
|
||||
@@ -10,7 +10,6 @@ import Move from '../dialogs/move';
|
||||
import Rename from '../dialogs/rename';
|
||||
import Routes from '../dialogs/routes';
|
||||
import Tags from '../dialogs/tags';
|
||||
|
||||
interface MenuProps {
|
||||
node: PopulatedNode;
|
||||
users: User[];
|
||||
@@ -29,6 +28,9 @@ export default function MachineMenu({
|
||||
isDisabled,
|
||||
}: MenuProps) {
|
||||
const [modal, setModal] = useState<Modal>(null);
|
||||
const supportsTailscaleSSH =
|
||||
node.hostInfo?.sshHostKeys && node.hostInfo?.sshHostKeys.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal === 'remove' && (
|
||||
@@ -90,21 +92,69 @@ export default function MachineMenu({
|
||||
|
||||
<Menu isDisabled={isDisabled}>
|
||||
{isFullButton ? (
|
||||
<Menu.Button className="flex items-center gap-x-2">
|
||||
<Cog className="h-5" />
|
||||
<p>Machine Settings</p>
|
||||
</Menu.Button>
|
||||
<div className="flex items-center justify-end gap-1.5 pr-4">
|
||||
{supportsTailscaleSSH ? (
|
||||
<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
|
||||
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 className="flex items-center justify-end gap-1.5 pr-4">
|
||||
{supportsTailscaleSSH ? (
|
||||
<Menu.Button
|
||||
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',
|
||||
);
|
||||
}}
|
||||
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
|
||||
onAction={(key) => setModal(key as Modal)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { CheckCircle, CircleSlash, Info, UserCircle } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
|
||||
import { Link as RemixLink, useLoaderData } from 'react-router';
|
||||
import { mapTag } from 'yaml/util';
|
||||
import Attribute from '~/components/Attribute';
|
||||
import Button from '~/components/Button';
|
||||
import Card from '~/components/Card';
|
||||
|
||||
@@ -71,11 +71,8 @@ export async function loader({
|
||||
const qp = new URL(request.url).searchParams;
|
||||
const username = qp.get('username') || undefined;
|
||||
const hostname = qp.get('hostname') || undefined;
|
||||
const port = qp.get('port')
|
||||
? Number.parseInt(qp.get('port')!, 10)
|
||||
: undefined;
|
||||
if (!username || !hostname || !port) {
|
||||
throw data('Missing required parameters: username, hostname, port', 400);
|
||||
if (!username || !hostname) {
|
||||
throw data('Missing required parameters: username, hostname', 400);
|
||||
}
|
||||
// TODO: Verify the Host headers to ensure CORS friendly
|
||||
// TODO: Check if the URL actually resolves correctly
|
||||
|
||||
Reference in New Issue
Block a user